From 28a7bd31fdc995cffa413b7ce8104e4ea510a8ce Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Mon, 24 May 2021 15:14:37 -0500 Subject: [PATCH 01/78] [DOCS] Dashboard changes for 7.13 (#99681) * [DOCS] Dashboard changes for 7.13 * Review comments * Review comments * Fixes link * Fixes lens link --- docs/concepts/index.asciidoc | 1 + docs/discover/save-search.asciidoc | 9 +- docs/setup/settings.asciidoc | 6 + .../user/dashboard/aggregation-based.asciidoc | 31 +- .../create-panels-with-editors.asciidoc | 4 +- docs/user/dashboard/dashboard.asciidoc | 320 +++++++----------- docs/user/dashboard/drilldowns.asciidoc | 2 +- .../dashboard/enhance-dashboards.asciidoc | 14 +- docs/user/dashboard/lens.asciidoc | 14 +- docs/user/dashboard/timelion.asciidoc | 53 +-- docs/user/dashboard/tsvb.asciidoc | 4 +- docs/user/dashboard/vega.asciidoc | 10 +- 12 files changed, 164 insertions(+), 304 deletions(-) diff --git a/docs/concepts/index.asciidoc b/docs/concepts/index.asciidoc index cb37dceb535649..43e5ae733a7604 100644 --- a/docs/concepts/index.asciidoc +++ b/docs/concepts/index.asciidoc @@ -87,6 +87,7 @@ image:concepts/images/refresh-every.png["section of time filter where you can co [float] +[[semi-structured-search]] ==== Semi-structured search Combine free text search with field-based search using the Kibana Query Language (KQL). diff --git a/docs/discover/save-search.asciidoc b/docs/discover/save-search.asciidoc index edfdae9a6b0811..b59f14180b1ff2 100644 --- a/docs/discover/save-search.asciidoc +++ b/docs/discover/save-search.asciidoc @@ -35,8 +35,9 @@ image::discover/images/read-only-badge.png[Example of Discover's read only acces If the saved search is associated with a different index pattern than is currently selected, opening the saved search changes the selected index pattern. The query language used for the saved search is also automatically selected. -. To add your search results to an existing dashboard: -.. Open the dashboard, then click *Edit*. +. To add your search results to a dashboard: +.. Open the main menu, then click *Dashboard*. +.. Open or create the dashboard, then click *Edit*. .. Click *Add from library*. -.. Open the *Types* menu, then select *Saved search*. -.. Select the the saved search that you want. +.. From the *Types* dropdown, select *Saved search*. +.. Select the saved search that you want to visualize, then click *X* to close the list. diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 0aab86fb5a9e2c..bac2b0ebdf15fa 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -723,6 +723,12 @@ out through *Advanced Settings*. *Default: `true`* | Set this value to true to allow Vega to use any URL to access external data sources and images. When false, Vega can only get data from {es}. *Default: `false`* +| `xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled` + | Enables the *Explore underlying data* option that allows you to open *Discover* from a dashboard panel and view the panel data. *Default: `false`* + + | `xpack.discoverEnhanced.actions.exploreDataInChart.enabled` + | Enables you to view the underlying documents in a data series from a dashboard panel. *Default: `false`* + | `xpack.license_management.enabled` | Set this value to false to disable the License Management UI. *Default: `true`* diff --git a/docs/user/dashboard/aggregation-based.asciidoc b/docs/user/dashboard/aggregation-based.asciidoc index 49c092f8baa4cd..cb102b73f93b43 100644 --- a/docs/user/dashboard/aggregation-based.asciidoc +++ b/docs/user/dashboard/aggregation-based.asciidoc @@ -92,17 +92,17 @@ create visual art for a specific topic. Choose the type of visualization you want to create, then use the editor to configure the options. -. From the dashboard, click *Create panel*, then click *Aggregation based* on the *New visualization* window. +. On the dashboard, click *All types > Aggregation based*. -.. Click the type of visualization you want to create. +.. Select the visualization type you want to create. -.. Click the data source you want to visualize. +.. Select the data source you want to visualize. -. From the editor, add the <> you want to visualize, then click *Update*. +. Add the <> you want to visualize using the editor, then click *Update*. + NOTE: For the *Date Histogram* to use an *auto interval*, the date field must match the primary time field of the index pattern. -. To change the order, drag the aggregations along the editor. +. To change the order, drag and drop the aggregations in the editor. + [role="screenshot"] image:images/bar-chart-tutorial-3.png[Option to change the order of aggregations] @@ -137,13 +137,9 @@ Add the sample web logs data that you'll use to create the bar chart, then creat Open the bar chart visualization builder and change the time range. -. On the dashboard, click *Create panel*. +. On the dashboard, click *All types > Aggregation based*, select *Vertical bar*, then select *kibana_sample_data_logs*. -. On the *New visualization* window, click *Aggregation based > Vertical bar*. - -. On the *Choose a source* window, click *kibana_sample_data_logs*. - -. Change the <>> to *Last 7 days*. +. Make sure the <>> is *Last 7 days*. [float] [[tutorial-configure-the-bar-chart]] @@ -177,19 +173,6 @@ TIP: Aggregation-based panels support a maximum of three *Split series*. [role="screenshot"] image:images/bar-chart-tutorial-2.png[Bar chart with sample logs data] -[float] -===== Save the panel - -Save and add the visualization panel to the dashboard. - -. From the toolbar, click *Save*. - -. Enter the *Title* and optional *Description*. - -. From the *Tags* drop down, select any applicable tags. - -. Select *Add to Dashboard after saving*. - . Click *Save and return*. diff --git a/docs/user/dashboard/create-panels-with-editors.asciidoc b/docs/user/dashboard/create-panels-with-editors.asciidoc index 8e047819fd1c6c..17d3b5fb8a8a5a 100644 --- a/docs/user/dashboard/create-panels-with-editors.asciidoc +++ b/docs/user/dashboard/create-panels-with-editors.asciidoc @@ -1,13 +1,13 @@ [[create-panels-with-editors]] == Create panels with editors -{kib} provides several editors that you can use to create dashboard panels. +{kib} provides several editors that you can use to create panels of your data. [cols="2"] |=== | <> -| Create visualizations with the drag and drop editor. *Lens* is recommended for most users. +| Create visualizations with the drag and drop editor. | <> | Create visualizations with your geographical data. diff --git a/docs/user/dashboard/dashboard.asciidoc b/docs/user/dashboard/dashboard.asciidoc index 070d511ed8073e..8226e9c6ed073f 100644 --- a/docs/user/dashboard/dashboard.asciidoc +++ b/docs/user/dashboard/dashboard.asciidoc @@ -6,35 +6,34 @@ **_Visualize your data with dashboards._** The best way to understand your data is to visualize it. With dashboards, you can turn your data from one or more <> into a collection of panels -that bring clarity to your data, tell a story about your data, and allow you to focus on only the data that's important to you. Configure each panel to display your data in a chart, table, map, and more, -then compare the panels side-by-side to identify the patterns and connections in your data. +that bring clarity to your data, tell a story about your data, and allow you to focus on only the data that's important to you. [role="screenshot"] image:images/Dashboard_example.png[Example dashboard] -Dashboards support many types of panels, and provide several editors that you can use to create panels. +Panels display your data in charts, tables, maps, and more, which allow you to compare your data side-by-side to identify patterns and connections. Dashboards support several editors you can use to create panels, and support many types of panels to display your data. [cols="2"] |=== -| <> -| Use the *Lens*, *TSVB*, *Vega*, and *Timelion* editors to help you create visualizations of your data, or create aggregation-based visualizations using {es} <>. -*Lens* is recommended for most users. +| <> +| Use the *Lens*, *TSVB*, *Vega*, and *Timelion* editors to create visualizations of your data, or create *Aggregation based* visualizations using {es} aggregations. +*Lens* is the recommended editor. | <> | Create beautiful displays of your geographical data. -| <> -| Add context to your panels with <>, or add dynamic filters with <>. +| <> +| Display the results from machine learning anomaly detection jobs. -| <> -| Display a saved search table from <>. The table results are not aggregated. +| <> +| Display an anomaly chart from the *Anomaly Explorer. | <> | Display a table of live streaming logs. -| <> -| Display the results from machine learning anomaly detection jobs. +| <> +| Add context to your panels with <>, or add dynamic filters with <>. |=== @@ -54,10 +53,8 @@ To create dashboards, you must meet the minimum requirements. * Make sure you have {ref}/getting-started-index.html[data indexed into {es}] and an <>. -* Have an understanding of {ref}/documents-indices.html[{es} documents and indices]. - * When the read-only indicator appears, you have insufficient privileges -to create or save dashboards. The options to create and save dashboards are not visible. For more information, +to create or save dashboards, and the options to create and save dashboards are not visible. For more information, refer to <>. [float] @@ -74,281 +71,225 @@ Begin with an empty dashboard, or open an existing dashboard. * To open an existing dashboard, click the dashboard *Title* you want to open. -[float] -[[add-panels]] -=== Add panels - -To add panels to the dashboard, you can use one of the editors to create a new panel, -add an existing panel from the *Visualize Library*, add a table of live streaming logs, or the add the results from a machine learning anomaly detection job. - [float] [[create-panels-with-lens]] -==== Create panels - -To create panels, use one of the editors, then add the panel to the dashboard. - -. On the dashboard, click *Create panel*. +=== Add panels -. On the *New visualization* window, click the editor you want to use. *Lens* is recommended for most users. +Create and add panels of your data to the dashboard, or add existing panels from the library. -. To create the panel, configure the editor options. +* *Create visualization* — Opens *Lens*, the recommended editor to create visualizations of your data. -. To add the panel to the dashboard, choose one of the following options: +* *All types* — Select the editor to create the panel, or select the panel type you want to add to the dashboard. -* To add the panel to the dashboard without saving to the *Visualize Library*, click *Save and return*. +* *Add from library* — Add panels from the *Visualize Library*, including search results from <>. The search results from *Discover* are not aggregated. + -To add a title to the panel, click *No Title*, enter the *Panel title*, then click *Save*. +When a panel contains a saved query, both queries are applied. -* To save the panel to the *Visualize Library*, click *Save to Library*, configure the options, then click *Save and return*. -+ -When panels are saved in the *Visualize Library*, image:dashboard/images/visualize-library-icon.png[Visualize Library icon] appears in the header. +[[tsvb]] [float] -[[add-panels-from-the-library]] -==== Add panels from the library - -Add panels that you've already created from the *Visualize Library*. - -. On the dashboard, click *Add from library*. +[[save-panels]] +=== Save panels -. On the *Add from library* flyout, click the panels you want to add to the dashboard. +Consider where you want to save the panel in {kib}. You can save the panel just on the dashboard you are working on, or save the panel in the *Visualize Library*. -. To close the flyout, click *X*. -+ -When a panel contains a stored query, both queries are applied. - -. To make changes to the panel, open the panel menu, then select the following options: - -* *Edit visualization* — Opens an editor so that you can reconfigure the panel. -+ -To make changes to the panel without affecting the original version, open the panel menu, then click *More > Unlink from library*. +[float] +[[save-to-visualize-library]] +==== Save to the Visualize Library -* *Edit panel title* — Opens the *Customize panel* window to change the *Panel title* and specify whether you want to display the panel title. +To use the panel on *Canvas* workpads and other dashboards, save the panel to the *Visualize Library*. -[float] -[[add-a-table-of-live-streaming-logs]] -==== Add a logs panel +. Click *Save to library*. -Add a panel that displays a table of live streaming logs. +. Enter the *Title* and add any applicable *Tags*. -. On the dashboard, click *Add from library*. +. Make sure that *Add to Dashboard after saving* is selected. -. On the *Add from library* flyout, click *Create new*, then select *Log stream*. +. Click *Save and return*. ++ +When panels are saved in the *Visualize Library*, image:dashboard/images/visualize-library-icon.png[Visualize Library icon] appears in the panel header. [float] -[[add-machine-learning-results]] -==== Add machine learning results +[[save-to-the-dashboard]] +==== Save to the dashboard -Add a panel that displays the results from machine learning anomaly detection jobs. +Quickly add the panel and return to the dashboard without specifying the save options or adding the panel to the *Visualize Library*. -. On the dashboard, click *Add from library*. +. Click *Save and return*. -. On the *Add from library* flyout, click *Create new*, then select *ML Anomaly Swim Lane*. +. Add more panels to the dashboard, or specify the panel title. -[[tsvb]] +.. In the panel header, click *No Title*. -[float] -[[arrange-panels]] -[[moving-containers]] -[[resizing-containers]] -=== Arrange the panels +.. Select *Show panel title*. -To compare the data in the panels, reorganize or remove the panels on the dashboard. +.. Enter the *Panel title*. -. From the toolbar, click *Edit*, then use the following options: +If you change your mind and want to add the panel to the *Visualize Library*: -* To move, click and hold the panel header, then drag to the new location. +. Open the panel menu, then select *More > Save to library*. -* To resize, click the resize control, then drag to the new dimensions. +. Enter the panel title, then click *Save*. -* To maximize the panel to fullscreen, open the panel menu, then click *More > Maximize panel*. +[float] +[[edit-panels]] +== Edit panels -* To delete, open the panel menu, then click *More > Delete from dashboard*. +To make changes to the panel, use the panel menu options. -. To save your changes, click *Save* in the toolbar. +. In the toolbar, click *Edit*. -[float] -[[apply-design-options]] -=== Apply design options +. Open the panel menu, then use the following options: -Apply a set of design options to the entire dashboard. +* *Edit lens* — Opens *Lens* so you can make changes to the visualization. -. From the toolbar, click *Edit > Options*. - -. Select the following options: +* *Edit visualization* — Opens the editor so you can make changes to the panel. ++ +To make changes without changing the original version, open the panel menu, then click *More > Unlink from library*. -* *Use margins between panels* — Specifies a margin of space between each panel. +* *Edit panel title* — Opens the *Customize panel* window to change the *Panel title*. -* *Show panel titles* — Specifies the appearance of titles in the header of each panel. +* *More > Replace panel* — Opens the *Visualize Library* so you can select a new panel to replace the existing panel. -* *Sync color pallettes across panels* — Specifies whether the color pallette is applied to all panels. +* *More > Delete from dashboard* — Removes the panel from the dashboard. ++ +If you want to use the panel later, make sure that you save the panel to the *Visualize Library*. [float] [[search-or-filter-your-data]] -=== Search or filter your data +== Search and filter your data -{kib} provides you with several ways to search your data and apply {es} filters. You can combine the filters with any panel +{kib} supports several ways to search your data and apply {es} filters. You can combine the filters with any panel filter to display the data want to you see. -[float] -[[semi-structured-search]] -==== Semi-structured search - -Combine free text search with field-based search using the <>. -Type a search term to match across all fields, or begin typing a field name to -get prompted with field names and operators you can use to build a structured query. +For more information about {kib} and {es} filters, refer to <>. -For example, in the sample web logs data, the following query displays data only for the US: +To apply a panel-level time filter: -. Enter `g`, then select *geo.source*. +. Open the panel menu, then select *More > Customize time range*. -. Select *equals some value* and *US*, then click *Update*. +. Enter the time range you want to view, then click *Add to panel*. -. For a more complex search, try: +[float] +[[arrange-panels]] +[[moving-containers]] +[[resizing-containers]] +== Arrange panels -[source,text] -------------------- -geo.src : "US" and url.keyword : "https://www.elastic.co/downloads/beats/metricbeat" -------------------- +To compare the data side-by-side, move and arrange the panels. -[float] -[[time-filter]] -==== Time filter +In the toolbar, click *Edit*, then use the following options: -The <> restrict the data that appears on the dashboard, but you can override the time filter with panel filters. - -. To update the time filter, add a panel that displays time-based data along the x-axis. +* To move, click and hold the panel header, then drag to the new location. -. Open the panel menu, then select *More > Customize time range*. +* To resize, click the resize control, then drag to the new dimensions. -. On the *Customize panel time range* window, specify the time range, then click *Add to panel*. -+ -[role="screenshot"] -image:images/time_range_per_panel.gif[Time range per dashboard panel] +* To maximize to fullscreen, open the panel menu, then click *More > Maximize panel*. [float] -[[additional-filters-with-and]] -==== Additional filters with AND - -Add filters to a dashboard, or pin filters to multiple places in {kib}. To add filters, you can use the *Edit Filter* options, or the advanced JSON editor for the {es} {ref}/query-dsl.html[Query DSL]. -When there is one or more index patterns on the dashboard, you can select the index pattern that contains the fields you want to create the filter. +[[apply-design-options]] +== Apply design options -For example, to filter the dashboard to display only ios data from *kibana_sample_data_logs*: +Apply a set of design options to the entire dashboard. -. Click *Add filter*. +In the toolbar, click *Edit > Options*, then use the following options: -. From the *Index Pattern* dropdown, select *kibana_sample_data_logs*. +* *Use margins between panels* — Specifies a margin of space between each panel. -. Set *Field* to *machine.os*, *Operator* to *is*, and *Value* to *ios*. +* *Show panel titles* — Specifies the appearance of titles in the header of each panel. -. *Save* the filter. -+ -To remove the filter, click *x*. +* *Sync color pallettes across panels* — Specifies whether the color pallette is applied to all panels. [float] -[[add-dynamic-filters]] -==== Add dynamic filters +[[duplicate-panels]] +== Duplicate panels -When you see data in a panel that you want to use as a filter, you can dynamically create the filter. To dynamically add filters, click the data in a panel. - -. Click the data in the panel. - -. Select filters you want to apply to all of the dashboard panels, then click *Apply*. -+ -To remove the filters, click *x*. +To duplicate a panel and the configured functionality, use the clone and copy panel options. Cloned and copied panels replicate all of the functionality from the original panel, +including renaming, editing, and cloning. [float] [[clone-panels]] === Clone panels -To duplicate a panel and the configured functionality, clone the panel. Cloned panels continue to replicate all of the functionality from the original panel, -including renaming, editing, and cloning. When you clone a panel, the clone appears beside the original panel, and moves other panels to provide a space on the -dashboard. +Cloned panels appear next to the original panel, and move the other panels to provide a space on the dashboard. -. From the toolbar, click *Edit*. +. In the toolbar, click *Edit*. . Open the panel menu, then select *Clone panel*. + -[role="screenshot"] -image:images/clone_panel.gif[clone panel] -+ When cloned panels are saved in the *Visualize Library*, image:dashboard/images/visualize-library-icon.png[Visualize Library icon] appears in the header. [float] [[copy-to-dashboard]] === Copy panels -To add a panel to another dashboard, copy the panel. +Copy panels from one dashboard to another dashboard. . Open the panel menu, then select *More > Copy to dashboard*. . On the *Copy to dashboard* window, select the dashboard, then click *Copy and go to dashboard*. [float] -[[explore-the-underlying-data]] -=== Explore the underlying documents +[[explore-the-underlying-documents]] +== Explore the underlying documents -View the underlying documents in a panel, or in a data series. +To gain insight to the data, open the underlying panel or data series documents in *Discover*. The panel documents that you open in *Discover* have the same time range and filters as the source panel. -. In kibana.yml, add the following: -+ -["source","yml"] ------------ -xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled: true ------------ - -TIP: *Explore underlying data* is supported only for visualization panels with a single index pattern. +[float] +[[explore-underlying-panel-documents]] +=== Explore the underlying panel documents -To view the underlying documents in the panel: +When your visualization panel contains a single index pattern, you can open the panel documents in *Discover*. . Open the panel menu. . Click *Explore underlying data*. + -*Discover* opens with the same time range and filters as the panel. -+ [role="screenshot"] image::images/explore_data_context_menu.png[Explore underlying data from panel context menu] -To view the underlying documents in a data series: +[float] +[[explore-underlying-data-series-documents]] +=== Explore the underlying data series documents -. In kibana.yml, add the following: -+ -["source","yml"] ------------ -xpack.discoverEnhanced.actions.exploreDataInChart.enabled: true ------------ +To gain insight to a data series, open the documents in *Discover*. -. Open the dashboard, then click on the data series you want to view. +. Click the data series in the panel that you want to view. + +. Select *Explore underlying data*. + [role="screenshot"] image::images/explore_data_in_chart.png[Explore underlying data from chart] [float] [[download-csv]] -=== Download the panel data +== Download panel data Download panel data in a CSV file. You can download most panels in a CSV file, but there is a shortcut available for *Lens* panels. +[float] [role="xpack"] -To download *Lens* panel data in a CSV file: +[[download-lens-data]] +=== Download Lens data + +When you download *Lens* panel data, each layer produces a single CSV file with columns. +When you download multiple layers, the file names combine the visualization and layer index names. -. Open the *Lens* panel menu. +. Open the *Lens* panel menu . Select *More > Download as CSV*. -+ -[role="screenshot"] -image::images/download_csv_context_menu.png[Download as CSV from panel context menu] -Each layer produces a single CSV file with columns. -When you download multiple layers, the file names combine the visualization and layer index names. +[float] +[[download-other-panel-data]] +=== Download all other panel data -To download all other panel data in a CSV file: +Download the data for non-*Lens* panels. . Open the panel menu, then select *Inspect*. . Click *Download CSV*, then select the CSV type from the dropdown: + * *Formatted CSV* — Contains human-readable dates and numbers. * *Unformatted* — Best used for computer use. @@ -356,40 +297,17 @@ To download all other panel data in a CSV file: [role="screenshot"] image:images/Dashboard_inspect.png[Inspect in dashboard] -[float] -[[save-the-dashboard]] -=== Save the dashboard - -When you're finished making changes, save the dashboard. - -From the toolbar, choose one of the following options: - -* *Save as* — Opens the *Save dashboard* window, which allows you to specify the title and dashboard options. - -* *Save* — Allows you to save the changes you've made to an existing dashboard. - -* *Switch to view mode* — Allows you to exit *Edit* mode without saving your changes, or you can discard the changes you've made. All dashboards with unsaved changes display *Unsaved changes* in the toolbar. - [float] [[share-the-dashboard]] -=== Share the dashboard - -To share the dashboard with a larger audience, click *Share* in the toolbar, then choose one of the following options: - -* *Embed code* — Embed a fully interactive dashboard as an iframe on a web page. To access embedded dashboards, you can require users to -log in using their {kib} credentials, via reverse proxy, or enable <>. - -* *Permalinks* — Share a direct link to a {kib} dashboard. User authentication is required. - -* *PDF Reports* — Generate a PDF report. For more information, refer to <>. +== Share dashboards -* *PNG Reports* — Generate a PNG report. For more information, refer to <>. +To share the dashboard with a larger audience, click *Share* in the toolbar. For detailed information, refer to <>. [float] [[import-dashboards]] -=== Export the dashboard +== Export dashboards -To automate {kib}, you can export dashboards as JSON using the <>. It is important to export dashboards with all references needed. +To automate {kib}, you can export dashboards as JSON using the <>. It is important to export dashboards with all necessary references. -- include::tutorial-create-a-dashboard-of-lens-panels.asciidoc[] diff --git a/docs/user/dashboard/drilldowns.asciidoc b/docs/user/dashboard/drilldowns.asciidoc index fc25f84030ee21..0eb4b43466ff9a 100644 --- a/docs/user/dashboard/drilldowns.asciidoc +++ b/docs/user/dashboard/drilldowns.asciidoc @@ -152,7 +152,7 @@ To create dashboard drilldowns, you create or locate the dashboards you want to * *[Logs] Total Requests and Bytes* * *[Logs] Visitors by OS* + -If you don’t see the data on a panel, try changing the <>. +If you don’t see the data on a panel, change the <>. . Save the dashboard. In the *Title* field, enter `Host Overview`. diff --git a/docs/user/dashboard/enhance-dashboards.asciidoc b/docs/user/dashboard/enhance-dashboards.asciidoc index 7176a2e2834579..c999ec9b68251d 100644 --- a/docs/user/dashboard/enhance-dashboards.asciidoc +++ b/docs/user/dashboard/enhance-dashboards.asciidoc @@ -1,7 +1,7 @@ [[enhance-dashboards]] == Enhance dashboards -Now that you have added panels to your dashboard, you can add filter panels to interact with the data, and Markdown panels to add context to the data. +You can add filter panels to interact with the data in your visualization panels, and Markdown panels to add context to the data. To make your dashboard look the way you want, use the editing options. [float] @@ -21,11 +21,9 @@ min and max aggregation. For example, use the range slider when you want to filt [role="screenshot"] image::images/dashboard-controls.png[] -. From the dashboard, click *Create panel*. +. On the dashboard, click *All types*, then select *Controls*. -. On the *New Visualization* window, click *Controls*. - -. Click *Options*, then configure the following, then click *Update*: +. Click *Options*, then configure the following options: * *Update {kib} filters on each change* — When selected, all interactive inputs create filters that refresh the dashboard. When unselected, {kib} filters are created only when you click *Apply changes*. @@ -34,6 +32,8 @@ image::images/dashboard-controls.png[] * *Pin filters for all applications* — When selected, all filters created by interacting with the inputs are automatically pinned. +. Click *Update* + [float] [[add-text]] === Add text @@ -42,9 +42,7 @@ Add text panels with *Markdown* when you want to provide context to the other pa *Markdown* is a text entry field that accepts GitHub-flavored Markdown text. For information about GitHub-flavored Markdown text, click *Help*. -. From the dashboard, click *Create panel*. - -. On the *New Visualization* window, click *Text*. +. From the dashboard, click *All types*, then select *Text*. . In the *Markdown* field, enter the text, then click *Update*. diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc index 94c9db1462760f..613432908df3d3 100644 --- a/docs/user/dashboard/lens.asciidoc +++ b/docs/user/dashboard/lens.asciidoc @@ -13,11 +13,9 @@ image:dashboard/images/lens.png[Lens] Open *Lens*, then explore the fields in your data. The list of fields are determined by the index pattern and time filter. -. On the dashboard, click *Create panel*. +. On the dashboard, click *Create visualization*. -. On the *New visualization* window, click *Lens*. - -. <>. +. In *Lens*, <>. . To view the fields in the a different index pattern, click the index pattern, then select a different index pattern from the dropdown. @@ -56,11 +54,11 @@ TIP: *Other* can equal more than 100% by a small amount. [float] [[create-the-visualization-panel]] -==== Create the visualization panel +==== Create visualizations -Drag and drop the fields on to the visualization builder, then +Drag and drop the fields on to the visualization builder, then customize the visualization. -. Drag and drop the fields to the visualization builder. +. Drag and drop the fields on to the visualization builder. . To change the visualization type, use the following options: @@ -102,7 +100,7 @@ For more information about adding fields to index patterns and Painless scriptin [float] [[drag-and-drop-keyboard-navigation]] -===== Create visualization panels with keyboard navigation +===== Create visualizations with keyboard navigation *Lens* has a fully accessible, continuously improved drag and drop system, which allows you to use a keyboard instead of a mouse. diff --git a/docs/user/dashboard/timelion.asciidoc b/docs/user/dashboard/timelion.asciidoc index 12d0169c13f661..675fd03df36484 100644 --- a/docs/user/dashboard/timelion.asciidoc +++ b/docs/user/dashboard/timelion.asciidoc @@ -1,7 +1,7 @@ [[timelion]] === Timelion -Instead of using a visual editor to create charts, you define a graph by chaining functions together, using the *Timelion*-specific syntax. +To use *Timelion*, you define a graph by chaining functions together, using the *Timelion*-specific syntax. The syntax enables some features that classical point series charts don't offer, such as pulling data from different indices or data sources into one graph. deprecated::[7.0.0,"*Timelion* is still supported. The *Timelion app* is deprecated in 7.0, replaced by dashboard features. In the last 7.x minor version and later, the *Timelion app* is removed from {kib}. To prepare for the removal of *Timelion app*, you must migrate *Timelion app* worksheets to a dashboard. For information on how to migrate *Timelion app* worksheets, refer to the link:https://www.elastic.co/guide/en/kibana/7.10/release-notes-7.10.0.html#deprecation-v7.10.0[7.10.0 Release Notes]."] @@ -90,11 +90,9 @@ Set up Metricbeat, then create the dashboard. Open *Timelion* and change the time range. -. On the dashboard, click *Create panel*. +. On the dashboard, click *All types > Aggregation based*, then select *Timelion*. -. On the *New visualization* window, click *Aggregation based > Timelion*. - -. Change the <> to *Last 7 days*. +. Make sure the <> is *Last 7 days*. [float] [[timelion-tutorial-create-time-series-visualizations]] @@ -242,20 +240,7 @@ Move the legend to the north west position with two columns, then click *Update image::images/timelion-customize04.png[Final time series visualization] {nbsp} -[float] -==== Save the panel - -Save and add the panel to the dashboard. - -. From the toolbar, click *Save*. - -. Enter the *Title* and optional *Description*. - -. From the *Tags* drop down, select any applicable tags. - -. Select *Add to Dashboard after saving*. - -. Click *Save and return*. +To save the panel, click *Save and return* in the toolbar. [float] [[timelion-tutorial-create-visualizations-with-mathematical-functions]] @@ -364,20 +349,7 @@ Customize and format the visualization using the following functions, then click image::images/timelion-math05.png[Final visualization that displays inbound and outbound network traffic] {nbsp} -[float] -==== Save the panel - -Save and add the panel to the dashboard. - -. From the toolbar, click *Save*. - -. Enter the *Title* and optional *Description*. - -. From the *Tags* drop down, select any applicable tags. - -. Select *Add to Dashboard after saving*. - -. Click *Save and return*. +To save the panel, click *Save and return* in the toolbar. [float] [[timelion-tutorial-create-visualizations-withconditional-logic-and-tracking-trends]] @@ -539,19 +511,6 @@ Customize and format the visualization using the following functions, then click image::images/timelion-conditional04.png[Final visualization that displays outliers and patterns over time] {nbsp} -[float] -==== Save the panel - -Save and add the panel to the dashboard. - -. From the toolbar, click *Save*. - -. Enter the *Title* and optional *Description*. - -. From the *Tags* drop down, select any applicable tags. - -. Select *Add to Dashboard after saving*. - -. Click *Save and return*. +To save the panel, click *Save and return* in the toolbar. For more information about *Timelion* conditions, refer to https://www.elastic.co/blog/timeseries-if-then-else-with-timelion[I have but one .condition()]. diff --git a/docs/user/dashboard/tsvb.asciidoc b/docs/user/dashboard/tsvb.asciidoc index 5c4ce8e365e865..b69df7c7d26d68 100644 --- a/docs/user/dashboard/tsvb.asciidoc +++ b/docs/user/dashboard/tsvb.asciidoc @@ -14,9 +14,7 @@ image::visualize/images/tsvb-screenshot.png[TSVB overview] Open *TSVB*, then make sure the required settings are configured. -. On the dashboard, click *Create panel*. - -. On the *New visualization* window, click *TSVB*. +. On the dashboard, click *All types*, then select *TSVB*. . In *TSVB*, click *Panel options*, then specify the required *Data* settings. diff --git a/docs/user/dashboard/vega.asciidoc b/docs/user/dashboard/vega.asciidoc index b90370af5a12a3..faf54e2b7acfc8 100644 --- a/docs/user/dashboard/vega.asciidoc +++ b/docs/user/dashboard/vega.asciidoc @@ -37,13 +37,11 @@ Before starting, add the eCommerce sample data that you'll use in your spec, the Open *Vega-Lite* and change the time range. -. On the dashboard, click *Create panel*. - -. On the *New visualization* window, click *Custom visualization*. +. On the dashboard, click *All types*, then select *Custom visualization*. + A pre-populated line chart displays the total number of documents. -. Change the <> to *Last 7 days*. +. Make sure the <> is *Last 7 days*. [float] [[vega-tutorial-create-a-stacked-area-chart]] @@ -595,9 +593,9 @@ Add the {es} search query with the `data` block, then click *Update*: ``` [float] -===== Change the X- and Y-axes +===== Change the x- and y-axes -Display labels for the X- and Y-axes. +Display labels for the x- and y-axes. In the *Vega* spec, add the `scales` block, then click *Update*: From eb0deac8d08ecfbf29d34edd11564ee6f2860ce6 Mon Sep 17 00:00:00 2001 From: ymao1 Date: Mon, 24 May 2021 16:24:15 -0400 Subject: [PATCH 02/78] [Alerting] Update README (#100478) * Updating readme * Updating readme * Fix plugin list docs --- docs/developer/plugin-list.asciidoc | 2 +- x-pack/plugins/alerting/README.md | 386 ++++++++++++++++------------ 2 files changed, 216 insertions(+), 172 deletions(-) diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 4ba5e32eec8b53..94384024e09357 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -321,7 +321,7 @@ which will load the visualization's editor. |{kib-repo}blob/{branch}/x-pack/plugins/alerting/README.md[alerting] -|The Kibana alerting plugin provides a common place to set up alerts. You can: +|The Kibana Alerting plugin provides a common place to set up rules. You can: |{kib-repo}blob/{branch}/x-pack/plugins/apm/readme.md[apm] diff --git a/x-pack/plugins/alerting/README.md b/x-pack/plugins/alerting/README.md index eb64d71be565ea..0d5b0bf415fed9 100644 --- a/x-pack/plugins/alerting/README.md +++ b/x-pack/plugins/alerting/README.md @@ -1,21 +1,21 @@ -# Kibana alerting +# Kibana Alerting -The Kibana alerting plugin provides a common place to set up alerts. You can: +The Kibana Alerting plugin provides a common place to set up rules. You can: -- Register types of alerts -- List the types of registered alerts -- Perform CRUD actions on alerts +- Register types of rules +- List the types of registered rules +- Perform CRUD actions on rules ---- Table of Contents -- [Kibana alerting](#kibana-alerting) +- [Kibana Alerting](#kibana-alerting) - [Terminology](#terminology) - [Usage](#usage) - - [Alerts API keys](#alerts-api-keys) - - [Plugin status](#plugin-status) - - [Alert types](#alert-types) + - [Alerting API Keys](#alerting-api-keys) + - [Plugin Status](#plugin-status) + - [Rule Types](#rule-types) - [Methods](#methods) - [Executor](#executor) - [Action variables](#action-variables) @@ -24,52 +24,62 @@ Table of Contents - [Tests](#tests) - [Example](#example) - [Role Based Access-Control](#role-based-access-control) - - [Alert Navigation](#alert-navigation) - - [Experimental RESTful API](#restful-api) - - [`GET /api/alerts/alert/{id}/state`: Get alert state](#get-apialertidstate-get-alert-state) - - [`GET /api/alerts/alert/{id}/_instance_summary`: Get alert instance summary](#get-apialertidstate-get-alert-instance-summary) - - [`POST /api/alerts/alert/{id}/_update_api_key`: Update alert API key](#post-apialertidupdateapikey-update-alert-api-key) - - [Alert instance factory](#alert-instance-factory) - - [Templating actions](#templating-actions) + - [Alerting Navigation](#alert-navigation) + - [Internal HTTP APIs](#internal-http-apis) + - [`GET /internal/alerting/rule/{id}/state`: Get rule state](#get-internalalertingruleidstate-get-rule-state) + - [`GET /internal/alerting/rule/{id}/_alert_summary`: Get rule alert summary](#get-internalalertingruleidalertsummary-get-rule-alert-summary) + - [`POST /internal/alerting/rule/{id}/_update_api_key`: Update rule API key](#post-internalalertingruleidupdateapikey-update-rule-api-key) + - [Alert Factory](#alert-factory) + - [Templating Actions](#templating-actions) - [Examples](#examples) - ## Terminology -**Alert Type**: A function that takes parameters and executes actions to alert instances. +> Disclaimer: We are actively working to update the terminology of the Alerting Framework. While all user-facing terminology has been updated, much of the codebase is still a work in progress. + + +> References to `rule` and `rule type` entities are still named `AlertType` within the codebase. -**Alert**: A configuration that defines a schedule, an alert type w/ parameters, state information and actions. +> References to `alert` and `alert factory` entities are still named `AlertInstance` and `alertInstanceFactory` within the codebase. -**Alert Instance**: The instance(s) created from an alert type execution. +**Rule Type**: A function that takes parameters and executes actions on alerts. -A Kibana alert detects a condition and executes one or more actions when that condition occurs. Alerts work by going through the followings steps: +**Rule**: A configuration that defines a schedule, a rule type w/ parameters, state information and actions. -1. Run a periodic check to detect a condition (the check is provided by an Alert Type) -2. Convert that condition into one or more stateful Alert Instances -3. Map Alert Instances to pre-defined Actions, using templating -4. Execute the Actions +**Alert**: The alert(s) created from a rule execution. + +A Kibana rule detects a condition and executes one or more actions when that condition occurs. Rules work by going through the followings steps: + +1. Run a periodic check to detect a condition (the check is provided by a rule type). +2. Convert that condition into one or more stateful alerts. +3. Map alerts to pre-defined actions, using templating. +4. Execute the actions. ## Usage -1. Develop and register an alert type (see alert types -> example). -2. Configure feature level privileges using RBAC -3. Create an alert using the RESTful API [Documentation](https://www.elastic.co/guide/en/kibana/master/alerts-api-update.html) (see alerts -> create). +1. Develop and register a rule type (see rule types -> example). +2. Configure feature level privileges using RBAC. +3. Create a rule using the RESTful API [Documentation](https://www.elastic.co/guide/en/kibana/master/alerting-apis.html) (see rules -> create). + +## Alerting API Keys -## Alerts API keys +When we create a rule, we generate a new API key. -When we create an alert, we generate a new API key. +When we update, enable, or disable a rule, we must invalidate the old API key and create a new one. -When we update, enable, or disable an alert, we must invalidate the old API key and create a new one. +To manage the invalidation process for API keys, we use the saved object type `api_key_pending_invalidation`. This saved object stores all API keys that were marked for invalidation anytime rules were updated, enabled or disabled. + +For security plugin invalidation, we schedule a task to check if the `api_key_pending_invalidation` saved object contains new API keys that were marked for invalidation earlier than the configured delay. The default schedule for running this task is every 5 minutes. -To manage the invalidation process for API keys, we use the saved object `api_key_pending_invalidation`. This object stores all API keys that were marked for invalidation when alerts were updated. -For security plugin invalidation, we schedule a task to check if the`api_key_pending_invalidation` saved object contains new API keys that are marked for invalidation earlier than the configured delay. The default value for running the task is 5 mins. To change the schedule for the invalidation task, use the kibana.yml configuration option `xpack.alerting.invalidateApiKeysTask.interval`. + To change the default delay for the API key invalidation, use the kibana.yml configuration option `xpack.alerting.invalidateApiKeysTask.removalDelay`. -## Plugin status +## Plugin Status -The plugin status of an alert is customized by including information about checking failures for the framework decryption: -``` +The plugin status of the Alerting Framework is customized by including information about checking for failures during framework decryption: + +```js core.status.set( combineLatest([ core.status.derivedStatus$, @@ -85,9 +95,10 @@ core.status.set( ) ); ``` + To check for framework decryption failures, we use the task `alerting_health_check`, which runs every 60 minutes by default. To change the default schedule, use the kibana.yml configuration option `xpack.alerting.healthCheck.interval`. -## Alert types +## Rule Types ### Methods @@ -97,44 +108,59 @@ The following table describes the properties of the `options` object. |Property|Description|Type| |---|---|---| -|id|Unique identifier for the alert type. For convention purposes, ids starting with `.` are reserved for built in alert types. We recommend using a convention like `.mySpecialAlert` for your alert types to avoid conflicting with another plugin.|string| -|name|A user-friendly name for the alert type. These will be displayed in dropdowns when choosing alert types.|string| -|actionGroups|An explicit list of groups the alert type may schedule actions for, each specifying the ActionGroup's unique ID and human readable name. Alert `actions` validation will use this configuartion to ensure groups are valid. We highly encourage using `kbn-i18n` to translate the names of actionGroup when registering the AlertType. |Array<{id:string, name:string}>| -|defaultActionGroupId|Default ID value for the group of the alert type.|string| -|recoveryActionGroup|An action group to use when an alert instance goes from an active state, to an inactive one. This action group should not be specified under the `actionGroups` property. If no recoveryActionGroup is specified, the default `recovered` action group will be used. |{id:string, name:string}| -|actionVariables|An explicit list of action variables the alert type makes available via context and state in action parameter templates, and a short human readable description. Alert UI will use this to display prompts for the users for these variables, in action parameter editors. We highly encourage using `kbn-i18n` to translate the descriptions. |{ context: Array<{name:string, description:string}, state: Array<{name:string, description:string}>| -|validate.params|When developing an alert type, you can choose to accept a series of parameters. You may also have the parameters validated before they are passed to the `executor` function or created as an alert saved object. In order to do this, provide a `@kbn/config-schema` schema that we will use to validate the `params` attribute.|@kbn/config-schema| -|executor|This is where the code of the alert type lives. This is a function to be called when executing an alert on an interval basis. For full details, see executor section below.|Function| -|producer|The id of the application producing this alert type.|string| -|minimumLicenseRequired|The value of a minimum license. Most of the alerts are licensed as "basic".|string| +|id|Unique identifier for the rule type. By convention, IDs starting with `.` are reserved for built-in rule types. We recommend using a convention like `.mySpecialRule` for your rule types to avoid conflicting with another plugin.|string| +|name|A user-friendly name for the rule type. These will be displayed in dropdowns when choosing rule types.|string| +|actionGroups|An explicit list of groups the rule type may schedule actions for, each specifying the ActionGroup's unique ID and human readable name. Each rule type's `actions` validation will use this list to ensure configured groups are valid. We highly encourage using `kbn-i18n` to translate the names of actionGroup when registering the rule type. |Array<{id:string, name:string}>| +|defaultActionGroupId|ID value for the default action group for the rule type.|string| +|recoveryActionGroup|The action group to use when an alert goes from an active state to an inactive one. This action group should not be specified under the `actionGroups` property. If no recoveryActionGroup is specified, the default `recovered` action group will be used. |{id:string, name:string}| +|actionVariables|An explicit list of action variables that the rule type makes available via context and state in action parameter templates, and a short human readable description for each. The Alerting UI will use this to display prompts for the users for these variables, in action parameter editors. We highly encourage using `kbn-i18n` to translate the descriptions. |{ context: Array<{name:string, description:string}, state: Array<{name:string, description:string}>| +|validate.params|When developing a rule type, you can choose to accept a series of parameters. You may also choose to have the parameters validated before they are passed to the `executor` function or created as a saved object. In order to do this, provide a `@kbn/config-schema` schema that we will use to validate the `params` attribute.|@kbn/config-schema| +|executor|This is where the code for the rule type lives. This is a function to be called when executing a rule on an interval basis. For full details, see the executor section below.|Function| +|producer|The id of the application producing this rule type.|string| +|minimumLicenseRequired|The value of a minimum license. Most of the rules are licensed as "basic".|string| ### Executor -This is the primary function for an alert type. Whenever the alert needs to execute, this function will perform the execution. It receives a variety of parameters. The following table describes the properties the executor receives. +This is the primary function for a rule type. Whenever the rule needs to execute, this function will perform the execution. It receives a variety of parameters. The following table describes the properties the executor receives. **executor(options)** |Property|Description| |---|---| |services.scopedClusterClient|This is an instance of the Elasticsearch client. Use this to do Elasticsearch queries in the context of the user who created the alert when security is enabled.| -|services.savedObjectsClient|This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.

The scope of the saved objects client is tied to the user who created the alert (only when security isenabled).| -|services.alertInstanceFactory(id)|This [alert instance factory](#alert-instance-factory) creates instances of alerts and must be used in order to execute actions. The id you give to the alert instance factory is a unique identifier to the alert instance.| +|services.savedObjectsClient|This is an instance of the saved objects client. This provides the ability to perform CRUD operations on any saved object that lives in the same space as the rule.

The scope of the saved objects client is tied to the user who created the rule (only when security is enabled).| +|services.alertInstanceFactory(id)|This [alert factory](#alert-factory) creates alerts and must be used in order to execute actions. The id you give to the alert factory is a unique identifier for the alert.| |services.log(tags, [data], [timestamp])|Use this to create server logs. (This is the same function as server.log)| -|startedAt|The date and time the alert type started execution.| -|previousStartedAt|The previous date and time the alert type started a successful execution.| -|params|Parameters for the execution. This is where the parameters you require will be passed in. (example threshold). Use alert type validation to ensure values are set before execution.| -|state|State returned from previous execution. This is the alert level state. What the executor returns will be serialized and provided here at the next execution.| -|alertId|The id of this alert.| -|spaceId|The id of the space of this alert.| -|namespace|The namespace of the space of this alert; same as spaceId, unless spaceId === 'default', then namespace = undefined.| -|name|The name of this alert.| -|tags|The tags associated with this alert.| -|createdBy|The userid that created this alert.| -|updatedBy|The userid that last updated this alert.| +|startedAt|The date and time the rule type started execution.| +|previousStartedAt|The previous date and time the rule type started a successful execution.| +|params|Parameters for the execution. This is where the parameters you require will be passed in. (e.g. threshold). Use rule type validation to ensure values are set before execution.| +|state|State returned from the previous execution. This is the rule level state. What the executor returns will be serialized and provided here at the next execution.| +|alertId|The id of this rule.| +|spaceId|The id of the space of this rule.| +|namespace|The namespace of the space of this rule. This is the same as `spaceId`, unless `spaceId === "default"`, in which case the namespace = `undefined`.| +|name|The name of this rule. This will eventually be removed in favor of `rule.name`.| +|tags|The tags associated with this rule. This will eventually be removed in favor of `rule.tags`.| +|createdBy|The user ID of the user that created this rule. This will eventually be removed in favor of `rule.createdBy`.| +|updatedBy|The user ID of the user that last updated this rule. This will eventually be removed in favor of `rule.updatedBy`.| +|rule.name|The name of this rule.| +|rule.tags|The tags associated with this rule.| +|rule.consumer|The consumer of this rule type.| +|rule.producer|The producer of this rule type.| +|rule.ruleTypeId|The ID of the rule type for this rule.| +|rule.ruleTypeName|The user-friendly name of the rule type for this rule.| +|rule.enabled|Whether this rule is currently enabled.| +|rule.schedule|The configured schedule interval of this rule.| +|rule.actions|The configured actions for this rule.| +|rule.createdBy|The user ID of the user that created this rule.| +|rule.updatedBy|The user ID of the user that last updated this rule.| +|rule.createdAt|The date and time this rule was created.| +|rule.updatedAt|The date and this this rule was last updated.| +|rule.throttle|The configured throttle interval for this rule.| +|rule.notifyWhen|The configured notification type for this rule.| ### Action Variables -The `actionVariables` property should contain the **flattened** names of the state and context variables available when an executor calls `alertInstance.scheduleActions(actionGroup, context)`. These names are meant to be used in prompters in the alerting user interface, are used as text values for display, and can be inserted into to an action parameter text entry field via UI gesture (eg, clicking a menu item from a menu built with these names). They should be flattened, so if a state or context variable is an object with properties, these should be listed with the "parent" property/properties in the name, separated by a `.` (period). +The `actionVariables` property should contain the **flattened** names of the state and context variables available when an executor calls `alertInstance.scheduleActions(actionGroup, context)`. These names are meant to be used in prompters in the Alerting UI, are used as text values for display, and can be inserted into to an action parameter text entry field via a UI gesture (e.g., clicking a menu item from a menu built with these names). They should be flattened, so if a state or context variable is an object with properties, these should be listed with the "parent" property/properties in the name, separated by a `.` (period). For example, if the `context` has one variable `foo` which is an object that has one property `bar`, and there are no `state` variables, the `actionVariables` value would be in the following shape: @@ -148,65 +174,67 @@ For example, if the `context` has one variable `foo` which is an object that has ## Licensing -Currently most of the alerts are free features. But some alert types are subscription features, such as the tracking containment alert. +Currently most rule types are free features. But some rule types are subscription features, such as the tracking containment rule. ## Documentation -You should create asciidoc for the new alert type. -* For stack alerts, add an entry to the alert type index - [`docs/user/alerting/alert-types.asciidoc`](../../../docs/user/alerting/alert-types.asciidoc) which points to a new document for the alert type that should be in the directory [`docs/user/alerting/stack-alerts`](../../../docs/user/alerting/stack-alerts). +You should create asciidoc for each new rule type you develop: -* Solution specific alert documentation should live within the docs for the solution. +- For stack rules, add an entry to the rule type index - [`docs/user/alerting/stack-rules.asciidoc`](../../../docs/user/alerting/stack-rules.asciidoc) which points to a new document for the rule type that should live in the directory [`docs/user/alerting/stack-rules`](../../../docs/user/alerting/stack-rules). -We suggest following the template provided in `docs/alert-type-template.asciidoc`. The [Index Threshold alert type](https://www.elastic.co/guide/en/kibana/master/alert-type-index-threshold.html) is an example of documentation created following the template. +- Solution specific rule documentation should live within the docs for the solution. + +We suggest following the template provided in `docs/rule-type-template.asciidoc`. The [Index Threshold rule type](https://www.elastic.co/guide/en/kibana/master/rule-type-index-threshold.html) is an example of documentation created following the template. ## Tests -The alert type should have jest tests and optionaly functional tests. -In the the tests we recomend to test the expected alert execution result with a different input params, the structure of the created alert and the params validation. The rest will be guaranteed as a framework functionality. +The rule type should have jest tests and, optionally, functional tests. +In the tests, we recommend testing the expected rule execution result with different input params, testing the structure of the created rule and testing the parameter validation. The rest will be guaranteed as a framework functionality. ### Example -This example receives server and threshold as parameters. It will read the CPU usage of the server and schedule actions to be executed (asynchronously by the task manager) if the reading is greater than the threshold. +This example rule type receives server and threshold as parameters. It will read the CPU usage of the server and schedule actions to be executed (asynchronously by the task manager) if the usage is greater than the threshold. ```typescript import { schema } from '@kbn/config-schema'; import { AlertType, AlertExecutorOptions } from '../../../alerting/server'; +// These type names will eventually be updated to reflect the new terminology import { - AlertTypeParams, - AlertTypeState, - AlertInstanceState, - AlertInstanceContext, + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, } from '../../../alerting/common'; ... -interface MyAlertTypeParams extends AlertTypeParams { +interface MyRuleTypeParams extends AlertTypeParams { server: string; threshold: number; } -interface MyAlertTypeState extends AlertTypeState { +interface MyRuleTypeState extends AlertTypeState { lastChecked: Date; } -interface MyAlertTypeInstanceState extends AlertInstanceState { +interface MyRuleTypeAlertState extends AlertInstanceState { cpuUsage: number; } -interface MyAlertTypeInstanceContext extends AlertInstanceContext { +interface MyRuleTypeAlertContext extends AlertInstanceContext { server: string; hasCpuUsageIncreased: boolean; } -type MyAlertTypeActionGroups = 'default' | 'warning'; +type MyRuleTypeActionGroups = 'default' | 'warning'; -const myAlertType: AlertType< - MyAlertTypeParams, - MyAlertTypeState, - MyAlertTypeInstanceState, - MyAlertTypeInstanceContext, - MyAlertTypeActionGroups +const myRuleType: AlertType< + MyRuleTypeParams, + MyRuleTypeState, + MyRuleTypeAlertState, + MyRuleTypeAlertContext, + MyRuleTypeActionGroups > = { - id: 'my-alert-type', - name: 'My alert type', + id: 'my-rule-type', + name: 'My rule type', validate: { params: schema.object({ server: schema.string(), @@ -235,13 +263,20 @@ const myAlertType: AlertType< }, minimumLicenseRequired: 'basic', async executor({ - alertId, + alertId, startedAt, previousStartedAt, services, params, state, - }: AlertExecutorOptions) { + rule, + }: AlertExecutorOptions< + MyRuleTypeParams, + MyRuleTypeState, + MyRuleTypeAlertState, + MyRuleTypeAlertContext, + MyRuleTypeActionGroups + >) { // Let's assume params is { server: 'server_1', threshold: 0.8 } const { server, threshold } = params; @@ -250,47 +285,50 @@ const myAlertType: AlertType< // Only execute if CPU usage is greater than threshold if (currentCpuUsage > threshold) { - // The first argument is a unique identifier the alert instance is about. In this scenario - // the provided server will be used. Also, this id will be used to make `getState()` return - // previous state, if any, on matching identifiers. - const alertInstance = services.alertInstanceFactory(server); + // The first argument is a unique identifier for the alert. In this + // scenario the provided server will be used. Also, this ID will be + // used to make `getState()` return previous state, if any, on + // matching identifiers. + const alert = services.alertInstanceFactory(server); - // State from last execution. This will exist if an alert instance was created and executed - // in the previous execution - const { cpuUsage: previousCpuUsage } = alertInstance.getState(); + // State from the last execution. This will exist if an alert was + // created and executed in the previous execution + const { cpuUsage: previousCpuUsage } = alert.getState(); // Replace state entirely with new values - alertInstance.replaceState({ + alert.replaceState({ cpuUsage: currentCpuUsage, }); - // 'default' refers to the id of a group of actions to be scheduled for execution, see 'actions' in create alert section - alertInstance.scheduleActions('default', { + // 'default' refers to the id of a group of actions to be scheduled + // for execution, see 'actions' in create rule section + alert.scheduleActions('default', { server, hasCpuUsageIncreased: currentCpuUsage > previousCpuUsage, }); } - // Returning updated alert type level state, this will become available + // Returning updated rule type level state, this will become available // within the `state` function parameter at the next execution return { - // This is an example attribute you could set, it makes more sense to use this state when - // the alert type executes multiple instances but wants a single place to track certain values. + // This is an example attribute you could set, it makes more sense + // to use this state when the rule type executes multiple + // alerts but wants a single place to track certain values. lastChecked: new Date(), }; }, producer: 'alerting', }; -server.newPlatform.setup.plugins.alerting.registerType(myAlertType); +server.newPlatform.setup.plugins.alerting.registerType(myRuleType); ``` ## Role Based Access-Control -Once you have registered your AlertType, you need to grant your users privileges to use it. -When registering a feature in Kibana you can specify multiple types of privileges which are granted to users when they're assigned certain roles. +Once you have registered your AlertType, you need to grant your users privileges to use it. +When registering a feature in Kibana, you can specify multiple types of privileges which are granted to users when they're assigned certain roles. Assuming your feature introduces its own AlertTypes, you'll want to control which roles have all/read privileges for these AlertTypes when they're inside the feature. -In addition, when users are inside your feature you might want to grant them access to AlertTypes from other features, such as built-in AlertTypes or AlertTypes provided by other features. +In addition, when users are inside your feature, you might want to grant them access to AlertTypes from other features, such as built-in stack rules or rule types provided by other features. You can control all of these abilities by assigning privileges to the Alerting Framework from within your own feature, for example: @@ -304,11 +342,11 @@ features.registerKibanaFeature({ alerting: { all: [ // grant `all` over our own types - 'my-application-id.my-alert-type', - 'my-application-id.my-restricted-alert-type', + 'my-application-id.my-rule-type', + 'my-application-id.my-restricted-rule-type', // grant `all` over the built-in IndexThreshold '.index-threshold', - // grant `all` over Uptime's TLS AlertType + // grant `all` over Uptime's TLS rule type 'xpack.uptime.alerts.actionGroups.tls' ], }, @@ -317,10 +355,10 @@ features.registerKibanaFeature({ alerting: { read: [ // grant `read` over our own type - 'my-application-id.my-alert-type', + 'my-application-id.my-rule-type', // grant `read` over the built-in IndexThreshold '.index-threshold', - // grant `read` over Uptime's TLS AlertType + // grant `read` over Uptime's TLS rule type 'xpack.uptime.alerts.actionGroups.tls' ], }, @@ -330,11 +368,12 @@ features.registerKibanaFeature({ ``` In this example we can see the following: -- Our feature grants any user who's assigned the `all` role in our feature the `all` role in the Alerting framework over every alert of the `my-application-id.my-alert-type` type which is created _inside_ the feature. What that means is that this privilege will allow the user to execute any of the `all` operations (listed below) on these alerts as long as their `consumer` is `my-application-id`. Below that you'll notice we've done the same with the `read` role, which is grants the Alerting Framework's `read` role privileges over these very same alerts. -- In addition, our feature grants the same privileges over any alert of type `my-application-id.my-restricted-alert-type`, which is another hypothetical alertType registered by this feature. It's worth noting though that this type has been omitted from the `read` role. What this means is that only users with the `all` role will be able to interact with alerts of this type. -- Next, lets look at the `.index-threshold` and `xpack.uptime.alerts.actionGroups.tls` types. These have been specified in both `read` and `all`, which means that all the users in the feature will gain privileges over alerts of these types (as long as their `consumer` is `my-application-id`). The difference between these two and the previous two is that they are _produced_ by other features! `.index-threshold` is a built-in type, provided by the _Built-In Alerts_ feature, and `xpack.uptime.alerts.actionGroups.tls` is an AlertType provided by the _Uptime_ feature. Specifying these type here tells the Alerting Framework that as far as the `my-application-id` feature is concerned, the user is privileged to use them (with `all` and `read` applied), but that isn't enough. Using another feature's AlertType is only possible if both the producer of the AlertType, and the consumer of the AlertType, explicitly grant privileges to do so. In this case, the _Built-In Alerts_ & _Uptime_ features would have to explicitly add these privileges to a role and this role would have to be granted to this user. -It's important to note that any role can be granted a mix of `all` and `read` privileges accross multiple type, for example: +- Our feature grants any user who's assigned the `all` role in our feature the `all` role in the Alerting framework over every rule of the `my-application-id.my-rule-type` type which is created _inside_ the feature. What that means is that this privilege will allow the user to execute any of the `all` operations (listed below) on these rules as long as their `consumer` is `my-application-id`. Below that you'll notice we've done the same with the `read` role, which grants the Alerting Framework's `read` role privileges over these very same rules. +- In addition, our feature grants the same privileges over any rule of type `my-application-id.my-restricted-rule-type`, which is another hypothetical rule type registered by this feature. It's worth noting that this type has been omitted from the `read` role. What this means is that only users with the `all` role will be able to interact with rules of this type. +- Next, lets look at the `.index-threshold` and `xpack.uptime.alerts.actionGroups.tls` types. These have been specified in both `read` and `all`, which means that all the users in the feature will gain privileges over rules of these types (as long as their `consumer` is `my-application-id`). The difference between these two and the previous two is that they are _produced_ by other features! `.index-threshold` is a built-in stack rule type, provided by the _Stack Rules_ feature, and `xpack.uptime.alerts.actionGroups.tls` is a rule type provided by the _Uptime_ feature. Specifying these types here tells the Alerting Framework that as far as the `my-application-id` feature is concerned, the user is privileged to use them (with `all` and `read` applied), but that isn't enough. Using another feature's rule type is only possible if both the producer of the rule type and the consumer of the rule type explicitly grant privileges to do so. In this case, the _Stack Rules_ & _Uptime_ features would have to explicitly add these privileges to a role and this role would have to be granted to this user. + +It's important to note that any role can be granted a mix of `all` and `read` privileges accross multiple types, for example: ```typescript features.registerKibanaFeature({ @@ -355,10 +394,10 @@ features.registerKibanaFeature({ app: ['lens', 'kibana'], alerting: { all: [ - 'my-application-id.my-alert-type' + 'my-application-id.my-rule-type' ], read: [ - 'my-application-id.my-restricted-alert-type' + 'my-application-id.my-restricted-rule-type' ], }, savedObject: { @@ -372,16 +411,19 @@ features.registerKibanaFeature({ }); ``` -In the above example, you note that instead of denying users with the `read` role any access to the `my-application-id.my-restricted-alert-type` type, we've decided that these users _should_ be granted `read` privileges over the _resitricted_ AlertType. -As part of that same change, we also decided that not only should they be allowed to `read` the _restricted_ AlertType, but actually, despite having `read` privileges to the feature as a whole, we do actually want to allow them to create our basic 'my-application-id.my-alert-type' AlertType, as we consider it an extension of _reading_ data in our feature, rather than _writing_ it. +In the above example, note that instead of denying users with the `read` role any access to the `my-application-id.my-restricted-rule-type` type, we've decided that these users _should_ be granted `read` privileges over the _restricted_ rule type. +As part of that same change, we also decided that not only should they be allowed to `read` the _restricted_ rule type, but actually, despite having `read` privileges to the feature as a whole, we do actually want to allow them to create our basic 'my-application-id.my-rule-type' rule type, as we consider it an extension of _reading_ data in our feature, rather than _writing_ it. ### `read` privileges vs. `all` privileges When a user is granted the `read` role in the Alerting Framework, they will be able to execute the following api calls: + - `get` -- `getAlertState` +- `getRuleState` +- `getAlertSummary` - `find` When a user is granted the `all` role in the Alerting Framework, they will be able to execute all of the `read` privileged api calls, but in addition they'll be granted the following calls: + - `create` - `delete` - `update` @@ -390,24 +432,26 @@ When a user is granted the `all` role in the Alerting Framework, they will be ab - `updateApiKey` - `muteAll` - `unmuteAll` -- `muteInstance` -- `unmuteInstance` +- `muteAlert` +- `unmuteAlert` Finally, all users, whether they're granted any role or not, are privileged to call the following: -- `listAlertTypes`, but the output is limited to displaying the AlertTypes the user is perivileged to `get` + +- `listAlertTypes`, but the output is limited to displaying the rule types the user is privileged to `get`. Attempting to execute any operation the user isn't privileged to execute will result in an Authorization error thrown by the AlertsClient. ## Alert Navigation -When registering an Alert Type, you'll likely want to provide a way of viewing alerts of that type within your own plugin, or perhaps you want to provide a view for all alerts created from within your solution within your own UI. -In order for the Alerting framework to know that your plugin has its own internal view for displaying an alert, you must resigter a navigation handler within the framework. +When registering a rule type, you'll likely want to provide a way of viewing rules of that type within your own plugin, or perhaps you want to provide a view for all rules created from within your solution within your own UI. + +In order for the Alerting Framework to know that your plugin has its own internal view for displaying a rule, you must register a navigation handler within the framework. -A navigation handler is nothing more than a function that receives an Alert and its corresponding AlertType, and is expected to then return the path *within your plugin* which knows how to display this alert. +A navigation handler is nothing more than a function that receives a rule and its corresponding AlertType, and is expected to then return the path *within your plugin* which knows how to display this rule. The signature of such a handler is: -``` +```typescript type AlertNavigationHandler = ( alert: SanitizedAlert, alertType: AlertType @@ -420,43 +464,43 @@ By specifying _alerting_ as a dependency of your *public* (client side) plugin, ### registerNavigation The _registerNavigation_ api allows you to register a handler for a specific alert type within your solution: -``` +```typescript alerting.registerNavigation( 'my-application-id', - 'my-application-id.my-alert-type', - (alert: SanitizedAlert, alertType: AlertType) => `/my-unique-alert/${alert.id}` + 'my-application-id.my-rule-type', + (alert: SanitizedAlert, alertType: AlertType) => `/my-unique-rule/${rule.id}` ); ``` -This tells the Alerting framework that, given an alert of the AlertType whose ID is `my-application-id.my-unique-alert-type`, if that Alert's `consumer` value (which is set when the alert is created by your plugin) is your application (whose id is `my-application-id`), then it will navigate to your application using the path `/my-unique-alert/${the id of the alert}`. +This tells the Alerting Framework that, given a rule of the AlertType whose ID is `my-application-id.my-unique-rule-type`, if that rule's `consumer` value (which is set when the rule is created by your plugin) is your application (whose id is `my-application-id`), then it will navigate to your application using the path `/my-unique-rule/${the id of the rule}`. -The navigation is handled using the `navigateToApp` api, meaning that the path will be automatically picked up by your `react-router-dom` **Route** component, so all you have top do is configure a Route that handles the path `/my-unique-alert/:id`. +The navigation is handled using the `navigateToApp` API, meaning that the path will be automatically picked up by your `react-router-dom` **Route** component, so all you have top do is configure a Route that handles the path `/my-unique-rule/:id`. You can look at the `alerting-example` plugin to see an example of using this API, which is enabled using the `--run-examples` flag when you run `yarn start`. ### registerDefaultNavigation -The _registerDefaultNavigation_ api allows you to register a handler for any alert type within your solution: +The _registerDefaultNavigation_ API allows you to register a handler for any rule type within your solution: ``` alerting.registerDefaultNavigation( 'my-application-id', - (alert: SanitizedAlert, alertType: AlertType) => `/my-other-alerts/${alert.id}` + (alert: SanitizedAlert, alertType: AlertType) => `/my-other-rules/${rule.id}` ); ``` -This tells the Alerting framework that, given any alert whose `consumer` value is your application, as long as then it will navigate to your application using the path `/my-other-alerts/${the id of the alert}`. +This tells the Alerting Framework that any rule whose `consumer` value is your application can be navigated to in your application using the path `/my-other-rules/${the id of the rule}`. -### balancing both APIs side by side -As we mentioned, using `registerDefaultNavigation` will tell the Alerting Framework that your application can handle any type of Alert we throw at it, as long as your application created it, using the handler you provide it. +### Balancing both APIs side by side +As we mentioned, using `registerDefaultNavigation` will tell the Alerting Framework that your application can handle any type of rule we throw at it, as long as your application created it, using the handler you provided. -The only case in which this handler will not be used to evaluate the navigation for an alert (assuming your application is the `consumer`) is if you have also used `registerNavigation` api, along side your `registerDefaultNavigation` usage, to handle that alert's specific AlertType. +The only case in which this handler will not be used to evaluate the navigation for a rule (assuming your application is the `consumer`) is if you have also used the `registerNavigation` API, alongside your `registerDefaultNavigation` usage, to handle that rule's specific AlertType. -You can use the `registerNavigation` api to specify as many AlertType specific handlers as you like, but you can only use it once per AlertType as we wouldn't know which handler to use if you specified two for the same AlertType. For the same reason, you can only use `registerDefaultNavigation` once per plugin, as it covers all cases for your specific plugin. +You can use the `registerNavigation` API to specify as many AlertType specific handlers as you like, but you can only use it once per AlertType as we wouldn't know which handler to use if you specified two for the same AlertType. For the same reason, you can only use `registerDefaultNavigation` once per plugin, as it covers all cases for your specific plugin. ## Internal HTTP APIs -Using of the rule type requires you to create a rule that will contain parameters and actions for a given rule type. API description for CRUD operations is a part of the [user documentation](https://www.elastic.co/guide/en/kibana/master/alerting-apis.html). -API listed below are internal and should not be consumed by plugin outside the alerting plugins. +We provide public APIs for performing CRUD operations on rules. Descriptions for these APIs are available in the [user documentation](https://www.elastic.co/guide/en/kibana/master/alerting-apis.html). +In addition to the public APIs, we provide the following internal APIs. Internal APIs should not be consumed by plugins outside of the alerting plugins. ### `GET /internal/alerting/rule/{id}/state`: Get rule state @@ -489,56 +533,56 @@ Query: |---|---|---| |id|The id of the rule you're trying to update the API key for. System will use user in request context to generate an API key for.|string| -## Alert instance factory +## Alert Factory **alertInstanceFactory(id)** -One service passed in to alert types is an alert instance factory. This factory creates instances of alerts and must be used in order to execute actions. The `id` you give to the alert instance factory is a unique identifier to the alert instance (ex: server identifier if the instance is about the server). The instance factory will use this identifier to retrieve the state of previous instances with the same `id`. These instances support state persisting between alert type execution, but will clear out once the alert instance stops executing. +One service passed in to each rule type is the alert factory. This factory creates alerts and must be used in order to execute actions. The `id` you give to the alert factory is the unique identifier for the alert (e.g. the server identifier if the alert is about servers). The alert factory will use this identifier to retrieve the state of previous alerts with the same `id`. These alerts support persisting state between rule executions, but will clear out once the alert stops firing. -Note that the `id` only needs to be unique **within the scope of a specific alert**, not unique across all alerts or alert types. For example, Alert 1 and Alert 2 can both create an alert instance with an `id` of `"a"` without conflicting with one another. But if Alert 1 creates 2 alert instances, then they must be differentiated with `id`s of `"a"` and `"b"`. +Note that the `id` only needs to be unique **within the scope of a specific rule**, not unique across all rules or rule types. For example, Rule 1 and Rule 2 can both create an alert with an `id` of `"a"` without conflicting with one another. But if Rule 1 creates 2 alerts, then they must be differentiated with `id`s of `"a"` and `"b"`. -This factory returns an instance of `AlertInstance`. The alert instance class has the following methods, note that we have removed the methods that you shouldn't touch. +This factory returns an instance of `AlertInstance`. The `AlertInstance` class has the following methods. Note that we have removed the methods that you shouldn't touch. |Method|Description| |---|---| -|getState()|Get the current state of the alert instance.| -|scheduleActions(actionGroup, context)|Called to schedule the execution of actions. The actionGroup is a string `id` that relates to the group of alert `actions` to execute and the context will be used for templating purposes. `scheduleActions` or `scheduleActionsWithSubGroup` should only be called once per alert instance.| -|scheduleActionsWithSubGroup(actionGroup, subgroup, context)|Called to schedule the execution of actions within a subgroup. The actionGroup is a string `id` that relates to the group of alert `actions` to execute, the `subgroup` is a dynamic string that denotes a subgroup within the actionGroup and the context will be used for templating purposes. `scheduleActions` or `scheduleActionsWithSubGroup` should only be called once per alert instance.| -|replaceState(state)|Used to replace the current state of the alert instance. This doesn't work like react, the entire state must be provided. Use this feature as you see fit. The state that is set will persist between alert type executions whenever you re-create an alert instance with the same id. The instance state will be erased when `scheduleActions` or `scheduleActionsWithSubGroup` aren't called during an execution.| +|getState()|Get the current state of the alert.| +|scheduleActions(actionGroup, context)|Call this to schedule the execution of actions. The actionGroup is a string `id` that relates to the group of alert `actions` to execute and the context will be used for templating purposes. `scheduleActions` or `scheduleActionsWithSubGroup` should only be called once per alert.| +|scheduleActionsWithSubGroup(actionGroup, subgroup, context)|Call this to schedule the execution of actions within a subgroup. The actionGroup is a string `id` that relates to the group of alert `actions` to execute, the `subgroup` is a dynamic string that denotes a subgroup within the actionGroup and the context will be used for templating purposes. `scheduleActions` or `scheduleActionsWithSubGroup` should only be called once per alert.| +|replaceState(state)|Used to replace the current state of the alert. This doesn't work like React, the entire state must be provided. Use this feature as you see fit. The state that is set will persist between rule executions whenever you re-create an alert with the same id. The alert state will be erased when `scheduleActions` or `scheduleActionsWithSubGroup` aren't called during an execution.| -### when should I use `scheduleActions` and `scheduleActionsWithSubGroup`? +### When should I use `scheduleActions` and `scheduleActionsWithSubGroup`? The `scheduleActions` or `scheduleActionsWithSubGroup` methods are both used to achieve the same thing: schedule actions to be run under a specific action group. -It's important to note though, that when an actions are scheduled for an instance, we check whether the instance was already active in this action group after the previous execution. If it was, then we might throttle the actions (adhering to the user's configuration), as we don't consider this a change in the instance. +It's important to note that when actions are scheduled for an alert, we check whether the alert was already active in this action group after the previous execution. If it was, then we might throttle the actions (adhering to the user's configuration), as we don't consider this a change in the alert. -What happens though, if the instance _has_ changed, but they just happen to be in the same action group after this change? This is where subgroups come in. By specifying a subgroup (using the `scheduleActionsWithSubGroup` method), the instance becomes active within the action group, but it will also keep track of the subgroup. -If the subgroup changes, then the framework will treat the instance as if it had been placed in a new action group. It is important to note though, we only use the subgroup to denote a change if both the current execution and the previous one specified a subgroup. +What happens though, if the alert _has_ changed, but they just happen to be in the same action group after this change? This is where subgroups come in. By specifying a subgroup (using the `scheduleActionsWithSubGroup` method), the alert becomes active within the action group, but it will also keep track of the subgroup. +If the subgroup changes, then the framework will treat the alert as if it had been placed in a new action group. It is important to note that we only use the subgroup to denote a change if both the current execution and the previous one specified a subgroup. You might wonder, why bother using a subgroup if you can just add a new action group? -Action Groups are static, and have to be define when the Alert Type is defined. +Action Groups are static, and have to be define when the rule type is defined. Action Subgroups are dynamic, and can be defined on the fly. This approach enables users to specify actions under specific action groups, but they can't specify actions that are specific to subgroups. As subgroups fall under action groups, we will schedule the actions specified for the action group, but the subgroup allows the AlertType implementer to reuse the same action group for multiple different active subgroups. -## Templating actions +## Templating Actions -There needs to be a way to map alert context into action parameters. For this, we started off by adding template support. Any string within the `params` of an alert saved object's `actions` will be processed as a template and can inject context or state values. +There needs to be a way to map rule context into action parameters. For this, we started off by adding template support. Any string within the `params` of a rule saved object's `actions` will be processed as a template and can inject context or state values. -When an alert instance executes, the first argument is the `group` of actions to execute and the second is the context the alert exposes to templates. We iterate through each action params attributes recursively and render templates if they are a string. Templates have access to the following "variables": +When an alert executes, the first argument is the `group` of actions to execute and the second is the context the rule exposes to templates. We iterate through each action parameter attributes recursively and render templates if they are a string. Templates have access to the following "variables": -- `context` - provided by context argument of `.scheduleActions(...)` and `.scheduleActionsWithSubGroup(...)` on an alert instance -- `state` - the alert instance's `state` provided by the most recent `replaceState` call on an alert instance -- `alertId` - the id of the alert -- `alertInstanceId` - the alert instance id -- `alertName` - the name of the alert -- `spaceId` - the id of the space the alert exists in -- `tags` - the tags set in the alert +- `context` - provided by context argument of `.scheduleActions(...)` and `.scheduleActionsWithSubGroup(...)` on an alert. +- `state` - the alert's `state` provided by the most recent `replaceState` call on an alert. +- `alertId` - the id of the rule +- `alertInstanceId` - the alert id +- `alertName` - the name of the rule +- `spaceId` - the id of the space the rule exists in +- `tags` - the tags set in the rule -The templating engine is [mustache]. General definition for the [mustache variable] is a double-brace {{}}. All variables are HTML-escaped by default and if there is a requirement to render unescaped HTML, it should be applied the triple mustache: `{{{name}}}`. Also, can be used `&` to unescape a variable. +The templating engine is [mustache]. General definition for the [mustache variable] is a double-brace {{}}. All variables are HTML-escaped by default and if there is a requirement to render unescaped HTML, it should be applied with the triple mustache: `{{{name}}}`. Also, `&` can be used to unescape a variable. ### Examples -The following code would be within an alert type. As you can see `cpuUsage ` will replace the state of the alert instance and `server` is the context for the alert instance to execute. The difference between the two is `cpuUsage ` will be accessible at the next execution. +The following code would be within a rule type. As you can see `cpuUsage` will replace the state of the alert and `server` is the context for the alert to execute. The difference between the two is that `cpuUsage` will be accessible at the next execution. ``` alertInstanceFactory('server_1') @@ -550,13 +594,13 @@ alertInstanceFactory('server_1') }); ``` -Below is an example of an alert that takes advantage of templating: +Below is an example of a rule that takes advantage of templating: ``` { ... "id": "123", - "name": "cpu alert", + "name": "cpu rule", "actions": [ { "group": "default", @@ -565,21 +609,21 @@ Below is an example of an alert that takes advantage of templating: "from": "example@elastic.co", "to": ["destination@elastic.co"], "subject": "A notification about {{context.server}}", - "body": "The server {{context.server}} has a CPU usage of {{state.cpuUsage}}%. This message for {{alertInstanceId}} was created by the alert {{alertId}} {{alertName}}." + "body": "The server {{context.server}} has a CPU usage of {{state.cpuUsage}}%. This message for {{alertInstanceId}} was created by the rule {{alertId}} {{alertName}}." } } ] } ``` -The templating system will take the alert and alert type as described above and convert the action parameters to the following: +The templating system will take the rule and rule type as described above and convert the action parameters to the following: ``` { "from": "example@elastic.co", "to": ["destination@elastic.co"], "subject": "A notification about server_1" - "body": "The server server_1 has a CPU usage of 80%. This message for server_1 was created by the alert 123 cpu alert" + "body": "The server server_1 has a CPU usage of 80%. This message for server_1 was created by the rule 123 cpu rule" } ``` From cc9c5be022b063bee777c531870336cf13dc6e73 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Mon, 24 May 2021 15:02:23 -0700 Subject: [PATCH 03/78] [Fleet] Pass policy namespace to agent monitoring settings (#100500) * Pass agent policy namespace to agent monitoring settings * Adjust copy --- x-pack/plugins/fleet/common/types/models/agent_policy.ts | 1 + .../sections/agent_policy/components/agent_policy_form.tsx | 2 +- x-pack/plugins/fleet/server/services/agent_policy.test.ts | 4 ++++ x-pack/plugins/fleet/server/services/agent_policy.ts | 1 + 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/common/types/models/agent_policy.ts b/x-pack/plugins/fleet/common/types/models/agent_policy.ts index a8e1f6ce584d4a..753100f6225567 100644 --- a/x-pack/plugins/fleet/common/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/agent_policy.ts @@ -91,6 +91,7 @@ export interface FullAgentPolicy { revision?: number; agent?: { monitoring: { + namespace?: string; use_output?: string; enabled: boolean; metrics: boolean; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx index 7bfb2961a6d65e..a44edb5ce9a422 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx @@ -223,7 +223,7 @@ export const AgentPolicyForm: React.FunctionComponent = ({ description={ } > diff --git a/x-pack/plugins/fleet/server/services/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policy.test.ts index 68bd9e721d714b..a020b95ca3302b 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts @@ -185,6 +185,7 @@ describe('agent policy', () => { it('should return a policy with monitoring if monitoring is enabled for logs', async () => { const soClient = getSavedObjectMock({ + namespace: 'default', revision: 1, monitoring_enabled: ['logs'], }); @@ -207,6 +208,7 @@ describe('agent policy', () => { }, agent: { monitoring: { + namespace: 'default', use_output: 'default', enabled: true, logs: true, @@ -218,6 +220,7 @@ describe('agent policy', () => { it('should return a policy with monitoring if monitoring is enabled for metrics', async () => { const soClient = getSavedObjectMock({ + namespace: 'default', revision: 1, monitoring_enabled: ['metrics'], }); @@ -240,6 +243,7 @@ describe('agent policy', () => { }, agent: { monitoring: { + namespace: 'default', use_output: 'default', enabled: true, logs: false, diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index b575c1de1616d2..62b4578ab87b22 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -730,6 +730,7 @@ class AgentPolicyService { ? { agent: { monitoring: { + namespace: agentPolicy.namespace, use_output: defaultOutput.name, enabled: true, logs: agentPolicy.monitoring_enabled.includes(dataTypes.Logs), From f95bbb38c9de338009a4ef25871b0ef56a754604 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Mon, 24 May 2021 17:29:27 -0500 Subject: [PATCH 04/78] [Fleet] Fix incomplete agent count message on policy tab (#100497) --- .../fleet/components/linked_agent_count.tsx | 22 +++++++++++++++---- .../agent_policy/details_page/index.tsx | 1 + 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/linked_agent_count.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/linked_agent_count.tsx index e42917700284ee..dddb1f6531afd0 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/linked_agent_count.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/linked_agent_count.tsx @@ -6,6 +6,7 @@ */ import React, { memo } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; import type { EuiLinkAnchorProps } from '@elastic/eui'; import { EuiLink } from '@elastic/eui'; @@ -16,9 +17,22 @@ import { AGENT_SAVED_OBJECT_TYPE } from '../constants'; * Displays the provided `count` number as a link to the Agents list if it is greater than zero */ export const LinkedAgentCount = memo< - Omit & { count: number; agentPolicyId: string } ->(({ count, agentPolicyId, ...otherEuiLinkProps }) => { + Omit & { + count: number; + agentPolicyId: string; + showAgentText?: boolean; + } +>(({ count, agentPolicyId, showAgentText, ...otherEuiLinkProps }) => { const { getHref } = useLink(); + const displayValue = showAgentText ? ( + + ) : ( + count + ); return count > 0 ? ( - {count} + {displayValue} ) : ( - {count} + {displayValue} ); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx index 65cf62a279a223..e96ef832240137 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/index.tsx @@ -173,6 +173,7 @@ export const AgentPolicyDetailsPage: React.FunctionComponent = () => { ), }, From 71acd98082f73aacefa830fc0d5e8f27f6460c30 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Mon, 24 May 2021 17:30:06 -0500 Subject: [PATCH 05/78] [Fleet] Add color distinction between offline and inactive agents (#100490) --- .../fleet/sections/agents/services/agent_status.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx index 02b3a941437ecd..275d0c83da65e1 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx @@ -19,6 +19,7 @@ const colorToHexMap = { accent: visColors[2], warning: visColors[5], danger: visColors[9], + inactive: '#98A2B3', }; export const AGENT_STATUSES: SimplifiedAgentStatus[] = [ @@ -34,8 +35,9 @@ export function getColorForAgentStatus(agentStatus: SimplifiedAgentStatus): stri case 'healthy': return colorToHexMap.secondary; case 'offline': - case 'inactive': return colorToHexMap.default; + case 'inactive': + return colorToHexMap.inactive; case 'unhealthy': return colorToHexMap.warning; case 'updating': From 676d40ebbe5dc222313c5d368f5b31e6a6058ce6 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Mon, 24 May 2021 17:30:18 -0500 Subject: [PATCH 06/78] [Fleet] Add clear button to search input bars (#100476) --- .../fleet/public/applications/fleet/components/search_bar.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx index a5b937d4590d70..f064cf1e72f188 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx @@ -97,6 +97,7 @@ export const SearchBar: React.FunctionComponent = ({ onChange(newQuery.query as string, true); }} submitOnBlur + isClearable /> ); }; From aa2f5b535d0c0a7bcdaa34eb9807246d4e858f01 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Mon, 24 May 2021 18:38:14 -0600 Subject: [PATCH 07/78] [Security Solution] Utilizes constants package and deletes duplicate code (#100513) ## Summary Utilizes constants package and deletes duplicate code * Renames the `securitysolution-constants` to be `securitysolution-list-constants` to be specific * Deletes duplicated code found during cleanup * Moves more tests into the packages found along the way with the duplicated code * Moves `parseScheduleDates` from `@kbn/securitysolution-io-ts-types` to `@kbn/securitysolution-io-ts-utils` ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../monorepo-packages.asciidoc | 2 +- package.json | 2 +- packages/BUILD.bazel | 2 +- .../kbn-securitysolution-constants/README.md | 6 - .../package.json | 9 - .../src/constants/index.ts | 12 +- .../src/from/index.ts | 2 +- .../src/index.ts | 1 - .../src/common/lists/index.mock.ts | 2 +- .../src/constants/index.ts | 34 --- .../src/index.ts | 1 - .../exception_list_schema/index.mock.ts | 2 +- .../src/index.ts | 1 - .../src/index.ts | 1 + .../src/parse_schedule_dates/index.test.ts | 38 +++ .../src/parse_schedule_dates/index.ts | 0 .../kbn-securitysolution-list-api/BUILD.bazel | 1 + .../src/api/index.ts | 4 +- .../src/constants/index.ts | 39 --- .../src/list_api/index.ts | 8 +- .../BUILD.bazel | 4 +- .../README.md | 6 + .../jest.config.js | 2 +- .../package.json | 9 + .../src/index.ts | 3 - .../tsconfig.json | 2 +- .../BUILD.bazel | 2 + .../src/constants/index.ts | 34 --- .../src/index.ts | 1 - .../src/use_api/index.ts | 2 +- .../src/use_exception_list_items/index.ts | 2 +- .../src/use_exception_lists/index.ts | 2 +- .../src/utils/index.test.ts | 272 ------------------ .../src/utils/index.ts | 118 -------- .../BUILD.bazel | 1 + .../src/get_exception_list_type/index.ts | 23 ++ .../src/get_filters/index.test.ts | 117 ++++++++ .../src/get_filters/index.ts | 23 ++ .../src/get_general_filters/index.test.ts | 36 +++ .../src/get_general_filters/index.ts | 32 +++ .../src/get_ids_and_namespaces/index.test.ts | 105 +++++++ .../src/get_ids_and_namespaces/index.ts | 36 +++ .../src/get_saved_object_type/index.ts | 27 ++ .../src/get_saved_object_types/index.ts | 22 ++ .../src/get_trusted_apps_filter/index.test.ts | 39 +++ .../src/get_trusted_apps_filter/index.ts | 27 ++ .../src/index.ts | 7 + .../src/types/index.ts | 10 + x-pack/plugins/lists/common/constants.ts | 71 ----- x-pack/plugins/lists/common/index.ts | 2 - .../response/exception_list_schema.mock.ts | 12 +- x-pack/plugins/lists/common/shared_exports.ts | 19 -- x-pack/plugins/lists/common/types.ts | 27 -- .../lists/public/exceptions/utils.test.ts | 271 ----------------- .../plugins/lists/public/exceptions/utils.ts | 119 -------- .../routes/create_endpoint_list_item_route.ts | 2 +- .../routes/create_endpoint_list_route.ts | 2 +- .../create_exception_list_item_route.ts | 2 +- .../routes/create_exception_list_route.ts | 2 +- .../server/routes/create_list_index_route.ts | 2 +- .../server/routes/create_list_item_route.ts | 2 +- .../lists/server/routes/create_list_route.ts | 2 +- .../routes/delete_endpoint_list_item_route.ts | 2 +- .../delete_exception_list_item_route.ts | 2 +- .../routes/delete_exception_list_route.ts | 2 +- .../server/routes/delete_list_index_route.ts | 2 +- .../server/routes/delete_list_item_route.ts | 2 +- .../lists/server/routes/delete_list_route.ts | 4 +- .../routes/export_exception_list_route.ts | 2 +- .../server/routes/export_list_item_route.ts | 2 +- .../routes/find_endpoint_list_item_route.ts | 2 +- .../routes/find_exception_list_item_route.ts | 2 +- .../routes/find_exception_list_route.ts | 2 +- .../server/routes/find_list_item_route.ts | 2 +- .../lists/server/routes/find_list_route.ts | 2 +- .../server/routes/import_list_item_route.ts | 2 +- .../server/routes/patch_list_item_route.ts | 2 +- .../lists/server/routes/patch_list_route.ts | 2 +- .../routes/read_endpoint_list_item_route.ts | 2 +- .../routes/read_exception_list_item_route.ts | 2 +- .../routes/read_exception_list_route.ts | 2 +- .../server/routes/read_list_index_route.ts | 2 +- .../server/routes/read_list_item_route.ts | 2 +- .../lists/server/routes/read_list_route.ts | 2 +- .../server/routes/read_privileges_route.ts | 2 +- .../routes/update_endpoint_list_item_route.ts | 2 +- .../update_exception_list_item_route.ts | 2 +- .../routes/update_exception_list_route.ts | 2 +- .../server/routes/update_list_item_route.ts | 2 +- .../lists/server/routes/update_list_route.ts | 2 +- .../plugins/lists/server/routes/validate.ts | 2 +- .../server/saved_objects/exception_list.ts | 3 +- .../server/saved_objects/migrations.test.ts | 5 +- .../lists/server/saved_objects/migrations.ts | 5 +- .../create_endoint_event_filters_list.ts | 7 +- .../exception_lists/create_endpoint_list.ts | 7 +- .../create_endpoint_trusted_apps_list.ts | 7 +- .../exception_lists/create_exception_list.ts | 3 +- .../create_exception_list_item.ts | 2 +- .../exception_lists/delete_exception_list.ts | 2 +- .../delete_exception_list_item.ts | 2 +- .../delete_exception_list_items_by_list.ts | 2 +- .../exception_lists/exception_list_client.ts | 3 +- .../exception_lists/find_exception_list.ts | 4 +- .../find_exception_list_items.ts | 7 +- .../exception_lists/get_exception_list.ts | 3 +- .../get_exception_list_item.ts | 3 +- .../exception_lists/update_exception_list.ts | 3 +- .../update_exception_list_item.ts | 2 +- .../server/services/exception_lists/utils.ts | 42 +-- .../detection_engine/parse_schedule_dates.ts | 21 -- .../schemas/types/lists.mock.ts | 2 +- .../common/detection_engine/utils.test.ts | 3 +- .../common/detection_engine/utils.ts | 6 +- .../data_generators/event_filter_generator.ts | 2 +- .../security_solution/common/fp_utils.ts | 21 -- .../common/shared_imports.ts | 16 -- ...se_fetch_or_create_rule_exception_list.tsx | 2 +- .../common/hooks/eql/use_eql_preview.ts | 2 +- .../detection_engine/rules/create/helpers.ts | 2 +- .../pages/detection_engine/rules/helpers.tsx | 2 +- .../pages/event_filters/constants.ts | 2 +- .../pages/event_filters/service/index.ts | 2 +- .../pages/event_filters/test_utils/index.ts | 2 +- .../public/shared_imports.ts | 2 - .../scripts/endpoint/event_filters/index.ts | 4 +- .../endpoint/lib/artifacts/lists.test.ts | 5 +- .../server/endpoint/lib/artifacts/lists.ts | 7 +- .../endpoint/routes/trusted_apps/mapping.ts | 2 +- .../routes/trusted_apps/service.test.ts | 2 +- .../endpoint/routes/trusted_apps/service.ts | 2 +- .../manifest_manager/manifest_manager.test.ts | 5 +- .../create_migration_saved_object.ts | 2 +- .../update_migration_saved_object.ts | 2 +- .../rules_notification_alert_type.ts | 2 +- .../filters/filter_events_against_list.ts | 3 +- .../signals/signal_rule_alert_type.test.ts | 22 +- .../signals/signal_rule_alert_type.ts | 5 +- .../detection_engine/signals/utils.test.ts | 29 -- .../lib/detection_engine/signals/utils.ts | 6 +- .../apis/lists/create_exception_list_item.ts | 2 +- .../tests/create_exceptions.ts | 5 +- .../detection_engine_api_integration/utils.ts | 2 +- .../tests/create_exception_list_items.ts | 5 +- .../tests/create_exception_lists.ts | 2 +- .../tests/create_list_items.ts | 2 +- .../security_and_spaces/tests/create_lists.ts | 2 +- .../tests/delete_exception_list_items.ts | 5 +- .../tests/delete_exception_lists.ts | 2 +- .../tests/delete_list_items.ts | 2 +- .../security_and_spaces/tests/delete_lists.ts | 4 +- .../tests/export_list_items.ts | 3 +- .../tests/find_exception_list_items.ts | 5 +- .../tests/find_exception_lists.ts | 2 +- .../tests/find_list_items.ts | 2 +- .../security_and_spaces/tests/find_lists.ts | 2 +- .../tests/import_list_items.ts | 3 +- .../tests/read_exception_list_items.ts | 5 +- .../tests/read_exception_lists.ts | 2 +- .../tests/read_list_items.ts | 2 +- .../tests/read_list_privileges.ts | 2 +- .../security_and_spaces/tests/read_lists.ts | 2 +- .../tests/update_exception_list_items.ts | 5 +- .../tests/update_exception_lists.ts | 2 +- .../tests/update_list_items.ts | 2 +- .../security_and_spaces/tests/update_lists.ts | 2 +- x-pack/test/lists_api_integration/utils.ts | 2 +- yarn.lock | 8 +- 168 files changed, 751 insertions(+), 1356 deletions(-) delete mode 100644 packages/kbn-securitysolution-constants/README.md delete mode 100644 packages/kbn-securitysolution-constants/package.json delete mode 100644 packages/kbn-securitysolution-io-ts-list-types/src/constants/index.ts create mode 100644 packages/kbn-securitysolution-io-ts-utils/src/parse_schedule_dates/index.test.ts rename packages/{kbn-securitysolution-io-ts-types => kbn-securitysolution-io-ts-utils}/src/parse_schedule_dates/index.ts (100%) delete mode 100644 packages/kbn-securitysolution-list-api/src/constants/index.ts rename packages/{kbn-securitysolution-constants => kbn-securitysolution-list-constants}/BUILD.bazel (91%) create mode 100644 packages/kbn-securitysolution-list-constants/README.md rename packages/{kbn-securitysolution-constants => kbn-securitysolution-list-constants}/jest.config.js (85%) create mode 100644 packages/kbn-securitysolution-list-constants/package.json rename packages/{kbn-securitysolution-constants => kbn-securitysolution-list-constants}/src/index.ts (96%) rename packages/{kbn-securitysolution-constants => kbn-securitysolution-list-constants}/tsconfig.json (78%) delete mode 100644 packages/kbn-securitysolution-list-hooks/src/constants/index.ts delete mode 100644 packages/kbn-securitysolution-list-hooks/src/utils/index.test.ts delete mode 100644 packages/kbn-securitysolution-list-hooks/src/utils/index.ts create mode 100644 packages/kbn-securitysolution-list-utils/src/get_exception_list_type/index.ts create mode 100644 packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts create mode 100644 packages/kbn-securitysolution-list-utils/src/get_filters/index.ts create mode 100644 packages/kbn-securitysolution-list-utils/src/get_general_filters/index.test.ts create mode 100644 packages/kbn-securitysolution-list-utils/src/get_general_filters/index.ts create mode 100644 packages/kbn-securitysolution-list-utils/src/get_ids_and_namespaces/index.test.ts create mode 100644 packages/kbn-securitysolution-list-utils/src/get_ids_and_namespaces/index.ts create mode 100644 packages/kbn-securitysolution-list-utils/src/get_saved_object_type/index.ts create mode 100644 packages/kbn-securitysolution-list-utils/src/get_saved_object_types/index.ts create mode 100644 packages/kbn-securitysolution-list-utils/src/get_trusted_apps_filter/index.test.ts create mode 100644 packages/kbn-securitysolution-list-utils/src/get_trusted_apps_filter/index.ts delete mode 100644 x-pack/plugins/lists/common/constants.ts delete mode 100644 x-pack/plugins/lists/common/shared_exports.ts delete mode 100644 x-pack/plugins/lists/common/types.ts delete mode 100644 x-pack/plugins/lists/public/exceptions/utils.test.ts delete mode 100644 x-pack/plugins/lists/public/exceptions/utils.ts delete mode 100644 x-pack/plugins/security_solution/common/detection_engine/parse_schedule_dates.ts delete mode 100644 x-pack/plugins/security_solution/common/fp_utils.ts delete mode 100644 x-pack/plugins/security_solution/common/shared_imports.ts diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index 78af727f3e0dbc..8f033029cfac42 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -82,13 +82,13 @@ yarn kbn watch-bazel - @kbn/i18n - @kbn/legacy-logging - @kbn/logging -- @kbn/securitysolution-constants - @kbn/securitysolution-es-utils - kbn/securitysolution-io-ts-alerting-types - kbn/securitysolution-io-ts-list-types - kbn/securitysolution-io-ts-types - @kbn/securitysolution-io-ts-utils - @kbn/securitysolution-list-api +- @kbn/securitysolution-list-constants - @kbn/securitysolution-list-hooks - @kbn/securitysolution-list-utils - @kbn/securitysolution-utils diff --git a/package.json b/package.json index f7fe3f0fc31257..5737bce303e09c 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "@kbn/legacy-logging": "link:bazel-bin/packages/kbn-legacy-logging/npm_module", "@kbn/logging": "link:bazel-bin/packages/kbn-logging/npm_module", "@kbn/monaco": "link:packages/kbn-monaco", - "@kbn/securitysolution-constants": "link:bazel-bin/packages/kbn-securitysolution-constants/npm_module", + "@kbn/securitysolution-list-constants": "link:bazel-bin/packages/kbn-securitysolution-list-constants/npm_module", "@kbn/securitysolution-es-utils": "link:bazel-bin/packages/kbn-securitysolution-es-utils/npm_module", "@kbn/securitysolution-io-ts-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-types/npm_module", "@kbn/securitysolution-io-ts-alerting-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-alerting-types/npm_module", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index d6c73da3565a30..ec8252cb6144da 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -25,7 +25,7 @@ filegroup( "//packages/kbn-legacy-logging:build", "//packages/kbn-logging:build", "//packages/kbn-plugin-generator:build", - "//packages/kbn-securitysolution-constants:build", + "//packages/kbn-securitysolution-list-constants:build", "//packages/kbn-securitysolution-io-ts-types:build", "//packages/kbn-securitysolution-io-ts-alerting-types:build", "//packages/kbn-securitysolution-io-ts-list-types:build", diff --git a/packages/kbn-securitysolution-constants/README.md b/packages/kbn-securitysolution-constants/README.md deleted file mode 100644 index dd1ab8da6a2a8d..00000000000000 --- a/packages/kbn-securitysolution-constants/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# kbn-securitysolution-constants - -This is where shared constants for security solution should go that are going to be shared among plugins. -This was originally created to remove the dependencies between security_solution and other projects such as lists. - - diff --git a/packages/kbn-securitysolution-constants/package.json b/packages/kbn-securitysolution-constants/package.json deleted file mode 100644 index bb60f79aa03e5c..00000000000000 --- a/packages/kbn-securitysolution-constants/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "@kbn/securitysolution-constants", - "version": "1.0.0", - "description": "security solution constants to use across plugins such lists, security_solution, cases, etc...", - "license": "SSPL-1.0 OR Elastic License 2.0", - "main": "./target/index.js", - "types": "./target/index.d.ts", - "private": true -} diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/constants/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/constants/index.ts index 91a3951ef11fc7..4013a3a0497db5 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/src/constants/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/constants/index.ts @@ -7,15 +7,7 @@ */ /** - * This ID is used for _both_ the Saved Object ID and for the list_id - * for the single global space agnostic endpoint list - * TODO: Create a kbn-securitysolution-constants and add this to it. - * @deprecated Use the ENDPOINT_LIST_ID from the kbn-securitysolution-constants. - */ -export const ENDPOINT_LIST_ID = 'endpoint_list'; - -/** - * TODO: Create a kbn-securitysolution-constants and add this to it. - * @deprecated Use the DEFAULT_MAX_SIGNALS from the kbn-securitysolution-constants. + * TODO: Create a kbn-alerting-constants and add this to it. + * @deprecated Use a DEFAULT_MAX_SIGNALS from a kbn-alerting-constants package. */ export const DEFAULT_MAX_SIGNALS = 100; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/from/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/from/index.ts index 37ed4b2daa5100..30b3c727d87a28 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/src/from/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/from/index.ts @@ -8,7 +8,7 @@ import { Either } from 'fp-ts/lib/Either'; import * as t from 'io-ts'; -import { parseScheduleDates } from '@kbn/securitysolution-io-ts-types'; +import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils'; const stringValidator = (input: unknown): input is string => typeof input === 'string'; diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/src/index.ts b/packages/kbn-securitysolution-io-ts-alerting-types/src/index.ts index c6f29862206e6b..2b8ba39fdf4f3b 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/src/index.ts +++ b/packages/kbn-securitysolution-io-ts-alerting-types/src/index.ts @@ -7,7 +7,6 @@ */ export * from './actions'; -export * from './constants'; export * from './default_actions_array'; export * from './default_export_file_name'; export * from './default_from_string'; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/common/lists/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/common/lists/index.mock.ts index c6f54b57d937b0..b5e47510254394 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/common/lists/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/common/lists/index.mock.ts @@ -7,7 +7,7 @@ */ import { List, ListArray } from '.'; -import { ENDPOINT_LIST_ID } from '../../constants'; +import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants'; export const getListMock = (): List => ({ id: 'some_uuid', diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.ts deleted file mode 100644 index 2f520e79bf42c0..00000000000000 --- a/packages/kbn-securitysolution-io-ts-list-types/src/constants/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -/** - * This ID is used for _both_ the Saved Object ID and for the list_id - * for the single global space agnostic endpoint list. - * - * TODO: Create a kbn-securitysolution-constants and add this to it. - * @deprecated Use the ENDPOINT_LIST_ID from the kbn-securitysolution-constants. - */ -export const ENDPOINT_LIST_ID = 'endpoint_list'; - -/** - * Description of trusted apps agnostic list - * @deprecated Use the ENDPOINT_LIST_ID from the kbn-securitysolution-constants. - */ -export const ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION = 'Endpoint Security Trusted Apps List'; - -/** - * ID of trusted apps agnostic list - * @deprecated Use the ENDPOINT_LIST_ID from the kbn-securitysolution-constants. - */ -export const ENDPOINT_TRUSTED_APPS_LIST_ID = 'endpoint_trusted_apps'; - -/** - * Name of trusted apps agnostic list - * @deprecated Use the ENDPOINT_LIST_ID from the kbn-securitysolution-constants. - */ -export const ENDPOINT_TRUSTED_APPS_LIST_NAME = 'Endpoint Security Trusted Apps List'; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/index.ts index 426e0f54963b8e..3d1b267c01d3b9 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/index.ts @@ -7,7 +7,6 @@ */ export * from './common'; -export * from './constants'; export * from './request'; export * from './response'; export * from './typescript_types'; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/response/exception_list_schema/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/response/exception_list_schema/index.mock.ts index 5928c420c88e3f..c77fb35a40b603 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/response/exception_list_schema/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/response/exception_list_schema/index.mock.ts @@ -25,7 +25,7 @@ import { ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION, ENDPOINT_TRUSTED_APPS_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_NAME, -} from '../..'; +} from '@kbn/securitysolution-list-constants'; import { ExceptionListSchema } from '.'; diff --git a/packages/kbn-securitysolution-io-ts-types/src/index.ts b/packages/kbn-securitysolution-io-ts-types/src/index.ts index fc0f017016e9f6..2847894d63690e 100644 --- a/packages/kbn-securitysolution-io-ts-types/src/index.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/index.ts @@ -22,7 +22,6 @@ export * from './non_empty_string'; export * from './non_empty_string_array'; export * from './operator'; export * from './only_false_allowed'; -export * from './parse_schedule_dates'; export * from './positive_integer'; export * from './positive_integer_greater_than_zero'; export * from './string_to_positive_number'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/index.ts index c21096e497134b..8082574296e3f0 100644 --- a/packages/kbn-securitysolution-io-ts-utils/src/index.ts +++ b/packages/kbn-securitysolution-io-ts-utils/src/index.ts @@ -7,6 +7,7 @@ */ export * from './format_errors'; +export * from './parse_schedule_dates'; export * from './exact_check'; export * from './format_errors'; export * from './test_utils'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/parse_schedule_dates/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/parse_schedule_dates/index.test.ts new file mode 100644 index 00000000000000..8919f63aad51e8 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/parse_schedule_dates/index.test.ts @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import moment from 'moment'; +import { parseScheduleDates } from '.'; + +describe('parseScheduleDates', () => { + test('it returns a moment when given an ISO string', () => { + const result = parseScheduleDates('2020-01-01T00:00:00.000Z'); + expect(result).not.toBeNull(); + expect(result).toEqual(moment('2020-01-01T00:00:00.000Z')); + }); + + test('it returns a moment when given `now`', () => { + const result = parseScheduleDates('now'); + + expect(result).not.toBeNull(); + expect(moment.isMoment(result)).toBeTruthy(); + }); + + test('it returns a moment when given `now-x`', () => { + const result = parseScheduleDates('now-6m'); + + expect(result).not.toBeNull(); + expect(moment.isMoment(result)).toBeTruthy(); + }); + + test('it returns null when given a string that is not an ISO string, `now` or `now-x`', () => { + const result = parseScheduleDates('invalid'); + + expect(result).toBeNull(); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-types/src/parse_schedule_dates/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/parse_schedule_dates/index.ts similarity index 100% rename from packages/kbn-securitysolution-io-ts-types/src/parse_schedule_dates/index.ts rename to packages/kbn-securitysolution-io-ts-utils/src/parse_schedule_dates/index.ts diff --git a/packages/kbn-securitysolution-list-api/BUILD.bazel b/packages/kbn-securitysolution-list-api/BUILD.bazel index 149bbf9a0c5c6e..9055cf0804e496 100644 --- a/packages/kbn-securitysolution-list-api/BUILD.bazel +++ b/packages/kbn-securitysolution-list-api/BUILD.bazel @@ -29,6 +29,7 @@ NPM_MODULE_EXTRA_FILES = [ SRC_DEPS = [ "//packages/kbn-securitysolution-io-ts-utils", + "//packages/kbn-securitysolution-list-constants", "//packages/kbn-securitysolution-io-ts-list-types", "@npm//fp-ts", "@npm//io-ts", diff --git a/packages/kbn-securitysolution-list-api/src/api/index.ts b/packages/kbn-securitysolution-list-api/src/api/index.ts index eb3caad2a0cab2..d70417a29971f8 100644 --- a/packages/kbn-securitysolution-list-api/src/api/index.ts +++ b/packages/kbn-securitysolution-list-api/src/api/index.ts @@ -31,14 +31,14 @@ import { UpdateExceptionListProps, } from '@kbn/securitysolution-io-ts-list-types'; -import { toError, toPromise } from '../fp_utils'; import { ENDPOINT_LIST_URL, EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_NAMESPACE, EXCEPTION_LIST_NAMESPACE_AGNOSTIC, EXCEPTION_LIST_URL, -} from '../constants'; +} from '@kbn/securitysolution-list-constants'; +import { toError, toPromise } from '../fp_utils'; /** * Add new ExceptionList diff --git a/packages/kbn-securitysolution-list-api/src/constants/index.ts b/packages/kbn-securitysolution-list-api/src/constants/index.ts deleted file mode 100644 index fe3de21664ca11..00000000000000 --- a/packages/kbn-securitysolution-list-api/src/constants/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -// TODO: These should be all replaced with constants from a shared kbn constants package - -export const LIST_URL = '/api/lists'; -export const LIST_INDEX = `${LIST_URL}/index`; -export const LIST_ITEM_URL = `${LIST_URL}/items`; -export const LIST_PRIVILEGES_URL = `${LIST_URL}/privileges`; - -/** - * Exception list routes - */ -export const EXCEPTION_LIST_URL = '/api/exception_lists'; -export const EXCEPTION_LIST_ITEM_URL = '/api/exception_lists/items'; - -/** - * Exception list spaces - */ -export const EXCEPTION_LIST_NAMESPACE_AGNOSTIC = 'exception-list-agnostic'; -export const EXCEPTION_LIST_NAMESPACE = 'exception-list'; - -/** - * Specific routes for the single global space agnostic endpoint list - */ -export const ENDPOINT_LIST_URL = '/api/endpoint_list'; - -/** - * Specific routes for the single global space agnostic endpoint list. These are convenience - * routes where they are going to try and create the global space agnostic endpoint list if it - * does not exist yet or if it was deleted at some point and re-create it before adding items to - * the list - */ -export const ENDPOINT_LIST_ITEM_URL = '/api/endpoint_list/items'; diff --git a/packages/kbn-securitysolution-list-api/src/list_api/index.ts b/packages/kbn-securitysolution-list-api/src/list_api/index.ts index 7914a5e59d712d..b9d5417f761c0f 100644 --- a/packages/kbn-securitysolution-list-api/src/list_api/index.ts +++ b/packages/kbn-securitysolution-list-api/src/list_api/index.ts @@ -30,10 +30,14 @@ import { listItemIndexExistSchema, listSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { + LIST_INDEX, + LIST_ITEM_URL, + LIST_PRIVILEGES_URL, + LIST_URL, +} from '@kbn/securitysolution-list-constants'; import { toError, toPromise } from '../fp_utils'; -import { LIST_INDEX, LIST_ITEM_URL, LIST_PRIVILEGES_URL, LIST_URL } from '../constants'; - import { ApiParams, DeleteListParams, diff --git a/packages/kbn-securitysolution-constants/BUILD.bazel b/packages/kbn-securitysolution-list-constants/BUILD.bazel similarity index 91% rename from packages/kbn-securitysolution-constants/BUILD.bazel rename to packages/kbn-securitysolution-list-constants/BUILD.bazel index 20f1b51c7d4266..8d6779bfa122e3 100644 --- a/packages/kbn-securitysolution-constants/BUILD.bazel +++ b/packages/kbn-securitysolution-list-constants/BUILD.bazel @@ -1,9 +1,9 @@ load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") -PKG_BASE_NAME = "kbn-securitysolution-constants" +PKG_BASE_NAME = "kbn-securitysolution-list-constants" -PKG_REQUIRE_NAME = "@kbn/securitysolution-constants" +PKG_REQUIRE_NAME = "@kbn/securitysolution-list-constants" SOURCE_FILES = glob( [ diff --git a/packages/kbn-securitysolution-list-constants/README.md b/packages/kbn-securitysolution-list-constants/README.md new file mode 100644 index 00000000000000..c6f10d3bd009b2 --- /dev/null +++ b/packages/kbn-securitysolution-list-constants/README.md @@ -0,0 +1,6 @@ +# kbn-securitysolution-list-constants + +This is where shared constants for security solution lists should go that are going to be shared among plugins. +This was originally created to remove the dependencies between security_solution and other projects. + + diff --git a/packages/kbn-securitysolution-constants/jest.config.js b/packages/kbn-securitysolution-list-constants/jest.config.js similarity index 85% rename from packages/kbn-securitysolution-constants/jest.config.js rename to packages/kbn-securitysolution-list-constants/jest.config.js index f0bb13e39417c6..21dffdfcf5a68a 100644 --- a/packages/kbn-securitysolution-constants/jest.config.js +++ b/packages/kbn-securitysolution-list-constants/jest.config.js @@ -9,5 +9,5 @@ module.exports = { preset: '@kbn/test', rootDir: '../..', - roots: ['/packages/kbn-securitysolution-constants'], + roots: ['/packages/kbn-securitysolution-list-constants'], }; diff --git a/packages/kbn-securitysolution-list-constants/package.json b/packages/kbn-securitysolution-list-constants/package.json new file mode 100644 index 00000000000000..b9d65734aff56c --- /dev/null +++ b/packages/kbn-securitysolution-list-constants/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/securitysolution-list-constants", + "version": "1.0.0", + "description": "security solution list constants to use across plugins such lists, security_solution, cases, etc...", + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "private": true +} diff --git a/packages/kbn-securitysolution-constants/src/index.ts b/packages/kbn-securitysolution-list-constants/src/index.ts similarity index 96% rename from packages/kbn-securitysolution-constants/src/index.ts rename to packages/kbn-securitysolution-list-constants/src/index.ts index 06b741d7613672..dae414aad0deb4 100644 --- a/packages/kbn-securitysolution-constants/src/index.ts +++ b/packages/kbn-securitysolution-list-constants/src/index.ts @@ -70,6 +70,3 @@ export const ENDPOINT_EVENT_FILTERS_LIST_NAME = 'Endpoint Security Event Filters /** Description of event filters agnostic list */ export const ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION = 'Endpoint Security Event Filters List'; - -/** The default max signals without any additional configuration */ -export const DEFAULT_MAX_SIGNALS = 100; diff --git a/packages/kbn-securitysolution-constants/tsconfig.json b/packages/kbn-securitysolution-list-constants/tsconfig.json similarity index 78% rename from packages/kbn-securitysolution-constants/tsconfig.json rename to packages/kbn-securitysolution-list-constants/tsconfig.json index cf06f4ced4b9f4..84edcdd1d5794d 100644 --- a/packages/kbn-securitysolution-constants/tsconfig.json +++ b/packages/kbn-securitysolution-list-constants/tsconfig.json @@ -7,7 +7,7 @@ "outDir": "target", "rootDir": "src", "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-securitysolution-constants/src", + "sourceRoot": "../../../../packages/kbn-securitysolution-list-constants/src", "types": [ "jest", "node" diff --git a/packages/kbn-securitysolution-list-hooks/BUILD.bazel b/packages/kbn-securitysolution-list-hooks/BUILD.bazel index 3c5444092676f6..1078d9bf3d329c 100644 --- a/packages/kbn-securitysolution-list-hooks/BUILD.bazel +++ b/packages/kbn-securitysolution-list-hooks/BUILD.bazel @@ -30,6 +30,8 @@ NPM_MODULE_EXTRA_FILES = [ SRC_DEPS = [ "//packages/kbn-securitysolution-io-ts-list-types", "//packages/kbn-securitysolution-list-api", + "//packages/kbn-securitysolution-list-constants", + "//packages/kbn-securitysolution-list-utils", "//packages/kbn-securitysolution-utils", "@npm//lodash", "@npm//tslib", diff --git a/packages/kbn-securitysolution-list-hooks/src/constants/index.ts b/packages/kbn-securitysolution-list-hooks/src/constants/index.ts deleted file mode 100644 index 2f520e79bf42c0..00000000000000 --- a/packages/kbn-securitysolution-list-hooks/src/constants/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -/** - * This ID is used for _both_ the Saved Object ID and for the list_id - * for the single global space agnostic endpoint list. - * - * TODO: Create a kbn-securitysolution-constants and add this to it. - * @deprecated Use the ENDPOINT_LIST_ID from the kbn-securitysolution-constants. - */ -export const ENDPOINT_LIST_ID = 'endpoint_list'; - -/** - * Description of trusted apps agnostic list - * @deprecated Use the ENDPOINT_LIST_ID from the kbn-securitysolution-constants. - */ -export const ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION = 'Endpoint Security Trusted Apps List'; - -/** - * ID of trusted apps agnostic list - * @deprecated Use the ENDPOINT_LIST_ID from the kbn-securitysolution-constants. - */ -export const ENDPOINT_TRUSTED_APPS_LIST_ID = 'endpoint_trusted_apps'; - -/** - * Name of trusted apps agnostic list - * @deprecated Use the ENDPOINT_LIST_ID from the kbn-securitysolution-constants. - */ -export const ENDPOINT_TRUSTED_APPS_LIST_NAME = 'Endpoint Security Trusted Apps List'; diff --git a/packages/kbn-securitysolution-list-hooks/src/index.ts b/packages/kbn-securitysolution-list-hooks/src/index.ts index a00086aa94b0da..46d6a20deb0ac5 100644 --- a/packages/kbn-securitysolution-list-hooks/src/index.ts +++ b/packages/kbn-securitysolution-list-hooks/src/index.ts @@ -21,5 +21,4 @@ export * from './use_persist_exception_item'; export * from './use_persist_exception_list'; export * from './use_read_list_index'; export * from './use_read_list_privileges'; -export * from './utils'; export * from './with_optional_signal'; diff --git a/packages/kbn-securitysolution-list-hooks/src/use_api/index.ts b/packages/kbn-securitysolution-list-hooks/src/use_api/index.ts index 04ba0fc762f9c2..3b980f84d82a8f 100644 --- a/packages/kbn-securitysolution-list-hooks/src/use_api/index.ts +++ b/packages/kbn-securitysolution-list-hooks/src/use_api/index.ts @@ -25,7 +25,7 @@ interface HttpStart { fetch: (...args: any) => any; } -import { getIdsAndNamespaces } from '../utils'; +import { getIdsAndNamespaces } from '@kbn/securitysolution-list-utils'; import { transformInput, transformNewItemOutput, transformOutput } from '../transforms'; export interface ExceptionsApi { diff --git a/packages/kbn-securitysolution-list-hooks/src/use_exception_list_items/index.ts b/packages/kbn-securitysolution-list-hooks/src/use_exception_list_items/index.ts index 43f39401a81d1d..4962ecee580168 100644 --- a/packages/kbn-securitysolution-list-hooks/src/use_exception_list_items/index.ts +++ b/packages/kbn-securitysolution-list-hooks/src/use_exception_list_items/index.ts @@ -15,7 +15,7 @@ import type { } from '@kbn/securitysolution-io-ts-list-types'; import { fetchExceptionListsItemsByListIds } from '@kbn/securitysolution-list-api'; -import { getIdsAndNamespaces } from '../utils'; +import { getIdsAndNamespaces } from '@kbn/securitysolution-list-utils'; import { transformInput } from '../transforms'; type Func = () => void; diff --git a/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts b/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts index 5d7aada4b95298..a9a93aa8df49a6 100644 --- a/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts +++ b/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts @@ -14,7 +14,7 @@ import type { } from '@kbn/securitysolution-io-ts-list-types'; import { fetchExceptionLists } from '@kbn/securitysolution-list-api'; -import { getFilters } from '../utils'; +import { getFilters } from '@kbn/securitysolution-list-utils'; export type Func = () => void; export type ReturnExceptionLists = [boolean, ExceptionListSchema[], Pagination, Func | null]; diff --git a/packages/kbn-securitysolution-list-hooks/src/utils/index.test.ts b/packages/kbn-securitysolution-list-hooks/src/utils/index.test.ts deleted file mode 100644 index 8d8c8518dc2982..00000000000000 --- a/packages/kbn-securitysolution-list-hooks/src/utils/index.test.ts +++ /dev/null @@ -1,272 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { getFilters, getGeneralFilters, getIdsAndNamespaces, getTrustedAppsFilter } from '.'; - -describe('Exceptions utils', () => { - describe('#getIdsAndNamespaces', () => { - test('it returns empty arrays if no lists found', async () => { - const output = getIdsAndNamespaces({ - lists: [], - showDetection: false, - showEndpoint: false, - }); - - expect(output).toEqual({ ids: [], namespaces: [] }); - }); - - test('it returns all lists if "showDetection" and "showEndpoint" are "false"', async () => { - const output = getIdsAndNamespaces({ - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - { - id: 'myListIdEndpoint', - listId: 'list_id_endpoint', - namespaceType: 'agnostic', - type: 'endpoint', - }, - ], - showDetection: false, - showEndpoint: false, - }); - - expect(output).toEqual({ - ids: ['list_id', 'list_id_endpoint'], - namespaces: ['single', 'agnostic'], - }); - }); - - test('it returns only detections lists if "showDetection" is "true"', async () => { - const output = getIdsAndNamespaces({ - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - { - id: 'myListIdEndpoint', - listId: 'list_id_endpoint', - namespaceType: 'agnostic', - type: 'endpoint', - }, - ], - showDetection: true, - showEndpoint: false, - }); - - expect(output).toEqual({ - ids: ['list_id'], - namespaces: ['single'], - }); - }); - - test('it returns only endpoint lists if "showEndpoint" is "true"', async () => { - const output = getIdsAndNamespaces({ - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - { - id: 'myListIdEndpoint', - listId: 'list_id_endpoint', - namespaceType: 'agnostic', - type: 'endpoint', - }, - ], - showDetection: false, - showEndpoint: true, - }); - - expect(output).toEqual({ - ids: ['list_id_endpoint'], - namespaces: ['agnostic'], - }); - }); - - test('it returns only detection lists if both "showEndpoint" and "showDetection" are "true"', async () => { - const output = getIdsAndNamespaces({ - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - { - id: 'myListIdEndpoint', - listId: 'list_id_endpoint', - namespaceType: 'agnostic', - type: 'endpoint', - }, - ], - showDetection: true, - showEndpoint: true, - }); - - expect(output).toEqual({ - ids: ['list_id'], - namespaces: ['single'], - }); - }); - }); - - describe('getGeneralFilters', () => { - test('it returns empty string if no filters', () => { - const filters = getGeneralFilters({}, ['exception-list']); - - expect(filters).toEqual(''); - }); - - test('it properly formats filters when one namespace type passed in', () => { - const filters = getGeneralFilters({ created_by: 'moi', name: 'Sample' }, ['exception-list']); - - expect(filters).toEqual( - '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample)' - ); - }); - - test('it properly formats filters when two namespace types passed in', () => { - const filters = getGeneralFilters({ created_by: 'moi', name: 'Sample' }, [ - 'exception-list', - 'exception-list-agnostic', - ]); - - expect(filters).toEqual( - '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample)' - ); - }); - }); - - describe('getTrustedAppsFilter', () => { - test('it returns filter to search for "exception-list" namespace trusted apps', () => { - const filter = getTrustedAppsFilter(true, ['exception-list']); - - expect(filter).toEqual('(exception-list.attributes.list_id: endpoint_trusted_apps*)'); - }); - - test('it returns filter to search for "exception-list" and "agnostic" namespace trusted apps', () => { - const filter = getTrustedAppsFilter(true, ['exception-list', 'exception-list-agnostic']); - - expect(filter).toEqual( - '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' - ); - }); - - test('it returns filter to exclude "exception-list" namespace trusted apps', () => { - const filter = getTrustedAppsFilter(false, ['exception-list']); - - expect(filter).toEqual('(not exception-list.attributes.list_id: endpoint_trusted_apps*)'); - }); - - test('it returns filter to exclude "exception-list" and "agnostic" namespace trusted apps', () => { - const filter = getTrustedAppsFilter(false, ['exception-list', 'exception-list-agnostic']); - - expect(filter).toEqual( - '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' - ); - }); - }); - - describe('getFilters', () => { - describe('single', () => { - test('it properly formats when no filters passed and "showTrustedApps" is false', () => { - const filter = getFilters({}, ['single'], false); - - expect(filter).toEqual('(not exception-list.attributes.list_id: endpoint_trusted_apps*)'); - }); - - test('it properly formats when no filters passed and "showTrustedApps" is true', () => { - const filter = getFilters({}, ['single'], true); - - expect(filter).toEqual('(exception-list.attributes.list_id: endpoint_trusted_apps*)'); - }); - - test('it properly formats when filters passed and "showTrustedApps" is false', () => { - const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['single'], false); - - expect(filter).toEqual( - '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*)' - ); - }); - - test('it if filters passed and "showTrustedApps" is true', () => { - const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['single'], true); - - expect(filter).toEqual( - '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps*)' - ); - }); - }); - - describe('agnostic', () => { - test('it properly formats when no filters passed and "showTrustedApps" is false', () => { - const filter = getFilters({}, ['agnostic'], false); - - expect(filter).toEqual( - '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' - ); - }); - - test('it properly formats when no filters passed and "showTrustedApps" is true', () => { - const filter = getFilters({}, ['agnostic'], true); - - expect(filter).toEqual( - '(exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' - ); - }); - - test('it properly formats when filters passed and "showTrustedApps" is false', () => { - const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['agnostic'], false); - - expect(filter).toEqual( - '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' - ); - }); - - test('it if filters passed and "showTrustedApps" is true', () => { - const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['agnostic'], true); - - expect(filter).toEqual( - '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' - ); - }); - }); - - describe('single, agnostic', () => { - test('it properly formats when no filters passed and "showTrustedApps" is false', () => { - const filter = getFilters({}, ['single', 'agnostic'], false); - - expect(filter).toEqual( - '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' - ); - }); - - test('it properly formats when no filters passed and "showTrustedApps" is true', () => { - const filter = getFilters({}, ['single', 'agnostic'], true); - - expect(filter).toEqual( - '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' - ); - }); - - test('it properly formats when filters passed and "showTrustedApps" is false', () => { - const filter = getFilters( - { created_by: 'moi', name: 'Sample' }, - ['single', 'agnostic'], - false - ); - - expect(filter).toEqual( - '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' - ); - }); - - test('it properly formats when filters passed and "showTrustedApps" is true', () => { - const filter = getFilters( - { created_by: 'moi', name: 'Sample' }, - ['single', 'agnostic'], - true - ); - - expect(filter).toEqual( - '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' - ); - }); - }); - }); -}); diff --git a/packages/kbn-securitysolution-list-hooks/src/utils/index.ts b/packages/kbn-securitysolution-list-hooks/src/utils/index.ts deleted file mode 100644 index b08a4b49cd590c..00000000000000 --- a/packages/kbn-securitysolution-list-hooks/src/utils/index.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import type { - NamespaceType, - NamespaceTypeArray, - ExceptionListFilter, - ExceptionListIdentifiers, -} from '@kbn/securitysolution-io-ts-list-types'; -import { get } from 'lodash/fp'; -import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../constants'; - -export const exceptionListSavedObjectType = 'exception-list'; -export const exceptionListAgnosticSavedObjectType = 'exception-list-agnostic'; -export type SavedObjectType = 'exception-list' | 'exception-list-agnostic'; - -export const getSavedObjectType = ({ - namespaceType, -}: { - namespaceType: NamespaceType; -}): SavedObjectType => { - if (namespaceType === 'agnostic') { - return exceptionListAgnosticSavedObjectType; - } else { - return exceptionListSavedObjectType; - } -}; - -export const getSavedObjectTypes = ({ - namespaceType, -}: { - namespaceType: NamespaceTypeArray; -}): SavedObjectType[] => { - return namespaceType.map((singleNamespaceType) => - getSavedObjectType({ namespaceType: singleNamespaceType }) - ); -}; - -export const getIdsAndNamespaces = ({ - lists, - showDetection, - showEndpoint, -}: { - lists: ExceptionListIdentifiers[]; - showDetection: boolean; - showEndpoint: boolean; -}): { ids: string[]; namespaces: NamespaceType[] } => - lists - .filter((list) => { - if (showDetection) { - return list.type === 'detection'; - } else if (showEndpoint) { - return list.type === 'endpoint'; - } else { - return true; - } - }) - .reduce<{ ids: string[]; namespaces: NamespaceType[] }>( - (acc, { listId, namespaceType }) => ({ - ids: [...acc.ids, listId], - namespaces: [...acc.namespaces, namespaceType], - }), - { ids: [], namespaces: [] } - ); - -export const getGeneralFilters = ( - filters: ExceptionListFilter, - namespaceTypes: SavedObjectType[] -): string => { - return Object.keys(filters) - .map((filterKey) => { - const value = get(filterKey, filters); - if (value != null && value.trim() !== '') { - const filtersByNamespace = namespaceTypes - .map((namespace) => { - const fieldToSearch = filterKey === 'name' ? 'name.text' : filterKey; - return `${namespace}.attributes.${fieldToSearch}:${value}`; - }) - .join(' OR '); - return `(${filtersByNamespace})`; - } else return null; - }) - .filter((item) => item != null) - .join(' AND '); -}; - -export const getTrustedAppsFilter = ( - showTrustedApps: boolean, - namespaceTypes: SavedObjectType[] -): string => { - if (showTrustedApps) { - const filters = namespaceTypes.map((namespace) => { - return `${namespace}.attributes.list_id: ${ENDPOINT_TRUSTED_APPS_LIST_ID}*`; - }); - return `(${filters.join(' OR ')})`; - } else { - const filters = namespaceTypes.map((namespace) => { - return `not ${namespace}.attributes.list_id: ${ENDPOINT_TRUSTED_APPS_LIST_ID}*`; - }); - return `(${filters.join(' AND ')})`; - } -}; - -export const getFilters = ( - filters: ExceptionListFilter, - namespaceTypes: NamespaceType[], - showTrustedApps: boolean -): string => { - const namespaces = getSavedObjectTypes({ namespaceType: namespaceTypes }); - const generalFilters = getGeneralFilters(filters, namespaces); - const trustedAppsFilter = getTrustedAppsFilter(showTrustedApps, namespaces); - return [generalFilters, trustedAppsFilter].filter((filter) => filter.trim() !== '').join(' AND '); -}; diff --git a/packages/kbn-securitysolution-list-utils/BUILD.bazel b/packages/kbn-securitysolution-list-utils/BUILD.bazel index f9063290d9f734..0d257a95f02593 100644 --- a/packages/kbn-securitysolution-list-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-list-utils/BUILD.bazel @@ -29,6 +29,7 @@ NPM_MODULE_EXTRA_FILES = [ SRC_DEPS = [ "//packages/kbn-i18n", + "//packages/kbn-securitysolution-list-constants", "//packages/kbn-securitysolution-io-ts-list-types", "//packages/kbn-securitysolution-utils", "@npm//lodash", diff --git a/packages/kbn-securitysolution-list-utils/src/get_exception_list_type/index.ts b/packages/kbn-securitysolution-list-utils/src/get_exception_list_type/index.ts new file mode 100644 index 00000000000000..eb68e2486686f8 --- /dev/null +++ b/packages/kbn-securitysolution-list-utils/src/get_exception_list_type/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; + +import { exceptionListAgnosticSavedObjectType } from '../types'; + +export const getExceptionListType = ({ + savedObjectType, +}: { + savedObjectType: string; +}): NamespaceType => { + if (savedObjectType === exceptionListAgnosticSavedObjectType) { + return 'agnostic'; + } else { + return 'single'; + } +}; diff --git a/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts b/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts new file mode 100644 index 00000000000000..327a29dc1b987a --- /dev/null +++ b/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getFilters } from '.'; + +describe('getFilters', () => { + describe('single', () => { + test('it properly formats when no filters passed and "showTrustedApps" is false', () => { + const filter = getFilters({}, ['single'], false); + + expect(filter).toEqual('(not exception-list.attributes.list_id: endpoint_trusted_apps*)'); + }); + + test('it properly formats when no filters passed and "showTrustedApps" is true', () => { + const filter = getFilters({}, ['single'], true); + + expect(filter).toEqual('(exception-list.attributes.list_id: endpoint_trusted_apps*)'); + }); + + test('it properly formats when filters passed and "showTrustedApps" is false', () => { + const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['single'], false); + + expect(filter).toEqual( + '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*)' + ); + }); + + test('it if filters passed and "showTrustedApps" is true', () => { + const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['single'], true); + + expect(filter).toEqual( + '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps*)' + ); + }); + }); + + describe('agnostic', () => { + test('it properly formats when no filters passed and "showTrustedApps" is false', () => { + const filter = getFilters({}, ['agnostic'], false); + + expect(filter).toEqual( + '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + ); + }); + + test('it properly formats when no filters passed and "showTrustedApps" is true', () => { + const filter = getFilters({}, ['agnostic'], true); + + expect(filter).toEqual( + '(exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + ); + }); + + test('it properly formats when filters passed and "showTrustedApps" is false', () => { + const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['agnostic'], false); + + expect(filter).toEqual( + '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + ); + }); + + test('it if filters passed and "showTrustedApps" is true', () => { + const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['agnostic'], true); + + expect(filter).toEqual( + '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + ); + }); + }); + + describe('single, agnostic', () => { + test('it properly formats when no filters passed and "showTrustedApps" is false', () => { + const filter = getFilters({}, ['single', 'agnostic'], false); + + expect(filter).toEqual( + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + ); + }); + + test('it properly formats when no filters passed and "showTrustedApps" is true', () => { + const filter = getFilters({}, ['single', 'agnostic'], true); + + expect(filter).toEqual( + '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + ); + }); + + test('it properly formats when filters passed and "showTrustedApps" is false', () => { + const filter = getFilters( + { created_by: 'moi', name: 'Sample' }, + ['single', 'agnostic'], + false + ); + + expect(filter).toEqual( + '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + ); + }); + + test('it properly formats when filters passed and "showTrustedApps" is true', () => { + const filter = getFilters( + { created_by: 'moi', name: 'Sample' }, + ['single', 'agnostic'], + true + ); + + expect(filter).toEqual( + '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + ); + }); + }); +}); diff --git a/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts b/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts new file mode 100644 index 00000000000000..c9dd6ccae484c2 --- /dev/null +++ b/packages/kbn-securitysolution-list-utils/src/get_filters/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ExceptionListFilter, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; +import { getGeneralFilters } from '../get_general_filters'; +import { getSavedObjectTypes } from '../get_saved_object_types'; +import { getTrustedAppsFilter } from '../get_trusted_apps_filter'; + +export const getFilters = ( + filters: ExceptionListFilter, + namespaceTypes: NamespaceType[], + showTrustedApps: boolean +): string => { + const namespaces = getSavedObjectTypes({ namespaceType: namespaceTypes }); + const generalFilters = getGeneralFilters(filters, namespaces); + const trustedAppsFilter = getTrustedAppsFilter(showTrustedApps, namespaces); + return [generalFilters, trustedAppsFilter].filter((filter) => filter.trim() !== '').join(' AND '); +}; diff --git a/packages/kbn-securitysolution-list-utils/src/get_general_filters/index.test.ts b/packages/kbn-securitysolution-list-utils/src/get_general_filters/index.test.ts new file mode 100644 index 00000000000000..8786b48b73c82e --- /dev/null +++ b/packages/kbn-securitysolution-list-utils/src/get_general_filters/index.test.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getGeneralFilters } from '.'; + +describe('getGeneralFilters', () => { + test('it returns empty string if no filters', () => { + const filters = getGeneralFilters({}, ['exception-list']); + + expect(filters).toEqual(''); + }); + + test('it properly formats filters when one namespace type passed in', () => { + const filters = getGeneralFilters({ created_by: 'moi', name: 'Sample' }, ['exception-list']); + + expect(filters).toEqual( + '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample)' + ); + }); + + test('it properly formats filters when two namespace types passed in', () => { + const filters = getGeneralFilters({ created_by: 'moi', name: 'Sample' }, [ + 'exception-list', + 'exception-list-agnostic', + ]); + + expect(filters).toEqual( + '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample)' + ); + }); +}); diff --git a/packages/kbn-securitysolution-list-utils/src/get_general_filters/index.ts b/packages/kbn-securitysolution-list-utils/src/get_general_filters/index.ts new file mode 100644 index 00000000000000..f44e37e547fe98 --- /dev/null +++ b/packages/kbn-securitysolution-list-utils/src/get_general_filters/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ExceptionListFilter } from '@kbn/securitysolution-io-ts-list-types'; +import { get } from 'lodash/fp'; +import { SavedObjectType } from '../types'; + +export const getGeneralFilters = ( + filters: ExceptionListFilter, + namespaceTypes: SavedObjectType[] +): string => { + return Object.keys(filters) + .map((filterKey) => { + const value = get(filterKey, filters); + if (value != null && value.trim() !== '') { + const filtersByNamespace = namespaceTypes + .map((namespace) => { + const fieldToSearch = filterKey === 'name' ? 'name.text' : filterKey; + return `${namespace}.attributes.${fieldToSearch}:${value}`; + }) + .join(' OR '); + return `(${filtersByNamespace})`; + } else return null; + }) + .filter((item) => item != null) + .join(' AND '); +}; diff --git a/packages/kbn-securitysolution-list-utils/src/get_ids_and_namespaces/index.test.ts b/packages/kbn-securitysolution-list-utils/src/get_ids_and_namespaces/index.test.ts new file mode 100644 index 00000000000000..6ecba8b97207ab --- /dev/null +++ b/packages/kbn-securitysolution-list-utils/src/get_ids_and_namespaces/index.test.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getIdsAndNamespaces } from '.'; + +describe('getIdsAndNamespaces', () => { + test('it returns empty arrays if no lists found', async () => { + const output = getIdsAndNamespaces({ + lists: [], + showDetection: false, + showEndpoint: false, + }); + + expect(output).toEqual({ ids: [], namespaces: [] }); + }); + + test('it returns all lists if "showDetection" and "showEndpoint" are "false"', async () => { + const output = getIdsAndNamespaces({ + lists: [ + { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, + { + id: 'myListIdEndpoint', + listId: 'list_id_endpoint', + namespaceType: 'agnostic', + type: 'endpoint', + }, + ], + showDetection: false, + showEndpoint: false, + }); + + expect(output).toEqual({ + ids: ['list_id', 'list_id_endpoint'], + namespaces: ['single', 'agnostic'], + }); + }); + + test('it returns only detections lists if "showDetection" is "true"', async () => { + const output = getIdsAndNamespaces({ + lists: [ + { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, + { + id: 'myListIdEndpoint', + listId: 'list_id_endpoint', + namespaceType: 'agnostic', + type: 'endpoint', + }, + ], + showDetection: true, + showEndpoint: false, + }); + + expect(output).toEqual({ + ids: ['list_id'], + namespaces: ['single'], + }); + }); + + test('it returns only endpoint lists if "showEndpoint" is "true"', async () => { + const output = getIdsAndNamespaces({ + lists: [ + { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, + { + id: 'myListIdEndpoint', + listId: 'list_id_endpoint', + namespaceType: 'agnostic', + type: 'endpoint', + }, + ], + showDetection: false, + showEndpoint: true, + }); + + expect(output).toEqual({ + ids: ['list_id_endpoint'], + namespaces: ['agnostic'], + }); + }); + + test('it returns only detection lists if both "showEndpoint" and "showDetection" are "true"', async () => { + const output = getIdsAndNamespaces({ + lists: [ + { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, + { + id: 'myListIdEndpoint', + listId: 'list_id_endpoint', + namespaceType: 'agnostic', + type: 'endpoint', + }, + ], + showDetection: true, + showEndpoint: true, + }); + + expect(output).toEqual({ + ids: ['list_id'], + namespaces: ['single'], + }); + }); +}); diff --git a/packages/kbn-securitysolution-list-utils/src/get_ids_and_namespaces/index.ts b/packages/kbn-securitysolution-list-utils/src/get_ids_and_namespaces/index.ts new file mode 100644 index 00000000000000..a1ab4c14728e40 --- /dev/null +++ b/packages/kbn-securitysolution-list-utils/src/get_ids_and_namespaces/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ExceptionListIdentifiers, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; + +export const getIdsAndNamespaces = ({ + lists, + showDetection, + showEndpoint, +}: { + lists: ExceptionListIdentifiers[]; + showDetection: boolean; + showEndpoint: boolean; +}): { ids: string[]; namespaces: NamespaceType[] } => + lists + .filter((list) => { + if (showDetection) { + return list.type === 'detection'; + } else if (showEndpoint) { + return list.type === 'endpoint'; + } else { + return true; + } + }) + .reduce<{ ids: string[]; namespaces: NamespaceType[] }>( + (acc, { listId, namespaceType }) => ({ + ids: [...acc.ids, listId], + namespaces: [...acc.namespaces, namespaceType], + }), + { ids: [], namespaces: [] } + ); diff --git a/packages/kbn-securitysolution-list-utils/src/get_saved_object_type/index.ts b/packages/kbn-securitysolution-list-utils/src/get_saved_object_type/index.ts new file mode 100644 index 00000000000000..1d59694e43366d --- /dev/null +++ b/packages/kbn-securitysolution-list-utils/src/get_saved_object_type/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; + +import { + exceptionListAgnosticSavedObjectType, + exceptionListSavedObjectType, + SavedObjectType, +} from '../types'; + +export const getSavedObjectType = ({ + namespaceType, +}: { + namespaceType: NamespaceType; +}): SavedObjectType => { + if (namespaceType === 'agnostic') { + return exceptionListAgnosticSavedObjectType; + } else { + return exceptionListSavedObjectType; + } +}; diff --git a/packages/kbn-securitysolution-list-utils/src/get_saved_object_types/index.ts b/packages/kbn-securitysolution-list-utils/src/get_saved_object_types/index.ts new file mode 100644 index 00000000000000..f61dfe071802d0 --- /dev/null +++ b/packages/kbn-securitysolution-list-utils/src/get_saved_object_types/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { NamespaceTypeArray } from '@kbn/securitysolution-io-ts-list-types'; + +import { SavedObjectType } from '../types'; +import { getSavedObjectType } from '../get_saved_object_type'; + +export const getSavedObjectTypes = ({ + namespaceType, +}: { + namespaceType: NamespaceTypeArray; +}): SavedObjectType[] => { + return namespaceType.map((singleNamespaceType) => + getSavedObjectType({ namespaceType: singleNamespaceType }) + ); +}; diff --git a/packages/kbn-securitysolution-list-utils/src/get_trusted_apps_filter/index.test.ts b/packages/kbn-securitysolution-list-utils/src/get_trusted_apps_filter/index.test.ts new file mode 100644 index 00000000000000..da178b15390e69 --- /dev/null +++ b/packages/kbn-securitysolution-list-utils/src/get_trusted_apps_filter/index.test.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getTrustedAppsFilter } from '.'; + +describe('getTrustedAppsFilter', () => { + test('it returns filter to search for "exception-list" namespace trusted apps', () => { + const filter = getTrustedAppsFilter(true, ['exception-list']); + + expect(filter).toEqual('(exception-list.attributes.list_id: endpoint_trusted_apps*)'); + }); + + test('it returns filter to search for "exception-list" and "agnostic" namespace trusted apps', () => { + const filter = getTrustedAppsFilter(true, ['exception-list', 'exception-list-agnostic']); + + expect(filter).toEqual( + '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + ); + }); + + test('it returns filter to exclude "exception-list" namespace trusted apps', () => { + const filter = getTrustedAppsFilter(false, ['exception-list']); + + expect(filter).toEqual('(not exception-list.attributes.list_id: endpoint_trusted_apps*)'); + }); + + test('it returns filter to exclude "exception-list" and "agnostic" namespace trusted apps', () => { + const filter = getTrustedAppsFilter(false, ['exception-list', 'exception-list-agnostic']); + + expect(filter).toEqual( + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' + ); + }); +}); diff --git a/packages/kbn-securitysolution-list-utils/src/get_trusted_apps_filter/index.ts b/packages/kbn-securitysolution-list-utils/src/get_trusted_apps_filter/index.ts new file mode 100644 index 00000000000000..9c969068d4edfa --- /dev/null +++ b/packages/kbn-securitysolution-list-utils/src/get_trusted_apps_filter/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants'; +import { SavedObjectType } from '../types'; + +export const getTrustedAppsFilter = ( + showTrustedApps: boolean, + namespaceTypes: SavedObjectType[] +): string => { + if (showTrustedApps) { + const filters = namespaceTypes.map((namespace) => { + return `${namespace}.attributes.list_id: ${ENDPOINT_TRUSTED_APPS_LIST_ID}*`; + }); + return `(${filters.join(' OR ')})`; + } else { + const filters = namespaceTypes.map((namespace) => { + return `not ${namespace}.attributes.list_id: ${ENDPOINT_TRUSTED_APPS_LIST_ID}*`; + }); + return `(${filters.join(' AND ')})`; + } +}; diff --git a/packages/kbn-securitysolution-list-utils/src/index.ts b/packages/kbn-securitysolution-list-utils/src/index.ts index 9e3cb78c1ae256..9e88cac6b5d194 100644 --- a/packages/kbn-securitysolution-list-utils/src/index.ts +++ b/packages/kbn-securitysolution-list-utils/src/index.ts @@ -7,6 +7,13 @@ */ export * from './autocomplete_operators'; export * from './build_exception_filter'; +export * from './get_exception_list_type'; +export * from './get_filters'; +export * from './get_general_filters'; +export * from './get_ids_and_namespaces'; +export * from './get_saved_object_type'; +export * from './get_saved_object_types'; +export * from './get_trusted_apps_filter'; export * from './has_large_value_list'; export * from './helpers'; export * from './types'; diff --git a/packages/kbn-securitysolution-list-utils/src/types/index.ts b/packages/kbn-securitysolution-list-utils/src/types/index.ts index c8603fa01157cd..faf68ca1579812 100644 --- a/packages/kbn-securitysolution-list-utils/src/types/index.ts +++ b/packages/kbn-securitysolution-list-utils/src/types/index.ts @@ -18,6 +18,10 @@ import type { ListOperatorEnum as OperatorEnum, ListOperatorTypeEnum as OperatorTypeEnum, } from '@kbn/securitysolution-io-ts-list-types'; +import { + EXCEPTION_LIST_NAMESPACE, + EXCEPTION_LIST_NAMESPACE_AGNOSTIC, +} from '@kbn/securitysolution-list-constants'; import type { OperatorOption } from '../autocomplete_operators/types'; @@ -98,3 +102,9 @@ export type CreateExceptionListItemBuilderSchema = Omit< export type ExceptionsBuilderExceptionItem = | ExceptionListItemBuilderSchema | CreateExceptionListItemBuilderSchema; + +export const exceptionListSavedObjectType = EXCEPTION_LIST_NAMESPACE; +export const exceptionListAgnosticSavedObjectType = EXCEPTION_LIST_NAMESPACE_AGNOSTIC; +export type SavedObjectType = + | typeof EXCEPTION_LIST_NAMESPACE + | typeof EXCEPTION_LIST_NAMESPACE_AGNOSTIC; diff --git a/x-pack/plugins/lists/common/constants.ts b/x-pack/plugins/lists/common/constants.ts deleted file mode 100644 index 4f897c83cb41d6..00000000000000 --- a/x-pack/plugins/lists/common/constants.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/** - * Value list routes - */ -export const LIST_URL = '/api/lists'; -export const LIST_INDEX = `${LIST_URL}/index`; -export const LIST_ITEM_URL = `${LIST_URL}/items`; -export const LIST_PRIVILEGES_URL = `${LIST_URL}/privileges`; - -/** - * Exception list routes - */ -export const EXCEPTION_LIST_URL = '/api/exception_lists'; -export const EXCEPTION_LIST_ITEM_URL = '/api/exception_lists/items'; - -/** - * Exception list spaces - */ -export const EXCEPTION_LIST_NAMESPACE_AGNOSTIC = 'exception-list-agnostic'; -export const EXCEPTION_LIST_NAMESPACE = 'exception-list'; - -/** - * Specific routes for the single global space agnostic endpoint list - */ -export const ENDPOINT_LIST_URL = '/api/endpoint_list'; - -/** - * Specific routes for the single global space agnostic endpoint list. These are convenience - * routes where they are going to try and create the global space agnostic endpoint list if it - * does not exist yet or if it was deleted at some point and re-create it before adding items to - * the list - */ -export const ENDPOINT_LIST_ITEM_URL = '/api/endpoint_list/items'; - -/** - * This ID is used for _both_ the Saved Object ID and for the list_id - * for the single global space agnostic endpoint list - */ -export const ENDPOINT_LIST_ID = 'endpoint_list'; - -/** The name of the single global space agnostic endpoint list */ -export const ENDPOINT_LIST_NAME = 'Endpoint Security Exception List'; - -/** The description of the single global space agnostic endpoint list */ -export const ENDPOINT_LIST_DESCRIPTION = 'Endpoint Security Exception List'; - -export const MAX_EXCEPTION_LIST_SIZE = 10000; - -/** ID of trusted apps agnostic list */ -export const ENDPOINT_TRUSTED_APPS_LIST_ID = 'endpoint_trusted_apps'; - -/** Name of trusted apps agnostic list */ -export const ENDPOINT_TRUSTED_APPS_LIST_NAME = 'Endpoint Security Trusted Apps List'; - -/** Description of trusted apps agnostic list */ -export const ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION = 'Endpoint Security Trusted Apps List'; - -/** ID of event filters agnostic list */ -export const ENDPOINT_EVENT_FILTERS_LIST_ID = 'endpoint_event_filters'; - -/** Name of event filters agnostic list */ -export const ENDPOINT_EVENT_FILTERS_LIST_NAME = 'Endpoint Security Event Filters List'; - -/** Description of event filters agnostic list */ -export const ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION = 'Endpoint Security Event Filters List'; diff --git a/x-pack/plugins/lists/common/index.ts b/x-pack/plugins/lists/common/index.ts index 9f4af059632c48..1fec1c76430ebd 100644 --- a/x-pack/plugins/lists/common/index.ts +++ b/x-pack/plugins/lists/common/index.ts @@ -4,5 +4,3 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -export * from './shared_exports'; diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts index 59e1138972ce0d..f1062d7ff1f4ac 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts @@ -6,6 +6,12 @@ */ import type { ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { + ENDPOINT_LIST_ID, + ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION, + ENDPOINT_TRUSTED_APPS_LIST_ID, + ENDPOINT_TRUSTED_APPS_LIST_NAME, +} from '@kbn/securitysolution-list-constants'; import { DATE_NOW, @@ -21,12 +27,6 @@ import { VERSION, _VERSION, } from '../../constants.mock'; -import { ENDPOINT_LIST_ID } from '../..'; -import { - ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION, - ENDPOINT_TRUSTED_APPS_LIST_ID, - ENDPOINT_TRUSTED_APPS_LIST_NAME, -} from '../../constants'; export const getExceptionListSchemaMock = (): ExceptionListSchema => ({ _version: _VERSION, diff --git a/x-pack/plugins/lists/common/shared_exports.ts b/x-pack/plugins/lists/common/shared_exports.ts deleted file mode 100644 index 119eab3b245de5..00000000000000 --- a/x-pack/plugins/lists/common/shared_exports.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/** DEPRECATED, TRY NOT NOT TO ADD ANYTHING HERE. INSTEAD TRY TO USE/CREATE kibana packages @kbn/... */ - -// TODO: Move this into one of the kibana packages and remove it from here -export { - ENDPOINT_LIST_ID, - ENDPOINT_TRUSTED_APPS_LIST_ID, - EXCEPTION_LIST_URL, - EXCEPTION_LIST_ITEM_URL, - ENDPOINT_EVENT_FILTERS_LIST_ID, - ENDPOINT_EVENT_FILTERS_LIST_NAME, - ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, -} from './constants'; diff --git a/x-pack/plugins/lists/common/types.ts b/x-pack/plugins/lists/common/types.ts deleted file mode 100644 index a3cbb870faa5b4..00000000000000 --- a/x-pack/plugins/lists/common/types.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const exceptionListSavedObjectType = 'exception-list'; -export const exceptionListAgnosticSavedObjectType = 'exception-list-agnostic'; -export type SavedObjectType = 'exception-list' | 'exception-list-agnostic'; - -/** - * This makes any optional property the same as Required would but also has the - * added benefit of keeping your undefined. - * - * For example: - * type A = RequiredKeepUndefined<{ a?: undefined; b: number }>; - * - * will yield a type of: - * type A = { a: undefined; b: number; } - * @deprecated This has no replacement. We should stop using/relying on this and just remove it. - */ -export type RequiredKeepUndefined = { [K in keyof T]-?: [T[K]] } extends infer U - ? U extends Record - ? { [K in keyof U]: U[K][0] } - : never - : never; diff --git a/x-pack/plugins/lists/public/exceptions/utils.test.ts b/x-pack/plugins/lists/public/exceptions/utils.test.ts deleted file mode 100644 index 47279de0a84c8e..00000000000000 --- a/x-pack/plugins/lists/public/exceptions/utils.test.ts +++ /dev/null @@ -1,271 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getFilters, getGeneralFilters, getIdsAndNamespaces, getTrustedAppsFilter } from './utils'; - -describe('Exceptions utils', () => { - describe('#getIdsAndNamespaces', () => { - test('it returns empty arrays if no lists found', async () => { - const output = getIdsAndNamespaces({ - lists: [], - showDetection: false, - showEndpoint: false, - }); - - expect(output).toEqual({ ids: [], namespaces: [] }); - }); - - test('it returns all lists if "showDetection" and "showEndpoint" are "false"', async () => { - const output = getIdsAndNamespaces({ - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - { - id: 'myListIdEndpoint', - listId: 'list_id_endpoint', - namespaceType: 'agnostic', - type: 'endpoint', - }, - ], - showDetection: false, - showEndpoint: false, - }); - - expect(output).toEqual({ - ids: ['list_id', 'list_id_endpoint'], - namespaces: ['single', 'agnostic'], - }); - }); - - test('it returns only detections lists if "showDetection" is "true"', async () => { - const output = getIdsAndNamespaces({ - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - { - id: 'myListIdEndpoint', - listId: 'list_id_endpoint', - namespaceType: 'agnostic', - type: 'endpoint', - }, - ], - showDetection: true, - showEndpoint: false, - }); - - expect(output).toEqual({ - ids: ['list_id'], - namespaces: ['single'], - }); - }); - - test('it returns only endpoint lists if "showEndpoint" is "true"', async () => { - const output = getIdsAndNamespaces({ - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - { - id: 'myListIdEndpoint', - listId: 'list_id_endpoint', - namespaceType: 'agnostic', - type: 'endpoint', - }, - ], - showDetection: false, - showEndpoint: true, - }); - - expect(output).toEqual({ - ids: ['list_id_endpoint'], - namespaces: ['agnostic'], - }); - }); - - test('it returns only detection lists if both "showEndpoint" and "showDetection" are "true"', async () => { - const output = getIdsAndNamespaces({ - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - { - id: 'myListIdEndpoint', - listId: 'list_id_endpoint', - namespaceType: 'agnostic', - type: 'endpoint', - }, - ], - showDetection: true, - showEndpoint: true, - }); - - expect(output).toEqual({ - ids: ['list_id'], - namespaces: ['single'], - }); - }); - }); - - describe('getGeneralFilters', () => { - test('it returns empty string if no filters', () => { - const filters = getGeneralFilters({}, ['exception-list']); - - expect(filters).toEqual(''); - }); - - test('it properly formats filters when one namespace type passed in', () => { - const filters = getGeneralFilters({ created_by: 'moi', name: 'Sample' }, ['exception-list']); - - expect(filters).toEqual( - '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample)' - ); - }); - - test('it properly formats filters when two namespace types passed in', () => { - const filters = getGeneralFilters({ created_by: 'moi', name: 'Sample' }, [ - 'exception-list', - 'exception-list-agnostic', - ]); - - expect(filters).toEqual( - '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample)' - ); - }); - }); - - describe('getTrustedAppsFilter', () => { - test('it returns filter to search for "exception-list" namespace trusted apps', () => { - const filter = getTrustedAppsFilter(true, ['exception-list']); - - expect(filter).toEqual('(exception-list.attributes.list_id: endpoint_trusted_apps*)'); - }); - - test('it returns filter to search for "exception-list" and "agnostic" namespace trusted apps', () => { - const filter = getTrustedAppsFilter(true, ['exception-list', 'exception-list-agnostic']); - - expect(filter).toEqual( - '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' - ); - }); - - test('it returns filter to exclude "exception-list" namespace trusted apps', () => { - const filter = getTrustedAppsFilter(false, ['exception-list']); - - expect(filter).toEqual('(not exception-list.attributes.list_id: endpoint_trusted_apps*)'); - }); - - test('it returns filter to exclude "exception-list" and "agnostic" namespace trusted apps', () => { - const filter = getTrustedAppsFilter(false, ['exception-list', 'exception-list-agnostic']); - - expect(filter).toEqual( - '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' - ); - }); - }); - - describe('getFilters', () => { - describe('single', () => { - test('it properly formats when no filters passed and "showTrustedApps" is false', () => { - const filter = getFilters({}, ['single'], false); - - expect(filter).toEqual('(not exception-list.attributes.list_id: endpoint_trusted_apps*)'); - }); - - test('it properly formats when no filters passed and "showTrustedApps" is true', () => { - const filter = getFilters({}, ['single'], true); - - expect(filter).toEqual('(exception-list.attributes.list_id: endpoint_trusted_apps*)'); - }); - - test('it properly formats when filters passed and "showTrustedApps" is false', () => { - const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['single'], false); - - expect(filter).toEqual( - '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*)' - ); - }); - - test('it if filters passed and "showTrustedApps" is true', () => { - const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['single'], true); - - expect(filter).toEqual( - '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps*)' - ); - }); - }); - - describe('agnostic', () => { - test('it properly formats when no filters passed and "showTrustedApps" is false', () => { - const filter = getFilters({}, ['agnostic'], false); - - expect(filter).toEqual( - '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' - ); - }); - - test('it properly formats when no filters passed and "showTrustedApps" is true', () => { - const filter = getFilters({}, ['agnostic'], true); - - expect(filter).toEqual( - '(exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' - ); - }); - - test('it properly formats when filters passed and "showTrustedApps" is false', () => { - const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['agnostic'], false); - - expect(filter).toEqual( - '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' - ); - }); - - test('it if filters passed and "showTrustedApps" is true', () => { - const filter = getFilters({ created_by: 'moi', name: 'Sample' }, ['agnostic'], true); - - expect(filter).toEqual( - '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' - ); - }); - }); - - describe('single, agnostic', () => { - test('it properly formats when no filters passed and "showTrustedApps" is false', () => { - const filter = getFilters({}, ['single', 'agnostic'], false); - - expect(filter).toEqual( - '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' - ); - }); - - test('it properly formats when no filters passed and "showTrustedApps" is true', () => { - const filter = getFilters({}, ['single', 'agnostic'], true); - - expect(filter).toEqual( - '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' - ); - }); - - test('it properly formats when filters passed and "showTrustedApps" is false', () => { - const filter = getFilters( - { created_by: 'moi', name: 'Sample' }, - ['single', 'agnostic'], - false - ); - - expect(filter).toEqual( - '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' - ); - }); - - test('it properly formats when filters passed and "showTrustedApps" is true', () => { - const filter = getFilters( - { created_by: 'moi', name: 'Sample' }, - ['single', 'agnostic'], - true - ); - - expect(filter).toEqual( - '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*)' - ); - }); - }); - }); -}); diff --git a/x-pack/plugins/lists/public/exceptions/utils.ts b/x-pack/plugins/lists/public/exceptions/utils.ts deleted file mode 100644 index 69803aebc6f6c9..00000000000000 --- a/x-pack/plugins/lists/public/exceptions/utils.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { get } from 'lodash/fp'; -import type { - ExceptionListFilter, - ExceptionListIdentifiers, - NamespaceType, - NamespaceTypeArray, -} from '@kbn/securitysolution-io-ts-list-types'; - -import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../common/constants'; -import { - SavedObjectType, - exceptionListAgnosticSavedObjectType, - exceptionListSavedObjectType, -} from '../../common/types'; - -export const getSavedObjectType = ({ - namespaceType, -}: { - namespaceType: NamespaceType; -}): SavedObjectType => { - if (namespaceType === 'agnostic') { - return exceptionListAgnosticSavedObjectType; - } else { - return exceptionListSavedObjectType; - } -}; - -export const getSavedObjectTypes = ({ - namespaceType, -}: { - namespaceType: NamespaceTypeArray; -}): SavedObjectType[] => { - return namespaceType.map((singleNamespaceType) => - getSavedObjectType({ namespaceType: singleNamespaceType }) - ); -}; - -export const getIdsAndNamespaces = ({ - lists, - showDetection, - showEndpoint, -}: { - lists: ExceptionListIdentifiers[]; - showDetection: boolean; - showEndpoint: boolean; -}): { ids: string[]; namespaces: NamespaceType[] } => - lists - .filter((list) => { - if (showDetection) { - return list.type === 'detection'; - } else if (showEndpoint) { - return list.type === 'endpoint'; - } else { - return true; - } - }) - .reduce<{ ids: string[]; namespaces: NamespaceType[] }>( - (acc, { listId, namespaceType }) => ({ - ids: [...acc.ids, listId], - namespaces: [...acc.namespaces, namespaceType], - }), - { ids: [], namespaces: [] } - ); - -export const getGeneralFilters = ( - filters: ExceptionListFilter, - namespaceTypes: SavedObjectType[] -): string => { - return Object.keys(filters) - .map((filterKey) => { - const value = get(filterKey, filters); - if (value != null && value.trim() !== '') { - const filtersByNamespace = namespaceTypes - .map((namespace) => { - const fieldToSearch = filterKey === 'name' ? 'name.text' : filterKey; - return `${namespace}.attributes.${fieldToSearch}:${value}`; - }) - .join(' OR '); - return `(${filtersByNamespace})`; - } else return null; - }) - .filter((item) => item != null) - .join(' AND '); -}; - -export const getTrustedAppsFilter = ( - showTrustedApps: boolean, - namespaceTypes: SavedObjectType[] -): string => { - if (showTrustedApps) { - const filters = namespaceTypes.map((namespace) => { - return `${namespace}.attributes.list_id: ${ENDPOINT_TRUSTED_APPS_LIST_ID}*`; - }); - return `(${filters.join(' OR ')})`; - } else { - const filters = namespaceTypes.map((namespace) => { - return `not ${namespace}.attributes.list_id: ${ENDPOINT_TRUSTED_APPS_LIST_ID}*`; - }); - return `(${filters.join(' AND ')})`; - } -}; - -export const getFilters = ( - filters: ExceptionListFilter, - namespaceTypes: NamespaceType[], - showTrustedApps: boolean -): string => { - const namespaces = getSavedObjectTypes({ namespaceType: namespaceTypes }); - const generalFilters = getGeneralFilters(filters, namespaces); - const trustedAppsFilter = getTrustedAppsFilter(showTrustedApps, namespaces); - return [generalFilters, trustedAppsFilter].filter((filter) => filter.trim() !== '').join(' AND '); -}; diff --git a/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts index be2125b6f250ae..46c139d1f37325 100644 --- a/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_endpoint_list_item_route.ts @@ -12,9 +12,9 @@ import { createEndpointListItemSchema, exceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { ENDPOINT_LIST_ID, ENDPOINT_LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { ENDPOINT_LIST_ID, ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, getExceptionListClient } from './utils'; import { validateExceptionListSize } from './validate'; diff --git a/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts index 0f30b7d40217f3..134682d3a42831 100644 --- a/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_endpoint_list_route.ts @@ -8,9 +8,9 @@ import { validate } from '@kbn/securitysolution-io-ts-utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { createEndpointListSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { ENDPOINT_LIST_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { ENDPOINT_LIST_URL } from '../../common/constants'; import { buildSiemResponse } from './utils'; import { getExceptionListClient } from './utils/get_exception_list_client'; diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts index e85c6480262f06..968f341b9decf5 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts @@ -12,9 +12,9 @@ import { createExceptionListItemSchema, exceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse } from './utils'; import { getExceptionListClient } from './utils/get_exception_list_client'; diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts index fb515591745e44..22be6432a70c10 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_route.ts @@ -12,9 +12,9 @@ import { createExceptionListSchema, exceptionListSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse } from './utils'; import { getExceptionListClient } from './utils/get_exception_list_client'; diff --git a/x-pack/plugins/lists/server/routes/create_list_index_route.ts b/x-pack/plugins/lists/server/routes/create_list_index_route.ts index db085befa5a2ee..cebd6140c36aa2 100644 --- a/x-pack/plugins/lists/server/routes/create_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_index_route.ts @@ -8,9 +8,9 @@ import { validate } from '@kbn/securitysolution-io-ts-utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { acknowledgeSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { LIST_INDEX } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { LIST_INDEX } from '../../common/constants'; import { buildSiemResponse } from './utils'; diff --git a/x-pack/plugins/lists/server/routes/create_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_list_item_route.ts index 961be259cd8d54..bbdc507ebdb699 100644 --- a/x-pack/plugins/lists/server/routes/create_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_item_route.ts @@ -8,9 +8,9 @@ import { validate } from '@kbn/securitysolution-io-ts-utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { createListItemSchema, listItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse } from './utils'; diff --git a/x-pack/plugins/lists/server/routes/create_list_route.ts b/x-pack/plugins/lists/server/routes/create_list_route.ts index 4135d6d794188d..898f9b142d9dbc 100644 --- a/x-pack/plugins/lists/server/routes/create_list_route.ts +++ b/x-pack/plugins/lists/server/routes/create_list_route.ts @@ -12,9 +12,9 @@ import { createListSchema, listSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { LIST_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse } from './utils'; diff --git a/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts index 725594c4cbf9e6..650d2d11b0d1f6 100644 --- a/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_endpoint_list_item_route.ts @@ -12,9 +12,9 @@ import { deleteEndpointListItemSchema, exceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { ENDPOINT_LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts index 0467b46c084911..7ec220bfb3a601 100644 --- a/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_exception_list_item_route.ts @@ -12,9 +12,9 @@ import { deleteExceptionListItemSchema, exceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, diff --git a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts index 7df509690ae904..b4928d0262528c 100644 --- a/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_exception_list_route.ts @@ -12,9 +12,9 @@ import { deleteExceptionListSchema, exceptionListSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, diff --git a/x-pack/plugins/lists/server/routes/delete_list_index_route.ts b/x-pack/plugins/lists/server/routes/delete_list_index_route.ts index 15b15ec746f231..448c6c76921642 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_index_route.ts @@ -8,9 +8,9 @@ import { validate } from '@kbn/securitysolution-io-ts-utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { acknowledgeSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { LIST_INDEX } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { LIST_INDEX } from '../../common/constants'; import { buildSiemResponse } from './utils'; diff --git a/x-pack/plugins/lists/server/routes/delete_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_list_item_route.ts index e9cc035dda85ae..cd6c0ee038b4d9 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_item_route.ts @@ -12,9 +12,9 @@ import { listItemArraySchema, listItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse } from './utils'; diff --git a/x-pack/plugins/lists/server/routes/delete_list_route.ts b/x-pack/plugins/lists/server/routes/delete_list_route.ts index e22fdc5df121e1..35ac490826703d 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_route.ts @@ -15,10 +15,10 @@ import { exceptionListItemSchema, listSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { getSavedObjectType } from '@kbn/securitysolution-list-utils'; +import { LIST_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { LIST_URL } from '../../common/constants'; -import { getSavedObjectType } from '../services/exception_lists/utils'; import { ExceptionListClient } from '../services/exception_lists/exception_list_client'; import { escapeQuotes } from '../services/utils/escape_query'; diff --git a/x-pack/plugins/lists/server/routes/export_exception_list_route.ts b/x-pack/plugins/lists/server/routes/export_exception_list_route.ts index 8f274d10a870be..a238d0e6529ff2 100644 --- a/x-pack/plugins/lists/server/routes/export_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/export_exception_list_route.ts @@ -7,9 +7,9 @@ import { transformError } from '@kbn/securitysolution-es-utils'; import { exportExceptionListQuerySchema } from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, getExceptionListClient } from './utils'; diff --git a/x-pack/plugins/lists/server/routes/export_list_item_route.ts b/x-pack/plugins/lists/server/routes/export_list_item_route.ts index d104f11024e934..413c911560e107 100644 --- a/x-pack/plugins/lists/server/routes/export_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/export_list_item_route.ts @@ -9,9 +9,9 @@ import { Stream } from 'stream'; import { transformError } from '@kbn/securitysolution-es-utils'; import { exportListItemQuerySchema } from '@kbn/securitysolution-io-ts-list-types'; +import { LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse } from './utils'; diff --git a/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts index 0284321ef4619c..36b5a66c2830f6 100644 --- a/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_endpoint_list_item_route.ts @@ -12,9 +12,9 @@ import { findEndpointListItemSchema, foundExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { ENDPOINT_LIST_ID, ENDPOINT_LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { ENDPOINT_LIST_ID, ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, getExceptionListClient } from './utils'; diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts index fc3450d2277d56..fe7ffaa0662815 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts @@ -12,9 +12,9 @@ import { findExceptionListItemSchema, foundExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, getExceptionListClient } from './utils'; diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts index 966aa1391a3125..5d1b78747a89e6 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_route.ts @@ -12,9 +12,9 @@ import { findExceptionListSchema, foundExceptionListSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse, getExceptionListClient } from './utils'; diff --git a/x-pack/plugins/lists/server/routes/find_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_list_item_route.ts index 1ae60f7faba357..b0a4a386e21e87 100644 --- a/x-pack/plugins/lists/server/routes/find_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_list_item_route.ts @@ -12,9 +12,9 @@ import { findListItemSchema, foundListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { LIST_ITEM_URL } from '../../common/constants'; import { decodeCursor } from '../services/utils'; import { buildRouteValidation, buildSiemResponse, getListClient } from './utils'; diff --git a/x-pack/plugins/lists/server/routes/find_list_route.ts b/x-pack/plugins/lists/server/routes/find_list_route.ts index 31e2773359ee1c..98697cc79030ca 100644 --- a/x-pack/plugins/lists/server/routes/find_list_route.ts +++ b/x-pack/plugins/lists/server/routes/find_list_route.ts @@ -8,9 +8,9 @@ import { validate } from '@kbn/securitysolution-io-ts-utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { findListSchema, foundListSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { LIST_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { LIST_URL } from '../../common/constants'; import { decodeCursor } from '../services/utils'; import { buildRouteValidation, buildSiemResponse, getListClient } from './utils'; diff --git a/x-pack/plugins/lists/server/routes/import_list_item_route.ts b/x-pack/plugins/lists/server/routes/import_list_item_route.ts index 3843f8389a4ed2..a911a6bc26eb07 100644 --- a/x-pack/plugins/lists/server/routes/import_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/import_list_item_route.ts @@ -9,9 +9,9 @@ import { schema } from '@kbn/config-schema'; import { validate } from '@kbn/securitysolution-io-ts-utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { importListItemQuerySchema, listSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { LIST_ITEM_URL } from '../../common/constants'; import { ConfigType } from '../config'; import { buildRouteValidation, buildSiemResponse } from './utils'; diff --git a/x-pack/plugins/lists/server/routes/patch_list_item_route.ts b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts index 3e85e501bd2162..7bbcab1b1dd622 100644 --- a/x-pack/plugins/lists/server/routes/patch_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts @@ -8,9 +8,9 @@ import { validate } from '@kbn/securitysolution-io-ts-utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { listItemSchema, patchListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse } from './utils'; diff --git a/x-pack/plugins/lists/server/routes/patch_list_route.ts b/x-pack/plugins/lists/server/routes/patch_list_route.ts index fb88432bb19602..da2ea0b65e4134 100644 --- a/x-pack/plugins/lists/server/routes/patch_list_route.ts +++ b/x-pack/plugins/lists/server/routes/patch_list_route.ts @@ -8,9 +8,9 @@ import { validate } from '@kbn/securitysolution-io-ts-utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { listSchema, patchListSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { LIST_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse } from './utils'; diff --git a/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts index be4a258cd5fb0f..aed51fbb4a5d0c 100644 --- a/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_endpoint_list_item_route.ts @@ -12,9 +12,9 @@ import { exceptionListItemSchema, readEndpointListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { ENDPOINT_LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts index e114625cde6a30..d1afb35d41a302 100644 --- a/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_exception_list_item_route.ts @@ -12,9 +12,9 @@ import { exceptionListItemSchema, readExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, diff --git a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts index e88eb9cbb07453..3b652db6fab634 100644 --- a/x-pack/plugins/lists/server/routes/read_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/read_exception_list_route.ts @@ -12,9 +12,9 @@ import { exceptionListSchema, readExceptionListSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, diff --git a/x-pack/plugins/lists/server/routes/read_list_index_route.ts b/x-pack/plugins/lists/server/routes/read_list_index_route.ts index 34344fb1051d29..6fd15e628edb01 100644 --- a/x-pack/plugins/lists/server/routes/read_list_index_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_index_route.ts @@ -8,9 +8,9 @@ import { validate } from '@kbn/securitysolution-io-ts-utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { listItemIndexExistSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { LIST_INDEX } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { LIST_INDEX } from '../../common/constants'; import { buildSiemResponse } from './utils'; diff --git a/x-pack/plugins/lists/server/routes/read_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_list_item_route.ts index 80a0b4b83514f6..2cb2c4e042884a 100644 --- a/x-pack/plugins/lists/server/routes/read_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_item_route.ts @@ -12,9 +12,9 @@ import { listItemSchema, readListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse } from './utils'; diff --git a/x-pack/plugins/lists/server/routes/read_list_route.ts b/x-pack/plugins/lists/server/routes/read_list_route.ts index 3446d5af7b5f1d..e4806f274d511a 100644 --- a/x-pack/plugins/lists/server/routes/read_list_route.ts +++ b/x-pack/plugins/lists/server/routes/read_list_route.ts @@ -8,9 +8,9 @@ import { validate } from '@kbn/securitysolution-io-ts-utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { listSchema, readListSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { LIST_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse } from './utils'; diff --git a/x-pack/plugins/lists/server/routes/read_privileges_route.ts b/x-pack/plugins/lists/server/routes/read_privileges_route.ts index 8c7faa7f7eb9d2..e0806a9f8f82ec 100644 --- a/x-pack/plugins/lists/server/routes/read_privileges_route.ts +++ b/x-pack/plugins/lists/server/routes/read_privileges_route.ts @@ -7,9 +7,9 @@ import { readPrivileges, transformError } from '@kbn/securitysolution-es-utils'; import { merge } from 'lodash/fp'; +import { LIST_PRIVILEGES_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { LIST_PRIVILEGES_URL } from '../../common/constants'; import { buildSiemResponse, getListClient } from './utils'; diff --git a/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts index 4f00c94bda73ae..2c960736460dd7 100644 --- a/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_endpoint_list_item_route.ts @@ -12,9 +12,9 @@ import { exceptionListItemSchema, updateEndpointListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { ENDPOINT_LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { ENDPOINT_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse } from './utils'; diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts index 453fdcbdfd916a..910d78e68061e7 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts @@ -13,9 +13,9 @@ import { updateExceptionListItemSchema, updateExceptionListItemValidate, } from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { EXCEPTION_LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse } from './utils'; diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts index ef3da5506ab23e..43279e1f1045de 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts @@ -12,9 +12,9 @@ import { exceptionListSchema, updateExceptionListSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { EXCEPTION_LIST_URL } from '../../common/constants'; import { buildRouteValidation, diff --git a/x-pack/plugins/lists/server/routes/update_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_list_item_route.ts index a8a0189dbb24b7..07ba8539ae624b 100644 --- a/x-pack/plugins/lists/server/routes/update_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_list_item_route.ts @@ -8,9 +8,9 @@ import { validate } from '@kbn/securitysolution-io-ts-utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { listItemSchema, updateListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { LIST_ITEM_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse } from './utils'; diff --git a/x-pack/plugins/lists/server/routes/update_list_route.ts b/x-pack/plugins/lists/server/routes/update_list_route.ts index c4293d7ca72f4a..47ab77b444196f 100644 --- a/x-pack/plugins/lists/server/routes/update_list_route.ts +++ b/x-pack/plugins/lists/server/routes/update_list_route.ts @@ -8,9 +8,9 @@ import { validate } from '@kbn/securitysolution-io-ts-utils'; import { transformError } from '@kbn/securitysolution-es-utils'; import { listSchema, updateListSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { LIST_URL } from '@kbn/securitysolution-list-constants'; import type { ListsPluginRouter } from '../types'; -import { LIST_URL } from '../../common/constants'; import { buildRouteValidation, buildSiemResponse } from './utils'; diff --git a/x-pack/plugins/lists/server/routes/validate.ts b/x-pack/plugins/lists/server/routes/validate.ts index 21e2ad187d0456..5a118bf2c5ae09 100644 --- a/x-pack/plugins/lists/server/routes/validate.ts +++ b/x-pack/plugins/lists/server/routes/validate.ts @@ -15,9 +15,9 @@ import { foundExceptionListItemSchema, nonEmptyEndpointEntriesArray, } from '@kbn/securitysolution-io-ts-list-types'; +import { MAX_EXCEPTION_LIST_SIZE } from '@kbn/securitysolution-list-constants'; import { ExceptionListClient } from '../services/exception_lists/exception_list_client'; -import { MAX_EXCEPTION_LIST_SIZE } from '../../common/constants'; export const validateExceptionListSize = async ( exceptionLists: ExceptionListClient, diff --git a/x-pack/plugins/lists/server/saved_objects/exception_list.ts b/x-pack/plugins/lists/server/saved_objects/exception_list.ts index d380e821034e91..8354e64d64a6ee 100644 --- a/x-pack/plugins/lists/server/saved_objects/exception_list.ts +++ b/x-pack/plugins/lists/server/saved_objects/exception_list.ts @@ -6,11 +6,10 @@ */ import { SavedObjectsType } from 'kibana/server'; - import { exceptionListAgnosticSavedObjectType, exceptionListSavedObjectType, -} from '../../common/types'; +} from '@kbn/securitysolution-list-utils'; import { migrations } from './migrations'; diff --git a/x-pack/plugins/lists/server/saved_objects/migrations.test.ts b/x-pack/plugins/lists/server/saved_objects/migrations.test.ts index 27c883d8b96743..7fea07f479cc89 100644 --- a/x-pack/plugins/lists/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/lists/server/saved_objects/migrations.test.ts @@ -7,8 +7,11 @@ import { SavedObjectUnsanitizedDoc } from 'kibana/server'; import uuid from 'uuid'; +import { + ENDPOINT_LIST_ID, + ENDPOINT_TRUSTED_APPS_LIST_ID, +} from '@kbn/securitysolution-list-constants'; -import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../common/constants'; import { ExceptionListSoSchema } from '../schemas/saved_objects'; import { OldExceptionListSoSchema, migrations } from './migrations'; diff --git a/x-pack/plugins/lists/server/saved_objects/migrations.ts b/x-pack/plugins/lists/server/saved_objects/migrations.ts index 485bd493f309e0..ec6617f6b47304 100644 --- a/x-pack/plugins/lists/server/saved_objects/migrations.ts +++ b/x-pack/plugins/lists/server/saved_objects/migrations.ts @@ -14,8 +14,11 @@ import { entriesNested, entry, } from '@kbn/securitysolution-io-ts-list-types'; +import { + ENDPOINT_LIST_ID, + ENDPOINT_TRUSTED_APPS_LIST_ID, +} from '@kbn/securitysolution-list-constants'; -import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../common/constants'; import { ExceptionListSoSchema } from '../schemas/saved_objects'; const entryType = t.union([entry, entriesNested]); diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts index c2a7218f1cef83..fda8de5da8aae9 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_endoint_event_filters_list.ts @@ -9,15 +9,16 @@ import { SavedObjectsClientContract } from 'kibana/server'; import uuid from 'uuid'; import { Version } from '@kbn/securitysolution-io-ts-types'; import type { ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types'; - +import { getSavedObjectType } from '@kbn/securitysolution-list-utils'; import { ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, ENDPOINT_EVENT_FILTERS_LIST_ID, ENDPOINT_EVENT_FILTERS_LIST_NAME, -} from '../../../common/constants'; +} from '@kbn/securitysolution-list-constants'; + import { ExceptionListSoSchema } from '../../schemas/saved_objects'; -import { getSavedObjectType, transformSavedObjectToExceptionList } from './utils'; +import { transformSavedObjectToExceptionList } from './utils'; interface CreateEndpointEventFiltersListOptions { savedObjectsClient: SavedObjectsClientContract; diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_list.ts index aaf18362ec7454..a677e260d08483 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_list.ts @@ -9,15 +9,16 @@ import { SavedObjectsClientContract } from 'kibana/server'; import uuid from 'uuid'; import { Version } from '@kbn/securitysolution-io-ts-types'; import type { ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types'; - +import { getSavedObjectType } from '@kbn/securitysolution-list-utils'; import { ENDPOINT_LIST_DESCRIPTION, ENDPOINT_LIST_ID, ENDPOINT_LIST_NAME, -} from '../../../common/constants'; +} from '@kbn/securitysolution-list-constants'; + import { ExceptionListSoSchema } from '../../schemas/saved_objects'; -import { getSavedObjectType, transformSavedObjectToExceptionList } from './utils'; +import { transformSavedObjectToExceptionList } from './utils'; interface CreateEndpointListOptions { savedObjectsClient: SavedObjectsClientContract; diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_trusted_apps_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_trusted_apps_list.ts index a85f6da0f8b8f5..4275e23032a76b 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_trusted_apps_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_endpoint_trusted_apps_list.ts @@ -9,15 +9,16 @@ import { SavedObjectsClientContract } from 'kibana/server'; import uuid from 'uuid'; import type { Version } from '@kbn/securitysolution-io-ts-types'; import type { ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types'; - +import { getSavedObjectType } from '@kbn/securitysolution-list-utils'; import { ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION, ENDPOINT_TRUSTED_APPS_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_NAME, -} from '../../../common/constants'; +} from '@kbn/securitysolution-list-constants'; + import { ExceptionListSoSchema } from '../../schemas/saved_objects'; -import { getSavedObjectType, transformSavedObjectToExceptionList } from './utils'; +import { transformSavedObjectToExceptionList } from './utils'; interface CreateEndpointListOptions { savedObjectsClient: SavedObjectsClientContract; diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts index f1d1f15a576a0f..60a53af4deb753 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list.ts @@ -19,10 +19,11 @@ import type { Tags, } from '@kbn/securitysolution-io-ts-list-types'; import { Version } from '@kbn/securitysolution-io-ts-types'; +import { getSavedObjectType } from '@kbn/securitysolution-list-utils'; import { ExceptionListSoSchema } from '../../schemas/saved_objects'; -import { getSavedObjectType, transformSavedObjectToExceptionList } from './utils'; +import { transformSavedObjectToExceptionList } from './utils'; interface CreateExceptionListOptions { listId: ListId; diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts index 763ee8e3d85c79..9771c7d485e50d 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts @@ -21,11 +21,11 @@ import type { OsTypeArray, Tags, } from '@kbn/securitysolution-io-ts-list-types'; +import { getSavedObjectType } from '@kbn/securitysolution-list-utils'; import { ExceptionListSoSchema } from '../../schemas/saved_objects'; import { - getSavedObjectType, transformCreateCommentsToComments, transformSavedObjectToExceptionListItem, } from './utils'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts index def0585740d453..be71f4c79577a2 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list.ts @@ -12,8 +12,8 @@ import type { ListIdOrUndefined, NamespaceType, } from '@kbn/securitysolution-io-ts-list-types'; +import { getSavedObjectType } from '@kbn/securitysolution-list-utils'; -import { getSavedObjectType } from './utils'; import { getExceptionList } from './get_exception_list'; import { deleteExceptionListItemByList } from './delete_exception_list_items_by_list'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts index 65df08a445b58e..89ea0baf22caf0 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_item.ts @@ -13,8 +13,8 @@ import type { ItemIdOrUndefined, NamespaceType, } from '@kbn/securitysolution-io-ts-list-types'; +import { getSavedObjectType } from '@kbn/securitysolution-list-utils'; -import { getSavedObjectType } from './utils'; import { getExceptionListItem } from './get_exception_list_item'; interface DeleteExceptionListItemOptions { diff --git a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts index b3190d174bb71b..aa2bd55e249999 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/delete_exception_list_items_by_list.ts @@ -6,11 +6,11 @@ */ import type { ListId, NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; +import { getSavedObjectType } from '@kbn/securitysolution-list-utils'; import { SavedObjectsClientContract } from '../../../../../../src/core/server/'; import { findExceptionListItem } from './find_exception_list_item'; -import { getSavedObjectType } from './utils'; const PER_PAGE = 100; diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts index a6132197760044..803cd04c1d1b44 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts @@ -12,8 +12,7 @@ import type { FoundExceptionListItemSchema, FoundExceptionListSchema, } from '@kbn/securitysolution-io-ts-list-types'; - -import { ENDPOINT_LIST_ID } from '../../../common/constants'; +import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants'; import { ConstructorOptions, diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts index 314cfc75e5a115..b3d5dd9ddb32b9 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list.ts @@ -15,11 +15,11 @@ import type { SortFieldOrUndefined, SortOrderOrUndefined, } from '@kbn/securitysolution-io-ts-list-types'; +import { SavedObjectType, getSavedObjectTypes } from '@kbn/securitysolution-list-utils'; -import { SavedObjectType } from '../../../common/types'; import { ExceptionListSoSchema } from '../../schemas/saved_objects'; -import { getSavedObjectTypes, transformSavedObjectsToFoundExceptionList } from './utils'; +import { transformSavedObjectsToFoundExceptionList } from './utils'; interface FindExceptionListOptions { namespaceType: NamespaceTypeArray; diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts index 04eca6a042acef..99298c0304c7dc 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts @@ -19,16 +19,17 @@ import type { EmptyStringArrayDecoded, NonEmptyStringArrayDecoded, } from '@kbn/securitysolution-io-ts-types'; - import { SavedObjectType, exceptionListAgnosticSavedObjectType, exceptionListSavedObjectType, -} from '../../../common/types'; + getSavedObjectTypes, +} from '@kbn/securitysolution-list-utils'; + import { escapeQuotes } from '../utils/escape_query'; import { ExceptionListSoSchema } from '../../schemas/saved_objects'; -import { getSavedObjectTypes, transformSavedObjectsToFoundExceptionListItem } from './utils'; +import { transformSavedObjectsToFoundExceptionListItem } from './utils'; import { getExceptionList } from './get_exception_list'; interface FindExceptionListItemsOptions { diff --git a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts index e1dc8bfe988472..2b09642f59c9c6 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list.ts @@ -11,6 +11,7 @@ import type { ListIdOrUndefined, NamespaceType, } from '@kbn/securitysolution-io-ts-list-types'; +import { getSavedObjectType } from '@kbn/securitysolution-list-utils'; import { SavedObjectsClientContract, @@ -18,7 +19,7 @@ import { } from '../../../../../../src/core/server/'; import { ExceptionListSoSchema } from '../../schemas/saved_objects'; -import { getSavedObjectType, transformSavedObjectToExceptionList } from './utils'; +import { transformSavedObjectToExceptionList } from './utils'; interface GetExceptionListOptions { id: IdOrUndefined; diff --git a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts index a8b201cff03972..ef825b4cf5e3e7 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/get_exception_list_item.ts @@ -11,6 +11,7 @@ import type { ItemIdOrUndefined, NamespaceType, } from '@kbn/securitysolution-io-ts-list-types'; +import { getSavedObjectType } from '@kbn/securitysolution-list-utils'; import { SavedObjectsClientContract, @@ -18,7 +19,7 @@ import { } from '../../../../../../src/core/server/'; import { ExceptionListSoSchema } from '../../schemas/saved_objects'; -import { getSavedObjectType, transformSavedObjectToExceptionListItem } from './utils'; +import { transformSavedObjectToExceptionListItem } from './utils'; interface GetExceptionListItemOptions { id: IdOrUndefined; diff --git a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts index 0cdae4375fa590..53e0f82a2ba765 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list.ts @@ -20,10 +20,11 @@ import type { _VersionOrUndefined, } from '@kbn/securitysolution-io-ts-list-types'; import { VersionOrUndefined } from '@kbn/securitysolution-io-ts-types'; +import { getSavedObjectType } from '@kbn/securitysolution-list-utils'; import { ExceptionListSoSchema } from '../../schemas/saved_objects'; -import { getSavedObjectType, transformSavedObjectUpdateToExceptionList } from './utils'; +import { transformSavedObjectUpdateToExceptionList } from './utils'; import { getExceptionList } from './get_exception_list'; interface UpdateExceptionListOptions { diff --git a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts index 2c1f5b81b2bcf4..d0505a95133857 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts @@ -21,11 +21,11 @@ import type { UpdateCommentsArrayOrUndefined, _VersionOrUndefined, } from '@kbn/securitysolution-io-ts-list-types'; +import { getSavedObjectType } from '@kbn/securitysolution-list-utils'; import { ExceptionListSoSchema } from '../../schemas/saved_objects'; import { - getSavedObjectType, transformSavedObjectUpdateToExceptionListItem, transformUpdateCommentsToComments, } from './utils'; diff --git a/x-pack/plugins/lists/server/services/exception_lists/utils.ts b/x-pack/plugins/lists/server/services/exception_lists/utils.ts index 7479510110709e..610f73d4c2e809 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/utils.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/utils.ts @@ -15,54 +15,14 @@ import { ExceptionListSchema, FoundExceptionListItemSchema, FoundExceptionListSchema, - NamespaceType, - NamespaceTypeArray, UpdateCommentsArrayOrUndefined, exceptionListItemType, exceptionListType, } from '@kbn/securitysolution-io-ts-list-types'; +import { getExceptionListType } from '@kbn/securitysolution-list-utils'; -import { - SavedObjectType, - exceptionListAgnosticSavedObjectType, - exceptionListSavedObjectType, -} from '../../../common/types'; import { ExceptionListSoSchema } from '../../schemas/saved_objects'; -export const getSavedObjectType = ({ - namespaceType, -}: { - namespaceType: NamespaceType; -}): SavedObjectType => { - if (namespaceType === 'agnostic') { - return exceptionListAgnosticSavedObjectType; - } else { - return exceptionListSavedObjectType; - } -}; - -export const getExceptionListType = ({ - savedObjectType, -}: { - savedObjectType: string; -}): NamespaceType => { - if (savedObjectType === exceptionListAgnosticSavedObjectType) { - return 'agnostic'; - } else { - return 'single'; - } -}; - -export const getSavedObjectTypes = ({ - namespaceType, -}: { - namespaceType: NamespaceTypeArray; -}): SavedObjectType[] => { - return namespaceType.map((singleNamespaceType) => - getSavedObjectType({ namespaceType: singleNamespaceType }) - ); -}; - export const transformSavedObjectToExceptionList = ({ savedObject, }: { diff --git a/x-pack/plugins/security_solution/common/detection_engine/parse_schedule_dates.ts b/x-pack/plugins/security_solution/common/detection_engine/parse_schedule_dates.ts deleted file mode 100644 index efb015ef73175a..00000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/parse_schedule_dates.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import moment from 'moment'; -import dateMath from '@elastic/datemath'; - -export const parseScheduleDates = (time: string): moment.Moment | null => { - const isValidDateString = !isNaN(Date.parse(time)); - const isValidInput = isValidDateString || time.trim().startsWith('now'); - const formattedDate = isValidDateString - ? moment(time) - : isValidInput - ? dateMath.parse(time) - : null; - - return formattedDate ?? null; -}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.mock.ts index 70f41539e84664..1114c2b5d0fe93 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.mock.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.mock.ts @@ -6,7 +6,7 @@ */ import { List, ListArray } from '@kbn/securitysolution-io-ts-list-types'; -import { ENDPOINT_LIST_ID } from '../../../shared_imports'; +import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants'; export const getListMock = (): List => ({ id: 'some_uuid', diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts index 1e0f7e087a5b31..52cd4e79e6a6b9 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts @@ -7,13 +7,14 @@ import { hasEqlSequenceQuery, - hasLargeValueList, hasNestedEntry, isThreatMatchRule, normalizeMachineLearningJobIds, normalizeThresholdField, } from './utils'; +import { hasLargeValueList } from '@kbn/securitysolution-list-utils'; + import type { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; describe('#hasLargeValueList', () => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts index 56f2a11900dd3c..f3f2280c4b8377 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts @@ -14,6 +14,7 @@ import type { } from '@kbn/securitysolution-io-ts-list-types'; import { Type } from '@kbn/securitysolution-io-ts-alerting-types'; +import { hasLargeValueList } from '@kbn/securitysolution-list-utils'; import { JobStatus, Threshold, ThresholdNormalized } from './schemas/common/schemas'; @@ -23,11 +24,6 @@ export const hasLargeValueItem = ( return exceptionItems.some((exceptionItem) => hasLargeValueList(exceptionItem.entries)); }; -export const hasLargeValueList = (entries: EntriesArray): boolean => { - const found = entries.filter(({ type }) => type === 'list'); - return found.length > 0; -}; - export const hasNestedEntry = (entries: EntriesArray): boolean => { const found = entries.filter(({ type }) => type === 'nested'); return found.length > 0; diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/event_filter_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/event_filter_generator.ts index c09f0989791600..8f0be96f61ddc3 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/event_filter_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/event_filter_generator.ts @@ -6,8 +6,8 @@ */ import type { CreateExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '@kbn/securitysolution-list-constants'; import { BaseDataGenerator } from './base_data_generator'; -import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '../../../../lists/common/constants'; import { getCreateExceptionListItemSchemaMock } from '../../../../lists/common/schemas/request/create_exception_list_item_schema.mock'; export class EventFilterGenerator extends BaseDataGenerator { diff --git a/x-pack/plugins/security_solution/common/fp_utils.ts b/x-pack/plugins/security_solution/common/fp_utils.ts deleted file mode 100644 index f6c0425073f862..00000000000000 --- a/x-pack/plugins/security_solution/common/fp_utils.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { TaskEither } from 'fp-ts/lib/TaskEither'; -import { fold } from 'fp-ts/lib/Either'; - -export const toPromise = async (taskEither: TaskEither): Promise => - pipe( - await taskEither(), - fold( - (e) => Promise.reject(e), - (a) => Promise.resolve(a) - ) - ); - -export const toError = (e: unknown): Error => (e instanceof Error ? e : new Error(String(e))); diff --git a/x-pack/plugins/security_solution/common/shared_imports.ts b/x-pack/plugins/security_solution/common/shared_imports.ts deleted file mode 100644 index 0fb63f6a646866..00000000000000 --- a/x-pack/plugins/security_solution/common/shared_imports.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { - ENDPOINT_LIST_ID, - ENDPOINT_TRUSTED_APPS_LIST_ID, - EXCEPTION_LIST_URL, - EXCEPTION_LIST_ITEM_URL, - ENDPOINT_EVENT_FILTERS_LIST_ID, - ENDPOINT_EVENT_FILTERS_LIST_NAME, - ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, -} from '../../lists/common'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx index 527f470217b421..4d6464d09ee608 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx @@ -17,6 +17,7 @@ import { addExceptionList, addEndpointExceptionList, } from '@kbn/securitysolution-list-api'; +import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants'; import { HttpStart } from '../../../../../../../src/core/public'; import { Rule } from '../../../detections/containers/detection_engine/rules/types'; @@ -24,7 +25,6 @@ import { fetchRuleById, patchRule, } from '../../../detections/containers/detection_engine/rules/api'; -import { ENDPOINT_LIST_ID } from '../../../../common/shared_imports'; export type ReturnUseFetchOrCreateRuleExceptionList = [boolean, ExceptionListSchema | null]; diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts index 788ce00ba1b1d3..2c9e37c6930f40 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts @@ -10,6 +10,7 @@ import { noop } from 'lodash/fp'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; +import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils'; import * as i18n from '../translations'; import { useKibana } from '../../../common/lib/kibana'; import { @@ -24,7 +25,6 @@ import { formatInspect, getEqlAggsData } from './helpers'; import { EqlPreviewResponse, EqlPreviewRequest, Source } from './types'; import { hasEqlSequenceQuery } from '../../../../common/detection_engine/utils'; import { EqlSearchResponse } from '../../../../common/detection_engine/types'; -import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; import { inputsModel } from '../../../common/store'; import { EQL_SEARCH_STRATEGY } from '../../../../../data_enhanced/public'; import { useAppToasts } from '../use_app_toasts'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index 5259b95a09ae6d..44a85cb2028c05 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -20,10 +20,10 @@ import { ThreatTechnique, Type, } from '@kbn/securitysolution-io-ts-alerting-types'; +import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants'; import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../../common/constants'; import { assertUnreachable } from '../../../../../../common/utility_types'; import { transformAlertToRuleAction } from '../../../../../../common/detection_engine/transform_actions'; -import { ENDPOINT_LIST_ID } from '../../../../../shared_imports'; import { Rule } from '../../../../containers/detection_engine/rules'; import { AboutStepRule, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index 25785f6bbcb2d4..a88ff9bb2c9210 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -18,13 +18,13 @@ import { SeverityMapping, Severity, } from '@kbn/securitysolution-io-ts-alerting-types'; +import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants'; import { ActionVariables } from '../../../../../../triggers_actions_ui/public'; import { normalizeThresholdField } from '../../../../../common/detection_engine/utils'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { assertUnreachable } from '../../../../../common/utility_types'; import { transformRuleToAlertAction } from '../../../../../common/detection_engine/transform_actions'; import { Filter } from '../../../../../../../../src/plugins/data/public'; -import { ENDPOINT_LIST_ID } from '../../../../shared_imports'; import { Rule } from '../../../containers/detection_engine/rules'; import { AboutStepRule, diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts index e1fa1107fcb019..6fb6b8081596e2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/constants.ts @@ -12,7 +12,7 @@ import { ENDPOINT_EVENT_FILTERS_LIST_ID, ENDPOINT_EVENT_FILTERS_LIST_NAME, ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, -} from '../../../../common/shared_imports'; +} from '@kbn/securitysolution-list-constants'; export const EVENT_FILTER_LIST_TYPE: ExceptionListType = ExceptionListTypeEnum.ENDPOINT_EVENTS; export const EVENT_FILTER_LIST = { diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts index 30b4c81ba0c3b8..8863acaafbf5a5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts @@ -12,7 +12,7 @@ import type { CreateExceptionListItemSchema, UpdateExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; -import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '../../../../shared_imports'; +import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '@kbn/securitysolution-list-constants'; import { Immutable } from '../../../../../common/endpoint/types'; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/test_utils/index.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/test_utils/index.ts index 69a8ee383be8e6..dc235cf511157a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/test_utils/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/test_utils/index.ts @@ -10,13 +10,13 @@ import type { FoundExceptionListItemSchema, ExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; import { Ecs } from '../../../../../common/ecs'; import { MANAGEMENT_STORE_GLOBAL_NAMESPACE, MANAGEMENT_STORE_EVENT_FILTERS_NAMESPACE, } from '../../../common/constants'; -import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '../../../../shared_imports'; import { eventFiltersPageReducer } from '../store/reducer'; import { diff --git a/x-pack/plugins/security_solution/public/shared_imports.ts b/x-pack/plugins/security_solution/public/shared_imports.ts index fb55d16fe9dfb1..dda4179cd853cf 100644 --- a/x-pack/plugins/security_solution/public/shared_imports.ts +++ b/x-pack/plugins/security_solution/public/shared_imports.ts @@ -5,8 +5,6 @@ * 2.0. */ -export * from '../common/shared_imports'; - export { getUseField, getFieldValidityAndErrorMessage, diff --git a/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts b/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts index e3579d11331ded..8ccc7c3913326e 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/event_filters/index.ts @@ -10,14 +10,14 @@ import { KbnClient } from '@kbn/test'; import { AxiosError } from 'axios'; import bluebird from 'bluebird'; import type { CreateExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { EventFilterGenerator } from '../../../common/endpoint/data_generators/event_filter_generator'; import { ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION, ENDPOINT_EVENT_FILTERS_LIST_ID, ENDPOINT_EVENT_FILTERS_LIST_NAME, EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL, -} from '../../../../lists/common/constants'; +} from '@kbn/securitysolution-list-constants'; +import { EventFilterGenerator } from '../../../common/endpoint/data_generators/event_filter_generator'; export const cli = () => { run( diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts index 9df242469752ed..16cbe618c5076a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts @@ -18,7 +18,10 @@ import { } from './lists'; import { TranslatedEntry, TranslatedExceptionListItem } from '../../schemas/artifacts'; import { ArtifactConstants } from './common'; -import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common'; +import { + ENDPOINT_LIST_ID, + ENDPOINT_TRUSTED_APPS_LIST_ID, +} from '@kbn/securitysolution-list-constants'; describe('artifacts lists', () => { let mockExceptionClient: ExceptionListClient; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts index 26212da1355db9..d2fad90d9e7d60 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts @@ -14,8 +14,12 @@ import type { } from '@kbn/securitysolution-io-ts-list-types'; import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { + ENDPOINT_EVENT_FILTERS_LIST_ID, + ENDPOINT_LIST_ID, + ENDPOINT_TRUSTED_APPS_LIST_ID, +} from '@kbn/securitysolution-list-constants'; import { ExceptionListClient } from '../../../../../lists/server'; -import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../common/shared_imports'; import { internalArtifactCompleteSchema, InternalArtifactCompleteSchema, @@ -33,7 +37,6 @@ import { WrappedTranslatedExceptionList, wrappedTranslatedExceptionList, } from '../../schemas'; -import { ENDPOINT_EVENT_FILTERS_LIST_ID } from '../../../../../lists/common/constants'; export async function buildArtifact( exceptions: WrappedTranslatedExceptionList, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts index 1a4ff2812cd235..8558f9a24d2135 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts @@ -17,7 +17,7 @@ import type { ExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; -import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; +import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants'; import type { CreateExceptionListItemOptions, UpdateExceptionListItemOptions, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts index 081010ea0098a6..33fbcd47a1c93b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts @@ -25,7 +25,7 @@ import { import { TrustedAppNotFoundError, TrustedAppVersionConflictError } from './errors'; import { toUpdateTrustedApp } from '../../../../common/endpoint/service/trusted_apps/to_update_trusted_app'; import { updateExceptionListItemImplementationMock } from './test_utils'; -import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common'; +import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants'; const exceptionsListClient = listMock.getExceptionListClient() as jest.Mocked; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts index c30a7a9a38cdca..4456004022261f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts @@ -6,8 +6,8 @@ */ import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants'; import { ExceptionListClient } from '../../../../../lists/server'; -import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common'; import { DeleteTrustedAppsRequestParams, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts index 71be2400e2681c..e1de39482428d7 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts @@ -7,7 +7,10 @@ import { inflateSync } from 'zlib'; import { savedObjectsClientMock } from 'src/core/server/mocks'; -import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../../lists/common'; +import { + ENDPOINT_LIST_ID, + ENDPOINT_TRUSTED_APPS_LIST_ID, +} from '@kbn/securitysolution-list-constants'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { PackagePolicy } from '../../../../../../fleet/common/types/models'; import { getEmptyInternalArtifactMock } from '../../../schemas/artifacts/saved_objects.mock'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/create_migration_saved_object.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/create_migration_saved_object.ts index 3aee9db31bf1be..bb9f7599ca6d53 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/create_migration_saved_object.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/create_migration_saved_object.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { SavedObjectsClientContract } from 'src/core/server'; import { validateTaskEither } from '@kbn/securitysolution-io-ts-utils'; -import { toError, toPromise } from '../../../../common/fp_utils'; +import { toError, toPromise } from '@kbn/securitysolution-list-api'; import { signalsMigrationSOClient } from './saved_objects_client'; import { signalsMigrationSO, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/update_migration_saved_object.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/update_migration_saved_object.ts index 5bd8b47c3b74a8..91919246c15a39 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/update_migration_saved_object.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/migrations/update_migration_saved_object.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { SavedObjectsClientContract, SavedObjectsUpdateResponse } from 'src/core/server'; import { validateTaskEither } from '@kbn/securitysolution-io-ts-utils'; -import { toError, toPromise } from '../../../../common/fp_utils'; +import { toError, toPromise } from '@kbn/securitysolution-list-api'; import { signalsMigrationSOClient } from './saved_objects_client'; import { SignalsMigrationSOUpdateAttributes, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts index c1393924e3d29e..6f8dac5b49b31f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/rules_notification_alert_type.ts @@ -7,6 +7,7 @@ import { Logger } from 'src/core/server'; import { schema } from '@kbn/config-schema'; +import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils'; import { DEFAULT_RULE_NOTIFICATION_QUERY_SIZE, NOTIFICATIONS_ID, @@ -18,7 +19,6 @@ import { AlertAttributes } from '../signals/types'; import { siemRuleActionGroups } from '../signals/siem_rule_action_groups'; import { scheduleNotificationActions } from './schedule_notification_actions'; import { getNotificationResultsLink } from './utils'; -import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; import { getSignals } from './get_signals'; export const rulesNotificationAlertType = ({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts index 198a1e805febed..5907cbdfb7fdd5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts @@ -8,7 +8,8 @@ import type { estypes } from '@elastic/elasticsearch'; import { entriesList, ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { hasLargeValueList } from '../../../../../common/detection_engine/utils'; +import { hasLargeValueList } from '@kbn/securitysolution-list-utils'; + import { FilterEventsAgainstListOptions } from './types'; import { filterEvents } from './filter_events'; import { createFieldAndSetTuples } from './create_field_and_set_tuples'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 0f6ff40eaffb80..66f7d41df94ab6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -18,7 +18,7 @@ import { checkPrivileges, createSearchAfterReturnType, } from './utils'; -import * as parseScheduleDates from '../../../../common/detection_engine/parse_schedule_dates'; +import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils'; import { RuleExecutorOptions, SearchAfterAndBulkCreateReturnType } from './types'; import { scheduleNotificationActions } from '../notifications/schedule_notification_actions'; import { RuleAlertType } from '../rules/types'; @@ -48,6 +48,13 @@ jest.mock('./utils', () => { jest.mock('../notifications/schedule_notification_actions'); jest.mock('./executors/query'); jest.mock('./executors/ml'); +jest.mock('@kbn/securitysolution-io-ts-utils', () => { + const original = jest.requireActual('@kbn/securitysolution-io-ts-utils'); + return { + ...original, + parseScheduleDates: jest.fn(), + }; +}); const getPayload = ( ruleAlert: RuleAlertType, @@ -149,6 +156,7 @@ describe('signal_rule_alert_type', () => { (queryExecutor as jest.Mock).mockResolvedValue(executorReturnValue); (mlExecutor as jest.Mock).mockClear(); (mlExecutor as jest.Mock).mockResolvedValue(executorReturnValue); + (parseScheduleDates as jest.Mock).mockReturnValue(moment(100)); const value: Partial> = { statusCode: 200, body: { @@ -348,11 +356,7 @@ describe('signal_rule_alert_type', () => { }); payload.params.meta = {}; - const parseScheduleDatesSpy = jest - .spyOn(parseScheduleDates, 'parseScheduleDates') - .mockReturnValue(moment(100)); await alert.executor(payload); - parseScheduleDatesSpy.mockRestore(); expect(scheduleNotificationActions).toHaveBeenCalledWith( expect.objectContaining({ @@ -385,11 +389,7 @@ describe('signal_rule_alert_type', () => { }); delete payload.params.meta; - const parseScheduleDatesSpy = jest - .spyOn(parseScheduleDates, 'parseScheduleDates') - .mockReturnValue(moment(100)); await alert.executor(payload); - parseScheduleDatesSpy.mockRestore(); expect(scheduleNotificationActions).toHaveBeenCalledWith( expect.objectContaining({ @@ -422,11 +422,7 @@ describe('signal_rule_alert_type', () => { }); payload.params.meta = { kibana_siem_app_url: 'http://localhost' }; - const parseScheduleDatesSpy = jest - .spyOn(parseScheduleDates, 'parseScheduleDates') - .mockReturnValue(moment(100)); await alert.executor(payload); - parseScheduleDatesSpy.mockRestore(); expect(scheduleNotificationActions).toHaveBeenCalledWith( expect.objectContaining({ 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 d00bcc2a9f11e0..13a63df6ed8b61 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 @@ -12,8 +12,8 @@ import { chain, tryCatch } from 'fp-ts/lib/TaskEither'; import { flow } from 'fp-ts/lib/function'; import * as t from 'io-ts'; -import { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; -import { toError, toPromise } from '../../../../common/fp_utils'; +import { validateNonExact, parseScheduleDates } from '@kbn/securitysolution-io-ts-utils'; +import { toError, toPromise } from '@kbn/securitysolution-list-api'; import { SIGNALS_ID, @@ -27,7 +27,6 @@ import { isThreatMatchRule, isQueryRule, } from '../../../../common/detection_engine/utils'; -import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; import { SetupPlugins } from '../../../plugin'; import { getInputIndex } from './get_input_output_index'; import { AlertAttributes, SignalRuleAlertTypeDefinition } from './types'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index 37959a5ee877bf..b04eab1496e960 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -15,7 +15,6 @@ import { buildRuleMessageFactory } from './rule_messages'; import { ExceptionListClient } from '../../../../../lists/server'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; // @ts-expect-error moment.suppressDeprecationWarnings = true; @@ -115,34 +114,6 @@ describe('utils', () => { }); }); - describe('parseScheduleDates', () => { - test('it returns a moment when given an ISO string', () => { - const result = parseScheduleDates('2020-01-01T00:00:00.000Z'); - expect(result).not.toBeNull(); - expect(result).toEqual(moment('2020-01-01T00:00:00.000Z')); - }); - - test('it returns a moment when given `now`', () => { - const result = parseScheduleDates('now'); - - expect(result).not.toBeNull(); - expect(moment.isMoment(result)).toBeTruthy(); - }); - - test('it returns a moment when given `now-x`', () => { - const result = parseScheduleDates('now-6m'); - - expect(result).not.toBeNull(); - expect(moment.isMoment(result)).toBeTruthy(); - }); - - test('it returns null when given a string that is not an ISO string, `now` or `now-x`', () => { - const result = parseScheduleDates('invalid'); - - expect(result).toBeNull(); - }); - }); - describe('getDriftTolerance', () => { test('it returns a drift tolerance in milliseconds of 1 minute when "from" overlaps "to" by 1 minute and the interval is 5 minutes', () => { const drift = getDriftTolerance({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 488a46ab4748d2..03a067af6066d4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -15,6 +15,9 @@ import { ApiResponse, Context } from '@elastic/elasticsearch/lib/Transport'; import { SortResults } from '@elastic/elasticsearch/api/types'; import type { ListArray, ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { MAX_EXCEPTION_LIST_SIZE } from '@kbn/securitysolution-list-constants'; +import { hasLargeValueList } from '@kbn/securitysolution-list-utils'; +import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils'; import { TimestampOverrideOrUndefined, Privilege, @@ -37,9 +40,6 @@ import { RuleRangeTuple, } from './types'; import { BuildRuleMessage } from './rule_messages'; -import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; -import { hasLargeValueList } from '../../../../common/detection_engine/utils'; -import { MAX_EXCEPTION_LIST_SIZE } from '../../../../../lists/common/constants'; import { ShardError } from '../../types'; import { RuleStatusService } from './rule_status_service'; import { diff --git a/x-pack/test/api_integration/apis/lists/create_exception_list_item.ts b/x-pack/test/api_integration/apis/lists/create_exception_list_item.ts index d2f82b374e02a0..db3cdd17a89dc1 100644 --- a/x-pack/test/api_integration/apis/lists/create_exception_list_item.ts +++ b/x-pack/test/api_integration/apis/lists/create_exception_list_item.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect/expect.js'; +import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { ENDPOINT_LIST_ID } from '../../../../plugins/lists/common'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts index c014d08e91f66f..6a14de3c476085 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts @@ -9,6 +9,7 @@ import expect from '@kbn/expect'; import type { CreateExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; import { CreateRulesSchema, EqlCreateSchema, @@ -25,10 +26,6 @@ import { } from '../../../lists_api_integration/utils'; import { RulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/response'; import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; -import { - EXCEPTION_LIST_ITEM_URL, - EXCEPTION_LIST_URL, -} from '../../../../plugins/lists/common/constants'; import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 3a06ea1c8bc7a3..9c5beebd64b3d3 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -19,6 +19,7 @@ import type { ExceptionListItemSchema, ExceptionListSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; import { PrePackagedRulesAndTimelinesStatusSchema } from '../../plugins/security_solution/common/detection_engine/schemas/response'; import { getCreateExceptionListDetectionSchemaMock } from '../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; import { @@ -27,7 +28,6 @@ import { FullResponseSchema, QueryCreateSchema, } from '../../plugins/security_solution/common/detection_engine/schemas/request'; -import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '../../plugins/lists/common/constants'; import { Signal } from '../../plugins/security_solution/server/lib/detection_engine/signals/types'; import { signalsMigrationType } from '../../plugins/security_solution/server/lib/detection_engine/migrations/saved_objects'; import { diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/create_exception_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/create_exception_list_items.ts index b394b0a21e59c2..4541a758a02f1b 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/create_exception_list_items.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/create_exception_list_items.ts @@ -8,16 +8,13 @@ import expect from '@kbn/expect'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; import { getExceptionListItemResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_item_schema.mock'; import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; import { getCreateExceptionListItemMinimalSchemaMock, getCreateExceptionListItemMinimalSchemaMockWithoutId, } from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock'; -import { - EXCEPTION_LIST_ITEM_URL, - EXCEPTION_LIST_URL, -} from '../../../../plugins/lists/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/create_exception_lists.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/create_exception_lists.ts index 840a425b4bf5e4..7e34c80806cc44 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/create_exception_lists.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/create_exception_lists.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import type { ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { EXCEPTION_LIST_URL } from '../../../../plugins/lists/common/constants'; +import { EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { getExceptionResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_schema.mock'; import { diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/create_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/create_list_items.ts index 1b4e671d0bc498..b084e423e88eef 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/create_list_items.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/create_list_items.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; -import { LIST_URL, LIST_ITEM_URL } from '../../../../plugins/lists/common/constants'; +import { LIST_URL, LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { getCreateMinimalListSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_list_schema.mock'; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/create_lists.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/create_lists.ts index 2e48437e924128..1b955f88bf929b 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/create_lists.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/create_lists.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; -import { LIST_URL } from '../../../../plugins/lists/common/constants'; +import { LIST_URL } from '@kbn/securitysolution-list-constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { getCreateMinimalListSchemaMock, diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_exception_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_exception_list_items.ts index 4cf95daa146d3f..229d5737a99bb5 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_exception_list_items.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_exception_list_items.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_URL, EXCEPTION_LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import { getExceptionListItemResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_item_schema.mock'; import { getCreateExceptionListItemMinimalSchemaMock, @@ -15,10 +16,6 @@ import { } from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock'; import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { - EXCEPTION_LIST_URL, - EXCEPTION_LIST_ITEM_URL, -} from '../../../../plugins/lists/common/constants'; import { deleteAllExceptions, removeExceptionListItemServerGeneratedProperties } from '../../utils'; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_exception_lists.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_exception_lists.ts index 4b8b9b84f5dfc2..d83c4bdc2f1a9a 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_exception_lists.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_exception_lists.ts @@ -8,13 +8,13 @@ import expect from '@kbn/expect'; import type { ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; import { getExceptionResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_schema.mock'; import { getCreateExceptionListMinimalSchemaMock, getCreateExceptionListMinimalSchemaMockWithoutId, } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { EXCEPTION_LIST_URL } from '../../../../plugins/lists/common/constants'; import { deleteAllExceptions, removeExceptionListServerGeneratedProperties } from '../../utils'; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_list_items.ts index 48abc236b6bd61..9de5ec575ef327 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_list_items.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_list_items.ts @@ -7,10 +7,10 @@ import expect from '@kbn/expect'; +import { LIST_URL, LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import { getListItemResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/list_item_schema.mock'; import { getCreateMinimalListItemSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_list_item_schema.mock'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { LIST_URL, LIST_ITEM_URL } from '../../../../plugins/lists/common/constants'; import { getCreateMinimalListSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_list_schema.mock'; import { diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_lists.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_lists.ts index 4ce3c7f0e5661d..5d1abf6f74f7ef 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_lists.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_lists.ts @@ -7,13 +7,13 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL, LIST_ITEM_URL, LIST_URL, -} from '../../../../plugins/lists/common/constants'; +} from '@kbn/securitysolution-list-constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; import { getCreateMinimalListSchemaMock, diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/export_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/export_list_items.ts index 563c0c5b3c313f..efcc10518c6c06 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/export_list_items.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/export_list_items.ts @@ -7,13 +7,12 @@ import expect from '@kbn/expect'; import type { CreateListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { LIST_ITEM_URL, LIST_URL } from '@kbn/securitysolution-list-constants'; import { getCreateMinimalListItemSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_list_item_schema.mock'; import { getCreateMinimalListSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_list_schema.mock'; import { LIST_ID, NAME } from '../../../../plugins/lists/common/constants.mock'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { LIST_ITEM_URL, LIST_URL } from '../../../../plugins/lists/common/constants'; - import { createListsIndex, deleteListsIndex, binaryToString } from '../../utils'; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_list_items.ts index 1085498d7c3a68..7b23ab7d6f8668 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_list_items.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_list_items.ts @@ -7,14 +7,11 @@ import expect from '@kbn/expect'; +import { EXCEPTION_LIST_URL, EXCEPTION_LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import { getExceptionListItemResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_item_schema.mock'; import { getCreateExceptionListItemMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock'; import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { - EXCEPTION_LIST_URL, - EXCEPTION_LIST_ITEM_URL, -} from '../../../../plugins/lists/common/constants'; import { deleteAllExceptions, removeExceptionListItemServerGeneratedProperties } from '../../utils'; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_lists.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_lists.ts index 111300abf3a817..9972ed6a89171c 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_lists.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_lists.ts @@ -7,10 +7,10 @@ import expect from '@kbn/expect'; +import { EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; import { getExceptionResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_schema.mock'; import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { EXCEPTION_LIST_URL } from '../../../../plugins/lists/common/constants'; import { deleteAllExceptions, removeExceptionListServerGeneratedProperties } from '../../utils'; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/find_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/find_list_items.ts index 7e0441baec4b7b..9708abba4e2064 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/find_list_items.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/find_list_items.ts @@ -7,11 +7,11 @@ import expect from '@kbn/expect'; +import { LIST_URL, LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import { LIST_ITEM_ID, LIST_ID } from '../../../../plugins/lists/common/constants.mock'; import { getListItemResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/list_item_schema.mock'; import { getCreateMinimalListItemSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_list_item_schema.mock'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { LIST_URL, LIST_ITEM_URL } from '../../../../plugins/lists/common/constants'; import { getCreateMinimalListSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_list_schema.mock'; import { diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/find_lists.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/find_lists.ts index 3e843dbf39012c..b6677ec09cfeb2 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/find_lists.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/find_lists.ts @@ -7,8 +7,8 @@ import expect from '@kbn/expect'; +import { LIST_URL } from '@kbn/securitysolution-list-constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { LIST_URL } from '../../../../plugins/lists/common/constants'; import { getCreateMinimalListSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_list_schema.mock'; import { diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/import_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/import_list_items.ts index d80b538882bb89..db8b35a805fbc4 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/import_list_items.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/import_list_items.ts @@ -7,12 +7,11 @@ import expect from '@kbn/expect'; import type { ListSchema, ListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import { getListItemResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/list_item_schema.mock'; import { getListResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/list_schema.mock'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { LIST_ITEM_URL } from '../../../../plugins/lists/common/constants'; - import { createListsIndex, deleteListsIndex, diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/read_exception_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/read_exception_list_items.ts index b3af396e27021c..0a7caed8a5e149 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/read_exception_list_items.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/read_exception_list_items.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_URL, EXCEPTION_LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import { getExceptionListItemResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_item_schema.mock'; import { getCreateExceptionListItemMinimalSchemaMock, @@ -15,10 +16,6 @@ import { } from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock'; import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { - EXCEPTION_LIST_URL, - EXCEPTION_LIST_ITEM_URL, -} from '../../../../plugins/lists/common/constants'; import { deleteAllExceptions, removeExceptionListItemServerGeneratedProperties } from '../../utils'; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/read_exception_lists.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/read_exception_lists.ts index a53f3d1d2bded6..db53a0ed18a0c8 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/read_exception_lists.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/read_exception_lists.ts @@ -8,13 +8,13 @@ import expect from '@kbn/expect'; import type { ExceptionListSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; import { getExceptionResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_schema.mock'; import { getCreateExceptionListMinimalSchemaMock, getCreateExceptionListMinimalSchemaMockWithoutId, } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { EXCEPTION_LIST_URL } from '../../../../plugins/lists/common/constants'; import { deleteAllExceptions, removeExceptionListServerGeneratedProperties } from '../../utils'; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/read_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/read_list_items.ts index b463ea17c419ac..f53e9c7434e357 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/read_list_items.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/read_list_items.ts @@ -7,10 +7,10 @@ import expect from '@kbn/expect'; +import { LIST_URL, LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import { getListItemResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/list_item_schema.mock'; import { getCreateMinimalListItemSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_list_item_schema.mock'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { LIST_URL, LIST_ITEM_URL } from '../../../../plugins/lists/common/constants'; import { getCreateMinimalListSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_list_schema.mock'; import { diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/read_list_privileges.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/read_list_privileges.ts index 0ad39e177b4f38..b90a3f86a290d1 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/read_list_privileges.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/read_list_privileges.ts @@ -7,9 +7,9 @@ import expect from '@kbn/expect'; +import { LIST_PRIVILEGES_URL } from '@kbn/securitysolution-list-constants'; import { getReadPrivilegeMock } from '../../../../plugins/lists/server/routes/read_privileges_route.mock'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { LIST_PRIVILEGES_URL } from '../../../../plugins/lists/common/constants'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/read_lists.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/read_lists.ts index 050aeb9ffa9c6d..af81801fb5e918 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/read_lists.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/read_lists.ts @@ -7,8 +7,8 @@ import expect from '@kbn/expect'; +import { LIST_URL } from '@kbn/securitysolution-list-constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { LIST_URL } from '../../../../plugins/lists/common/constants'; import { getCreateMinimalListSchemaMock, diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/update_exception_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/update_exception_list_items.ts index d072a967722953..b611d5c31de672 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/update_exception_list_items.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/update_exception_list_items.ts @@ -11,14 +11,11 @@ import type { UpdateExceptionListItemSchema, ExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_URL, EXCEPTION_LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import { getExceptionListItemResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_item_schema.mock'; import { getCreateExceptionListItemMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock'; import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { - EXCEPTION_LIST_URL, - EXCEPTION_LIST_ITEM_URL, -} from '../../../../plugins/lists/common/constants'; import { deleteAllExceptions, removeExceptionListServerGeneratedProperties } from '../../utils'; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/update_exception_lists.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/update_exception_lists.ts index 6f5866e8968ff0..75064860da1c2d 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/update_exception_lists.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/update_exception_lists.ts @@ -11,10 +11,10 @@ import type { UpdateExceptionListSchema, ExceptionListSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; import { getExceptionResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_schema.mock'; import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { EXCEPTION_LIST_URL } from '../../../../plugins/lists/common/constants'; import { deleteAllExceptions, removeExceptionListServerGeneratedProperties } from '../../utils'; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/update_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/update_list_items.ts index fdcb7eeacdbad9..38d36ba3d7eee6 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/update_list_items.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/update_list_items.ts @@ -12,10 +12,10 @@ import type { CreateListItemSchema, ListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { LIST_URL, LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import { getListItemResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/list_item_schema.mock'; import { getCreateMinimalListItemSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_list_item_schema.mock'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { LIST_URL, LIST_ITEM_URL } from '../../../../plugins/lists/common/constants'; import { getCreateMinimalListSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_list_schema.mock'; import { diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/update_lists.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/update_lists.ts index ad42f6f9e9e6eb..2e3f48354b22a0 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/update_lists.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/update_lists.ts @@ -8,8 +8,8 @@ import expect from '@kbn/expect'; import type { UpdateListSchema, ListSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { LIST_URL } from '@kbn/securitysolution-list-constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { LIST_URL } from '../../../../plugins/lists/common/constants'; import { getCreateMinimalListSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_list_schema.mock'; import { diff --git a/x-pack/test/lists_api_integration/utils.ts b/x-pack/test/lists_api_integration/utils.ts index 81a4298ea1d0c1..2467e613ca2f58 100644 --- a/x-pack/test/lists_api_integration/utils.ts +++ b/x-pack/test/lists_api_integration/utils.ts @@ -16,8 +16,8 @@ import type { ExceptionListSchema, ExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; +import { LIST_INDEX, LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import { getImportListItemAsBuffer } from '../../plugins/lists/common/schemas/request/import_list_item_schema.mock'; -import { LIST_INDEX, LIST_ITEM_URL } from '../../plugins/lists/common/constants'; import { countDownES, countDownTest } from '../detection_engine_api_integration/utils'; /** diff --git a/yarn.lock b/yarn.lock index f0bea3118d8878..f1f421e2a766f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2702,10 +2702,6 @@ version "0.0.0" uid "" -"@kbn/securitysolution-constants@link:bazel-bin/packages/kbn-securitysolution-constants/npm_module": - version "0.0.0" - uid "" - "@kbn/securitysolution-es-utils@link:bazel-bin/packages/kbn-securitysolution-es-utils/npm_module": version "0.0.0" uid "" @@ -2730,6 +2726,10 @@ version "0.0.0" uid "" +"@kbn/securitysolution-list-constants@link:bazel-bin/packages/kbn-securitysolution-list-constants/npm_module": + version "0.0.0" + uid "" + "@kbn/securitysolution-list-hooks@link:bazel-bin/packages/kbn-securitysolution-list-hooks/npm_module": version "0.0.0" uid "" From 7bd46ec20493e68de3a6820e2dbeb4ada5c966d6 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 25 May 2021 01:01:47 -0600 Subject: [PATCH 08/78] [test/functional/context] include missing await (#100422) Co-authored-by: spalger Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- test/functional/apps/context/_discover_navigation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/apps/context/_discover_navigation.js b/test/functional/apps/context/_discover_navigation.js index 1c3862e07e9d7b..a09be8b35ba8f6 100644 --- a/test/functional/apps/context/_discover_navigation.js +++ b/test/functional/apps/context/_discover_navigation.js @@ -130,7 +130,7 @@ export default function ({ getService, getPageObjects }) { const alert = await browser.getAlert(); await alert?.accept(); expect(await browser.getCurrentUrl()).to.contain('#/doc'); - retry.waitFor('doc view being rendered', async () => { + await retry.waitFor('doc view being rendered', async () => { return await PageObjects.discover.isShowingDocViewer(); }); }); From 818fa90fbbec3f75d9603becd25098896f45b091 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Tue, 25 May 2021 12:30:20 +0300 Subject: [PATCH 09/78] [TSVB] Fix more than one empty labels in markdown breaks the values for all other labels (#100432) * Fixed multiple empty labels monitoring with []. * Fixed regexp group behavior and replaced by ''.replaceAll. * Changed ''.replaceAll with ''.split(...).join(...) * Added test for (empty) label case. * Removed not necessary comment. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/application/components/lib/replace_vars.js | 2 +- .../application/components/lib/replace_vars.test.js | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.js b/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.js index 6e5e423d868075..458866f2098a0d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.js @@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n'; export function replaceVars(str, args = {}, vars = {}) { try { // we need add '[]' for emptyLabel because this value contains special characters. (https://handlebarsjs.com/guide/expressions.html#literal-segments) - const template = handlebars.compile(str.replace(emptyLabel, `[${emptyLabel}]`), { + const template = handlebars.compile(str.split(emptyLabel).join(`[${emptyLabel}]`), { strict: true, knownHelpersOnly: true, }); diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.js index 95fa0b6255d03b..b1586718149e28 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.js @@ -7,6 +7,7 @@ */ import { replaceVars } from './replace_vars'; +import { emptyLabel } from '../../../../common/empty_label'; describe('replaceVars(str, args, vars)', () => { test('replaces vars with values', () => { @@ -27,4 +28,11 @@ describe('replaceVars(str, args, vars)', () => { const template = '# {{args.host}} {{total'; expect(replaceVars(template, args, vars)).toEqual('# {{args.host}} {{total'); }); + + test('replaces (empty).some_path with values', () => { + const vars = { [emptyLabel]: { d: { raw: 100 } } }; + const args = {}; + const template = `# {{ ${emptyLabel}.d.raw }} {{ ${emptyLabel}.d.raw }}`; + expect(replaceVars(template, args, vars)).toEqual('# 100 100'); + }); }); From 206a64fc4cb96d66bf1a32e683817deeeea10598 Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Tue, 25 May 2021 14:49:51 +0200 Subject: [PATCH 10/78] [Security Solution] Application register deepLinks instead of meta.searchDeepLinks (#100129) * refactor meta.searchDeepLinks to deepLinks and remove meta * remove comments * obsolete snapshot removed * documentation updated * [deepLinks] refactor ml nav ids and translation keys * flaky test fix attempt [sync with glo] * default deepLinks navLinkStatus * api_docs restored Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...kibana-plugin-core-public.app.deeplinks.md | 39 ++++ .../kibana-plugin-core-public.app.keywords.md | 13 ++ .../public/kibana-plugin-core-public.app.md | 3 +- .../kibana-plugin-core-public.app.meta.md | 43 ---- .../kibana-plugin-core-public.appdeeplink.md | 26 +++ ...ana-plugin-core-public.appmeta.keywords.md | 13 -- .../kibana-plugin-core-public.appmeta.md | 23 -- ...gin-core-public.appmeta.searchdeeplinks.md | 13 -- ...na-plugin-core-public.appsearchdeeplink.md | 26 --- ...a-plugin-core-public.appupdatablefields.md | 2 +- .../core/public/kibana-plugin-core-public.md | 6 +- ...lugin-core-public.publicappdeeplinkinfo.md | 17 ++ ...kibana-plugin-core-public.publicappinfo.md | 5 +- ...na-plugin-core-public.publicappmetainfo.md | 16 -- ...core-public.publicappsearchdeeplinkinfo.md | 16 -- .../application/application_service.test.ts | 10 +- .../application/application_service.tsx | 11 + src/core/public/application/index.ts | 6 +- src/core/public/application/types.ts | 99 ++++----- .../application/utils/get_app_info.test.ts | 126 +++++------ .../public/application/utils/get_app_info.ts | 30 ++- .../chrome/nav_links/to_nav_link.test.ts | 6 +- src/core/public/index.ts | 6 +- src/core/public/public.api.md | 59 +++--- src/plugins/dev_tools/public/plugin.ts | 6 +- src/plugins/management/public/plugin.ts | 32 ++- .../management/public/utils/management_app.ts | 9 +- .../app/Main/route_config/index.tsx | 6 +- x-pack/plugins/apm/public/plugin.ts | 84 ++++---- .../public/providers/application.test.ts | 6 +- .../public/providers/get_app_results.test.ts | 200 ++++++++---------- .../public/providers/get_app_results.ts | 21 +- .../infra/public/pages/logs/page_content.tsx | 2 +- .../infra/public/pages/metrics/index.tsx | 2 +- x-pack/plugins/infra/public/plugin.ts | 114 +++++----- .../register_search_links.ts | 16 +- .../search_deep_links.ts | 78 +++---- .../detection_alerts/alerts_details.spec.ts | 2 +- .../public/app/search/index.test.ts | 9 +- .../public/app/search/index.ts | 18 +- .../security_solution/public/app/types.ts | 6 +- .../security_solution/public/plugin.tsx | 16 +- .../translations/translations/ja-JP.json | 20 +- .../translations/translations/zh-CN.json | 20 +- x-pack/plugins/uptime/public/apps/plugin.ts | 42 ++-- 45 files changed, 598 insertions(+), 725 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.app.deeplinks.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.app.keywords.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.app.meta.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.appdeeplink.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.appmeta.keywords.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.appmeta.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.appmeta.searchdeeplinks.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.appsearchdeeplink.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.publicappdeeplinkinfo.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.publicappmetainfo.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.publicappsearchdeeplinkinfo.md diff --git a/docs/development/core/public/kibana-plugin-core-public.app.deeplinks.md b/docs/development/core/public/kibana-plugin-core-public.app.deeplinks.md new file mode 100644 index 00000000000000..0392cb7eaefb02 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.app.deeplinks.md @@ -0,0 +1,39 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [deepLinks](./kibana-plugin-core-public.app.deeplinks.md) + +## App.deepLinks property + +Input type for registering secondary in-app locations for an application. + +Deep links must include at least one of `path` or `deepLinks`. A deep link that does not have a `path` represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. + +Signature: + +```typescript +deepLinks?: AppDeepLink[]; +``` + +## Example + + +```ts +core.application.register({ + id: 'my_app', + title: 'Translated title', + keywords: ['translated keyword1', 'translated keyword2'], + deepLinks: [ + { id: 'sub1', title: 'Sub1', path: '/sub1', keywords: ['subpath1'] }, + { + id: 'sub2', + title: 'Sub2', + deepLinks: [ + { id: 'subsub', title: 'SubSub', path: '/sub2/sub', keywords: ['subpath2'] } + ] + } + ], + mount: () => { ... } +}) + +``` + diff --git a/docs/development/core/public/kibana-plugin-core-public.app.keywords.md b/docs/development/core/public/kibana-plugin-core-public.app.keywords.md new file mode 100644 index 00000000000000..585df1b48c16e7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.app.keywords.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [keywords](./kibana-plugin-core-public.app.keywords.md) + +## App.keywords property + +Optional keywords to match with in deep links search. Omit if this part of the hierarchy does not have a page URL. + +Signature: + +```typescript +keywords?: string[]; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.app.md b/docs/development/core/public/kibana-plugin-core-public.app.md index 9a508f293d8e8c..721d9a2f121c73 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.md @@ -19,12 +19,13 @@ export interface App | [capabilities](./kibana-plugin-core-public.app.capabilities.md) | Partial<Capabilities> | Custom capabilities defined by the app. | | [category](./kibana-plugin-core-public.app.category.md) | AppCategory | The category definition of the product See [AppCategory](./kibana-plugin-core-public.appcategory.md) See DEFAULT\_APP\_CATEGORIES for more reference | | [chromeless](./kibana-plugin-core-public.app.chromeless.md) | boolean | Hide the UI chrome when the application is mounted. Defaults to false. Takes precedence over chrome service visibility settings. | +| [deepLinks](./kibana-plugin-core-public.app.deeplinks.md) | AppDeepLink[] | Input type for registering secondary in-app locations for an application.Deep links must include at least one of path or deepLinks. A deep link that does not have a path represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. | | [defaultPath](./kibana-plugin-core-public.app.defaultpath.md) | string | Allow to define the default path a user should be directed to when navigating to the app. When defined, this value will be used as a default for the path option when calling [navigateToApp](./kibana-plugin-core-public.applicationstart.navigatetoapp.md)\`, and will also be appended to the [application navLink](./kibana-plugin-core-public.chromenavlink.md) in the navigation bar. | | [euiIconType](./kibana-plugin-core-public.app.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precendence over the icon property. | | [exactRoute](./kibana-plugin-core-public.app.exactroute.md) | boolean | If set to true, the application's route will only be checked against an exact match. Defaults to false. | | [icon](./kibana-plugin-core-public.app.icon.md) | string | A URL to an image file used as an icon. Used as a fallback if euiIconType is not provided. | | [id](./kibana-plugin-core-public.app.id.md) | string | The unique identifier of the application | -| [meta](./kibana-plugin-core-public.app.meta.md) | AppMeta | Meta data for an application that represent additional information for the app. See [AppMeta](./kibana-plugin-core-public.appmeta.md) | +| [keywords](./kibana-plugin-core-public.app.keywords.md) | string[] | Optional keywords to match with in deep links search. Omit if this part of the hierarchy does not have a page URL. | | [mount](./kibana-plugin-core-public.app.mount.md) | AppMount<HistoryLocationState> | A mount function called when the user navigates to this app's route. | | [navLinkStatus](./kibana-plugin-core-public.app.navlinkstatus.md) | AppNavLinkStatus | The initial status of the application's navLink. Defaulting to visible if status is accessible and hidden if status is inaccessible See [AppNavLinkStatus](./kibana-plugin-core-public.appnavlinkstatus.md) | | [order](./kibana-plugin-core-public.app.order.md) | number | An ordinal used to sort nav links relative to one another for display. | diff --git a/docs/development/core/public/kibana-plugin-core-public.app.meta.md b/docs/development/core/public/kibana-plugin-core-public.app.meta.md deleted file mode 100644 index 574fa11605aec9..00000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.app.meta.md +++ /dev/null @@ -1,43 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [meta](./kibana-plugin-core-public.app.meta.md) - -## App.meta property - -Meta data for an application that represent additional information for the app. See [AppMeta](./kibana-plugin-core-public.appmeta.md) - -Signature: - -```typescript -meta?: AppMeta; -``` - -## Remarks - -Used to populate navigational search results (where available). Can be updated using the [App.updater$](./kibana-plugin-core-public.app.updater_.md) observable. See [PublicAppSearchDeepLinkInfo](./kibana-plugin-core-public.publicappsearchdeeplinkinfo.md) for more details. - -## Example - - -```ts -core.application.register({ - id: 'my_app', - title: 'Translated title', - meta: { - keywords: ['translated keyword1', 'translated keyword2'], - searchDeepLinks: [ - { id: 'sub1', title: 'Sub1', path: '/sub1', keywords: ['subpath1'] }, - { - id: 'sub2', - title: 'Sub2', - searchDeepLinks: [ - { id: 'subsub', title: 'SubSub', path: '/sub2/sub', keywords: ['subpath2'] } - ] - } - ], - }, - mount: () => { ... } -}) - -``` - diff --git a/docs/development/core/public/kibana-plugin-core-public.appdeeplink.md b/docs/development/core/public/kibana-plugin-core-public.appdeeplink.md new file mode 100644 index 00000000000000..5aa951cffdcb54 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.appdeeplink.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppDeepLink](./kibana-plugin-core-public.appdeeplink.md) + +## AppDeepLink type + +Input type for registering secondary in-app locations for an application. + +Deep links must include at least one of `path` or `deepLinks`. A deep link that does not have a `path` represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. + +Signature: + +```typescript +export declare type AppDeepLink = { + id: string; + title: string; + keywords?: string[]; + navLinkStatus?: AppNavLinkStatus; +} & ({ + path: string; + deepLinks?: AppDeepLink[]; +} | { + path?: string; + deepLinks: AppDeepLink[]; +}); +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appmeta.keywords.md b/docs/development/core/public/kibana-plugin-core-public.appmeta.keywords.md deleted file mode 100644 index 13709df68e76a1..00000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appmeta.keywords.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppMeta](./kibana-plugin-core-public.appmeta.md) > [keywords](./kibana-plugin-core-public.appmeta.keywords.md) - -## AppMeta.keywords property - -Keywords to represent this application - -Signature: - -```typescript -keywords?: string[]; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appmeta.md b/docs/development/core/public/kibana-plugin-core-public.appmeta.md deleted file mode 100644 index a2b72f7ec799d2..00000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appmeta.md +++ /dev/null @@ -1,23 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppMeta](./kibana-plugin-core-public.appmeta.md) - -## AppMeta interface - -Input type for meta data for an application. - -Meta fields include `keywords` and `searchDeepLinks` Keywords is an array of string with which to associate the app, must include at least one unique string as an array. `searchDeepLinks` is an array of links that represent secondary in-app locations for the app. - -Signature: - -```typescript -export interface AppMeta -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [keywords](./kibana-plugin-core-public.appmeta.keywords.md) | string[] | Keywords to represent this application | -| [searchDeepLinks](./kibana-plugin-core-public.appmeta.searchdeeplinks.md) | AppSearchDeepLink[] | Array of links that represent secondary in-app locations for the app. | - diff --git a/docs/development/core/public/kibana-plugin-core-public.appmeta.searchdeeplinks.md b/docs/development/core/public/kibana-plugin-core-public.appmeta.searchdeeplinks.md deleted file mode 100644 index 7ec0bbaa4b418b..00000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appmeta.searchdeeplinks.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppMeta](./kibana-plugin-core-public.appmeta.md) > [searchDeepLinks](./kibana-plugin-core-public.appmeta.searchdeeplinks.md) - -## AppMeta.searchDeepLinks property - -Array of links that represent secondary in-app locations for the app. - -Signature: - -```typescript -searchDeepLinks?: AppSearchDeepLink[]; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appsearchdeeplink.md b/docs/development/core/public/kibana-plugin-core-public.appsearchdeeplink.md deleted file mode 100644 index 29aad675fb105d..00000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.appsearchdeeplink.md +++ /dev/null @@ -1,26 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppSearchDeepLink](./kibana-plugin-core-public.appsearchdeeplink.md) - -## AppSearchDeepLink type - -Input type for registering secondary in-app locations for an application. - -Deep links must include at least one of `path` or `searchDeepLinks`. A deep link that does not have a `path` represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. - -Signature: - -```typescript -export declare type AppSearchDeepLink = { - id: string; - title: string; -} & ({ - path: string; - searchDeepLinks?: AppSearchDeepLink[]; - keywords?: string[]; -} | { - path?: string; - searchDeepLinks: AppSearchDeepLink[]; - keywords?: string[]; -}); -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md b/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md index 55672d9339f615..d7b12d4b707010 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md +++ b/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md @@ -9,5 +9,5 @@ Defines the list of fields that can be updated via an [AppUpdater](./kibana-plug Signature: ```typescript -export declare type AppUpdatableFields = Pick; +export declare type AppUpdatableFields = Pick; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index 3972f737f66183..6239279f275d1c 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -37,7 +37,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AppLeaveDefaultAction](./kibana-plugin-core-public.appleavedefaultaction.md) | Action to return from a [AppLeaveHandler](./kibana-plugin-core-public.appleavehandler.md) to execute the default behaviour when leaving the application.See | | [ApplicationSetup](./kibana-plugin-core-public.applicationsetup.md) | | | [ApplicationStart](./kibana-plugin-core-public.applicationstart.md) | | -| [AppMeta](./kibana-plugin-core-public.appmeta.md) | Input type for meta data for an application.Meta fields include keywords and searchDeepLinks Keywords is an array of string with which to associate the app, must include at least one unique string as an array. searchDeepLinks is an array of links that represent secondary in-app locations for the app. | | [AppMountParameters](./kibana-plugin-core-public.appmountparameters.md) | | | [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) | A plugin with asynchronous lifecycle methods. | | [Capabilities](./kibana-plugin-core-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. | @@ -144,10 +143,10 @@ The plugin integrates with the core system via lifecycle events: `setup` | Type Alias | Description | | --- | --- | +| [AppDeepLink](./kibana-plugin-core-public.appdeeplink.md) | Input type for registering secondary in-app locations for an application.Deep links must include at least one of path or deepLinks. A deep link that does not have a path represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. | | [AppLeaveAction](./kibana-plugin-core-public.appleaveaction.md) | Possible actions to return from a [AppLeaveHandler](./kibana-plugin-core-public.appleavehandler.md)See [AppLeaveConfirmAction](./kibana-plugin-core-public.appleaveconfirmaction.md) and [AppLeaveDefaultAction](./kibana-plugin-core-public.appleavedefaultaction.md) | | [AppLeaveHandler](./kibana-plugin-core-public.appleavehandler.md) | A handler that will be executed before leaving the application, either when going to another application or when closing the browser tab or manually changing the url. Should return confirm to prompt a message to the user before leaving the page, or default to keep the default behavior (doing nothing).See [AppMountParameters](./kibana-plugin-core-public.appmountparameters.md) for detailed usage examples. | | [AppMount](./kibana-plugin-core-public.appmount.md) | A mount function called when the user navigates to this app's route. | -| [AppSearchDeepLink](./kibana-plugin-core-public.appsearchdeeplink.md) | Input type for registering secondary in-app locations for an application.Deep links must include at least one of path or searchDeepLinks. A deep link that does not have a path represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. | | [AppUnmount](./kibana-plugin-core-public.appunmount.md) | A function called when an application should be unmounted from the page. This function should be synchronous. | | [AppUpdatableFields](./kibana-plugin-core-public.appupdatablefields.md) | Defines the list of fields that can be updated via an [AppUpdater](./kibana-plugin-core-public.appupdater.md). | | [AppUpdater](./kibana-plugin-core-public.appupdater.md) | Updater for applications. see [ApplicationSetup](./kibana-plugin-core-public.applicationsetup.md) | @@ -161,9 +160,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [NavType](./kibana-plugin-core-public.navtype.md) | | | [PluginInitializer](./kibana-plugin-core-public.plugininitializer.md) | The plugin export at the root of a plugin's public directory should conform to this interface. | | [PluginOpaqueId](./kibana-plugin-core-public.pluginopaqueid.md) | | +| [PublicAppDeepLinkInfo](./kibana-plugin-core-public.publicappdeeplinkinfo.md) | Public information about a registered app's [deepLinks](./kibana-plugin-core-public.appdeeplink.md) | | [PublicAppInfo](./kibana-plugin-core-public.publicappinfo.md) | Public information about a registered [application](./kibana-plugin-core-public.app.md) | -| [PublicAppMetaInfo](./kibana-plugin-core-public.publicappmetainfo.md) | Public information about a registered app's [keywords](./kibana-plugin-core-public.appmeta.md) | -| [PublicAppSearchDeepLinkInfo](./kibana-plugin-core-public.publicappsearchdeeplinkinfo.md) | Public information about a registered app's [searchDeepLinks](./kibana-plugin-core-public.appsearchdeeplink.md) | | [PublicUiSettingsParams](./kibana-plugin-core-public.publicuisettingsparams.md) | A sub-set of [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) exposed to the client-side. | | [ResolveDeprecationResponse](./kibana-plugin-core-public.resolvedeprecationresponse.md) | | | [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value | diff --git a/docs/development/core/public/kibana-plugin-core-public.publicappdeeplinkinfo.md b/docs/development/core/public/kibana-plugin-core-public.publicappdeeplinkinfo.md new file mode 100644 index 00000000000000..d3a6a4de905fdf --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.publicappdeeplinkinfo.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [PublicAppDeepLinkInfo](./kibana-plugin-core-public.publicappdeeplinkinfo.md) + +## PublicAppDeepLinkInfo type + +Public information about a registered app's [deepLinks](./kibana-plugin-core-public.appdeeplink.md) + +Signature: + +```typescript +export declare type PublicAppDeepLinkInfo = Omit & { + deepLinks: PublicAppDeepLinkInfo[]; + keywords: string[]; + navLinkStatus: AppNavLinkStatus; +}; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md b/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md index 9f45a06935fe4c..a5563eae83563f 100644 --- a/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md +++ b/docs/development/core/public/kibana-plugin-core-public.publicappinfo.md @@ -9,10 +9,11 @@ Public information about a registered [application](./kibana-plugin-core-public. Signature: ```typescript -export declare type PublicAppInfo = Omit & { +export declare type PublicAppInfo = Omit & { status: AppStatus; navLinkStatus: AppNavLinkStatus; appRoute: string; - meta: PublicAppMetaInfo; + keywords: string[]; + deepLinks: PublicAppDeepLinkInfo[]; }; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.publicappmetainfo.md b/docs/development/core/public/kibana-plugin-core-public.publicappmetainfo.md deleted file mode 100644 index 3ef0460aec4670..00000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.publicappmetainfo.md +++ /dev/null @@ -1,16 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [PublicAppMetaInfo](./kibana-plugin-core-public.publicappmetainfo.md) - -## PublicAppMetaInfo type - -Public information about a registered app's [keywords](./kibana-plugin-core-public.appmeta.md) - -Signature: - -```typescript -export declare type PublicAppMetaInfo = Omit & { - keywords: string[]; - searchDeepLinks: PublicAppSearchDeepLinkInfo[]; -}; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.publicappsearchdeeplinkinfo.md b/docs/development/core/public/kibana-plugin-core-public.publicappsearchdeeplinkinfo.md deleted file mode 100644 index e88cdb7d55edd6..00000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.publicappsearchdeeplinkinfo.md +++ /dev/null @@ -1,16 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [PublicAppSearchDeepLinkInfo](./kibana-plugin-core-public.publicappsearchdeeplinkinfo.md) - -## PublicAppSearchDeepLinkInfo type - -Public information about a registered app's [searchDeepLinks](./kibana-plugin-core-public.appsearchdeeplink.md) - -Signature: - -```typescript -export declare type PublicAppSearchDeepLinkInfo = Omit & { - searchDeepLinks: PublicAppSearchDeepLinkInfo[]; - keywords: string[]; -}; -``` diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index 76b9c7a73d3bd1..2e2f1cad49f199 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -75,7 +75,10 @@ describe('#setup()', () => { const pluginId = Symbol('plugin'); const updater$ = new BehaviorSubject((app) => ({})); setup.register(pluginId, createApp({ id: 'app1', updater$ })); - setup.register(pluginId, createApp({ id: 'app2' })); + setup.register( + pluginId, + createApp({ id: 'app2', deepLinks: [{ id: 'subapp1', title: 'Subapp', path: '/subapp' }] }) + ); const { applications$ } = await service.start(startDeps); let applications = await applications$.pipe(take(1)).toPromise(); @@ -92,6 +95,11 @@ describe('#setup()', () => { id: 'app2', navLinkStatus: AppNavLinkStatus.visible, status: AppStatus.accessible, + deepLinks: [ + expect.objectContaining({ + navLinkStatus: AppNavLinkStatus.hidden, + }), + ], }) ); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 4a93c98205b842..bbfea61220b513 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -19,6 +19,7 @@ import { AppRouter } from './ui'; import { Capabilities, CapabilitiesService } from './capabilities'; import { App, + AppDeepLink, AppLeaveHandler, AppMount, AppNavLinkStatus, @@ -166,6 +167,7 @@ export class ApplicationService { ...appProps, status: app.status ?? AppStatus.accessible, navLinkStatus: app.navLinkStatus ?? AppNavLinkStatus.default, + deepLinks: populateDeepLinkDefaults(appProps.deepLinks), }); if (updater$) { registerStatusUpdater(app.id, updater$); @@ -392,3 +394,12 @@ const updateStatus = (app: App, statusUpdaters: AppUpdaterWrapper[]): App => { ...changes, }; }; + +const populateDeepLinkDefaults = (deepLinks?: AppDeepLink[]): AppDeepLink[] => { + if (!deepLinks) return []; + return deepLinks.map((deepLink) => ({ + ...deepLink, + navLinkStatus: deepLink.navLinkStatus ?? AppNavLinkStatus.default, + deepLinks: populateDeepLinkDefaults(deepLink.deepLinks), + })); +}; diff --git a/src/core/public/application/index.ts b/src/core/public/application/index.ts index 1e9a91717e81ae..68e1991646afbd 100644 --- a/src/core/public/application/index.ts +++ b/src/core/public/application/index.ts @@ -18,8 +18,7 @@ export type { AppMountParameters, AppUpdatableFields, AppUpdater, - AppMeta, - AppSearchDeepLink, + AppDeepLink, ApplicationSetup, ApplicationStart, AppLeaveHandler, @@ -29,8 +28,7 @@ export type { AppLeaveConfirmAction, NavigateToAppOptions, PublicAppInfo, - PublicAppMetaInfo, - PublicAppSearchDeepLinkInfo, + PublicAppDeepLinkInfo, // Internal types InternalApplicationSetup, InternalApplicationStart, diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 24f46752f28e58..ffc41955360bdf 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -63,7 +63,7 @@ export enum AppNavLinkStatus { */ export type AppUpdatableFields = Pick< App, - 'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath' | 'meta' + 'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath' | 'deepLinks' >; /** @@ -211,106 +211,92 @@ export interface App { */ exactRoute?: boolean; + /** Optional keywords to match with in deep links search. Omit if this part of the hierarchy does not have a page URL. */ + keywords?: string[]; + /** - * Meta data for an application that represent additional information for the app. - * See {@link AppMeta} + * Input type for registering secondary in-app locations for an application. * - * @remarks - * Used to populate navigational search results (where available). - * Can be updated using the {@link App.updater$} observable. See {@link PublicAppSearchDeepLinkInfo} for more details. + * Deep links must include at least one of `path` or `deepLinks`. A deep link that does not have a `path` + * represents a topological level in the application's hierarchy, but does not have a destination URL that is + * user-accessible. * * @example * ```ts * core.application.register({ * id: 'my_app', * title: 'Translated title', - * meta: { - * keywords: ['translated keyword1', 'translated keyword2'], - * searchDeepLinks: [ - * { id: 'sub1', title: 'Sub1', path: '/sub1', keywords: ['subpath1'] }, + * keywords: ['translated keyword1', 'translated keyword2'], + * deepLinks: [ + * { + * id: 'sub1', + * title: 'Sub1', + * path: '/sub1', + * keywords: ['subpath1'], + * }, * { * id: 'sub2', * title: 'Sub2', - * searchDeepLinks: [ - * { id: 'subsub', title: 'SubSub', path: '/sub2/sub', keywords: ['subpath2'] } - * ] - * } + * deepLinks: [ + * { + * id: 'subsub', + * title: 'SubSub', + * path: '/sub2/sub', + * keywords: ['subpath2'], + * }, + * ], + * }, * ], - * }, * mount: () => { ... } * }) * ``` */ - meta?: AppMeta; -} - -/** - * Input type for meta data for an application. - * - * Meta fields include `keywords` and `searchDeepLinks` - * Keywords is an array of string with which to associate the app, must include at least one unique string as an array. - * `searchDeepLinks` is an array of links that represent secondary in-app locations for the app. - * @public - */ -export interface AppMeta { - /** Keywords to represent this application */ - keywords?: string[]; - /** Array of links that represent secondary in-app locations for the app. */ - searchDeepLinks?: AppSearchDeepLink[]; + deepLinks?: AppDeepLink[]; } /** - * Public information about a registered app's {@link AppMeta | keywords } + * Public information about a registered app's {@link AppDeepLink | deepLinks} * * @public */ -export type PublicAppMetaInfo = Omit & { - keywords: string[]; - searchDeepLinks: PublicAppSearchDeepLinkInfo[]; -}; - -/** - * Public information about a registered app's {@link AppSearchDeepLink | searchDeepLinks} - * - * @public - */ -export type PublicAppSearchDeepLinkInfo = Omit< - AppSearchDeepLink, - 'searchDeepLinks' | 'keywords' +export type PublicAppDeepLinkInfo = Omit< + AppDeepLink, + 'deepLinks' | 'keywords' | 'navLinkStatus' > & { - searchDeepLinks: PublicAppSearchDeepLinkInfo[]; + deepLinks: PublicAppDeepLinkInfo[]; keywords: string[]; + navLinkStatus: AppNavLinkStatus; }; /** * Input type for registering secondary in-app locations for an application. * - * Deep links must include at least one of `path` or `searchDeepLinks`. A deep link that does not have a `path` + * Deep links must include at least one of `path` or `deepLinks`. A deep link that does not have a `path` * represents a topological level in the application's hierarchy, but does not have a destination URL that is * user-accessible. * @public */ -export type AppSearchDeepLink = { +export type AppDeepLink = { /** Identifier to represent this sublink, should be unique for this application */ id: string; /** Title to label represent this deep link */ title: string; + /** Optional keywords to match with in deep links search. Omit if this part of the hierarchy does not have a page URL. */ + keywords?: string[]; + /** Optional status of the chrome navigation, defaults to `hidden` */ + navLinkStatus?: AppNavLinkStatus; } & ( | { /** URL path to access this link, relative to the application's appRoute. */ path: string; /** Optional array of links that are 'underneath' this section in the hierarchy */ - searchDeepLinks?: AppSearchDeepLink[]; - /** Optional keywords to match with in deep links search for the page at the path */ - keywords?: string[]; + deepLinks?: AppDeepLink[]; } | { /** Optional path to access this section. Omit if this part of the hierarchy does not have a page URL. */ path?: string; /** Array links that are 'underneath' this section in this hierarchy. */ - searchDeepLinks: AppSearchDeepLink[]; - /** Optional keywords to match with in deep links search. Omit if this part of the hierarchy does not have a page URL. */ - keywords?: string[]; + deepLinks: AppDeepLink[]; } ); @@ -319,12 +305,13 @@ export type AppSearchDeepLink = { * * @public */ -export type PublicAppInfo = Omit & { +export type PublicAppInfo = Omit & { // remove optional on fields populated with default values status: AppStatus; navLinkStatus: AppNavLinkStatus; appRoute: string; - meta: PublicAppMetaInfo; + keywords: string[]; + deepLinks: PublicAppDeepLinkInfo[]; }; /** diff --git a/src/core/public/application/utils/get_app_info.test.ts b/src/core/public/application/utils/get_app_info.test.ts index 28824867234ff9..ef4a06707d6664 100644 --- a/src/core/public/application/utils/get_app_info.test.ts +++ b/src/core/public/application/utils/get_app_info.test.ts @@ -32,24 +32,20 @@ describe('getAppInfo', () => { status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, appRoute: `/app/some-id`, - meta: { - keywords: [], - searchDeepLinks: [], - }, + keywords: [], + deepLinks: [], }); }); - it('populates default values for nested searchDeepLinks', () => { + it('populates default values for nested deepLinks', () => { const app = createApp({ - meta: { - searchDeepLinks: [ - { - id: 'sub-id', - title: 'sub-title', - searchDeepLinks: [{ id: 'sub-sub-id', title: 'sub-sub-title', path: '/sub-sub' }], - }, - ], - }, + deepLinks: [ + { + id: 'sub-id', + title: 'sub-title', + deepLinks: [{ id: 'sub-sub-id', title: 'sub-sub-title', path: '/sub-sub' }], + }, + ], }); const info = getAppInfo(app); @@ -59,25 +55,23 @@ describe('getAppInfo', () => { status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, appRoute: `/app/some-id`, - meta: { - keywords: [], - searchDeepLinks: [ - { - id: 'sub-id', - title: 'sub-title', - keywords: [], - searchDeepLinks: [ - { - id: 'sub-sub-id', - title: 'sub-sub-title', - path: '/sub-sub', - keywords: [], - searchDeepLinks: [], // default empty array added - }, - ], - }, - ], - }, + keywords: [], + deepLinks: [ + { + id: 'sub-id', + title: 'sub-title', + keywords: [], + deepLinks: [ + { + id: 'sub-sub-id', + title: 'sub-sub-title', + path: '/sub-sub', + keywords: [], + deepLinks: [], // default empty array added + }, + ], + }, + ], }); }); @@ -110,22 +104,20 @@ describe('getAppInfo', () => { it('adds default meta fields to sublinks when needed', () => { const app = createApp({ - meta: { - searchDeepLinks: [ - { - id: 'sub-id', - title: 'sub-title', - searchDeepLinks: [ - { - id: 'sub-sub-id', - title: 'sub-sub-title', - path: '/sub-sub', - keywords: ['sub sub'], - }, - ], - }, - ], - }, + deepLinks: [ + { + id: 'sub-id', + title: 'sub-title', + deepLinks: [ + { + id: 'sub-sub-id', + title: 'sub-sub-title', + path: '/sub-sub', + keywords: ['sub sub'], + }, + ], + }, + ], }); const info = getAppInfo(app); @@ -135,25 +127,23 @@ describe('getAppInfo', () => { status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, appRoute: `/app/some-id`, - meta: { - keywords: [], - searchDeepLinks: [ - { - id: 'sub-id', - title: 'sub-title', - keywords: [], // default empty array - searchDeepLinks: [ - { - id: 'sub-sub-id', - title: 'sub-sub-title', - path: '/sub-sub', - keywords: ['sub sub'], - searchDeepLinks: [], - }, - ], - }, - ], - }, + keywords: [], + deepLinks: [ + { + id: 'sub-id', + title: 'sub-title', + keywords: [], // default empty array + deepLinks: [ + { + id: 'sub-sub-id', + title: 'sub-sub-title', + path: '/sub-sub', + keywords: ['sub sub'], + deepLinks: [], + }, + ], + }, + ], }); }); }); diff --git a/src/core/public/application/utils/get_app_info.ts b/src/core/public/application/utils/get_app_info.ts index ca1e8ac8076463..4c94e24f501bc4 100644 --- a/src/core/public/application/utils/get_app_info.ts +++ b/src/core/public/application/utils/get_app_info.ts @@ -10,9 +10,9 @@ import { App, AppNavLinkStatus, AppStatus, - AppSearchDeepLink, + AppDeepLink, PublicAppInfo, - PublicAppSearchDeepLinkInfo, + PublicAppDeepLinkInfo, } from '../types'; export function getAppInfo(app: App): PublicAppInfo { @@ -28,29 +28,27 @@ export function getAppInfo(app: App): PublicAppInfo { status: app.status!, navLinkStatus, appRoute: app.appRoute!, - meta: { - keywords: app.meta?.keywords ?? [], - searchDeepLinks: getSearchDeepLinkInfos(app, app.meta?.searchDeepLinks), - }, + keywords: app.keywords ?? [], + deepLinks: getDeepLinkInfos(app.deepLinks), }; } -function getSearchDeepLinkInfos( - app: App, - searchDeepLinks?: AppSearchDeepLink[] -): PublicAppSearchDeepLinkInfo[] { - if (!searchDeepLinks) { - return []; - } +function getDeepLinkInfos(deepLinks?: AppDeepLink[]): PublicAppDeepLinkInfo[] { + if (!deepLinks) return []; - return searchDeepLinks.map( - (rawDeepLink): PublicAppSearchDeepLinkInfo => { + return deepLinks.map( + (rawDeepLink): PublicAppDeepLinkInfo => { + const navLinkStatus = + rawDeepLink.navLinkStatus === AppNavLinkStatus.default + ? AppNavLinkStatus.hidden + : rawDeepLink.navLinkStatus!; return { id: rawDeepLink.id, title: rawDeepLink.title, path: rawDeepLink.path, keywords: rawDeepLink.keywords ?? [], - searchDeepLinks: getSearchDeepLinkInfos(app, rawDeepLink.searchDeepLinks), + navLinkStatus, + deepLinks: getDeepLinkInfos(rawDeepLink.deepLinks), }; } ); diff --git a/src/core/public/chrome/nav_links/to_nav_link.test.ts b/src/core/public/chrome/nav_links/to_nav_link.test.ts index 41c4ff178d7378..db783d0028f075 100644 --- a/src/core/public/chrome/nav_links/to_nav_link.test.ts +++ b/src/core/public/chrome/nav_links/to_nav_link.test.ts @@ -17,10 +17,8 @@ const app = (props: Partial = {}): PublicAppInfo => ({ status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.default, appRoute: `/app/some-id`, - meta: { - keywords: [], - searchDeepLinks: [], - }, + keywords: [], + deepLinks: [], ...props, }); diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 129ae1c16f99b1..24b48683cdd937 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -89,13 +89,11 @@ export type { AppLeaveAction, AppLeaveDefaultAction, AppLeaveConfirmAction, - AppMeta, AppUpdatableFields, AppUpdater, - AppSearchDeepLink, + AppDeepLink, PublicAppInfo, - PublicAppMetaInfo, - PublicAppSearchDeepLinkInfo, + PublicAppDeepLinkInfo, NavigateToAppOptions, } from './application'; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 31e7a1e2321dfc..9f0c5135e702fa 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -58,12 +58,13 @@ export interface App { capabilities?: Partial; category?: AppCategory; chromeless?: boolean; + deepLinks?: AppDeepLink[]; defaultPath?: string; euiIconType?: string; exactRoute?: boolean; icon?: string; id: string; - meta?: AppMeta; + keywords?: string[]; mount: AppMount; navLinkStatus?: AppNavLinkStatus; order?: number; @@ -85,6 +86,20 @@ export interface AppCategory { order?: number; } +// @public +export type AppDeepLink = { + id: string; + title: string; + keywords?: string[]; + navLinkStatus?: AppNavLinkStatus; +} & ({ + path: string; + deepLinks?: AppDeepLink[]; +} | { + path?: string; + deepLinks: AppDeepLink[]; +}); + // @public export type AppLeaveAction = AppLeaveDefaultAction | AppLeaveConfirmAction; @@ -142,12 +157,6 @@ export interface ApplicationStart { navigateToUrl(url: string): Promise; } -// @public -export interface AppMeta { - keywords?: string[]; - searchDeepLinks?: AppSearchDeepLink[]; -} - // @public export type AppMount = (params: AppMountParameters) => AppUnmount | Promise; @@ -170,20 +179,6 @@ export enum AppNavLinkStatus { visible = 1 } -// @public -export type AppSearchDeepLink = { - id: string; - title: string; -} & ({ - path: string; - searchDeepLinks?: AppSearchDeepLink[]; - keywords?: string[]; -} | { - path?: string; - searchDeepLinks: AppSearchDeepLink[]; - keywords?: string[]; -}); - // @public export enum AppStatus { accessible = 0, @@ -194,7 +189,7 @@ export enum AppStatus { export type AppUnmount = () => void; // @public -export type AppUpdatableFields = Pick; +export type AppUpdatableFields = Pick; // @public export type AppUpdater = (app: App) => Partial | undefined; @@ -1071,23 +1066,19 @@ export interface PluginInitializerContext export type PluginOpaqueId = symbol; // @public -export type PublicAppInfo = Omit & { - status: AppStatus; - navLinkStatus: AppNavLinkStatus; - appRoute: string; - meta: PublicAppMetaInfo; -}; - -// @public -export type PublicAppMetaInfo = Omit & { +export type PublicAppDeepLinkInfo = Omit & { + deepLinks: PublicAppDeepLinkInfo[]; keywords: string[]; - searchDeepLinks: PublicAppSearchDeepLinkInfo[]; + navLinkStatus: AppNavLinkStatus; }; // @public -export type PublicAppSearchDeepLinkInfo = Omit & { - searchDeepLinks: PublicAppSearchDeepLinkInfo[]; +export type PublicAppInfo = Omit & { + status: AppStatus; + navLinkStatus: AppNavLinkStatus; + appRoute: string; keywords: string[]; + deepLinks: PublicAppDeepLinkInfo[]; }; // @public diff --git a/src/plugins/dev_tools/public/plugin.ts b/src/plugins/dev_tools/public/plugin.ts index e9f5d206de9180..5ccf614533164a 100644 --- a/src/plugins/dev_tools/public/plugin.ts +++ b/src/plugins/dev_tools/public/plugin.ts @@ -7,7 +7,7 @@ */ import { BehaviorSubject } from 'rxjs'; -import { Plugin, CoreSetup, AppMountParameters, AppSearchDeepLink } from 'src/core/public'; +import { Plugin, CoreSetup, AppMountParameters, AppDeepLink } from 'src/core/public'; import { AppUpdater } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { sortBy } from 'lodash'; @@ -86,7 +86,7 @@ export class DevToolsPlugin implements Plugin { this.appStateUpdater.next(() => ({ navLinkStatus: AppNavLinkStatus.hidden })); } else { this.appStateUpdater.next(() => { - const deepLinks: AppSearchDeepLink[] = [...this.devTools.values()] + const deepLinks: AppDeepLink[] = [...this.devTools.values()] .filter( // Some tools do not use a string title, so we filter those out (tool) => !tool.enableRouting && !tool.isDisabled() && typeof tool.title === 'string' @@ -96,7 +96,7 @@ export class DevToolsPlugin implements Plugin { title: tool.title as string, path: `#/${tool.id}`, })); - return { meta: { searchDeepLinks: deepLinks } }; + return { deepLinks }; }); } } diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index 0429ea8c9bd7bd..1f96ec87171c5c 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -20,7 +20,7 @@ import { AppUpdater, AppStatus, AppNavLinkStatus, - AppSearchDeepLink, + AppDeepLink, } from '../../../core/public'; import { MANAGEMENT_APP_ID } from '../common/contants'; @@ -38,22 +38,20 @@ export class ManagementPlugin implements Plugin(() => { - const deepLinks: AppSearchDeepLink[] = Object.values( - this.managementSections.definedSections - ).map((section: ManagementSection) => ({ - id: section.id, - title: section.title, - searchDeepLinks: section.getAppsEnabled().map((mgmtApp) => ({ - id: mgmtApp.id, - title: mgmtApp.title, - path: mgmtApp.basePath, - meta: { ...mgmtApp.meta }, - })), - })); - - return { - meta: { searchDeepLinks: deepLinks }, - }; + const deepLinks: AppDeepLink[] = Object.values(this.managementSections.definedSections).map( + (section: ManagementSection) => ({ + id: section.id, + title: section.title, + deepLinks: section.getAppsEnabled().map((mgmtApp) => ({ + id: mgmtApp.id, + title: mgmtApp.title, + path: mgmtApp.basePath, + keywords: mgmtApp.keywords, + })), + }) + ); + + return { deepLinks }; }); private hasAnyEnabledApps = true; diff --git a/src/plugins/management/public/utils/management_app.ts b/src/plugins/management/public/utils/management_app.ts index c9385463def5b4..3578b2ab0b7f2d 100644 --- a/src/plugins/management/public/utils/management_app.ts +++ b/src/plugins/management/public/utils/management_app.ts @@ -6,28 +6,25 @@ * Side Public License, v 1. */ -import { AppMeta } from 'kibana/public'; import { CreateManagementItemArgs, Mount } from '../types'; import { ManagementItem } from './management_item'; export interface RegisterManagementAppArgs extends CreateManagementItemArgs { mount: Mount; basePath: string; - meta?: AppMeta; + keywords?: string[]; } export class ManagementApp extends ManagementItem { public readonly mount: Mount; public readonly basePath: string; - public readonly meta: AppMeta; + public readonly keywords: string[]; constructor(args: RegisterManagementAppArgs) { super(args); this.mount = args.mount; this.basePath = args.basePath; - this.meta = { - keywords: args.meta?.keywords || [], - }; + this.keywords = args.keywords || []; } } diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx index 9410fd00411e38..89b8db5f386dcd 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx @@ -173,7 +173,7 @@ export const routes: APMRouteDefinition[] = [ render: renderAsRedirectTo('/services'), breadcrumb: 'APM', }, - // !! Need to be kept in sync with the searchDeepLinks in x-pack/plugins/apm/public/plugin.ts + // !! Need to be kept in sync with the deepLinks in x-pack/plugins/apm/public/plugin.ts { exact: true, path: '/services', @@ -182,7 +182,7 @@ export const routes: APMRouteDefinition[] = [ defaultMessage: 'Services', }), }, - // !! Need to be kept in sync with the searchDeepLinks in x-pack/plugins/apm/public/plugin.ts + // !! Need to be kept in sync with the deepLinks in x-pack/plugins/apm/public/plugin.ts { exact: true, path: '/traces', @@ -328,7 +328,7 @@ export const routes: APMRouteDefinition[] = [ component: TraceLink, breadcrumb: null, }, - // !! Need to be kept in sync with the searchDeepLinks in x-pack/plugins/apm/public/plugin.ts + // !! Need to be kept in sync with the deepLinks in x-pack/plugins/apm/public/plugin.ts { exact: true, path: '/service-map', diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index b76849ccf30115..10af1837dab42f 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -140,32 +140,30 @@ export class ApmPlugin implements Plugin { appRoute: '/app/apm', icon: 'plugins/apm/public/icon.svg', category: DEFAULT_APP_CATEGORIES.observability, - meta: { - // !! Need to be kept in sync with the routes in x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx - searchDeepLinks: [ - { - id: 'services', - title: i18n.translate('xpack.apm.breadcrumb.servicesTitle', { - defaultMessage: 'Services', - }), - path: '/services', - }, - { - id: 'traces', - title: i18n.translate('xpack.apm.breadcrumb.tracesTitle', { - defaultMessage: 'Traces', - }), - path: '/traces', - }, - { - id: 'service-map', - title: i18n.translate('xpack.apm.breadcrumb.serviceMapTitle', { - defaultMessage: 'Service Map', - }), - path: '/service-map', - }, - ], - }, + // !! Need to be kept in sync with the routes in x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx + deepLinks: [ + { + id: 'services', + title: i18n.translate('xpack.apm.breadcrumb.servicesTitle', { + defaultMessage: 'Services', + }), + path: '/services', + }, + { + id: 'traces', + title: i18n.translate('xpack.apm.breadcrumb.tracesTitle', { + defaultMessage: 'Traces', + }), + path: '/traces', + }, + { + id: 'service-map', + title: i18n.translate('xpack.apm.breadcrumb.serviceMapTitle', { + defaultMessage: 'Service Map', + }), + path: '/service-map', + }, + ], async mount(appMountParameters: AppMountParameters) { // Load application bundle and Get start services @@ -196,24 +194,22 @@ export class ApmPlugin implements Plugin { navLinkStatus: config.ui.enabled ? AppNavLinkStatus.default : AppNavLinkStatus.hidden, - meta: { - keywords: [ - 'RUM', - 'Real User Monitoring', - 'DEM', - 'Digital Experience Monitoring', - 'EUM', - 'End User Monitoring', - 'UX', - 'Javascript', - 'APM', - 'Mobile', - 'digital', - 'performance', - 'web performance', - 'web perf', - ], - }, + keywords: [ + 'RUM', + 'Real User Monitoring', + 'DEM', + 'Digital Experience Monitoring', + 'EUM', + 'End User Monitoring', + 'UX', + 'Javascript', + 'APM', + 'Mobile', + 'digital', + 'performance', + 'web performance', + 'web perf', + ], async mount(appMountParameters: AppMountParameters) { // Load application bundle and Get start service const [{ renderApp }, [coreStart, corePlugins]] = await Promise.all([ diff --git a/x-pack/plugins/global_search_providers/public/providers/application.test.ts b/x-pack/plugins/global_search_providers/public/providers/application.test.ts index 9b084d7bb9a6ad..2d555f38d16d1e 100644 --- a/x-pack/plugins/global_search_providers/public/providers/application.test.ts +++ b/x-pack/plugins/global_search_providers/public/providers/application.test.ts @@ -29,10 +29,8 @@ const createApp = (props: Partial = {}): PublicAppInfo => ({ status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, chromeless: false, - meta: { - keywords: props.meta?.keywords || [], - searchDeepLinks: [], - }, + keywords: props.keywords || [], + deepLinks: [], ...props, }); diff --git a/x-pack/plugins/global_search_providers/public/providers/get_app_results.test.ts b/x-pack/plugins/global_search_providers/public/providers/get_app_results.test.ts index 8b875dbb7ed9bc..251dd84395aa00 100644 --- a/x-pack/plugins/global_search_providers/public/providers/get_app_results.test.ts +++ b/x-pack/plugins/global_search_providers/public/providers/get_app_results.test.ts @@ -26,7 +26,8 @@ const createApp = (props: Partial = {}): PublicAppInfo => ({ status: AppStatus.accessible, navLinkStatus: AppNavLinkStatus.visible, chromeless: false, - meta: { keywords: [], searchDeepLinks: [] }, + keywords: [], + deepLinks: [], ...props, }); @@ -34,7 +35,7 @@ const createAppLink = (props: Partial = {}): AppLink => ({ id: props.id ?? 'app1', path: props.appRoute ?? '/app/app1', subLinkTitles: [], - keywords: props.meta?.keywords ?? [], // start off with the top level app keywords + keywords: props.keywords ?? [], // start off with the top level app keywords app: createApp(props), }); @@ -51,30 +52,37 @@ describe('getAppResults', () => { expect(results[0]).toEqual(expect.objectContaining({ id: 'dashboard', score: 100 })); }); - it('creates multiple links for apps with searchDeepLinks', () => { + it('creates multiple links for apps with deepLinks', () => { const apps = [ createApp({ - meta: { - searchDeepLinks: [ - { id: 'sub1', title: 'Sub1', path: '/sub1', searchDeepLinks: [], keywords: [] }, - { - id: 'sub2', - title: 'Sub2', - path: '/sub2', - searchDeepLinks: [ - { - id: 'sub2sub1', - title: 'Sub2Sub1', - path: '/sub2/sub1', - searchDeepLinks: [], - keywords: [], - }, - ], - keywords: [], - }, - ], - keywords: [], - }, + deepLinks: [ + { + id: 'sub1', + title: 'Sub1', + path: '/sub1', + deepLinks: [], + keywords: [], + navLinkStatus: AppNavLinkStatus.hidden, + }, + { + id: 'sub2', + title: 'Sub2', + path: '/sub2', + deepLinks: [ + { + id: 'sub2sub1', + title: 'Sub2Sub1', + path: '/sub2/sub1', + deepLinks: [], + keywords: [], + navLinkStatus: AppNavLinkStatus.hidden, + }, + ], + keywords: [], + navLinkStatus: AppNavLinkStatus.hidden, + }, + ], + keywords: [], }), ]; @@ -89,21 +97,20 @@ describe('getAppResults', () => { ]); }); - it('only includes searchDeepLinks when search term is non-empty', () => { + it('only includes deepLinks when search term is non-empty', () => { const apps = [ createApp({ - meta: { - searchDeepLinks: [ - { - id: 'sub1', - title: 'Sub1', - path: '/sub1', - searchDeepLinks: [], - keywords: [], - }, - ], - keywords: [], - }, + deepLinks: [ + { + id: 'sub1', + title: 'Sub1', + path: '/sub1', + deepLinks: [], + keywords: [], + navLinkStatus: AppNavLinkStatus.hidden, + }, + ], + keywords: [], }), ]; @@ -112,11 +119,7 @@ describe('getAppResults', () => { }); it('retrieves the matching results from keywords', () => { - const apps = [ - createApp({ - meta: { searchDeepLinks: [], keywords: ['One'] }, - }), - ]; + const apps = [createApp({ deepLinks: [], keywords: ['One'] })]; const results = getAppResults('One', apps); expect(results.map(({ title }) => title)).toEqual(['App 1']); }); @@ -124,27 +127,34 @@ describe('getAppResults', () => { it('retrieves the matching results from deeplink keywords', () => { const apps = [ createApp({ - meta: { - searchDeepLinks: [ - { id: 'sub1', title: 'Sub1', path: '/sub1', searchDeepLinks: [], keywords: [] }, - { - id: 'sub2', - title: 'Sub2', - path: '/sub2', - searchDeepLinks: [ - { - id: 'sub2sub1', - title: 'Sub2Sub1', - path: '/sub2/sub1', - searchDeepLinks: [], - keywords: ['TwoOne'], - }, - ], - keywords: ['two'], - }, - ], - keywords: [], - }, + deepLinks: [ + { + id: 'sub1', + title: 'Sub1', + path: '/sub1', + deepLinks: [], + keywords: [], + navLinkStatus: AppNavLinkStatus.hidden, + }, + { + id: 'sub2', + title: 'Sub2', + path: '/sub2', + deepLinks: [ + { + id: 'sub2sub1', + title: 'Sub2Sub1', + path: '/sub2/sub1', + deepLinks: [], + keywords: ['TwoOne'], + navLinkStatus: AppNavLinkStatus.hidden, + }, + ], + keywords: ['two'], + navLinkStatus: AppNavLinkStatus.hidden, + }, + ], + keywords: [], }), ]; @@ -187,26 +197,17 @@ describe('scoreApp', () => { describe('when the term is included in the keywords but not in the title', () => { it(`returns 100 * ${keywordScoreWeighting} if one of the app meta keywords is an exact match`, () => { expect( - scoreApp( - 'bar', - createAppLink({ title: 'foo', meta: { keywords: ['bar'], searchDeepLinks: [] } }) - ) + scoreApp('bar', createAppLink({ title: 'foo', keywords: ['bar'], deepLinks: [] })) ).toBe(100 * keywordScoreWeighting); expect( - scoreApp( - 'bar', - createAppLink({ title: 'foo', meta: { keywords: ['BAR'], searchDeepLinks: [] } }) - ) + scoreApp('bar', createAppLink({ title: 'foo', keywords: ['BAR'], deepLinks: [] })) ).toBe(100 * keywordScoreWeighting); }); it(`returns 90 * ${keywordScoreWeighting} if any of the keywords start with the term`, () => { expect( scoreApp( 'viz', - createAppLink({ - title: 'Foo', - meta: { keywords: ['Vizualize', 'Viz view'], searchDeepLinks: [] }, - }) + createAppLink({ title: 'Foo', keywords: ['Vizualize', 'Viz view'], deepLinks: [] }) ) ).toBe(90 * keywordScoreWeighting); }); @@ -214,19 +215,13 @@ describe('scoreApp', () => { expect( scoreApp( 'board', - createAppLink({ - title: 'Foo', - meta: { keywords: ['dashboard app'], searchDeepLinks: [] }, - }) + createAppLink({ title: 'Foo', keywords: ['dashboard app'], deepLinks: [] }) ) ).toBe(75 * keywordScoreWeighting); expect( scoreApp( 'shboa', - createAppLink({ - title: 'Foo', - meta: { keywords: ['dashboard app'], searchDeepLinks: [] }, - }) + createAppLink({ title: 'Foo', keywords: ['dashboard app'], deepLinks: [] }) ) ).toBe(75 * keywordScoreWeighting); }); @@ -235,26 +230,17 @@ describe('scoreApp', () => { describe('when the term is included in the keywords and the title', () => { it('returns 100 if one of the app meta keywords and the title is an exact match', () => { expect( - scoreApp( - 'home', - createAppLink({ title: 'Home', meta: { keywords: ['home'], searchDeepLinks: [] } }) - ) + scoreApp('home', createAppLink({ title: 'Home', keywords: ['home'], deepLinks: [] })) ).toBe(100); expect( - scoreApp( - 'Home', - createAppLink({ title: 'Home', meta: { keywords: ['HOME'], searchDeepLinks: [] } }) - ) + scoreApp('Home', createAppLink({ title: 'Home', keywords: ['HOME'], deepLinks: [] })) ).toBe(100); }); it('returns 90 if either one of the keywords or the title start with the term', () => { expect( scoreApp( 'vis', - createAppLink({ - title: 'Visualize', - meta: { keywords: ['Visualise'], searchDeepLinks: [] }, - }) + createAppLink({ title: 'Visualize', keywords: ['Visualise'], deepLinks: [] }) ) ).toBe(90); }); @@ -262,19 +248,13 @@ describe('scoreApp', () => { expect( scoreApp( 'board', - createAppLink({ - title: 'Dashboard', - meta: { keywords: ['dashboard app'], searchDeepLinks: [] }, - }) + createAppLink({ title: 'Dashboard', keywords: ['dashboard app'], deepLinks: [] }) ) ).toBe(75); expect( scoreApp( 'shboa', - createAppLink({ - title: 'dashboard', - meta: { keywords: ['dashboard app'], searchDeepLinks: [] }, - }) + createAppLink({ title: 'dashboard', keywords: ['dashboard app'], deepLinks: [] }) ) ).toBe(75); }); @@ -285,19 +265,13 @@ describe('scoreApp', () => { expect( scoreApp( '0123456789', - createAppLink({ - title: '012345', - meta: { keywords: ['0345', '9987'], searchDeepLinks: [] }, - }) + createAppLink({ title: '012345', keywords: ['0345', '9987'], deepLinks: [] }) ) ).toBe(60); expect( scoreApp( '--1234567-', - createAppLink({ - title: '123456789', - meta: { keywords: ['--345--'], searchDeepLinks: [] }, - }) + createAppLink({ title: '123456789', keywords: ['--345--'], deepLinks: [] }) ) ).toBe(60); }); @@ -305,13 +279,13 @@ describe('scoreApp', () => { expect( scoreApp( '0123456789', - createAppLink({ title: '12345', meta: { keywords: ['12', '34'], searchDeepLinks: [] } }) + createAppLink({ title: '12345', keywords: ['12', '34'], deepLinks: [] }) ) ).toBe(0); expect( scoreApp( '1-2-3-4-5', - createAppLink({ title: '123456789', meta: { keywords: ['12-789'], searchDeepLinks: [] } }) + createAppLink({ title: '123456789', keywords: ['12-789'], deepLinks: [] }) ) ).toBe(0); }); diff --git a/x-pack/plugins/global_search_providers/public/providers/get_app_results.ts b/x-pack/plugins/global_search_providers/public/providers/get_app_results.ts index f5f0a2d34e91c1..3ae1a082cdebfa 100644 --- a/x-pack/plugins/global_search_providers/public/providers/get_app_results.ts +++ b/x-pack/plugins/global_search_providers/public/providers/get_app_results.ts @@ -6,10 +6,10 @@ */ import levenshtein from 'js-levenshtein'; -import { PublicAppInfo, PublicAppSearchDeepLinkInfo } from 'src/core/public'; +import { PublicAppInfo, PublicAppDeepLinkInfo } from 'src/core/public'; import { GlobalSearchProviderResult } from '../../../global_search/public'; -/** Type used internally to represent an application unrolled into its separate searchDeepLinks */ +/** Type used internally to represent an application unrolled into its separate deepLinks */ export interface AppLink { id: string; app: PublicAppInfo; @@ -27,7 +27,7 @@ export const getAppResults = ( ): GlobalSearchProviderResult[] => { return ( apps - // Unroll all searchDeepLinks, only if there is a search term + // Unroll all deepLinks, only if there is a search term .flatMap((app) => term.length > 0 ? flattenDeepLinks(app) @@ -37,7 +37,7 @@ export const getAppResults = ( app, path: app.appRoute, subLinkTitles: [], - keywords: app.meta?.keywords ?? [], + keywords: app.keywords ?? [], }, ] ) @@ -56,7 +56,7 @@ export const scoreApp = (term: string, appLink: AppLink): number => { const appScoreByTerms = scoreAppByTerms(term, title); const keywords = [ - ...appLink.app.meta.keywords.map((keyword) => keyword.toLowerCase()), + ...appLink.app.keywords.map((keyword) => keyword.toLowerCase()), ...appLink.keywords.map((keyword) => keyword.toLowerCase()), ]; const appScoreByKeywords = scoreAppByKeywords(term, keywords); @@ -115,10 +115,7 @@ export const appToResult = (appLink: AppLink, score: number): GlobalSearchProvid }; }; -const flattenDeepLinks = ( - app: PublicAppInfo, - deepLink?: PublicAppSearchDeepLinkInfo -): AppLink[] => { +const flattenDeepLinks = (app: PublicAppInfo, deepLink?: PublicAppDeepLinkInfo): AppLink[] => { if (!deepLink) { return [ { @@ -126,9 +123,9 @@ const flattenDeepLinks = ( app, path: app.appRoute, subLinkTitles: [], - keywords: app.meta?.keywords ?? [], + keywords: app?.keywords ?? [], }, - ...app.meta.searchDeepLinks.flatMap((appDeepLink) => flattenDeepLinks(app, appDeepLink)), + ...app.deepLinks.flatMap((appDeepLink) => flattenDeepLinks(app, appDeepLink)), ]; } return [ @@ -143,7 +140,7 @@ const flattenDeepLinks = ( }, ] : []), - ...deepLink.searchDeepLinks + ...deepLink.deepLinks .flatMap((deepDeepLink) => flattenDeepLinks(app, deepDeepLink)) .map((deepAppLink) => ({ ...deepAppLink, diff --git a/x-pack/plugins/infra/public/pages/logs/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/page_content.tsx index d43fe198c50770..9ae127a8eca664 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_content.tsx @@ -40,7 +40,7 @@ export const LogsPageContent: React.FunctionComponent = () => { initialize(); }); - // !! Need to be kept in sync with the searchDeepLinks in x-pack/plugins/infra/public/plugin.ts + // !! Need to be kept in sync with the deepLinks in x-pack/plugins/infra/public/plugin.ts const streamTab = { app: 'logs', title: streamTabTitle, diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index b43d7640f63907..819c764bfb7ba5 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -120,7 +120,7 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { > - {/** !! Need to be kept in sync with the searchDeepLinks in x-pack/plugins/infra/public/plugin.ts */} + {/** !! Need to be kept in sync with the deepLinks in x-pack/plugins/infra/public/plugin.ts */} { // mount callback should not use setup dependencies, get start dependencies instead @@ -115,32 +113,30 @@ export class Plugin implements InfraClientPluginClass { order: 8200, appRoute: '/app/metrics', category: DEFAULT_APP_CATEGORIES.observability, - meta: { - // !! Need to be kept in sync with the routes in x-pack/plugins/infra/public/pages/metrics/index.tsx - searchDeepLinks: [ - { - id: 'inventory', - title: i18n.translate('xpack.infra.homePage.inventoryTabTitle', { - defaultMessage: 'Inventory', - }), - path: '/inventory', - }, - { - id: 'metrics-explorer', - title: i18n.translate('xpack.infra.homePage.metricsExplorerTabTitle', { - defaultMessage: 'Metrics Explorer', - }), - path: '/explorer', - }, - { - id: 'settings', - title: i18n.translate('xpack.infra.homePage.settingsTabTitle', { - defaultMessage: 'Settings', - }), - path: '/settings', - }, - ], - }, + // !! Need to be kept in sync with the routes in x-pack/plugins/infra/public/pages/metrics/index.tsx + deepLinks: [ + { + id: 'inventory', + title: i18n.translate('xpack.infra.homePage.inventoryTabTitle', { + defaultMessage: 'Inventory', + }), + path: '/inventory', + }, + { + id: 'metrics-explorer', + title: i18n.translate('xpack.infra.homePage.metricsExplorerTabTitle', { + defaultMessage: 'Metrics Explorer', + }), + path: '/explorer', + }, + { + id: 'settings', + title: i18n.translate('xpack.infra.homePage.settingsTabTitle', { + defaultMessage: 'Settings', + }), + path: '/settings', + }, + ], mount: async (params: AppMountParameters) => { // mount callback should not use setup dependencies, get start dependencies instead const [coreStart, pluginsStart] = await core.getStartServices(); diff --git a/x-pack/plugins/ml/public/register_helper/register_search_links/register_search_links.ts b/x-pack/plugins/ml/public/register_helper/register_search_links/register_search_links.ts index 6c219340da817d..dd3ca0bb8fa309 100644 --- a/x-pack/plugins/ml/public/register_helper/register_search_links/register_search_links.ts +++ b/x-pack/plugins/ml/public/register_helper/register_search_links/register_search_links.ts @@ -9,20 +9,18 @@ import { i18n } from '@kbn/i18n'; import { BehaviorSubject } from 'rxjs'; import { AppUpdater } from 'src/core/public'; -import { getSearchDeepLinks } from './search_deep_links'; +import { getDeepLinks } from './search_deep_links'; export function registerSearchLinks( appUpdater: BehaviorSubject, isFullLicense: boolean ) { appUpdater.next(() => ({ - meta: { - keywords: [ - i18n.translate('xpack.ml.keyword.ml', { - defaultMessage: 'ML', - }), - ], - searchDeepLinks: getSearchDeepLinks(isFullLicense), - }, + keywords: [ + i18n.translate('xpack.ml.keyword.ml', { + defaultMessage: 'ML', + }), + ], + deepLinks: getDeepLinks(isFullLicense), })); } diff --git a/x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts b/x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts index d248df90889897..d682a93fa274c4 100644 --- a/x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts +++ b/x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts @@ -7,35 +7,35 @@ import { i18n } from '@kbn/i18n'; -import type { AppSearchDeepLink } from 'src/core/public'; +import type { AppDeepLink } from 'src/core/public'; import { ML_PAGES } from '../../../common/constants/ml_url_generator'; -const OVERVIEW_LINK_SEARCH_DEEP_LINK: AppSearchDeepLink = { - id: 'mlOverviewSearchDeepLink', - title: i18n.translate('xpack.ml.searchDeepLink.overview', { +const OVERVIEW_LINK_DEEP_LINK: AppDeepLink = { + id: 'mlOverviewDeepLink', + title: i18n.translate('xpack.ml.deepLink.overview', { defaultMessage: 'Overview', }), path: `/${ML_PAGES.OVERVIEW}`, }; -const ANOMALY_DETECTION_SEARCH_DEEP_LINK: AppSearchDeepLink = { - id: 'mlAnomalyDetectionSearchDeepLink', - title: i18n.translate('xpack.ml.searchDeepLink.anomalyDetection', { +const ANOMALY_DETECTION_DEEP_LINK: AppDeepLink = { + id: 'mlAnomalyDetectionDeepLink', + title: i18n.translate('xpack.ml.deepLink.anomalyDetection', { defaultMessage: 'Anomaly Detection', }), path: `/${ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE}`, }; -const DATA_FRAME_ANALYTICS_SEARCH_DEEP_LINK: AppSearchDeepLink = { - id: 'mlDataFrameAnalyticsSearchDeepLink', - title: i18n.translate('xpack.ml.searchDeepLink.dataFrameAnalytics', { +const DATA_FRAME_ANALYTICS_DEEP_LINK: AppDeepLink = { + id: 'mlDataFrameAnalyticsDeepLink', + title: i18n.translate('xpack.ml.deepLink.dataFrameAnalytics', { defaultMessage: 'Data Frame Analytics', }), path: `/${ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE}`, - searchDeepLinks: [ + deepLinks: [ { - id: 'mlTrainedModelsSearchDeepLink', - title: i18n.translate('xpack.ml.searchDeepLink.trainedModels', { + id: 'mlTrainedModelsDeepLink', + title: i18n.translate('xpack.ml.deepLink.trainedModels', { defaultMessage: 'Trained Models', }), path: `/${ML_PAGES.DATA_FRAME_ANALYTICS_MODELS_MANAGE}`, @@ -43,47 +43,47 @@ const DATA_FRAME_ANALYTICS_SEARCH_DEEP_LINK: AppSearchDeepLink = { ], }; -const DATA_VISUALIZER_SEARCH_DEEP_LINK: AppSearchDeepLink = { - id: 'mlDataVisualizerSearchDeepLink', - title: i18n.translate('xpack.ml.searchDeepLink.dataVisualizer', { +const DATA_VISUALIZER_DEEP_LINK: AppDeepLink = { + id: 'mlDataVisualizerDeepLink', + title: i18n.translate('xpack.ml.deepLink.dataVisualizer', { defaultMessage: 'Data Visualizer', }), path: `/${ML_PAGES.DATA_VISUALIZER}`, }; -const FILE_UPLOAD_SEARCH_DEEP_LINK: AppSearchDeepLink = { - id: 'mlFileUploadSearchDeepLink', - title: i18n.translate('xpack.ml.searchDeepLink.fileUpload', { +const FILE_UPLOAD_DEEP_LINK: AppDeepLink = { + id: 'mlFileUploadDeepLink', + title: i18n.translate('xpack.ml.deepLink.fileUpload', { defaultMessage: 'File Upload', }), path: `/${ML_PAGES.DATA_VISUALIZER_FILE}`, }; -const INDEX_DATA_VISUALIZER_SEARCH_DEEP_LINK: AppSearchDeepLink = { - id: 'mlIndexDataVisualizerSearchDeepLink', - title: i18n.translate('xpack.ml.searchDeepLink.indexDataVisualizer', { +const INDEX_DATA_VISUALIZER_DEEP_LINK: AppDeepLink = { + id: 'mlIndexDataVisualizerDeepLink', + title: i18n.translate('xpack.ml.deepLink.indexDataVisualizer', { defaultMessage: 'Index Data Visualizer', }), path: `/${ML_PAGES.DATA_VISUALIZER_INDEX_SELECT}`, }; -const SETTINGS_SEARCH_DEEP_LINK: AppSearchDeepLink = { - id: 'mlSettingsSearchDeepLink', - title: i18n.translate('xpack.ml.searchDeepLink.settings', { +const SETTINGS_DEEP_LINK: AppDeepLink = { + id: 'mlSettingsDeepLink', + title: i18n.translate('xpack.ml.deepLink.settings', { defaultMessage: 'Settings', }), path: `/${ML_PAGES.SETTINGS}`, - searchDeepLinks: [ + deepLinks: [ { - id: 'mlCalendarSettingsSearchDeepLink', - title: i18n.translate('xpack.ml.searchDeepLink.calendarSettings', { + id: 'mlCalendarSettingsDeepLink', + title: i18n.translate('xpack.ml.deepLink.calendarSettings', { defaultMessage: 'Calendars', }), path: `/${ML_PAGES.CALENDARS_MANAGE}`, }, { - id: 'mlFilterListsSettingsSearchDeepLink', - title: i18n.translate('xpack.ml.searchDeepLink.filterListsSettings', { + id: 'mlFilterListsSettingsDeepLink', + title: i18n.translate('xpack.ml.deepLink.filterListsSettings', { defaultMessage: 'Filter Lists', }), path: `/${ML_PAGES.SETTINGS}`, // Link to settings page as read only users cannot view filter lists. @@ -91,19 +91,19 @@ const SETTINGS_SEARCH_DEEP_LINK: AppSearchDeepLink = { ], }; -export function getSearchDeepLinks(isFullLicense: boolean) { - const deepLinks: AppSearchDeepLink[] = [ - DATA_VISUALIZER_SEARCH_DEEP_LINK, - FILE_UPLOAD_SEARCH_DEEP_LINK, - INDEX_DATA_VISUALIZER_SEARCH_DEEP_LINK, +export function getDeepLinks(isFullLicense: boolean) { + const deepLinks: AppDeepLink[] = [ + DATA_VISUALIZER_DEEP_LINK, + FILE_UPLOAD_DEEP_LINK, + INDEX_DATA_VISUALIZER_DEEP_LINK, ]; if (isFullLicense === true) { deepLinks.push( - OVERVIEW_LINK_SEARCH_DEEP_LINK, - ANOMALY_DETECTION_SEARCH_DEEP_LINK, - DATA_FRAME_ANALYTICS_SEARCH_DEEP_LINK, - SETTINGS_SEARCH_DEEP_LINK + OVERVIEW_LINK_DEEP_LINK, + ANOMALY_DETECTION_DEEP_LINK, + DATA_FRAME_ANALYTICS_DEEP_LINK, + SETTINGS_DEEP_LINK ); } diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts index 5af5a8adf95b7f..aeee7077ec9c04 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts @@ -23,7 +23,7 @@ import { unmappedRule } from '../../objects/rule'; import { DETECTIONS_URL } from '../../urls/navigation'; describe('Alert details with unmapped fields', () => { - before(() => { + beforeEach(() => { cleanKibana(); esArchiverLoad('unmapped_fields'); loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); diff --git a/x-pack/plugins/security_solution/public/app/search/index.test.ts b/x-pack/plugins/security_solution/public/app/search/index.test.ts index d6c36e89558d02..328395f9b85c9e 100644 --- a/x-pack/plugins/security_solution/public/app/search/index.test.ts +++ b/x-pack/plugins/security_solution/public/app/search/index.test.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { getSearchDeepLinksAndKeywords } from '.'; +import { getDeepLinksAndKeywords } from '.'; import { SecurityPageName } from '../../../common/constants'; describe('public search functions', () => { @@ -13,10 +13,9 @@ describe('public search functions', () => { const platinumLicense = 'platinum'; for (const pageName of Object.values(SecurityPageName)) { expect.assertions(Object.values(SecurityPageName).length * 2); - const basicLinkCount = - getSearchDeepLinksAndKeywords(pageName, basicLicense).searchDeepLinks?.length || 0; - const platinumLinks = getSearchDeepLinksAndKeywords(pageName, platinumLicense); - expect(platinumLinks.searchDeepLinks?.length).toBeGreaterThanOrEqual(basicLinkCount); + const basicLinkCount = getDeepLinksAndKeywords(pageName, basicLicense).deepLinks?.length || 0; + const platinumLinks = getDeepLinksAndKeywords(pageName, platinumLicense); + expect(platinumLinks.deepLinks?.length).toBeGreaterThanOrEqual(basicLinkCount); expect(platinumLinks.keywords?.length).not.toBe(null); } }); diff --git a/x-pack/plugins/security_solution/public/app/search/index.ts b/x-pack/plugins/security_solution/public/app/search/index.ts index 110356269e8917..93d931fc4d1370 100644 --- a/x-pack/plugins/security_solution/public/app/search/index.ts +++ b/x-pack/plugins/security_solution/public/app/search/index.ts @@ -11,7 +11,7 @@ import { Subject } from 'rxjs'; import { AppUpdater } from 'src/core/public'; import { LicenseType } from '../../../../licensing/common/types'; import { SecuritySubPluginNames, SecurityDeepLinks } from '../types'; -import { AppMeta } from '../../../../../../src/core/public'; +import { App } from '../../../../../../src/core/public'; const securityDeepLinks: SecurityDeepLinks = { detections: { @@ -198,10 +198,10 @@ const subpluginKeywords: { [key in SecuritySubPluginNames]: string[] } = { * @param subPluginName SubPluginName of the app to retrieve meta information for. * @param licenseType optional string for license level, if not provided basic is assumed. */ -export function getSearchDeepLinksAndKeywords( +export function getDeepLinksAndKeywords( subPluginName: SecuritySubPluginNames, licenseType?: LicenseType -): AppMeta { +): Pick { const baseRoutes = [...securityDeepLinks[subPluginName].base]; if ( licenseType === 'gold' || @@ -214,29 +214,27 @@ export function getSearchDeepLinksAndKeywords( if (premiumRoutes !== undefined) { return { keywords: subpluginKeywords[subPluginName], - searchDeepLinks: [...baseRoutes, ...premiumRoutes], + deepLinks: [...baseRoutes, ...premiumRoutes], }; } } return { keywords: subpluginKeywords[subPluginName], - searchDeepLinks: baseRoutes, + deepLinks: baseRoutes, }; } /** * A function that updates a subplugin's meta property as appropriate when license level changes. - * @param subPluginName SubPluginName of the app to register searchDeepLinks for + * @param subPluginName SubPluginName of the app to register deepLinks for * @param appUpdater an instance of appUpdater$ observable to update search links when needed. * @param licenseType A string representing the current license level. */ -export function registerSearchLinks( +export function registerDeepLinks( subPluginName: SecuritySubPluginNames, appUpdater?: Subject, licenseType?: LicenseType ) { if (appUpdater !== undefined) { - appUpdater.next(() => ({ - meta: getSearchDeepLinksAndKeywords(subPluginName, licenseType), - })); + appUpdater.next(() => ({ ...getDeepLinksAndKeywords(subPluginName, licenseType) })); } } diff --git a/x-pack/plugins/security_solution/public/app/types.ts b/x-pack/plugins/security_solution/public/app/types.ts index a617c6f14b9c48..77d5b99e1c3a3b 100644 --- a/x-pack/plugins/security_solution/public/app/types.ts +++ b/x-pack/plugins/security_solution/public/app/types.ts @@ -17,7 +17,7 @@ import { CombinedState, } from 'redux'; -import { AppMountParameters, AppSearchDeepLink } from '../../../../../src/core/public'; +import { AppMountParameters, AppDeepLink } from '../../../../../src/core/public'; import { StartServices } from '../types'; /** @@ -58,8 +58,8 @@ export type SecuritySubPluginKeyStore = export type SecuritySubPluginNames = keyof typeof SecurityPageName; interface SecurityDeepLink { - base: AppSearchDeepLink[]; - premium?: AppSearchDeepLink[]; + base: AppDeepLink[]; + premium?: AppDeepLink[]; } export type SecurityDeepLinks = { [key in SecuritySubPluginNames]: SecurityDeepLink }; diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index efbe857d168d85..c1f501d3f70945 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -47,7 +47,7 @@ import { } from '../common/constants'; import { SecurityPageName } from './app/types'; -import { registerSearchLinks, getSearchDeepLinksAndKeywords } from './app/search'; +import { registerDeepLinks, getDeepLinksAndKeywords } from './app/search'; import { manageOldSiemRoutes } from './helpers'; import { OVERVIEW, @@ -258,7 +258,7 @@ export class Plugin implements IPlugin { const [coreStart, startPlugins] = await core.getStartServices(); const { timelines: subPlugin } = await this.subPlugins(); @@ -300,7 +300,7 @@ export class Plugin implements IPlugin { const [coreStart, startPlugins] = await core.getStartServices(); const { management: managementSubPlugin } = await this.subPlugins(); @@ -366,19 +366,19 @@ export class Plugin implements IPlugin { if (currentLicense.type !== undefined) { - registerSearchLinks(SecurityPageName.network, this.networkUpdater$, currentLicense.type); - registerSearchLinks( + registerDeepLinks(SecurityPageName.network, this.networkUpdater$, currentLicense.type); + registerDeepLinks( SecurityPageName.detections, this.detectionsUpdater$, currentLicense.type ); - registerSearchLinks(SecurityPageName.hosts, this.hostsUpdater$, currentLicense.type); - registerSearchLinks(SecurityPageName.case, this.caseUpdater$, currentLicense.type); + registerDeepLinks(SecurityPageName.hosts, this.hostsUpdater$, currentLicense.type); + registerDeepLinks(SecurityPageName.case, this.caseUpdater$, currentLicense.type); } }); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 134f58236cfee3..af6cdd1d672a17 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -15978,16 +15978,16 @@ "xpack.ml.ruleEditor.selectRuleAction.orText": "OR ", "xpack.ml.ruleEditor.typicalAppliesTypeText": "通常", "xpack.ml.sampleDataLinkLabel": "ML ジョブ", - "xpack.ml.searchDeepLink.anomalyDetection": "異常検知", - "xpack.ml.searchDeepLink.calendarSettings": "カレンダー", - "xpack.ml.searchDeepLink.dataFrameAnalytics": "データフレーム分析", - "xpack.ml.searchDeepLink.dataVisualizer": "データビジュアライザー", - "xpack.ml.searchDeepLink.fileUpload": "ファイルアップロード", - "xpack.ml.searchDeepLink.filterListsSettings": "フィルターリスト", - "xpack.ml.searchDeepLink.indexDataVisualizer": "インデックスデータビジュアライザー", - "xpack.ml.searchDeepLink.overview": "概要", - "xpack.ml.searchDeepLink.settings": "設定", - "xpack.ml.searchDeepLink.trainedModels": "学習済みモデル", + "xpack.ml.deepLink.anomalyDetection": "異常検知", + "xpack.ml.deepLink.calendarSettings": "カレンダー", + "xpack.ml.deepLink.dataFrameAnalytics": "データフレーム分析", + "xpack.ml.deepLink.dataVisualizer": "データビジュアライザー", + "xpack.ml.deepLink.fileUpload": "ファイルアップロード", + "xpack.ml.deepLink.filterListsSettings": "フィルターリスト", + "xpack.ml.deepLink.indexDataVisualizer": "インデックスデータビジュアライザー", + "xpack.ml.deepLink.overview": "概要", + "xpack.ml.deepLink.settings": "設定", + "xpack.ml.deepLink.trainedModels": "学習済みモデル", "xpack.ml.settings.anomalyDetection.anomalyDetectionTitle": "異常検知", "xpack.ml.settings.anomalyDetection.calendarsText": "システム停止日や祝日など、異常値を生成したくないイベントについては、カレンダーに予定されているイベントのリストを登録できます。", "xpack.ml.settings.anomalyDetection.calendarsTitle": "カレンダー", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 67677f86ddbf78..c8376b72daef17 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -16203,16 +16203,16 @@ "xpack.ml.ruleEditor.selectRuleAction.orText": "或 ", "xpack.ml.ruleEditor.typicalAppliesTypeText": "典型", "xpack.ml.sampleDataLinkLabel": "ML 作业", - "xpack.ml.searchDeepLink.anomalyDetection": "异常检测", - "xpack.ml.searchDeepLink.calendarSettings": "日历", - "xpack.ml.searchDeepLink.dataFrameAnalytics": "数据帧分析", - "xpack.ml.searchDeepLink.dataVisualizer": "数据可视化工具", - "xpack.ml.searchDeepLink.fileUpload": "文件上传", - "xpack.ml.searchDeepLink.filterListsSettings": "筛选列表", - "xpack.ml.searchDeepLink.indexDataVisualizer": "索引数据可视化工具", - "xpack.ml.searchDeepLink.overview": "概览", - "xpack.ml.searchDeepLink.settings": "设置", - "xpack.ml.searchDeepLink.trainedModels": "已训练模型", + "xpack.ml.deepLink.anomalyDetection": "异常检测", + "xpack.ml.deepLink.calendarSettings": "日历", + "xpack.ml.deepLink.dataFrameAnalytics": "数据帧分析", + "xpack.ml.deepLink.dataVisualizer": "数据可视化工具", + "xpack.ml.deepLink.fileUpload": "文件上传", + "xpack.ml.deepLink.filterListsSettings": "筛选列表", + "xpack.ml.deepLink.indexDataVisualizer": "索引数据可视化工具", + "xpack.ml.deepLink.overview": "概览", + "xpack.ml.deepLink.settings": "设置", + "xpack.ml.deepLink.trainedModels": "已训练模型", "xpack.ml.settings.anomalyDetection.anomalyDetectionTitle": "异常检测", "xpack.ml.settings.anomalyDetection.calendarsSummaryCount": "您有 {calendarsCountBadge} 个{calendarsCount, plural, other {日历}}", "xpack.ml.settings.anomalyDetection.calendarsText": "日志包含不应生成异常的已计划事件列表,例如已计划系统中断或公共假期。", diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts index 0832274f0785a1..80a131676951e4 100644 --- a/x-pack/plugins/uptime/public/apps/plugin.ts +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -104,28 +104,26 @@ export class UptimePlugin order: 8400, title: PLUGIN.TITLE, category: DEFAULT_APP_CATEGORIES.observability, - meta: { - keywords: [ - 'Synthetics', - 'pings', - 'checks', - 'availability', - 'response duration', - 'response time', - 'outside in', - 'reachability', - 'reachable', - 'digital', - 'performance', - 'web performance', - 'web perf', - ], - searchDeepLinks: [ - { id: 'Down monitors', title: 'Down monitors', path: '/?statusFilter=down' }, - { id: 'Certificates', title: 'TLS Certificates', path: '/certificates' }, - { id: 'Settings', title: 'Settings', path: '/settings' }, - ], - }, + keywords: [ + 'Synthetics', + 'pings', + 'checks', + 'availability', + 'response duration', + 'response time', + 'outside in', + 'reachability', + 'reachable', + 'digital', + 'performance', + 'web performance', + 'web perf', + ], + deepLinks: [ + { id: 'Down monitors', title: 'Down monitors', path: '/?statusFilter=down' }, + { id: 'Certificates', title: 'TLS Certificates', path: '/certificates' }, + { id: 'Settings', title: 'Settings', path: '/settings' }, + ], mount: async (params: AppMountParameters) => { const [coreStart, corePlugins] = await core.getStartServices(); From 60b5c842cd0405e24901b4944f51e555358679f6 Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Tue, 25 May 2021 08:51:24 -0400 Subject: [PATCH 11/78] [Metrics UI] use EuiTooltip to control tooltip component and simplify ConditionalTooltip (#99224) * use EuiTooltip to control tooltip component * fix style * update unit tests * add functional waffle map tooltip tests * remove reload() from useEffect * fix type * update unit test --- .../conditional_tooltip.test.tsx.snap | 226 +++++++++++++----- .../waffle/conditional_tooltip.test.tsx | 106 ++------ .../components/waffle/conditional_tooltip.tsx | 168 +++++-------- .../inventory_view/components/waffle/node.tsx | 15 +- .../test/functional/apps/infra/home_page.ts | 3 +- .../page_objects/infra_home_page.ts | 29 +++ 6 files changed, 277 insertions(+), 270 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/__snapshots__/conditional_tooltip.test.tsx.snap b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/__snapshots__/conditional_tooltip.test.tsx.snap index a5d97813e4b14a..91bafb3b15e4db 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/__snapshots__/conditional_tooltip.test.tsx.snap +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/__snapshots__/conditional_tooltip.test.tsx.snap @@ -1,8 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ConditionalToolTip should just work 1`] = ` +exports[`ConditionalToolTip renders correctly 1`] = `
- - CPU usage - - - 10% - + +
+ CPU usage +
+
+ +
+ 10% +
+
+
- - Memory usage - - - 80% - + +
+ Memory usage +
+
+ +
+ 80% +
+
+
- - Outbound traffic - - - 8Mbit/s - + +
+ Outbound traffic +
+
+ +
+ 8Mbit/s +
+
+
- - Inbound traffic - - - 8Mbit/s - + +
+ Inbound traffic +
+
+ +
+ 8Mbit/s +
+
+
- - My Custom Label - - - 34.1% - + +
+ My Custom Label +
+
+ +
+ 34.1% +
+
+
- - Avg of host.network.out.packets - - - 4,392.2 - + +
+ Avg of host.network.out.packets +
+
+ +
+ 4,392.2 +
+
+
`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx index 6dde53efae761b..ac4fac394dacc5 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx @@ -7,15 +7,10 @@ import React from 'react'; import { mount } from 'enzyme'; -// import { act } from 'react-dom/test-utils'; +import toJson from 'enzyme-to-json'; import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common'; -import { EuiToolTip } from '@elastic/eui'; import { ConditionalToolTip } from './conditional_tooltip'; -import { - InfraWaffleMapNode, - InfraWaffleMapOptions, - InfraFormatterType, -} from '../../../../../lib/lib'; +import { InfraWaffleMapNode } from '../../../../../lib/lib'; jest.mock('../../../../../containers/metrics_source', () => ({ useSourceContext: () => ({ sourceId: 'default' }), @@ -38,61 +33,12 @@ const NODE: InfraWaffleMapNode = { metrics: [{ name: 'cpu' }], }; -const OPTIONS: InfraWaffleMapOptions = { - formatter: InfraFormatterType.percent, - formatTemplate: '{value}', - metric: { type: 'cpu' }, - groupBy: [], - legend: { - type: 'steppedGradient', - rules: [], - }, - sort: { by: 'value', direction: 'desc' }, -}; - export const nextTick = () => new Promise((res) => process.nextTick(res)); -const ChildComponent = () =>
child
; describe('ConditionalToolTip', () => { - afterEach(() => { - mockedUseSnapshot.mockReset(); - mockedUseWaffleOptionsContext.mockReset(); - }); - - function createWrapper(currentTime: number = Date.now(), hidden: boolean = false) { - return mount( - - - - ); - } - - it('should return children when hidden', () => { - mockedUseSnapshot.mockReturnValue({ - nodes: [], - error: null, - loading: true, - interval: '', - reload: jest.fn(() => Promise.resolve()), - }); - mockedUseWaffleOptionsContext.mockReturnValue(mockedUseWaffleOptionsContexReturnValue); - const currentTime = Date.now(); - const wrapper = createWrapper(currentTime, true); - expect(wrapper.find(ChildComponent).exists()).toBeTruthy(); - }); + const currentTime = Date.now(); - it('should just work', () => { - jest.useFakeTimers(); - const reloadMock = jest.fn(() => Promise.resolve()); + it('renders correctly', () => { mockedUseSnapshot.mockReturnValue({ nodes: [ { @@ -121,13 +67,9 @@ describe('ConditionalToolTip', () => { error: null, loading: false, interval: '60s', - reload: reloadMock, + reload: jest.fn(() => Promise.resolve()), }); mockedUseWaffleOptionsContext.mockReturnValue(mockedUseWaffleOptionsContexReturnValue); - const currentTime = Date.now(); - const wrapper = createWrapper(currentTime, false); - expect(wrapper.find(ChildComponent).exists()).toBeTruthy(); - expect(wrapper.find(EuiToolTip).exists()).toBeTruthy(); const expectedQuery = JSON.stringify({ bool: { filter: { @@ -154,6 +96,14 @@ describe('ConditionalToolTip', () => { type: 'custom', }, ]; + const wrapper = mount( + + + + ); + const tooltip = wrapper.find('[data-test-subj~="conditionalTooltipContent-host-01"]'); + expect(toJson(tooltip)).toMatchSnapshot(); + expect(mockedUseSnapshot).toBeCalledWith( expectedQuery, expectedMetrics, @@ -162,36 +112,8 @@ describe('ConditionalToolTip', () => { 'default', currentTime, '', - '', - false + '' ); - wrapper.find('[data-test-subj~="conditionalTooltipMouseHandler"]').simulate('mouseOver'); - wrapper.find(EuiToolTip).simulate('mouseOver'); - jest.advanceTimersByTime(500); - expect(reloadMock).toHaveBeenCalled(); - expect(wrapper.find(EuiToolTip).props().content).toMatchSnapshot(); - }); - - it('should not load data if mouse out before 200 ms', () => { - jest.useFakeTimers(); - const reloadMock = jest.fn(() => Promise.resolve()); - mockedUseSnapshot.mockReturnValue({ - nodes: [], - error: null, - loading: true, - interval: '', - reload: reloadMock, - }); - mockedUseWaffleOptionsContext.mockReturnValue(mockedUseWaffleOptionsContexReturnValue); - const currentTime = Date.now(); - const wrapper = createWrapper(currentTime, false); - expect(wrapper.find(ChildComponent).exists()).toBeTruthy(); - expect(wrapper.find(EuiToolTip).exists()).toBeTruthy(); - wrapper.find('[data-test-subj~="conditionalTooltipMouseHandler"]').simulate('mouseOver'); - jest.advanceTimersByTime(100); - wrapper.find('[data-test-subj~="conditionalTooltipMouseHandler"]').simulate('mouseOut'); - jest.advanceTimersByTime(200); - expect(reloadMock).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx index 6e334f4fbca752..a47512906abd13 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import React, { useCallback, useState, useEffect } from 'react'; -import { EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { first } from 'lodash'; import { getCustomMetricLabel } from '../../../../../../common/formatters/get_custom_metric_label'; import { SnapshotCustomMetricInput } from '../../../../../../common/http_api'; @@ -18,7 +18,7 @@ import { SnapshotMetricType, SnapshotMetricTypeRT, } from '../../../../../../common/inventory_models/types'; -import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../../lib/lib'; +import { InfraWaffleMapNode } from '../../../../../lib/lib'; import { useSnapshot } from '../../hooks/use_snaphot'; import { createInventoryMetricFormatter } from '../../lib/create_inventory_metric_formatter'; import { SNAPSHOT_METRIC_TRANSLATIONS } from '../../../../../../common/inventory_models/intl_strings'; @@ -27,113 +27,69 @@ import { createFormatterForMetric } from '../../../metrics_explorer/components/h export interface Props { currentTime: number; - hidden: boolean; node: InfraWaffleMapNode; - options: InfraWaffleMapOptions; - formatter: (val: number) => string; - children: React.ReactElement; nodeType: InventoryItemType; theme: EuiTheme | undefined; } - -export const ConditionalToolTip = withTheme( - ({ theme, hidden, node, children, nodeType, currentTime }: Props) => { - const { sourceId } = useSourceContext(); - const [timer, setTimer] = useState | null>(null); - const model = findInventoryModel(nodeType); - const { customMetrics } = useWaffleOptionsContext(); - const requestMetrics = model.tooltipMetrics - .map((type) => ({ type })) - .concat(customMetrics) as Array< - | { - type: SnapshotMetricType; - } - | SnapshotCustomMetricInput - >; - const query = JSON.stringify({ - bool: { - filter: { - match_phrase: { [model.fields.id]: node.id }, - }, +export const ConditionalToolTip = withTheme(({ theme, node, nodeType, currentTime }: Props) => { + const { sourceId } = useSourceContext(); + const model = findInventoryModel(nodeType); + const { customMetrics } = useWaffleOptionsContext(); + const requestMetrics = model.tooltipMetrics + .map((type) => ({ type })) + .concat(customMetrics) as Array< + | { + type: SnapshotMetricType; + } + | SnapshotCustomMetricInput + >; + const query = JSON.stringify({ + bool: { + filter: { + match_phrase: { [model.fields.id]: node.id }, }, - }); - const { nodes, reload } = useSnapshot( - query, - requestMetrics, - [], - nodeType, - sourceId, - currentTime, - '', - '', - false // Doesn't send request until reload() is called - ); - - const handleDataLoad = useCallback(() => { - const id = setTimeout(reload, 200); - setTimer(id); - }, [reload]); - - const cancelDataLoad = useCallback(() => { - return (timer && clearTimeout(timer)) || void 0; - }, [timer]); + }, + }); + const { nodes } = useSnapshot(query, requestMetrics, [], nodeType, sourceId, currentTime, '', ''); - useEffect(() => { - return cancelDataLoad; - }, [timer, cancelDataLoad]); - - if (hidden) { - return children; - } - const dataNode = first(nodes); - const metrics = (dataNode && dataNode.metrics) || []; - const content = ( -
-
- {node.name} -
- {metrics.map((metric) => { - const metricName = SnapshotMetricTypeRT.is(metric.name) ? metric.name : 'custom'; - const name = SNAPSHOT_METRIC_TRANSLATIONS[metricName] || metricName; - // if custom metric, find field and label from waffleOptionsContext result - // because useSnapshot does not return it - const customMetric = - name === 'custom' ? customMetrics.find((item) => item.id === metric.name) : null; - const formatter = customMetric - ? createFormatterForMetric(customMetric) - : createInventoryMetricFormatter({ type: metricName }); - return ( - - - {customMetric ? getCustomMetricLabel(customMetric) : name} - - - {(metric.value && formatter(metric.value)) || '-'} - - - ); - })} + const dataNode = first(nodes); + const metrics = (dataNode && dataNode.metrics) || []; + return ( +
+
+ {node.name}
- ); - - return ( - -
- {children} -
-
- ); - } -); + {metrics.map((metric) => { + const metricName = SnapshotMetricTypeRT.is(metric.name) ? metric.name : 'custom'; + const name = SNAPSHOT_METRIC_TRANSLATIONS[metricName] || metricName; + // if custom metric, find field and label from waffleOptionsContext result + // because useSnapshot does not return it + const customMetric = + name === 'custom' ? customMetrics.find((item) => item.id === metric.name) : null; + const formatter = customMetric + ? createFormatterForMetric(customMetric) + : createInventoryMetricFormatter({ type: metricName }); + return ( + + + {customMetric ? getCustomMetricLabel(customMetric) : name} + + + {(metric.value && formatter(metric.value)) || '-'} + + + ); + })} +
+ ); +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx index e972f9ca4f345f..031b826265e16f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx @@ -11,7 +11,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { first } from 'lodash'; -import { EuiPopover } from '@elastic/eui'; +import { EuiPopover, EuiToolTip } from '@elastic/eui'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { InfraWaffleMapBounds, @@ -64,13 +64,10 @@ export class Node extends React.PureComponent { const nodeBorder = this.state.isOverlayOpen ? { border: 'solid 4px #000' } : undefined; const button = ( - + ); return ( diff --git a/x-pack/test/functional/apps/infra/home_page.ts b/x-pack/test/functional/apps/infra/home_page.ts index 1cc7c87f3a1a84..7578abbad33e75 100644 --- a/x-pack/test/functional/apps/infra/home_page.ts +++ b/x-pack/test/functional/apps/infra/home_page.ts @@ -38,9 +38,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); after(async () => await esArchiver.unload('infra/metrics_and_logs')); - it('renders the waffle map for dates with data', async () => { + it('renders the waffle map and tooltips for dates with data', async () => { await pageObjects.infraHome.goToTime(DATE_WITH_DATA); await pageObjects.infraHome.getWaffleMap(); + await pageObjects.infraHome.getWaffleMapTooltips(); }); it('renders an empty data prompt for dates with no data', async () => { diff --git a/x-pack/test/functional/page_objects/infra_home_page.ts b/x-pack/test/functional/page_objects/infra_home_page.ts index 04dfbe5da00024..2f4575d45cc20f 100644 --- a/x-pack/test/functional/page_objects/infra_home_page.ts +++ b/x-pack/test/functional/page_objects/infra_home_page.ts @@ -5,6 +5,7 @@ * 2.0. */ +import expect from '@kbn/expect/expect.js'; import testSubjSelector from '@kbn/test-subj-selector'; import { FtrProviderContext } from '../ftr_provider_context'; @@ -34,6 +35,34 @@ export function InfraHomePageProvider({ getService }: FtrProviderContext) { return await testSubjects.find('waffleMap'); }, + async getWaffleMapTooltips() { + const node = await testSubjects.findAll('nodeContainer'); + await node[0].moveMouseTo(); + const tooltip = await testSubjects.find('conditionalTooltipContent-demo-stack-redis-01'); + const metrics = await tooltip.findAllByTestSubject('conditionalTooltipContent-metric'); + const values = await tooltip.findAllByTestSubject('conditionalTooltipContent-value'); + expect(await metrics[0].getVisibleText()).to.be('CPU usage'); + expect(await values[0].getVisibleText()).to.be('1%'); + expect(await metrics[1].getVisibleText()).to.be('Memory usage'); + expect(await values[1].getVisibleText()).to.be('15.9%'); + expect(await metrics[2].getVisibleText()).to.be('Outbound traffic'); + expect(await values[2].getVisibleText()).to.be('71.9kbit/s'); + expect(await metrics[3].getVisibleText()).to.be('Inbound traffic'); + expect(await values[3].getVisibleText()).to.be('25.6kbit/s'); + await node[1].moveMouseTo(); + const tooltip2 = await testSubjects.find('conditionalTooltipContent-demo-stack-nginx-01'); + const metrics2 = await tooltip2.findAllByTestSubject('conditionalTooltipContent-metric'); + const values2 = await tooltip2.findAllByTestSubject('conditionalTooltipContent-value'); + expect(await metrics2[0].getVisibleText()).to.be('CPU usage'); + expect(await values2[0].getVisibleText()).to.be('1.1%'); + expect(await metrics2[1].getVisibleText()).to.be('Memory usage'); + expect(await values2[1].getVisibleText()).to.be('18%'); + expect(await metrics2[2].getVisibleText()).to.be('Outbound traffic'); + expect(await values2[2].getVisibleText()).to.be('256.3kbit/s'); + expect(await metrics2[3].getVisibleText()).to.be('Inbound traffic'); + expect(await values2[3].getVisibleText()).to.be('255.1kbit/s'); + }, + async openInvenotrySwitcher() { await testSubjects.click('openInventorySwitcher'); return await testSubjects.find('goToHost'); From ca324c63be2d9f9bda21d372c125ac07d924dd25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Tue, 25 May 2021 15:19:42 +0200 Subject: [PATCH 12/78] Removes event filters feature flag and exposes this feature by default (#100389) * Removes event filters feature flag and expose this feature by default * Fixes manifest unit test * Fixes functional test adding event filter list case Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../exception_list_client.mock.ts | 6 +- .../common/experimental_features.ts | 1 - .../public/common/mock/global_state.ts | 1 - .../components/administration_list_page.tsx | 22 ++-- .../event_filter_delete_modal.test.tsx | 1 - .../view/event_filters_list_page.test.tsx | 1 - .../public/management/pages/index.tsx | 7 +- .../timeline/body/actions/index.tsx | 7 +- .../server/endpoint/lib/artifacts/mocks.ts | 30 +++++ .../manifest_manager/manifest_manager.mock.ts | 17 +-- .../manifest_manager/manifest_manager.test.ts | 45 +++++++- .../manifest_manager/manifest_manager.ts | 5 +- .../factory/hosts/details/index.test.tsx | 1 - .../apps/endpoint/policy_details.ts | 108 ++++++++++++++++++ 14 files changed, 204 insertions(+), 48 deletions(-) diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts index 973f5822cae2b2..1566241e7351e0 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.mock.ts @@ -32,9 +32,11 @@ export class ExceptionListClientMock extends ExceptionListClient { public createEndpointList = jest.fn().mockResolvedValue(getExceptionListSchemaMock()); } -export const getExceptionListClientMock = (): ExceptionListClient => { +export const getExceptionListClientMock = ( + savedObject?: ReturnType +): ExceptionListClient => { const mock = new ExceptionListClientMock({ - savedObjectsClient: savedObjectsClientMock.create(), + savedObjectsClient: savedObject ? savedObject : savedObjectsClientMock.create(), user: 'elastic', }); return mock; diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index 8d1cc4ca2c1f07..6195dd61a79841 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -14,7 +14,6 @@ export type ExperimentalFeatures = typeof allowedExperimentalValues; const allowedExperimentalValues = Object.freeze({ trustedAppsByPolicyEnabled: false, metricsEntitiesEnabled: false, - eventFilteringEnabled: false, hostIsolationEnabled: false, }); diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index b1b3147f4f4941..af278b09e719c4 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -40,7 +40,6 @@ export const mockGlobalState: State = { { id: 'error-id-2', title: 'title-2', message: ['error-message-2'] }, ], enableExperimental: { - eventFilteringEnabled: false, trustedAppsByPolicyEnabled: false, metricsEntitiesEnabled: false, hostIsolationEnabled: false, diff --git a/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx b/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx index 02fbb4f4b02962..72a6de2a2de8d1 100644 --- a/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx @@ -25,7 +25,6 @@ import { getEventFiltersListPath, getTrustedAppsListPath, } from '../common/routing'; -import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; /** Ensure that all flyouts z-index in Administation area show the flyout header */ const EuiPanelStyled = styled(EuiPanel)` @@ -44,7 +43,6 @@ interface AdministrationListPageProps { export const AdministrationListPage: FC = memo( ({ beta, title, subtitle, actions, children, headerBackComponent, ...otherProps }) => { - const isEventFilteringEnabled = useIsExperimentalFeatureEnabled('eventFilteringEnabled'); const badgeOptions = !beta ? undefined : { beta: true, text: BETA_BADGE_LABEL }; return ( @@ -77,18 +75,14 @@ export const AdministrationListPage: FC diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filter_delete_modal.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filter_delete_modal.test.tsx index cec3e34d9c98fd..c594aaa5c7e19d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filter_delete_modal.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filter_delete_modal.test.tsx @@ -64,7 +64,6 @@ describe('When event filters delete modal is shown', () => { }; waitForAction = mockedContext.middlewareSpy.waitForAction; - mockedContext.setExperimentalFlag({ eventFilteringEnabled: true }); }); it('should display name of event filter in body message', async () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx index 2fbabad746cad7..465f92dfda767f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx @@ -41,7 +41,6 @@ describe('When on the Event Filters List Page', () => { waitForAction = mockedContext.middlewareSpy.waitForAction; act(() => { - mockedContext.setExperimentalFlag({ eventFilteringEnabled: true }); history.push('/event_filters'); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/index.tsx b/x-pack/plugins/security_solution/public/management/pages/index.tsx index 4be75117daedad..8273f1a6e55c20 100644 --- a/x-pack/plugins/security_solution/public/management/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/index.tsx @@ -25,7 +25,6 @@ import { SecurityPageName } from '../../../common/constants'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled'; import { EventFiltersContainer } from './event_filters'; -import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; const NoPermissions = memo(() => { return ( @@ -58,7 +57,6 @@ NoPermissions.displayName = 'NoPermissions'; export const ManagementContainer = memo(() => { const history = useHistory(); - const isEventFilteringEnabled = useIsExperimentalFeatureEnabled('eventFilteringEnabled'); const { allEnabled: isIngestEnabled } = useIngestEnabledCheck(); if (!isIngestEnabled) { @@ -70,10 +68,7 @@ export const ManagementContainer = memo(() => { - - {isEventFilteringEnabled && ( - - )} + = ({ const emptyNotes: string[] = []; const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - const isEventFilteringEnabled = useIsExperimentalFeatureEnabled('eventFilteringEnabled'); - const handleSelectEvent = useCallback( (event: React.ChangeEvent) => onRowSelected({ @@ -116,8 +113,8 @@ const ActionsComponent: React.FC = ({ const eventType = getEventType(ecsData); const isEventContextMenuEnabled = useMemo( - () => isEventFilteringEnabled && !!ecsData.event?.kind && ecsData.event?.kind[0] === 'event', - [ecsData.event?.kind, isEventFilteringEnabled] + () => !!ecsData.event?.kind && ecsData.event?.kind[0] === 'event', + [ecsData.event?.kind] ); return ( diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts index 85857301d5f399..cda42bdf3f585e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/mocks.ts @@ -92,6 +92,36 @@ export const createPackagePolicyWithInitialManifestMock = (): PackagePolicy => { artifact_manifest: { value: { artifacts: { + 'endpoint-eventfilterlist-linux-v1': { + compression_algorithm: 'zlib', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/fleet/artifacts/endpoint-eventfilterlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-eventfilterlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/fleet/artifacts/endpoint-eventfilterlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-eventfilterlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/fleet/artifacts/endpoint-eventfilterlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, 'endpoint-exceptionlist-macos-v1': { compression_algorithm: 'zlib', encryption_algorithm: 'none', diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts index f471ace617a6dc..e0bbfc351a20f1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts @@ -69,13 +69,16 @@ export interface ManifestManagerMockOptions { export const buildManifestManagerMockOptions = ( opts: Partial -): ManifestManagerMockOptions => ({ - cache: new LRU({ max: 10, maxAge: 1000 * 60 * 60 }), - exceptionListClient: listMock.getExceptionListClient(), - packagePolicyService: createPackagePolicyServiceMock(), - savedObjectsClient: savedObjectsClientMock.create(), - ...opts, -}); +): ManifestManagerMockOptions => { + const savedObjectMock = savedObjectsClientMock.create(); + return { + cache: new LRU({ max: 10, maxAge: 1000 * 60 * 60 }), + exceptionListClient: listMock.getExceptionListClient(savedObjectMock), + packagePolicyService: createPackagePolicyServiceMock(), + savedObjectsClient: savedObjectMock, + ...opts, + }; +}; export const buildManifestManagerContextMock = ( opts: Partial diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts index e1de39482428d7..7719dbf30c72bf 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts @@ -65,6 +65,9 @@ describe('ManifestManager', () => { const ARTIFACT_NAME_TRUSTED_APPS_MACOS = 'endpoint-trustlist-macos-v1'; const ARTIFACT_NAME_TRUSTED_APPS_WINDOWS = 'endpoint-trustlist-windows-v1'; const ARTIFACT_NAME_TRUSTED_APPS_LINUX = 'endpoint-trustlist-linux-v1'; + const ARTIFACT_NAME_EVENT_FILTERS_MACOS = 'endpoint-eventfilterlist-macos-v1'; + const ARTIFACT_NAME_EVENT_FILTERS_WINDOWS = 'endpoint-eventfilterlist-windows-v1'; + const ARTIFACT_NAME_EVENT_FILTERS_LINUX = 'endpoint-eventfilterlist-linux-v1'; let ARTIFACTS: InternalArtifactCompleteSchema[] = []; let ARTIFACTS_BY_ID: { [K: string]: InternalArtifactCompleteSchema } = {}; @@ -219,6 +222,9 @@ describe('ManifestManager', () => { ARTIFACT_NAME_TRUSTED_APPS_MACOS, ARTIFACT_NAME_TRUSTED_APPS_WINDOWS, ARTIFACT_NAME_TRUSTED_APPS_LINUX, + ARTIFACT_NAME_EVENT_FILTERS_MACOS, + ARTIFACT_NAME_EVENT_FILTERS_WINDOWS, + ARTIFACT_NAME_EVENT_FILTERS_LINUX, ]; const getArtifactIds = (artifacts: InternalArtifactSchema[]) => [ @@ -249,6 +255,11 @@ describe('ManifestManager', () => { context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({}); context.packagePolicyService.listIds = mockPolicyListIdsResponse([TEST_POLICY_ID_1]); + context.savedObjectsClient.create = jest + .fn() + .mockImplementation((type: string, object: InternalManifestSchema) => ({ + attributes: object, + })); const manifest = await manifestManager.buildNewManifest(); expect(manifest?.getSchemaVersion()).toStrictEqual('v1'); @@ -257,7 +268,7 @@ describe('ManifestManager', () => { const artifacts = manifest.getAllArtifacts(); - expect(artifacts.length).toBe(6); + expect(artifacts.length).toBe(9); expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES); expect(artifacts.every(isCompressed)).toBe(true); @@ -280,6 +291,11 @@ describe('ManifestManager', () => { [ENDPOINT_LIST_ID]: { macos: [exceptionListItem] }, [ENDPOINT_TRUSTED_APPS_LIST_ID]: { linux: [trustedAppListItem] }, }); + context.savedObjectsClient.create = jest + .fn() + .mockImplementation((type: string, object: InternalManifestSchema) => ({ + attributes: object, + })); context.packagePolicyService.listIds = mockPolicyListIdsResponse([TEST_POLICY_ID_1]); const manifest = await manifestManager.buildNewManifest(); @@ -290,7 +306,7 @@ describe('ManifestManager', () => { const artifacts = manifest.getAllArtifacts(); - expect(artifacts.length).toBe(6); + expect(artifacts.length).toBe(9); expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES); expect(artifacts.every(isCompressed)).toBe(true); @@ -304,6 +320,9 @@ describe('ManifestManager', () => { expect(await uncompressArtifact(artifacts[5])).toStrictEqual({ entries: translateToEndpointExceptions([trustedAppListItem], 'v1'), }); + expect(await uncompressArtifact(artifacts[6])).toStrictEqual({ entries: [] }); + expect(await uncompressArtifact(artifacts[7])).toStrictEqual({ entries: [] }); + expect(await uncompressArtifact(artifacts[8])).toStrictEqual({ entries: [] }); for (const artifact of artifacts) { expect(manifest.isDefaultArtifact(artifact)).toBe(true); @@ -323,7 +342,11 @@ describe('ManifestManager', () => { [ENDPOINT_LIST_ID]: { macos: [exceptionListItem] }, }); context.packagePolicyService.listIds = mockPolicyListIdsResponse([TEST_POLICY_ID_1]); - + context.savedObjectsClient.create = jest + .fn() + .mockImplementation((type: string, object: InternalManifestSchema) => ({ + attributes: object, + })); const oldManifest = await manifestManager.buildNewManifest(); context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({ @@ -339,7 +362,7 @@ describe('ManifestManager', () => { const artifacts = manifest.getAllArtifacts(); - expect(artifacts.length).toBe(6); + expect(artifacts.length).toBe(9); expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES); expect(artifacts.every(isCompressed)).toBe(true); @@ -351,6 +374,9 @@ describe('ManifestManager', () => { expect(await uncompressArtifact(artifacts[5])).toStrictEqual({ entries: translateToEndpointExceptions([trustedAppListItem], 'v1'), }); + expect(await uncompressArtifact(artifacts[6])).toStrictEqual({ entries: [] }); + expect(await uncompressArtifact(artifacts[7])).toStrictEqual({ entries: [] }); + expect(await uncompressArtifact(artifacts[8])).toStrictEqual({ entries: [] }); for (const artifact of artifacts) { expect(manifest.isDefaultArtifact(artifact)).toBe(true); @@ -384,6 +410,12 @@ describe('ManifestManager', () => { TEST_POLICY_ID_2, ]); + context.savedObjectsClient.create = jest + .fn() + .mockImplementation((type: string, object: InternalManifestSchema) => ({ + attributes: object, + })); + const manifest = await manifestManager.buildNewManifest(); expect(manifest?.getSchemaVersion()).toStrictEqual('v1'); @@ -392,7 +424,7 @@ describe('ManifestManager', () => { const artifacts = manifest.getAllArtifacts(); - expect(artifacts.length).toBe(7); + expect(artifacts.length).toBe(10); expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES); expect(artifacts.every(isCompressed)).toBe(true); @@ -412,6 +444,9 @@ describe('ManifestManager', () => { 'v1' ), }); + expect(await uncompressArtifact(artifacts[7])).toStrictEqual({ entries: [] }); + expect(await uncompressArtifact(artifacts[8])).toStrictEqual({ entries: [] }); + expect(await uncompressArtifact(artifacts[9])).toStrictEqual({ entries: [] }); for (const artifact of artifacts.slice(0, 5)) { expect(manifest.isDefaultArtifact(artifact)).toBe(true); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index fe4aba165d2bda..6c25b6152938f7 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -361,10 +361,7 @@ export class ManifestManager { const results = await Promise.all([ this.buildExceptionListArtifacts(), this.buildTrustedAppsArtifacts(), - // If Endpoint Event Filtering feature is ON, then add in the exceptions for them - ...(this.experimentalFeatures.eventFilteringEnabled - ? [this.buildEventFiltersArtifacts()] - : []), + this.buildEventFiltersArtifacts(), ]); const manifest = new Manifest({ diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx index 4474b9f288570e..e43db6b86f8b90 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx @@ -32,7 +32,6 @@ const mockDeps = { experimentalFeatures: { trustedAppsByPolicyEnabled: false, metricsEntitiesEnabled: false, - eventFilteringEnabled: false, hostIsolationEnabled: false, }, service: {} as EndpointAppContextService, diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index d8bc9f6444f646..44348d1ad0d9c4 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -248,6 +248,42 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { relative_url: '/api/fleet/artifacts/endpoint-trustlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', }, + 'endpoint-eventfilterlist-linux-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/fleet/artifacts/endpoint-eventfilterlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-eventfilterlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/fleet/artifacts/endpoint-eventfilterlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-eventfilterlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/fleet/artifacts/endpoint-eventfilterlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, }, // The manifest version could have changed when the Policy was updated because the // policy details page ensures that a save action applies the udpated policy on top @@ -416,6 +452,42 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { relative_url: '/api/fleet/artifacts/endpoint-trustlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', }, + 'endpoint-eventfilterlist-linux-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/fleet/artifacts/endpoint-eventfilterlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-eventfilterlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/fleet/artifacts/endpoint-eventfilterlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-eventfilterlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/fleet/artifacts/endpoint-eventfilterlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, }, // The manifest version could have changed when the Policy was updated because the // policy details page ensures that a save action applies the udpated policy on top @@ -582,6 +654,42 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { relative_url: '/api/fleet/artifacts/endpoint-trustlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', }, + 'endpoint-eventfilterlist-linux-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/fleet/artifacts/endpoint-eventfilterlist-linux-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-eventfilterlist-macos-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/fleet/artifacts/endpoint-eventfilterlist-macos-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, + 'endpoint-eventfilterlist-windows-v1': { + compression_algorithm: 'zlib', + decoded_sha256: + 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + decoded_size: 14, + encoded_sha256: + 'f8e6afa1d5662f5b37f83337af774b5785b5b7f1daee08b7b00c2d6813874cda', + encoded_size: 22, + encryption_algorithm: 'none', + relative_url: + '/api/fleet/artifacts/endpoint-eventfilterlist-windows-v1/d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', + }, }, // The manifest version could have changed when the Policy was updated because the // policy details page ensures that a save action applies the udpated policy on top From 73b6048ba1076f53b826214ac9736bf7e2f8d5b4 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Tue, 25 May 2021 09:51:57 -0400 Subject: [PATCH 13/78] [Maps][Vega] Isolate mapbox-gl library into bazel package (#99931) --- package.json | 1 + packages/BUILD.bazel | 1 + packages/kbn-mapbox-gl/BUILD.bazel | 84 +++++++++++++++++++ packages/kbn-mapbox-gl/README.md | 3 + packages/kbn-mapbox-gl/package.json | 8 ++ packages/kbn-mapbox-gl/src/index.ts | 20 +++++ packages/kbn-mapbox-gl/src/typings.ts | 10 +++ packages/kbn-mapbox-gl/tsconfig.json | 16 ++++ .../vega_map_view/vega_map_view.scss | 2 - .../vega_view/vega_map_view/view.test.ts | 49 +++++------ .../public/vega_view/vega_map_view/view.ts | 13 +-- .../map_container/map_container.tsx | 1 - .../connected_components/mb_map/mb_map.tsx | 15 ++-- yarn.lock | 4 + 14 files changed, 180 insertions(+), 47 deletions(-) create mode 100644 packages/kbn-mapbox-gl/BUILD.bazel create mode 100644 packages/kbn-mapbox-gl/README.md create mode 100644 packages/kbn-mapbox-gl/package.json create mode 100644 packages/kbn-mapbox-gl/src/index.ts create mode 100644 packages/kbn-mapbox-gl/src/typings.ts create mode 100644 packages/kbn-mapbox-gl/tsconfig.json diff --git a/package.json b/package.json index 5737bce303e09c..73f3e5585faf74 100644 --- a/package.json +++ b/package.json @@ -130,6 +130,7 @@ "@kbn/config": "link:bazel-bin/packages/kbn-config/npm_module", "@kbn/config-schema": "link:bazel-bin/packages/kbn-config-schema/npm_module", "@kbn/crypto": "link:bazel-bin/packages/kbn-crypto/npm_module", + "@kbn/mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl/npm_module", "@kbn/i18n": "link:bazel-bin/packages/kbn-i18n/npm_module", "@kbn/interpreter": "link:packages/kbn-interpreter", "@kbn/io-ts-utils": "link:packages/kbn-io-ts-utils", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index ec8252cb6144da..43528e0ae41629 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -24,6 +24,7 @@ filegroup( "//packages/kbn-i18n:build", "//packages/kbn-legacy-logging:build", "//packages/kbn-logging:build", + "//packages/kbn-mapbox-gl:build", "//packages/kbn-plugin-generator:build", "//packages/kbn-securitysolution-list-constants:build", "//packages/kbn-securitysolution-io-ts-types:build", diff --git a/packages/kbn-mapbox-gl/BUILD.bazel b/packages/kbn-mapbox-gl/BUILD.bazel new file mode 100644 index 00000000000000..7d7186068832ec --- /dev/null +++ b/packages/kbn-mapbox-gl/BUILD.bazel @@ -0,0 +1,84 @@ + +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-mapbox-gl" +PKG_REQUIRE_NAME = "@kbn/mapbox-gl" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md" +] + +SRC_DEPS = [ + "@npm//@mapbox/mapbox-gl-rtl-text", + "@npm//file-loader", + "@npm//mapbox-gl", +] + +TYPES_DEPS = [ + "@npm//@types/mapbox-gl", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = DEPS + [":tsc"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-mapbox-gl/README.md b/packages/kbn-mapbox-gl/README.md new file mode 100644 index 00000000000000..c1fdea491feaae --- /dev/null +++ b/packages/kbn-mapbox-gl/README.md @@ -0,0 +1,3 @@ +# @kbn/mapbox-gl + +Default instantiation for mapbox-gl. \ No newline at end of file diff --git a/packages/kbn-mapbox-gl/package.json b/packages/kbn-mapbox-gl/package.json new file mode 100644 index 00000000000000..9de88dac54a5ab --- /dev/null +++ b/packages/kbn-mapbox-gl/package.json @@ -0,0 +1,8 @@ +{ + "name": "@kbn/mapbox-gl", + "version": "1.0.0", + "private": true, + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "./target/index.js", + "types": "./target/index.d.ts" +} diff --git a/packages/kbn-mapbox-gl/src/index.ts b/packages/kbn-mapbox-gl/src/index.ts new file mode 100644 index 00000000000000..117b874a28ffbd --- /dev/null +++ b/packages/kbn-mapbox-gl/src/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import './typings'; +import mapboxgl from 'mapbox-gl/dist/mapbox-gl-csp'; +// @ts-expect-error +import mbRtlPlugin from '!!file-loader!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js'; +// @ts-expect-error +import mbWorkerUrl from '!!file-loader!mapbox-gl/dist/mapbox-gl-csp-worker'; +import 'mapbox-gl/dist/mapbox-gl.css'; + +mapboxgl.workerUrl = mbWorkerUrl; +mapboxgl.setRTLTextPlugin(mbRtlPlugin); + +export { mapboxgl }; diff --git a/packages/kbn-mapbox-gl/src/typings.ts b/packages/kbn-mapbox-gl/src/typings.ts new file mode 100644 index 00000000000000..0cc6908aca4284 --- /dev/null +++ b/packages/kbn-mapbox-gl/src/typings.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// Mapbox-gl doesn't declare this type. +declare module 'mapbox-gl/dist/mapbox-gl-csp'; diff --git a/packages/kbn-mapbox-gl/tsconfig.json b/packages/kbn-mapbox-gl/tsconfig.json new file mode 100644 index 00000000000000..cf1cca0f5a0fd1 --- /dev/null +++ b/packages/kbn-mapbox-gl/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "incremental": true, + "outDir": "./target", + "declaration": true, + "declarationMap": true, + "rootDir": "src", + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-mapbox-gl/src", + "types": [] + }, + "include": [ + "src/**/*", + ] +} diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/vega_map_view.scss b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/vega_map_view.scss index 33e63e7ef317c1..3e3ef71faf0d7a 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/vega_map_view.scss +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/vega_map_view.scss @@ -1,5 +1,3 @@ -@import '~mapbox-gl/dist/mapbox-gl.css'; - .vgaVis { .mapboxgl-canvas-container { cursor: auto; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts index 4fd19aa45e69e7..ee3bf305e9427e 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts @@ -29,30 +29,31 @@ import { } from '../../services'; import { initVegaLayer, initTmsRasterLayer } from './layers'; -// @ts-expect-error -import mapboxgl from 'mapbox-gl/dist/mapbox-gl-csp'; - -jest.mock('mapbox-gl/dist/mapbox-gl-csp', () => ({ - setRTLTextPlugin: jest.fn(), - Map: jest.fn().mockImplementation(() => ({ - getLayer: () => '', - removeLayer: jest.fn(), - once: (eventName: string, handler: Function) => handler(), - remove: () => jest.fn(), - getCanvas: () => ({ clientWidth: 512, clientHeight: 512 }), - getCenter: () => ({ lat: 20, lng: 20 }), - getZoom: () => 3, - addControl: jest.fn(), - addLayer: jest.fn(), - dragRotate: { - disable: jest.fn(), - }, - touchZoomRotate: { - disableRotation: jest.fn(), - }, - })), - MapboxOptions: jest.fn(), - NavigationControl: jest.fn(), +import { mapboxgl } from '@kbn/mapbox-gl'; + +jest.mock('@kbn/mapbox-gl', () => ({ + mapboxgl: { + setRTLTextPlugin: jest.fn(), + Map: jest.fn().mockImplementation(() => ({ + getLayer: () => '', + removeLayer: jest.fn(), + once: (eventName: string, handler: Function) => handler(), + remove: () => jest.fn(), + getCanvas: () => ({ clientWidth: 512, clientHeight: 512 }), + getCenter: () => ({ lat: 20, lng: 20 }), + getZoom: () => 3, + addControl: jest.fn(), + addLayer: jest.fn(), + dragRotate: { + disable: jest.fn(), + }, + touchZoomRotate: { + disableRotation: jest.fn(), + }, + })), + MapboxOptions: jest.fn(), + NavigationControl: jest.fn(), + }, })); jest.mock('./layers', () => ({ diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts index e899057819a192..835ac36ceee471 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts @@ -10,8 +10,9 @@ import { i18n } from '@kbn/i18n'; import type { Map, Style, MapboxOptions } from 'mapbox-gl'; import { View, parse } from 'vega'; -// @ts-expect-error -import mapboxgl from 'mapbox-gl/dist/mapbox-gl-csp'; + +import { mapboxgl } from '@kbn/mapbox-gl'; + import { initTmsRasterLayer, initVegaLayer } from './layers'; import { VegaBaseView } from '../vega_base_view'; import { getMapServiceSettings } from '../../services'; @@ -27,14 +28,6 @@ import { import { validateZoomSettings, injectMapPropsIntoSpec } from './utils'; import './vega_map_view.scss'; -// @ts-expect-error -import mbRtlPlugin from '!!file-loader!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js'; -// @ts-expect-error -import mbWorkerUrl from '!!file-loader!mapbox-gl/dist/mapbox-gl-csp-worker'; - -mapboxgl.workerUrl = mbWorkerUrl; -mapboxgl.setRTLTextPlugin(mbRtlPlugin); - async function updateVegaView(mapBoxInstance: Map, vegaView: View) { const mapCanvas = mapBoxInstance.getCanvas(); const { lat, lng } = mapBoxInstance.getCenter(); diff --git a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx index e0cfe978bf45cf..851fd583b4251f 100644 --- a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx +++ b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx @@ -30,7 +30,6 @@ import { registerLayerWizards } from '../../classes/layers/load_layer_wizards'; import { RenderToolTipContent } from '../../classes/tooltips/tooltip_property'; import { GeoFieldWithIndex } from '../../components/geo_field_with_index'; import { MapRefreshConfig } from '../../../common/descriptor_types'; -import 'mapbox-gl/dist/mapbox-gl.css'; const RENDER_COMPLETE_EVENT = 'renderComplete'; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx index ac3e72545033fb..355e49564620dd 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx @@ -7,9 +7,8 @@ import _ from 'lodash'; import React, { Component } from 'react'; -import { Map as MapboxMap, MapboxOptions, MapMouseEvent } from 'mapbox-gl'; -// @ts-expect-error -import mapboxgl from 'mapbox-gl/dist/mapbox-gl-csp'; +import type { Map as MapboxMap, MapboxOptions, MapMouseEvent } from 'mapbox-gl'; + // @ts-expect-error import { spritesheet } from '@elastic/maki'; import sprites1 from '@elastic/maki/dist/sprite@1.png'; @@ -17,6 +16,9 @@ import sprites2 from '@elastic/maki/dist/sprite@2.png'; import { Adapters } from 'src/plugins/inspector/public'; import { Filter } from 'src/plugins/data/public'; import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; + +import { mapboxgl } from '@kbn/mapbox-gl'; + import { DrawFilterControl } from './draw_control'; import { ScaleControl } from './scale_control'; import { TooltipControl } from './tooltip_control'; @@ -45,13 +47,6 @@ import { GeoFieldWithIndex } from '../../components/geo_field_with_index'; import { RenderToolTipContent } from '../../classes/tooltips/tooltip_property'; import { MapExtentState } from '../../actions'; import { TileStatusTracker } from './tile_status_tracker'; -// @ts-expect-error -import mbRtlPlugin from '!!file-loader!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js'; -// @ts-expect-error -import mbWorkerUrl from '!!file-loader!mapbox-gl/dist/mapbox-gl-csp-worker'; - -mapboxgl.workerUrl = mbWorkerUrl; -mapboxgl.setRTLTextPlugin(mbRtlPlugin); export interface Props { isMapReady: boolean; diff --git a/yarn.lock b/yarn.lock index f1f421e2a766f5..9967cedea9fde1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2678,6 +2678,10 @@ version "0.0.0" uid "" +"@kbn/mapbox-gl@link:bazel-bin/packages/kbn-mapbox-gl/npm_module": + version "0.0.0" + uid "" + "@kbn/monaco@link:packages/kbn-monaco": version "0.0.0" uid "" From e2d47f72694014ff6063fc0c7858bed6ed07fb9c Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 25 May 2021 07:45:07 -0700 Subject: [PATCH 14/78] [DOCS] Remove redundant maps attribute (#100426) --- docs/maps/index.asciidoc | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/maps/index.asciidoc b/docs/maps/index.asciidoc index e4150fc280096c..45d24bfb5a7e4f 100644 --- a/docs/maps/index.asciidoc +++ b/docs/maps/index.asciidoc @@ -1,6 +1,5 @@ :ems-docker-repo: docker.elastic.co/elastic-maps-service/elastic-maps-server-ubi8 :ems-docker-image: {ems-docker-repo}:{version} -:hosted-ems: Elastic Maps Server [role="xpack"] [[maps]] From 662fe7475738d90120342503e5a56016c2a5ee95 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 25 May 2021 17:02:48 +0200 Subject: [PATCH 15/78] [Reporting] ILM policy for managing reporting indices (#100130) * wip; added logic for creating ILM policy at start up * added log when ilm policy is not created * added test for start function * updated ilm policy to not delete data * actually update jest snapshots and remove unused import * updated the ilm policy, removed the min_age for the hot phase * update jest snapshot * removed TODO comment * debug log -> info log Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/lib/store/report_ilm_policy.ts | 18 +++++ .../reporting/server/lib/store/store.test.ts | 39 +++++++++++ .../reporting/server/lib/store/store.ts | 66 +++++++++++++++---- x-pack/plugins/reporting/server/plugin.ts | 3 + 4 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 x-pack/plugins/reporting/server/lib/store/report_ilm_policy.ts diff --git a/x-pack/plugins/reporting/server/lib/store/report_ilm_policy.ts b/x-pack/plugins/reporting/server/lib/store/report_ilm_policy.ts new file mode 100644 index 00000000000000..f4cd69a0331d7d --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/store/report_ilm_policy.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PutLifecycleRequest } from '@elastic/elasticsearch/api/types'; + +export const reportingIlmPolicy: PutLifecycleRequest['body'] = { + policy: { + phases: { + hot: { + actions: {}, + }, + }, + }, +}; diff --git a/x-pack/plugins/reporting/server/lib/store/store.test.ts b/x-pack/plugins/reporting/server/lib/store/store.test.ts index 7f96433fcc6ceb..fa35240dfc8fbd 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.test.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.test.ts @@ -7,6 +7,7 @@ import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import { ElasticsearchClient } from 'src/core/server'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { ReportingCore } from '../../'; import { createMockConfigSchema, @@ -16,6 +17,8 @@ import { import { Report, ReportDocument } from './report'; import { ReportingStore } from './store'; +const { createApiResponse } = elasticsearchServiceMock; + describe('ReportingStore', () => { const mockLogger = createMockLevelLogger(); let mockCore: ReportingCore; @@ -403,4 +406,40 @@ describe('ReportingStore', () => { ] `); }); + + describe('start', () => { + it('creates an ILM policy for managing reporting indices if there is not already one', async () => { + mockEsClient.ilm.getLifecycle.mockRejectedValueOnce(createApiResponse({ statusCode: 404 })); + mockEsClient.ilm.putLifecycle.mockResolvedValueOnce(createApiResponse()); + + const store = new ReportingStore(mockCore, mockLogger); + await store.start(); + + expect(mockEsClient.ilm.getLifecycle).toHaveBeenCalledWith({ policy: 'kibana-reporting' }); + expect(mockEsClient.ilm.putLifecycle.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "body": Object { + "policy": Object { + "phases": Object { + "hot": Object { + "actions": Object {}, + }, + }, + }, + }, + "policy": "kibana-reporting", + } + `); + }); + + it('does not create an ILM policy for managing reporting indices if one already exists', async () => { + mockEsClient.ilm.getLifecycle.mockResolvedValueOnce(createApiResponse()); + + const store = new ReportingStore(mockCore, mockLogger); + await store.start(); + + expect(mockEsClient.ilm.getLifecycle).toHaveBeenCalledWith({ policy: 'kibana-reporting' }); + expect(mockEsClient.ilm.putLifecycle).not.toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts index fc7bd9c23d7693..9fb203fd5627ab 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.ts @@ -14,6 +14,7 @@ import { ReportTaskParams } from '../tasks'; import { indexTimestamp } from './index_timestamp'; import { mapping } from './mapping'; import { Report, ReportDocument, ReportSource } from './report'; +import { reportingIlmPolicy } from './report_ilm_policy'; /* * When searching for long-pending reports, we get a subset of fields @@ -71,19 +72,22 @@ export class ReportingStore { return exists; } - const indexSettings = { - number_of_shards: 1, - auto_expand_replicas: '0-1', - }; - const body = { - settings: indexSettings, - mappings: { - properties: mapping, - }, - }; - try { - await client.indices.create({ index: indexName, body }); + await client.indices.create({ + index: indexName, + body: { + settings: { + number_of_shards: 1, + auto_expand_replicas: '0-1', + lifecycle: { + name: this.ilmPolicyName, + }, + }, + mappings: { + properties: mapping, + }, + }, + }); return true; } catch (error) { @@ -130,6 +134,44 @@ export class ReportingStore { return client.indices.refresh({ index }); } + private readonly ilmPolicyName = 'kibana-reporting'; + + private async doesIlmPolicyExist(): Promise { + const client = await this.getClient(); + try { + await client.ilm.getLifecycle({ policy: this.ilmPolicyName }); + return true; + } catch (e) { + if (e.statusCode === 404) { + return false; + } + throw e; + } + } + + /** + * Function to be called during plugin start phase. This ensures the environment is correctly + * configured for storage of reports. + */ + public async start() { + const client = await this.getClient(); + try { + if (await this.doesIlmPolicyExist()) { + this.logger.debug(`Found ILM policy ${this.ilmPolicyName}; skipping creation.`); + return; + } + this.logger.info(`Creating ILM policy for managing reporting indices: ${this.ilmPolicyName}`); + await client.ilm.putLifecycle({ + policy: this.ilmPolicyName, + body: reportingIlmPolicy, + }); + } catch (e) { + this.logger.error('Error in start phase'); + this.logger.error(e.body.error); + throw e; + } + } + public async addReport(report: Report): Promise { let index = report._index; if (!index) { diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index fc52e10dd0cf94..efe1d9450bef31 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -107,6 +107,9 @@ export class ReportingPlugin logger: this.logger, }); + // Note: this must be called after ReportingCore.pluginStart + await store.start(); + this.logger.debug('Start complete'); })().catch((e) => { this.logger.error(`Error in Reporting start, reporting may not function properly`); From a818b2ad9d188eae1d510157605e916fddfd4bfd Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 25 May 2021 17:03:44 +0200 Subject: [PATCH 16/78] [Reporting] ILM policy for managing reporting indices (#100130) * wip; added logic for creating ILM policy at start up * added log when ilm policy is not created * added test for start function * updated ilm policy to not delete data * actually update jest snapshots and remove unused import * updated the ilm policy, removed the min_age for the hot phase * update jest snapshot * removed TODO comment * debug log -> info log Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> From d8c259478947d647f2781296f2ef6c3ff1441d54 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 25 May 2021 18:56:29 +0300 Subject: [PATCH 17/78] [XY] [Lens] Adds opacity slider (#100453) * [XY] Add opacity slider and dots size slider * [Lens] Adds fill opacity slider * Make the new sliders to appear fullwidth * Change property name and fix unit tests * Add a comment * useDebouncedValue hook Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/__snapshots__/to_ast.test.ts.snap | 2 +- .../vis_type_xy/public/config/get_config.ts | 2 + .../__snapshots__/index.test.tsx.snap | 2 + .../__snapshots__/line_options.test.tsx.snap | 9 --- .../__snapshots__/point_options.test.tsx.snap | 46 ++++++++++++++ .../metrics_axes/chart_options.test.tsx | 14 +++++ .../options/metrics_axes/chart_options.tsx | 4 ++ .../options/metrics_axes/line_options.tsx | 11 ---- .../components/options/metrics_axes/mocks.ts | 1 + .../metrics_axes/point_options.test.tsx | 44 +++++++++++++ .../options/metrics_axes/point_options.tsx | 63 +++++++++++++++++++ .../components/options/metrics_axes/utils.ts | 1 + .../point_series/elastic_charts_options.tsx | 37 +++++++++-- .../point_series/point_series.mocks.ts | 2 + .../expression_functions/series_param.ts | 8 +++ .../public/expression_functions/xy_vis_fn.ts | 7 +++ .../public/sample_vis.test.mocks.ts | 3 + src/plugins/vis_type_xy/public/to_ast.ts | 2 + .../vis_type_xy/public/types/config.ts | 1 + src/plugins/vis_type_xy/public/types/param.ts | 3 + .../public/utils/render_all_series.test.tsx | 1 + .../public/utils/render_all_series.tsx | 14 ++++- .../vis_type_xy/public/vis_types/area.ts | 1 + .../vis_type_xy/public/vis_types/histogram.ts | 1 + .../public/vis_types/horizontal_bar.ts | 1 + .../vis_type_xy/public/vis_types/line.ts | 1 + .../__snapshots__/to_expression.test.ts.snap | 3 + .../public/xy_visualization/expression.tsx | 7 +++ .../public/xy_visualization/to_expression.ts | 1 + .../lens/public/xy_visualization/types.ts | 2 + .../fill_opacity_option.test.tsx | 35 +++++++++++ .../fill_opacity_option.tsx | 59 +++++++++++++++++ .../missing_values_option.tsx | 10 ++- .../visual_options_popover.test.tsx | 51 +++++++++++++++ .../visual_options_popover.tsx | 16 +++++ .../public/xy_visualization/xy_suggestions.ts | 1 + 36 files changed, 437 insertions(+), 29 deletions(-) create mode 100644 src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/point_options.test.tsx.snap create mode 100644 src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/point_options.test.tsx create mode 100644 src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/point_options.tsx create mode 100644 x-pack/plugins/lens/public/xy_visualization/visual_options_popover/fill_opacity_option.test.tsx create mode 100644 x-pack/plugins/lens/public/xy_visualization/visual_options_popover/fill_opacity_option.tsx diff --git a/src/plugins/vis_type_vislib/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_type_vislib/public/__snapshots__/to_ast.test.ts.snap index 3ca2834a54fca2..8b720568c4d2c4 100644 --- a/src/plugins/vis_type_vislib/public/__snapshots__/to_ast.test.ts.snap +++ b/src/plugins/vis_type_vislib/public/__snapshots__/to_ast.test.ts.snap @@ -8,7 +8,7 @@ Object { "area", ], "visConfig": Array [ - "{\\"type\\":\\"area\\",\\"grid\\":{\\"categoryLines\\":false,\\"style\\":{\\"color\\":\\"#eee\\"}},\\"categoryAxes\\":[{\\"id\\":\\"CategoryAxis-1\\",\\"type\\":\\"category\\",\\"position\\":\\"bottom\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\"},\\"labels\\":{\\"show\\":true,\\"truncate\\":100},\\"title\\":{}}],\\"valueAxes\\":[{\\"id\\":\\"ValueAxis-1\\",\\"name\\":\\"LeftAxis-1\\",\\"type\\":\\"value\\",\\"position\\":\\"left\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\",\\"mode\\":\\"normal\\"},\\"labels\\":{\\"show\\":true,\\"rotate\\":0,\\"filter\\":false,\\"truncate\\":100},\\"title\\":{\\"text\\":\\"Sum of total_quantity\\"}}],\\"seriesParams\\":[{\\"show\\":\\"true\\",\\"type\\":\\"area\\",\\"mode\\":\\"stacked\\",\\"data\\":{\\"label\\":\\"Sum of total_quantity\\",\\"id\\":\\"1\\"},\\"drawLinesBetweenPoints\\":true,\\"showCircles\\":true,\\"interpolate\\":\\"linear\\",\\"valueAxis\\":\\"ValueAxis-1\\"}],\\"addTooltip\\":true,\\"addLegend\\":true,\\"legendPosition\\":\\"top\\",\\"times\\":[],\\"addTimeMarker\\":false,\\"thresholdLine\\":{\\"show\\":false,\\"value\\":10,\\"width\\":1,\\"style\\":\\"full\\",\\"color\\":\\"#E7664C\\"},\\"palette\\":{\\"name\\":\\"default\\"},\\"labels\\":{},\\"dimensions\\":{\\"x\\":{\\"accessor\\":1,\\"format\\":{\\"id\\":\\"date\\",\\"params\\":{\\"pattern\\":\\"HH:mm:ss.SSS\\"}},\\"params\\":{}},\\"y\\":[{\\"accessor\\":0,\\"format\\":{\\"id\\":\\"number\\",\\"params\\":{\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}],\\"series\\":[{\\"accessor\\":2,\\"format\\":{\\"id\\":\\"terms\\",\\"params\\":{\\"id\\":\\"string\\",\\"otherBucketLabel\\":\\"Other\\",\\"missingBucketLabel\\":\\"Missing\\",\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}]}}", + "{\\"type\\":\\"area\\",\\"grid\\":{\\"categoryLines\\":false,\\"style\\":{\\"color\\":\\"#eee\\"}},\\"categoryAxes\\":[{\\"id\\":\\"CategoryAxis-1\\",\\"type\\":\\"category\\",\\"position\\":\\"bottom\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\"},\\"labels\\":{\\"show\\":true,\\"truncate\\":100},\\"title\\":{}}],\\"valueAxes\\":[{\\"id\\":\\"ValueAxis-1\\",\\"name\\":\\"LeftAxis-1\\",\\"type\\":\\"value\\",\\"position\\":\\"left\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\",\\"mode\\":\\"normal\\"},\\"labels\\":{\\"show\\":true,\\"rotate\\":0,\\"filter\\":false,\\"truncate\\":100},\\"title\\":{\\"text\\":\\"Sum of total_quantity\\"}}],\\"seriesParams\\":[{\\"show\\":\\"true\\",\\"type\\":\\"area\\",\\"mode\\":\\"stacked\\",\\"data\\":{\\"label\\":\\"Sum of total_quantity\\",\\"id\\":\\"1\\"},\\"drawLinesBetweenPoints\\":true,\\"showCircles\\":true,\\"circlesRadius\\":5,\\"interpolate\\":\\"linear\\",\\"valueAxis\\":\\"ValueAxis-1\\"}],\\"addTooltip\\":true,\\"addLegend\\":true,\\"legendPosition\\":\\"top\\",\\"times\\":[],\\"addTimeMarker\\":false,\\"thresholdLine\\":{\\"show\\":false,\\"value\\":10,\\"width\\":1,\\"style\\":\\"full\\",\\"color\\":\\"#E7664C\\"},\\"palette\\":{\\"name\\":\\"default\\"},\\"labels\\":{},\\"dimensions\\":{\\"x\\":{\\"accessor\\":1,\\"format\\":{\\"id\\":\\"date\\",\\"params\\":{\\"pattern\\":\\"HH:mm:ss.SSS\\"}},\\"params\\":{}},\\"y\\":[{\\"accessor\\":0,\\"format\\":{\\"id\\":\\"number\\",\\"params\\":{\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}],\\"series\\":[{\\"accessor\\":2,\\"format\\":{\\"id\\":\\"terms\\",\\"params\\":{\\"id\\":\\"string\\",\\"otherBucketLabel\\":\\"Other\\",\\"missingBucketLabel\\":\\"Missing\\",\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}]}}", ], }, "getArgument": [Function], diff --git a/src/plugins/vis_type_xy/public/config/get_config.ts b/src/plugins/vis_type_xy/public/config/get_config.ts index 8ebac1b71940a1..ce01572060a408 100644 --- a/src/plugins/vis_type_xy/public/config/get_config.ts +++ b/src/plugins/vis_type_xy/public/config/get_config.ts @@ -39,6 +39,7 @@ export function getConfig(table: Datatable, params: VisParams): VisConfig { fittingFunction, detailedTooltip, isVislibVis, + fillOpacity, } = params; const aspects = getAspects(table.columns, params.dimensions); const xAxis = getAxis( @@ -63,6 +64,7 @@ export function getConfig(table: Datatable, params: VisParams): VisConfig { // NOTE: downscale ratio to match current vislib implementation markSizeRatio: radiusRatio * 0.6, fittingFunction, + fillOpacity, detailedTooltip, orderBucketsBySum, isTimeChart, diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/index.test.tsx.snap b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/index.test.tsx.snap index 40e53d88f99cfc..05e2532073eaf4 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/index.test.tsx.snap +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/index.test.tsx.snap @@ -7,6 +7,7 @@ exports[`MetricsAxisOptions component should init with the default set of props seriesParams={ Array [ Object { + "circlesRadius": 3, "data": Object { "id": "1", "label": "Count", @@ -79,6 +80,7 @@ exports[`MetricsAxisOptions component should init with the default set of props seriesParams={ Array [ Object { + "circlesRadius": 3, "data": Object { "id": "1", "label": "Count", diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/line_options.test.tsx.snap b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/line_options.test.tsx.snap index 7b45423f5f861e..8764db1dea06ad 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/line_options.test.tsx.snap +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/line_options.test.tsx.snap @@ -54,14 +54,5 @@ exports[`LineOptions component should init with the default set of props 1`] = ` /> - - `; diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/point_options.test.tsx.snap b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/point_options.test.tsx.snap new file mode 100644 index 00000000000000..fcd6f8d00a1385 --- /dev/null +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/__snapshots__/point_options.test.tsx.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PointOptions component should init with the default set of props 1`] = ` + + + + + + + + +`; diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.test.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.test.tsx index def24d51f49f39..1d5c8be2b92464 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.test.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.test.tsx @@ -12,6 +12,7 @@ import { shallow, mount } from 'enzyme'; import { ChartOptions, ChartOptionsParams } from './chart_options'; import { SeriesParam, ChartMode, AxisMode } from '../../../../types'; import { LineOptions } from './line_options'; +import { PointOptions } from './point_options'; import { valueAxis, seriesParam } from './mocks'; import { ChartType } from '../../../../../common'; @@ -41,6 +42,12 @@ describe('ChartOptions component', () => { expect(comp).toMatchSnapshot(); }); + it('should hide the PointOptions when type is bar', () => { + const comp = shallow(); + + expect(comp.find(PointOptions).exists()).toBeFalsy(); + }); + it('should show LineOptions when type is line', () => { chart.type = ChartType.Line; const comp = shallow(); @@ -48,6 +55,13 @@ describe('ChartOptions component', () => { expect(comp.find(LineOptions).exists()).toBeTruthy(); }); + it('should show PointOptions when type is area', () => { + chart.type = ChartType.Area; + const comp = shallow(); + + expect(comp.find(PointOptions).exists()).toBeTruthy(); + }); + it('should show line mode when type is area', () => { chart.type = ChartType.Area; const comp = shallow(); diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx index 23452a87aae605..34ee33781f269d 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/chart_options.tsx @@ -15,6 +15,7 @@ import { SelectOption } from '../../../../../../vis_default_editor/public'; import { SeriesParam, ValueAxis, ChartMode, AxisMode } from '../../../../types'; import { LineOptions } from './line_options'; +import { PointOptions } from './point_options'; import { SetParamByIndex, ChangeValueAxis } from '.'; import { ChartType } from '../../../../../common'; import { getConfigCollections } from '../../../collections'; @@ -143,6 +144,9 @@ function ChartOptions({ )} {chart.type === ChartType.Line && } + {(chart.type === ChartType.Area || chart.type === ChartType.Line) && ( + + )} ); } diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/line_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/line_options.tsx index 140f190c77181e..75dfe8627d73ea 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/line_options.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/line_options.tsx @@ -78,17 +78,6 @@ function LineOptions({ chart, setChart }: LineOptionsParams) { /> - - - - ); } diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/mocks.ts b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/mocks.ts index 7451f6dea9039b..eed224cf2a514a 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/mocks.ts +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/mocks.ts @@ -75,6 +75,7 @@ const seriesParam: SeriesParam = { drawLinesBetweenPoints: true, lineWidth: 2, showCircles: true, + circlesRadius: 3, interpolate: InterpolationMode.Linear, valueAxis: defaultValueAxisId, }; diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/point_options.test.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/point_options.test.tsx new file mode 100644 index 00000000000000..68ac1832d28a89 --- /dev/null +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/point_options.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { shallow, mount } from 'enzyme'; +import { findTestSubject } from '@elastic/eui/lib/test'; + +import { SeriesParam } from '../../../../types'; +import { PointOptions, PointOptionsParams } from './point_options'; +import { seriesParam } from './mocks'; + +describe('PointOptions component', () => { + let setChart: jest.Mock; + let defaultProps: PointOptionsParams; + let chart: SeriesParam; + + beforeEach(() => { + setChart = jest.fn(); + chart = { ...seriesParam }; + + defaultProps = { + chart, + setChart, + }; + }); + + it('should init with the default set of props', () => { + const comp = shallow(); + + expect(comp).toMatchSnapshot(); + }); + + it('should disable the dots size range if the show dots switch is off', () => { + chart.showCircles = false; + const comp = mount(); + const range = findTestSubject(comp, 'circlesRadius'); + expect(range.at(1).props().disabled).toBeTruthy(); + }); +}); diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/point_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/point_options.tsx new file mode 100644 index 00000000000000..d35a5a2374ca34 --- /dev/null +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/point_options.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +import { i18n } from '@kbn/i18n'; +import { EuiRange, EuiFormRow, EuiSpacer } from '@elastic/eui'; + +import { SwitchOption } from '../../../../../../vis_default_editor/public'; + +import { SeriesParam } from '../../../../types'; +import { SetChart } from './chart_options'; + +export interface PointOptionsParams { + chart: SeriesParam; + setChart: SetChart; +} + +function PointOptions({ chart, setChart }: PointOptionsParams) { + return ( + <> + + + + + { + setChart('circlesRadius', Number(e.currentTarget.value)); + }} + /> + + + ); +} + +export { PointOptions }; diff --git a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/utils.ts b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/utils.ts index d0d0c08060acf5..a8d53e45bc988b 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/utils.ts +++ b/src/plugins/vis_type_xy/public/editor/components/options/metrics_axes/utils.ts @@ -26,6 +26,7 @@ export const makeSerie = ( type: ChartType.Line, drawLinesBetweenPoints: true, showCircles: true, + circlesRadius: 3, interpolate: InterpolationMode.Linear, lineWidth: 2, valueAxis: defaultValueAxis, diff --git a/src/plugins/vis_type_xy/public/editor/components/options/point_series/elastic_charts_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/point_series/elastic_charts_options.tsx index 5398980e268d48..271c5445a95807 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/point_series/elastic_charts_options.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/point_series/elastic_charts_options.tsx @@ -10,7 +10,7 @@ import React, { useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { METRIC_TYPE } from '@kbn/analytics'; - +import { EuiFormRow, EuiRange } from '@elastic/eui'; import { SelectOption, SwitchOption, @@ -31,10 +31,14 @@ export function ElasticChartsOptions(props: ValidationVisOptionsProps const [palettesRegistry, setPalettesRegistry] = useState(null); const { stateParams, setValue, aggs } = props; - const hasLineChart = stateParams.seriesParams.some( + const isLineChart = stateParams.seriesParams.some( + ({ type, data: { id: paramId } }) => + type === ChartType.Line && aggs.aggs.find(({ id }) => id === paramId)?.enabled + ); + + const isAreaChart = stateParams.seriesParams.some( ({ type, data: { id: paramId } }) => - (type === ChartType.Line || type === ChartType.Area) && - aggs.aggs.find(({ id }) => id === paramId)?.enabled + type === ChartType.Area && aggs.aggs.find(({ id }) => id === paramId)?.enabled ); useEffect(() => { @@ -66,7 +70,7 @@ export function ElasticChartsOptions(props: ValidationVisOptionsProps }} /> - {hasLineChart && ( + {(isLineChart || isAreaChart) && ( }} /> )} + {isAreaChart && ( + + { + setValue('fillOpacity', Number(e.currentTarget.value)); + }} + /> + + )} ); } diff --git a/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.mocks.ts b/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.mocks.ts index eb8d4d1c440d70..f23d9e4ada3361 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.mocks.ts +++ b/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.mocks.ts @@ -410,6 +410,7 @@ export const getVis = (bucketType: string) => { drawLinesBetweenPoints: true, lineWidth: 2, showCircles: true, + circlesRadius: 3, interpolate: 'linear', valueAxis: 'ValueAxis-1', }, @@ -838,6 +839,7 @@ export const getStateParams = (type: string, thresholdPanelOn: boolean) => { }, drawLinesBetweenPoints: true, showCircles: true, + circlesRadius: 3, interpolate: 'cardinal', valueAxis: 'ValueAxis-1', }, diff --git a/src/plugins/vis_type_xy/public/expression_functions/series_param.ts b/src/plugins/vis_type_xy/public/expression_functions/series_param.ts index 402187cea65866..3fd62e33e257fe 100644 --- a/src/plugins/vis_type_xy/public/expression_functions/series_param.ts +++ b/src/plugins/vis_type_xy/public/expression_functions/series_param.ts @@ -29,6 +29,7 @@ export type ExpressionValueSeriesParam = ExpressionValueBoxed< mode: SeriesParam['mode']; show: boolean; showCircles: boolean; + circlesRadius: number; seriesParamType: SeriesParam['type']; valueAxis: string; } @@ -98,6 +99,12 @@ export const seriesParam = (): ExpressionFunctionDefinition< defaultMessage: 'Show circles', }), }, + circlesRadius: { + types: ['number'], + help: i18n.translate('visTypeXy.function.seriesParam.circlesRadius.help', { + defaultMessage: 'Defines the circles size (radius)', + }), + }, type: { types: ['string'], help: i18n.translate('visTypeXy.function.seriesParam.type.help', { @@ -121,6 +128,7 @@ export const seriesParam = (): ExpressionFunctionDefinition< mode: args.mode, show: args.show, showCircles: args.showCircles, + circlesRadius: args.circlesRadius, seriesParamType: args.type, valueAxis: args.valueAxis, }; diff --git a/src/plugins/vis_type_xy/public/expression_functions/xy_vis_fn.ts b/src/plugins/vis_type_xy/public/expression_functions/xy_vis_fn.ts index b8b8c0e8b8cca8..29403a12fdce63 100644 --- a/src/plugins/vis_type_xy/public/expression_functions/xy_vis_fn.ts +++ b/src/plugins/vis_type_xy/public/expression_functions/xy_vis_fn.ts @@ -161,6 +161,12 @@ export const visTypeXyVisFn = (): VisTypeXyExpressionFunctionDefinition => ({ defaultMessage: 'Defines the chart palette name', }), }, + fillOpacity: { + types: ['number'], + help: i18n.translate('visTypeXy.function.args.fillOpacity.help', { + defaultMessage: 'Defines the area chart fill opacity', + }), + }, xDimension: { types: ['xy_dimension', 'null'], help: i18n.translate('visTypeXy.function.args.xDimension.help', { @@ -242,6 +248,7 @@ export const visTypeXyVisFn = (): VisTypeXyExpressionFunctionDefinition => ({ type: 'palette', name: args.palette, }, + fillOpacity: args.fillOpacity, fittingFunction: args.fittingFunction, dimensions: { x: args.xDimension, diff --git a/src/plugins/vis_type_xy/public/sample_vis.test.mocks.ts b/src/plugins/vis_type_xy/public/sample_vis.test.mocks.ts index e15f9c42077020..39370d941b52ac 100644 --- a/src/plugins/vis_type_xy/public/sample_vis.test.mocks.ts +++ b/src/plugins/vis_type_xy/public/sample_vis.test.mocks.ts @@ -1397,6 +1397,7 @@ export const sampleAreaVis = { drawLinesBetweenPoints: true, lineWidth: 2, showCircles: true, + circlesRadius: 3, interpolate: 'linear', valueAxis: 'ValueAxis-1', }, @@ -1417,6 +1418,7 @@ export const sampleAreaVis = { palette: { name: 'default', }, + fillOpacity: 0.5, }, }, editorConfig: { @@ -1562,6 +1564,7 @@ export const sampleAreaVis = { }, drawLinesBetweenPoints: true, showCircles: true, + circlesRadius: 5, interpolate: 'linear', valueAxis: 'ValueAxis-1', }, diff --git a/src/plugins/vis_type_xy/public/to_ast.ts b/src/plugins/vis_type_xy/public/to_ast.ts index c0a0ee566a4453..f473cd77c2d2b7 100644 --- a/src/plugins/vis_type_xy/public/to_ast.ts +++ b/src/plugins/vis_type_xy/public/to_ast.ts @@ -98,6 +98,7 @@ const prepareSeriesParam = (data: SeriesParam) => { mode: data.mode, show: data.show, showCircles: data.showCircles, + circlesRadius: data.circlesRadius, type: data.type, valueAxis: data.valueAxis, }); @@ -207,6 +208,7 @@ export const toExpressionAst: VisToExpressionAst = async (vis, params fittingFunction: vis.params.fittingFunction, times: vis.params.times.map(prepareTimeMarker), palette: vis.params.palette.name, + fillOpacity: vis.params.fillOpacity, xDimension: dimensions.x ? prepareXYDimension(dimensions.x) : null, yDimension: dimensions.y.map(prepareXYDimension), zDimension: dimensions.z?.map(prepareXYDimension), diff --git a/src/plugins/vis_type_xy/public/types/config.ts b/src/plugins/vis_type_xy/public/types/config.ts index f025a36a82410a..e52b47366bc857 100644 --- a/src/plugins/vis_type_xy/public/types/config.ts +++ b/src/plugins/vis_type_xy/public/types/config.ts @@ -116,6 +116,7 @@ export interface VisConfig { showValueLabel: boolean; enableHistogramMode: boolean; fittingFunction?: Exclude; + fillOpacity?: number; detailedTooltip?: boolean; isVislibVis?: boolean; } diff --git a/src/plugins/vis_type_xy/public/types/param.ts b/src/plugins/vis_type_xy/public/types/param.ts index f90899620126aa..7a2ff7e2402640 100644 --- a/src/plugins/vis_type_xy/public/types/param.ts +++ b/src/plugins/vis_type_xy/public/types/param.ts @@ -78,6 +78,7 @@ export interface SeriesParam { mode: ChartMode; show: boolean; showCircles: boolean; + circlesRadius: number; type: ChartType; valueAxis: string; } @@ -155,6 +156,7 @@ export interface VisParams { */ detailedTooltip?: boolean; palette: PaletteOutput; + fillOpacity?: number; fittingFunction?: Exclude; } @@ -186,6 +188,7 @@ export interface XYVisConfig { */ detailedTooltip?: boolean; fittingFunction?: Exclude; + fillOpacity?: number; xDimension: ExpressionValueXYDimension | null; yDimension: ExpressionValueXYDimension[]; zDimension?: ExpressionValueXYDimension[]; diff --git a/src/plugins/vis_type_xy/public/utils/render_all_series.test.tsx b/src/plugins/vis_type_xy/public/utils/render_all_series.test.tsx index 628d3620090ca1..23dabef662d559 100644 --- a/src/plugins/vis_type_xy/public/utils/render_all_series.test.tsx +++ b/src/plugins/vis_type_xy/public/utils/render_all_series.test.tsx @@ -31,6 +31,7 @@ const defaultSeriesParams = [ mode: 'stacked', show: true, showCircles: true, + circlesRadius: 3, type: 'area', valueAxis: 'ValueAxis-1', }, diff --git a/src/plugins/vis_type_xy/public/utils/render_all_series.tsx b/src/plugins/vis_type_xy/public/utils/render_all_series.tsx index 3bce5ddc2e85e6..e915e6d4966c50 100644 --- a/src/plugins/vis_type_xy/public/utils/render_all_series.tsx +++ b/src/plugins/vis_type_xy/public/utils/render_all_series.tsx @@ -51,7 +51,15 @@ const getCurveType = (type?: 'linear' | 'cardinal' | 'step-after'): CurveType => * @param getSeriesColor */ export const renderAllSeries = ( - { aspects, yAxes, xAxis, showValueLabel, enableHistogramMode, fittingFunction }: VisConfig, + { + aspects, + yAxes, + xAxis, + showValueLabel, + enableHistogramMode, + fittingFunction, + fillOpacity, + }: VisConfig, seriesParams: SeriesParam[], data: DatatableRow[], getSeriesName: (series: XYChartSeriesIdentifier) => SeriesName, @@ -67,6 +75,7 @@ export const renderAllSeries = ( data: { id: paramId }, lineWidth: strokeWidth, showCircles, + circlesRadius, drawLinesBetweenPoints, mode, interpolate, @@ -158,7 +167,7 @@ export const renderAllSeries = ( stackMode={stackMode} areaSeriesStyle={{ area: { - ...(type === ChartType.Line && { opacity: 0 }), + ...(type === ChartType.Line ? { opacity: 0 } : { opacity: fillOpacity }), }, line: { strokeWidth, @@ -167,6 +176,7 @@ export const renderAllSeries = ( point: { visible: showCircles, fill: markSizeAccessor ? ColorVariant.Series : undefined, + radius: circlesRadius, }, }} /> diff --git a/src/plugins/vis_type_xy/public/vis_types/area.ts b/src/plugins/vis_type_xy/public/vis_types/area.ts index f22f8df1752d66..912b3d8d48e952 100644 --- a/src/plugins/vis_type_xy/public/vis_types/area.ts +++ b/src/plugins/vis_type_xy/public/vis_types/area.ts @@ -98,6 +98,7 @@ export const getAreaVisTypeDefinition = ( drawLinesBetweenPoints: true, lineWidth: 2, showCircles: true, + circlesRadius: 3, interpolate: InterpolationMode.Linear, valueAxis: 'ValueAxis-1', }, diff --git a/src/plugins/vis_type_xy/public/vis_types/histogram.ts b/src/plugins/vis_type_xy/public/vis_types/histogram.ts index 732833ffecc802..9af4cfd7b43a3e 100644 --- a/src/plugins/vis_type_xy/public/vis_types/histogram.ts +++ b/src/plugins/vis_type_xy/public/vis_types/histogram.ts @@ -102,6 +102,7 @@ export const getHistogramVisTypeDefinition = ( drawLinesBetweenPoints: true, lineWidth: 2, showCircles: true, + circlesRadius: 3, }, ], radiusRatio: 0, diff --git a/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts b/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts index 791d93bb646b23..874e69b246a4d9 100644 --- a/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts +++ b/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts @@ -103,6 +103,7 @@ export const getHorizontalBarVisTypeDefinition = ( drawLinesBetweenPoints: true, lineWidth: 2, showCircles: true, + circlesRadius: 3, }, ], addTooltip: true, diff --git a/src/plugins/vis_type_xy/public/vis_types/line.ts b/src/plugins/vis_type_xy/public/vis_types/line.ts index 6316fe44582290..2e8944f44daab8 100644 --- a/src/plugins/vis_type_xy/public/vis_types/line.ts +++ b/src/plugins/vis_type_xy/public/vis_types/line.ts @@ -100,6 +100,7 @@ export const getLineVisTypeDefinition = ( lineWidth: 2, interpolate: InterpolationMode.Linear, showCircles: true, + circlesRadius: 3, }, ], addTooltip: true, diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap index 339fb5a7ab68f3..08b3393fafe482 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap @@ -33,6 +33,9 @@ Object { "description": Array [ "", ], + "fillOpacity": Array [ + 0.3, + ], "fittingFunction": Array [ "Carry", ], diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 006727b05b9056..e3b4565913ad87 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -196,6 +196,12 @@ export const xyChart: ExpressionFunctionDefinition< defaultMessage: 'Define how curve type is rendered for a line chart', }), }, + fillOpacity: { + types: ['number'], + help: i18n.translate('xpack.lens.xyChart.fillOpacity.help', { + defaultMessage: 'Define the area chart fill opacity', + }), + }, hideEndzones: { types: ['boolean'], default: false, @@ -812,6 +818,7 @@ export function XYChart({ visible: !xAccessor, radius: 5, }, + ...(args.fillOpacity && { area: { opacity: args.fillOpacity } }), }, lineSeriesStyle: { point: { diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index dea6b1a7be0c57..269f10159892f4 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -149,6 +149,7 @@ export const buildExpression = ( ], fittingFunction: [state.fittingFunction || 'None'], curveType: [state.curveType || 'LINEAR'], + fillOpacity: [state.fillOpacity || 0.3], yLeftExtent: [ { type: 'expression', diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts index ea28b492477c18..531b034b532425 100644 --- a/x-pack/plugins/lens/public/xy_visualization/types.ts +++ b/x-pack/plugins/lens/public/xy_visualization/types.ts @@ -464,6 +464,7 @@ export interface XYArgs { tickLabelsVisibilitySettings?: AxesSettingsConfig & { type: 'lens_xy_tickLabelsConfig' }; gridlinesVisibilitySettings?: AxesSettingsConfig & { type: 'lens_xy_gridlinesConfig' }; curveType?: XYCurveType; + fillOpacity?: number; hideEndzones?: boolean; } @@ -485,6 +486,7 @@ export interface XYState { tickLabelsVisibilitySettings?: AxesSettingsConfig; gridlinesVisibilitySettings?: AxesSettingsConfig; curveType?: XYCurveType; + fillOpacity?: number; hideEndzones?: boolean; } diff --git a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/fill_opacity_option.test.tsx b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/fill_opacity_option.test.tsx new file mode 100644 index 00000000000000..3ba29e4f72c837 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/fill_opacity_option.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mountWithIntl as mount, shallowWithIntl as shallow } from '@kbn/test/jest'; +import { EuiRange } from '@elastic/eui'; +import { FillOpacityOption } from './fill_opacity_option'; + +describe('Line curve option', () => { + it('should show currently selected opacity value', () => { + const component = shallow(); + + expect(component.find(EuiRange).prop('value')).toEqual(0.3); + }); + + it('should show fill opacity option when enabled', () => { + const component = mount( + + ); + + expect(component.exists('[data-test-subj="lnsFillOpacity"]')).toEqual(true); + }); + + it('should hide curve option when disabled', () => { + const component = mount( + + ); + + expect(component.exists('[data-test-subj="lnsFillOpacity"]')).toEqual(false); + }); +}); diff --git a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/fill_opacity_option.tsx b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/fill_opacity_option.tsx new file mode 100644 index 00000000000000..eb8d35c54a99b9 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/fill_opacity_option.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFormRow, EuiRange } from '@elastic/eui'; +import { useDebouncedValue } from '../../shared_components'; + +export interface FillOpacityOptionProps { + /** + * Currently selected value + */ + value: number; + /** + * Callback on display option change + */ + onChange: (value: number) => void; + /** + * Flag for rendering or not the component + */ + isFillOpacityEnabled?: boolean; +} + +export const FillOpacityOption: React.FC = ({ + onChange, + value, + isFillOpacityEnabled = true, +}) => { + const { inputValue, handleInputChange } = useDebouncedValue({ value, onChange }); + return isFillOpacityEnabled ? ( + <> + + { + handleInputChange(Number(e.currentTarget.value)); + }} + /> + + + ) : null; +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/missing_values_option.tsx b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/missing_values_option.tsx index fb6ecec4d28013..a683d4fbf514c3 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/missing_values_option.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/missing_values_option.tsx @@ -7,7 +7,14 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiButtonGroup, EuiFormRow, EuiIconTip, EuiSuperSelect, EuiText } from '@elastic/eui'; +import { + EuiButtonGroup, + EuiFormRow, + EuiIconTip, + EuiSuperSelect, + EuiText, + EuiSpacer, +} from '@elastic/eui'; import { FittingFunction, fittingFunctionDefinitions } from '../fitting_functions'; import { ValueLabelConfig } from '../types'; @@ -133,6 +140,7 @@ export const MissingValuesOptions: React.FC = ({ /> )} + ); }; diff --git a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.test.tsx b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.test.tsx index e7ec395312bff4..b46ad1940491e6 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.test.tsx @@ -14,6 +14,7 @@ import { State } from '../types'; import { VisualOptionsPopover } from './visual_options_popover'; import { ToolbarPopover } from '../../shared_components'; import { MissingValuesOptions } from './missing_values_option'; +import { FillOpacityOption } from './fill_opacity_option'; describe('Visual options popover', () => { let frame: FramePublicAPI; @@ -74,6 +75,22 @@ describe('Visual options popover', () => { expect(component.find(MissingValuesOptions).prop('isFittingEnabled')).toEqual(false); }); + it('should not disable the fill opacity for percentage area charts', () => { + const state = testState(); + const component = shallow( + + ); + + expect(component.find(FillOpacityOption).prop('isFillOpacityEnabled')).toEqual(true); + }); + it('should not disable the visual options for percentage area charts', () => { const state = testState(); const component = shallow( @@ -128,6 +145,40 @@ describe('Visual options popover', () => { expect(component.find(MissingValuesOptions).prop('isFittingEnabled')).toEqual(false); }); + it('should hide the fill opacity option for bar series', () => { + const state = testState(); + const component = shallow( + + ); + + expect(component.find(FillOpacityOption).prop('isFillOpacityEnabled')).toEqual(false); + }); + + it('should hide the fill opacity option for line series', () => { + const state = testState(); + const component = shallow( + + ); + + expect(component.find(FillOpacityOption).prop('isFillOpacityEnabled')).toEqual(false); + }); + it('should show the popover and display field enabled for bar and horizontal_bar series', () => { const state = testState(); diff --git a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.tsx b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.tsx index b8b89f146bdc07..b07feb85892e53 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visual_options_popover/visual_options_popover.tsx @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import { ToolbarPopover } from '../../shared_components'; import { MissingValuesOptions } from './missing_values_option'; import { LineCurveOption } from './line_curve_option'; +import { FillOpacityOption } from './fill_opacity_option'; import { XYState } from '../types'; import { hasHistogramSeries } from '../state_helpers'; import { ValidLayer } from '../types'; @@ -61,6 +62,10 @@ export const VisualOptionsPopover: React.FC = ({ ['bar', 'bar_horizontal'].includes(seriesType) ); + const hasAreaSeries = state?.layers.some(({ seriesType }) => + ['area_stacked', 'area', 'area_percentage_stacked'].includes(seriesType) + ); + const isHistogramSeries = Boolean( hasHistogramSeries(state?.layers as ValidLayer[], datasourceLayers) ); @@ -110,6 +115,17 @@ export const VisualOptionsPopover: React.FC = ({ setState({ ...state, fittingFunction: newVal }); }} /> + + { + setState({ + ...state, + fillOpacity: newValue, + }); + }} + /> ); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts index 4554c34b97c555..aff33778258fed 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -524,6 +524,7 @@ function buildSuggestion({ valueLabels: currentState?.valueLabels || 'hide', fittingFunction: currentState?.fittingFunction || 'None', curveType: currentState?.curveType, + fillOpacity: currentState?.fillOpacity, xTitle: currentState?.xTitle, yTitle: currentState?.yTitle, yRightTitle: currentState?.yRightTitle, From 111e15a0549df284fa3254057797a7359ffca9a8 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 25 May 2021 10:25:09 -0600 Subject: [PATCH 18/78] [ftr] implement FtrService classes and migrate common services (#99546) Co-authored-by: spalger Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../development-functional-tests.asciidoc | 9 +- .../lib/providers/provider_collection.ts | 12 +- .../functional_test_runner/public_types.ts | 12 +- ...r_context.d.ts => ftr_provider_context.ts} | 3 +- test/common/services/deployment.ts | 60 +++++----- test/common/services/index.ts | 16 +-- .../services/kibana_server/kibana_server.ts | 2 +- test/common/services/randomness.ts | 93 ++++++++------- test/common/services/retry/index.ts | 2 +- test/common/services/retry/retry.ts | 104 ++++++++--------- test/common/services/saved_object_info.ts | 74 ++++++------ test/common/services/security/security.ts | 32 ++--- test/common/services/security/test_user.ts | 110 ++++++++++-------- test/functional/apps/visualize/index.ts | 2 +- .../functional/apps/visualize/legacy/index.ts | 2 +- ...r_context.d.ts => ftr_provider_context.ts} | 3 +- test/functional/page_objects/time_picker.ts | 2 +- .../page_objects/visual_builder_page.ts | 2 +- x-pack/test/functional/apps/lens/index.ts | 2 +- ...r_context.d.ts => ftr_provider_context.ts} | 3 +- 20 files changed, 292 insertions(+), 253 deletions(-) rename test/common/{ftr_provider_context.d.ts => ftr_provider_context.ts} (76%) rename test/functional/{ftr_provider_context.d.ts => ftr_provider_context.ts} (78%) rename x-pack/test/functional/{ftr_provider_context.d.ts => ftr_provider_context.ts} (74%) diff --git a/docs/developer/contributing/development-functional-tests.asciidoc b/docs/developer/contributing/development-functional-tests.asciidoc index 110704a8e569a9..f0041b85c14ebf 100644 --- a/docs/developer/contributing/development-functional-tests.asciidoc +++ b/docs/developer/contributing/development-functional-tests.asciidoc @@ -139,11 +139,14 @@ export default function (/* { providerAPI } */) { } ----------- -**Services**::: -Services are named singleton values produced by a Service Provider. Tests and other services can retrieve service instances by asking for them by name. All functionality except the mocha API is exposed via services. +**Service**::: +A Service is a named singleton created using a subclass of `FtrService`. Tests and other services can retrieve service instances by asking for them by name. All functionality except the mocha API is exposed via services. When you write your own functional tests check for existing services that help with the interactions you're looking to execute, and add new services for interactions which aren't already encoded in a service. + +**Service Providers**::: +For legacy purposes, and for when creating a subclass of `FtrService` is inconvenient, you can also create services using a "Service Provider". These are functions which which create service instances and return them. These instances are cached and provided to tests. Currently these providers may also return a Promise for the service instance, allowing the service to do some setup work before tests run. We expect to fully deprecate and remove support for async service providers in the near future and instead require that services use the `lifecycle` service to run setup before tests. Providers which return instances of classes other than `FtrService` will likely remain supported for as long as possible. **Page objects**::: -Page objects are a special type of service that encapsulate behaviors common to a particular page or plugin. When you write your own plugin, you’ll likely want to add a page object (or several) that describes the common interactions your tests need to execute. +Page objects are functionally equivalent to services, except they are loaded with a slightly different mechanism and generally defined separate from services. When you write your own functional tests you might want to write some of your services as Page objects, but it is not required. **Test Files**::: The `FunctionalTestRunner`'s primary purpose is to execute test files. These files export a Test Provider that is called with a Provider API but is not expected to return a value. Instead Test Providers define a suite using https://mochajs.org/#bdd[mocha's BDD interface]. diff --git a/packages/kbn-test/src/functional_test_runner/lib/providers/provider_collection.ts b/packages/kbn-test/src/functional_test_runner/lib/providers/provider_collection.ts index 1aa5df1105f460..2d05d5bba5ff6f 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/providers/provider_collection.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/providers/provider_collection.ts @@ -12,6 +12,7 @@ import { loadTracer } from '../load_tracer'; import { createAsyncInstance, isAsyncInstance } from './async_instance'; import { Providers } from './read_provider_spec'; import { createVerboseInstance } from './verbose_instance'; +import { GenericFtrService } from '../../public_types'; export class ProviderCollection { private readonly instances = new Map(); @@ -58,12 +59,19 @@ export class ProviderCollection { } public invokeProviderFn(provider: (args: any) => any) { - return provider({ + const ctx = { getService: this.getService, hasService: this.hasService, getPageObject: this.getPageObject, getPageObjects: this.getPageObjects, - }); + }; + + if (provider.prototype instanceof GenericFtrService) { + const Constructor = (provider as any) as new (ctx: any) => any; + return new Constructor(ctx); + } + + return provider(ctx); } private findProvider(type: string, name: string) { diff --git a/packages/kbn-test/src/functional_test_runner/public_types.ts b/packages/kbn-test/src/functional_test_runner/public_types.ts index 915cb34f6ffe50..4a30744c09b516 100644 --- a/packages/kbn-test/src/functional_test_runner/public_types.ts +++ b/packages/kbn-test/src/functional_test_runner/public_types.ts @@ -13,7 +13,7 @@ import { Test, Suite } from './fake_mocha_types'; export { Lifecycle, Config, FailureMetadata }; -interface AsyncInstance { +export interface AsyncInstance { /** * Services that are initialized async are not ready before the tests execute, so you might need * to call `init()` and await the promise it returns before interacting with the service @@ -39,7 +39,11 @@ export type ProvidedType any> = MaybeAsyncInstance * promise types into the async instances that other providers will receive. */ type ProvidedTypeMap = { - [K in keyof T]: T[K] extends (...args: any[]) => any ? ProvidedType : unknown; + [K in keyof T]: T[K] extends new (...args: any[]) => infer X + ? X + : T[K] extends (...args: any[]) => any + ? ProvidedType + : unknown; }; export interface GenericFtrProviderContext< @@ -84,6 +88,10 @@ export interface GenericFtrProviderContext< loadTestFile(path: string): void; } +export class GenericFtrService> { + constructor(protected readonly ctx: ProviderContext) {} +} + export interface FtrConfigProviderContext { log: ToolingLog; readConfigFile(path: string): Promise; diff --git a/test/common/ftr_provider_context.d.ts b/test/common/ftr_provider_context.ts similarity index 76% rename from test/common/ftr_provider_context.d.ts rename to test/common/ftr_provider_context.ts index 91d35a2dbc32a6..6d21aedfe1d5ec 100644 --- a/test/common/ftr_provider_context.d.ts +++ b/test/common/ftr_provider_context.ts @@ -6,8 +6,9 @@ * Side Public License, v 1. */ -import { GenericFtrProviderContext } from '@kbn/test'; +import { GenericFtrProviderContext, GenericFtrService } from '@kbn/test'; import { services } from './services'; export type FtrProviderContext = GenericFtrProviderContext; +export class FtrService extends GenericFtrService {} diff --git a/test/common/services/deployment.ts b/test/common/services/deployment.ts index 65466ca966ad25..b250d39ce65d65 100644 --- a/test/common/services/deployment.ts +++ b/test/common/services/deployment.ts @@ -10,39 +10,37 @@ import { get } from 'lodash'; import fetch from 'node-fetch'; import { getUrl } from '@kbn/test'; -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrService } from '../ftr_provider_context'; -export function DeploymentProvider({ getService }: FtrProviderContext) { - const config = getService('config'); +export class DeploymentService extends FtrService { + private readonly config = this.ctx.getService('config'); - return { - /** - * Returns Kibana host URL - */ - getHostPort() { - return getUrl.baseUrl(config.get('servers.kibana')); - }, + /** + * Returns Kibana host URL + */ + getHostPort() { + return getUrl.baseUrl(this.config.get('servers.kibana')); + } - /** - * Returns ES host URL - */ - getEsHostPort() { - return getUrl.baseUrl(config.get('servers.elasticsearch')); - }, + /** + * Returns ES host URL + */ + getEsHostPort() { + return getUrl.baseUrl(this.config.get('servers.elasticsearch')); + } - async isCloud(): Promise { - const baseUrl = this.getHostPort(); - const username = config.get('servers.kibana.username'); - const password = config.get('servers.kibana.password'); - const response = await fetch(baseUrl + '/api/stats?extended', { - method: 'get', - headers: { - 'Content-Type': 'application/json', - Authorization: 'Basic ' + Buffer.from(username + ':' + password).toString('base64'), - }, - }); - const data = await response.json(); - return get(data, 'usage.cloud.is_cloud_enabled', false); - }, - }; + async isCloud(): Promise { + const baseUrl = this.getHostPort(); + const username = this.config.get('servers.kibana.username'); + const password = this.config.get('servers.kibana.password'); + const response = await fetch(baseUrl + '/api/stats?extended', { + method: 'get', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Basic ' + Buffer.from(username + ':' + password).toString('base64'), + }, + }); + const data = await response.json(); + return get(data, 'usage.cloud.is_cloud_enabled', false); + } } diff --git a/test/common/services/index.ts b/test/common/services/index.ts index cc4859b7016bf3..02aafc59fa80f4 100644 --- a/test/common/services/index.ts +++ b/test/common/services/index.ts @@ -6,26 +6,26 @@ * Side Public License, v 1. */ -import { DeploymentProvider } from './deployment'; +import { DeploymentService } from './deployment'; import { LegacyEsProvider } from './legacy_es'; import { ElasticsearchProvider } from './elasticsearch'; import { EsArchiverProvider } from './es_archiver'; import { KibanaServerProvider } from './kibana_server'; -import { RetryProvider } from './retry'; -import { RandomnessProvider } from './randomness'; +import { RetryService } from './retry'; +import { RandomnessService } from './randomness'; import { SecurityServiceProvider } from './security'; import { EsDeleteAllIndicesProvider } from './es_delete_all_indices'; -import { SavedObjectInfoProvider } from './saved_object_info'; +import { SavedObjectInfoService } from './saved_object_info'; export const services = { - deployment: DeploymentProvider, + deployment: DeploymentService, legacyEs: LegacyEsProvider, es: ElasticsearchProvider, esArchiver: EsArchiverProvider, kibanaServer: KibanaServerProvider, - retry: RetryProvider, - randomness: RandomnessProvider, + retry: RetryService, + randomness: RandomnessService, security: SecurityServiceProvider, esDeleteAllIndices: EsDeleteAllIndicesProvider, - savedObjectInfo: SavedObjectInfoProvider, + savedObjectInfo: SavedObjectInfoService, }; diff --git a/test/common/services/kibana_server/kibana_server.ts b/test/common/services/kibana_server/kibana_server.ts index f366a864db980d..63803bd511bd14 100644 --- a/test/common/services/kibana_server/kibana_server.ts +++ b/test/common/services/kibana_server/kibana_server.ts @@ -11,7 +11,7 @@ import { KbnClient } from '@kbn/test'; import { FtrProviderContext } from '../../ftr_provider_context'; -export function KibanaServerProvider({ getService }: FtrProviderContext) { +export function KibanaServerProvider({ getService }: FtrProviderContext): KbnClient { const log = getService('log'); const config = getService('config'); const lifecycle = getService('lifecycle'); diff --git a/test/common/services/randomness.ts b/test/common/services/randomness.ts index 88b0411f98033e..82f06fb681066a 100644 --- a/test/common/services/randomness.ts +++ b/test/common/services/randomness.ts @@ -7,8 +7,20 @@ */ import Chance from 'chance'; +import { ToolingLog } from '@kbn/dev-utils'; -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrService } from '../ftr_provider_context'; + +let __CACHED_SEED__: number | undefined; +function getSeed(log: ToolingLog) { + if (__CACHED_SEED__ !== undefined) { + return __CACHED_SEED__; + } + + __CACHED_SEED__ = Date.now(); + log.debug('randomness seed: %j', __CACHED_SEED__); + return __CACHED_SEED__; +} interface CharOptions { pool?: string; @@ -27,52 +39,45 @@ interface NumberOptions { max?: number; } -export function RandomnessProvider({ getService }: FtrProviderContext) { - const log = getService('log'); - - const seed = Date.now(); - log.debug('randomness seed: %j', seed); - - const chance = new Chance(seed); +export class RandomnessService extends FtrService { + private readonly chance = new Chance(getSeed(this.ctx.getService('log'))); - return new (class RandomnessService { - /** - * Generate a random natural number - * - * range: 0 to 9007199254740991 - * - */ - naturalNumber(options?: NumberOptions) { - return chance.natural(options); - } + /** + * Generate a random natural number + * + * range: 0 to 9007199254740991 + * + */ + naturalNumber(options?: NumberOptions) { + return this.chance.natural(options); + } - /** - * Generate a random integer - */ - integer(options?: NumberOptions) { - return chance.integer(options); - } + /** + * Generate a random integer + */ + integer(options?: NumberOptions) { + return this.chance.integer(options); + } - /** - * Generate a random number, defaults to at least 4 and no more than 8 syllables - */ - word(options: { syllables?: number } = {}) { - const { syllables = this.naturalNumber({ min: 4, max: 8 }) } = options; + /** + * Generate a random number, defaults to at least 4 and no more than 8 syllables + */ + word(options: { syllables?: number } = {}) { + const { syllables = this.naturalNumber({ min: 4, max: 8 }) } = options; - return chance.word({ - syllables, - }); - } + return this.chance.word({ + syllables, + }); + } - /** - * Generate a random string, defaults to at least 8 and no more than 15 alpha-numeric characters - */ - string(options: StringOptions = {}) { - return chance.string({ - length: this.naturalNumber({ min: 8, max: 15 }), - ...(options.pool === 'undefined' ? { alpha: true, numeric: true, symbols: false } : {}), - ...options, - }); - } - })(); + /** + * Generate a random string, defaults to at least 8 and no more than 15 alpha-numeric characters + */ + string(options: StringOptions = {}) { + return this.chance.string({ + length: this.naturalNumber({ min: 8, max: 15 }), + ...(options.pool === 'undefined' ? { alpha: true, numeric: true, symbols: false } : {}), + ...options, + }); + } } diff --git a/test/common/services/retry/index.ts b/test/common/services/retry/index.ts index 4914b3cff2261e..08ce3f9bd46611 100644 --- a/test/common/services/retry/index.ts +++ b/test/common/services/retry/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { RetryProvider } from './retry'; +export { RetryService } from './retry'; diff --git a/test/common/services/retry/retry.ts b/test/common/services/retry/retry.ts index 8ea2a52b6adf69..5c823e256ddc8d 100644 --- a/test/common/services/retry/retry.ts +++ b/test/common/services/retry/retry.ts @@ -6,64 +6,62 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrService } from '../../ftr_provider_context'; import { retryForSuccess } from './retry_for_success'; import { retryForTruthy } from './retry_for_truthy'; -export function RetryProvider({ getService }: FtrProviderContext) { - const config = getService('config'); - const log = getService('log'); +export class RetryService extends FtrService { + private readonly config = this.ctx.getService('config'); + private readonly log = this.ctx.getService('log'); - return new (class Retry { - public async tryForTime( - timeout: number, - block: () => Promise, - onFailureBlock?: () => Promise - ) { - return await retryForSuccess(log, { - timeout, - methodName: 'retry.tryForTime', - block, - onFailureBlock, - }); - } + public async tryForTime( + timeout: number, + block: () => Promise, + onFailureBlock?: () => Promise + ) { + return await retryForSuccess(this.log, { + timeout, + methodName: 'retry.tryForTime', + block, + onFailureBlock, + }); + } - public async try(block: () => Promise, onFailureBlock?: () => Promise) { - return await retryForSuccess(log, { - timeout: config.get('timeouts.try'), - methodName: 'retry.try', - block, - onFailureBlock, - }); - } + public async try(block: () => Promise, onFailureBlock?: () => Promise) { + return await retryForSuccess(this.log, { + timeout: this.config.get('timeouts.try'), + methodName: 'retry.try', + block, + onFailureBlock, + }); + } - public async waitForWithTimeout( - description: string, - timeout: number, - block: () => Promise, - onFailureBlock?: () => Promise - ) { - await retryForTruthy(log, { - timeout, - methodName: 'retry.waitForWithTimeout', - description, - block, - onFailureBlock, - }); - } + public async waitForWithTimeout( + description: string, + timeout: number, + block: () => Promise, + onFailureBlock?: () => Promise + ) { + await retryForTruthy(this.log, { + timeout, + methodName: 'retry.waitForWithTimeout', + description, + block, + onFailureBlock, + }); + } - public async waitFor( - description: string, - block: () => Promise, - onFailureBlock?: () => Promise - ) { - await retryForTruthy(log, { - timeout: config.get('timeouts.waitFor'), - methodName: 'retry.waitFor', - description, - block, - onFailureBlock, - }); - } - })(); + public async waitFor( + description: string, + block: () => Promise, + onFailureBlock?: () => Promise + ) { + await retryForTruthy(this.log, { + timeout: this.config.get('timeouts.waitFor'), + methodName: 'retry.waitFor', + description, + block, + onFailureBlock, + }); + } } diff --git a/test/common/services/saved_object_info.ts b/test/common/services/saved_object_info.ts index 02ab38d4ecb1db..1558b364f53916 100644 --- a/test/common/services/saved_object_info.ts +++ b/test/common/services/saved_object_info.ts @@ -6,48 +6,44 @@ * Side Public License, v 1. */ -import { Client } from '@elastic/elasticsearch'; -import url from 'url'; -import { Either, fromNullable, chain, getOrElse } from 'fp-ts/Either'; -import { flow } from 'fp-ts/function'; -import { FtrProviderContext } from '../ftr_provider_context'; - -const pluck = (key: string) => (obj: any): Either => - fromNullable(new Error(`Missing ${key}`))(obj[key]); - -const types = (node: string) => async (index: string = '.kibana') => { - let res: unknown; - try { - const { body } = await new Client({ node }).search({ - index, - body: { - aggs: { - savedobjs: { - terms: { - field: 'type', +import { inspect } from 'util'; + +import { TermsAggregate } from '@elastic/elasticsearch/api/types'; + +import { FtrService } from '../ftr_provider_context'; + +export class SavedObjectInfoService extends FtrService { + private readonly es = this.ctx.getService('es'); + + public async getTypes(index = '.kibana') { + try { + const { body } = await this.es.search({ + index, + size: 0, + body: { + aggs: { + savedobjs: { + terms: { + field: 'type', + }, }, }, }, - }, - }); - - res = flow( - pluck('aggregations'), - chain(pluck('savedobjs')), - chain(pluck('buckets')), - getOrElse((err) => `${err.message}`) - )(body); - } catch (err) { - throw new Error(`Error while searching for saved object types: ${err}`); - } + }); - return res; -}; + const agg = body.aggregations?.savedobjs as + | TermsAggregate<{ key: string; doc_count: number }> + | undefined; -export const SavedObjectInfoProvider: any = ({ getService }: FtrProviderContext) => { - const config = getService('config'); + if (!agg?.buckets) { + throw new Error( + `expected es to return buckets of saved object types: ${inspect(body, { depth: 100 })}` + ); + } - return { - types: types(url.format(config.get('servers.elasticsearch'))), - }; -}; + return agg.buckets; + } catch (error) { + throw new Error(`Error while searching for saved object types: ${error}`); + } + } +} diff --git a/test/common/services/security/security.ts b/test/common/services/security/security.ts index 52fb6bdd70330e..b8fea0a0c59b26 100644 --- a/test/common/services/security/security.ts +++ b/test/common/services/security/security.ts @@ -10,23 +10,27 @@ import { Role } from './role'; import { User } from './user'; import { RoleMappings } from './role_mappings'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { createTestUserService, TestUserSupertestProvider } from './test_user'; +import { createTestUserService, TestUserSupertestProvider, TestUser } from './test_user'; -export async function SecurityServiceProvider(context: FtrProviderContext) { - const { getService } = context; - const log = getService('log'); - const kibanaServer = getService('kibanaServer'); +export class SecurityService { + constructor( + public readonly roleMappings: RoleMappings, + public readonly testUser: TestUser, + public readonly role: Role, + public readonly user: User, + public readonly testUserSupertest: ReturnType + ) {} +} + +export async function SecurityServiceProvider(ctx: FtrProviderContext) { + const log = ctx.getService('log'); + const kibanaServer = ctx.getService('kibanaServer'); const role = new Role(log, kibanaServer); const user = new User(log, kibanaServer); - const testUser = await createTestUserService(role, user, context); - const testUserSupertest = TestUserSupertestProvider(context); + const testUser = await createTestUserService(ctx, role, user); + const testUserSupertest = TestUserSupertestProvider(ctx); + const roleMappings = new RoleMappings(log, kibanaServer); - return new (class SecurityService { - roleMappings = new RoleMappings(log, kibanaServer); - testUser = testUser; - role = role; - user = user; - testUserSupertest = testUserSupertest; - })(); + return new SecurityService(roleMappings, testUser, role, user, testUserSupertest); } diff --git a/test/common/services/security/test_user.ts b/test/common/services/security/test_user.ts index d5e1f02e1bc8ca..8b0a1c34e790cb 100644 --- a/test/common/services/security/test_user.ts +++ b/test/common/services/security/test_user.ts @@ -11,41 +11,84 @@ import supertestAsPromised from 'supertest-as-promised'; import { Role } from './role'; import { User } from './user'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrService, FtrProviderContext } from '../../ftr_provider_context'; import { Browser } from '../../../functional/services/common'; import { TestSubjects } from '../../../functional/services/common'; const TEST_USER_NAME = 'test_user'; const TEST_USER_PASSWORD = 'changeme'; -export async function createTestUserService( - role: Role, - user: User, - { getService, hasService }: FtrProviderContext -) { - const log = getService('log'); - const config = getService('config'); - // @ts-ignore browser service is not normally available in common. - const browser: Browser | void = hasService('browser') && getService('browser'); - const testSubjects: TestSubjects | undefined = +export class TestUser extends FtrService { + private readonly config = this.ctx.getService('config'); + private readonly log = this.ctx.getService('log'); + + private readonly browser: Browser | void = + // browser service is not normally available in common. + this.ctx.hasService('browser') ? (this.ctx.getService('browser' as any) as Browser) : undefined; + + private readonly testSubjects: TestSubjects | undefined = // testSubject service is not normally available in common. - hasService('testSubjects') ? (getService('testSubjects' as any) as TestSubjects) : undefined; - const kibanaServer = getService('kibanaServer'); + this.ctx.hasService('testSubjects') + ? (this.ctx.getService('testSubjects' as any) as TestSubjects) + : undefined; + + constructor( + ctx: FtrProviderContext, + private readonly enabled: boolean, + private readonly user: User + ) { + super(ctx); + } + + async restoreDefaults(shouldRefreshBrowser: boolean = true) { + if (this.enabled) { + await this.setRoles(this.config.get('security.defaultRoles'), shouldRefreshBrowser); + } + } + + async setRoles(roles: string[], shouldRefreshBrowser: boolean = true) { + if (this.enabled) { + this.log.debug(`set roles = ${roles}`); + await this.user.create(TEST_USER_NAME, { + password: TEST_USER_PASSWORD, + roles, + full_name: 'test user', + }); + + if (this.browser && this.testSubjects && shouldRefreshBrowser) { + if (await this.testSubjects.exists('kibanaChrome', { allowHidden: true })) { + await this.browser.refresh(); + // accept alert if it pops up + const alert = await this.browser.getAlert(); + await alert?.accept(); + await this.testSubjects.find('kibanaChrome', this.config.get('timeouts.find') * 10); + } + } + } + } +} + +export async function createTestUserService(ctx: FtrProviderContext, role: Role, user: User) { + const log = ctx.getService('log'); + const config = ctx.getService('config'); + const kibanaServer = ctx.getService('kibanaServer'); const enabledPlugins = config.get('security.disableTestUser') ? [] : await kibanaServer.plugins.getEnabledIds(); - const isEnabled = () => { - return enabledPlugins.includes('security') && !config.get('security.disableTestUser'); - }; - if (isEnabled()) { + + const enabled = enabledPlugins.includes('security') && !config.get('security.disableTestUser'); + + if (enabled) { log.debug('===============creating roles and users==============='); + + // create the defined roles (need to map array to create roles) for (const [name, definition] of Object.entries(config.get('security.roles'))) { - // create the defined roles (need to map array to create roles) await role.create(name, definition); } + + // delete the test_user if present (will it error if the user doesn't exist?) try { - // delete the test_user if present (will it error if the user doesn't exist?) await user.delete(TEST_USER_NAME); } catch (exception) { log.debug('no test user to delete'); @@ -60,34 +103,7 @@ export async function createTestUserService( }); } - return new (class TestUser { - async restoreDefaults(shouldRefreshBrowser: boolean = true) { - if (isEnabled()) { - await this.setRoles(config.get('security.defaultRoles'), shouldRefreshBrowser); - } - } - - async setRoles(roles: string[], shouldRefreshBrowser: boolean = true) { - if (isEnabled()) { - log.debug(`set roles = ${roles}`); - await user.create(TEST_USER_NAME, { - password: TEST_USER_PASSWORD, - roles, - full_name: 'test user', - }); - - if (browser && testSubjects && shouldRefreshBrowser) { - if (await testSubjects.exists('kibanaChrome', { allowHidden: true })) { - await browser.refresh(); - // accept alert if it pops up - const alert = await browser.getAlert(); - await alert?.accept(); - await testSubjects.find('kibanaChrome', config.get('timeouts.find') * 10); - } - } - } - } - })(); + return new TestUser(ctx, enabled, user); } export function TestUserSupertestProvider({ getService }: FtrProviderContext) { diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts index eb224b3c9b8798..b87184bab3c0d7 100644 --- a/test/functional/apps/visualize/index.ts +++ b/test/functional/apps/visualize/index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../../ftr_provider_context.d'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, loadTestFile }: FtrProviderContext) { const browser = getService('browser'); diff --git a/test/functional/apps/visualize/legacy/index.ts b/test/functional/apps/visualize/legacy/index.ts index 187e8f3f3a663c..914559e5cea925 100644 --- a/test/functional/apps/visualize/legacy/index.ts +++ b/test/functional/apps/visualize/legacy/index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../../../ftr_provider_context.d'; +import { FtrProviderContext } from '../../../ftr_provider_context'; import { UI_SETTINGS } from '../../../../../src/plugins/data/common'; export default function ({ getPageObjects, getService, loadTestFile }: FtrProviderContext) { diff --git a/test/functional/ftr_provider_context.d.ts b/test/functional/ftr_provider_context.ts similarity index 78% rename from test/functional/ftr_provider_context.d.ts rename to test/functional/ftr_provider_context.ts index 4c827393e1ef3b..a1a29f50b77611 100644 --- a/test/functional/ftr_provider_context.d.ts +++ b/test/functional/ftr_provider_context.ts @@ -6,9 +6,10 @@ * Side Public License, v 1. */ -import { GenericFtrProviderContext } from '@kbn/test'; +import { GenericFtrProviderContext, GenericFtrService } from '@kbn/test'; import { pageObjects } from './page_objects'; import { services } from './services'; export type FtrProviderContext = GenericFtrProviderContext; +export class FtrService extends GenericFtrService {} diff --git a/test/functional/page_objects/time_picker.ts b/test/functional/page_objects/time_picker.ts index cfe250831e06cc..d3b6edaffdbd32 100644 --- a/test/functional/page_objects/time_picker.ts +++ b/test/functional/page_objects/time_picker.ts @@ -7,7 +7,7 @@ */ import moment from 'moment'; -import { FtrProviderContext } from '../ftr_provider_context.d'; +import { FtrProviderContext } from '../ftr_provider_context'; import { WebElementWrapper } from '../services/lib/web_element_wrapper'; export type CommonlyUsed = diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index 3ed5d74808fce5..997a1127005ee5 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../ftr_provider_context.d'; +import { FtrProviderContext } from '../ftr_provider_context'; import { WebElementWrapper } from '../services/lib/web_element_wrapper'; export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrProviderContext) { diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts index ab7cee13ffebda..d0466b8814fec1 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../ftr_provider_context.d'; +import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, loadTestFile }: FtrProviderContext) { const browser = getService('browser'); diff --git a/x-pack/test/functional/ftr_provider_context.d.ts b/x-pack/test/functional/ftr_provider_context.ts similarity index 74% rename from x-pack/test/functional/ftr_provider_context.d.ts rename to x-pack/test/functional/ftr_provider_context.ts index 24f5087ef7fe2f..e757164fa1de92 100644 --- a/x-pack/test/functional/ftr_provider_context.d.ts +++ b/x-pack/test/functional/ftr_provider_context.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { GenericFtrProviderContext } from '@kbn/test'; +import { GenericFtrProviderContext, GenericFtrService } from '@kbn/test'; import { pageObjects } from './page_objects'; import { services } from './services'; export type FtrProviderContext = GenericFtrProviderContext; +export class FtrService extends GenericFtrService {} From bca1c14f9c6531da416f28890c6121fae4cee96b Mon Sep 17 00:00:00 2001 From: Caroline Horn <549577+cchaos@users.noreply.github.com> Date: Tue, 25 May 2021 13:28:05 -0400 Subject: [PATCH 19/78] [KibanaPageLayout] Solution Nav specific styles & props (#100089) * Fixing sticky nav * Adding some side bar styles * Added a built-in solution nav title with avatar icon * Adding tutorial docs * Added KibanaPageTemplateSolutionNavAvatar * Added KibanaPageTemplateSolutionNav * Increased limit to `core` / `kibanaReact` plugin because of additional CSS --- .../assets/kibana_template_solution_nav.png | Bin 0 -> 142856 bytes .../kibana_template_solution_nav_mobile.png | Bin 0 -> 163747 bytes dev_docs/tutorials/kibana_page_template.mdx | 37 ++- packages/kbn-optimizer/limits.yml | 2 +- src/core/public/rendering/_base.scss | 7 + .../__snapshots__/page_template.test.tsx.snap | 93 +++++++ .../public/page_template/page_template.scss | 15 ++ .../page_template/page_template.test.tsx | 59 +++++ .../public/page_template/page_template.tsx | 35 ++- .../__snapshots__/solution_nav.test.tsx.snap | 238 ++++++++++++++++++ .../solution_nav_avatar.test.tsx.snap | 10 + .../page_template/solution_nav/index.ts | 13 + .../solution_nav/solution_nav.scss | 22 ++ .../solution_nav/solution_nav.test.tsx | 71 ++++++ .../solution_nav/solution_nav.tsx | 103 ++++++++ .../solution_nav/solution_nav_avatar.scss | 4 + .../solution_nav/solution_nav_avatar.test.tsx | 20 ++ .../solution_nav/solution_nav_avatar.tsx | 31 +++ 18 files changed, 755 insertions(+), 5 deletions(-) create mode 100644 dev_docs/assets/kibana_template_solution_nav.png create mode 100644 dev_docs/assets/kibana_template_solution_nav_mobile.png create mode 100644 src/plugins/kibana_react/public/page_template/page_template.scss create mode 100644 src/plugins/kibana_react/public/page_template/solution_nav/__snapshots__/solution_nav.test.tsx.snap create mode 100644 src/plugins/kibana_react/public/page_template/solution_nav/__snapshots__/solution_nav_avatar.test.tsx.snap create mode 100644 src/plugins/kibana_react/public/page_template/solution_nav/index.ts create mode 100644 src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.scss create mode 100644 src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.test.tsx create mode 100644 src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.tsx create mode 100644 src/plugins/kibana_react/public/page_template/solution_nav/solution_nav_avatar.scss create mode 100644 src/plugins/kibana_react/public/page_template/solution_nav/solution_nav_avatar.test.tsx create mode 100644 src/plugins/kibana_react/public/page_template/solution_nav/solution_nav_avatar.tsx diff --git a/dev_docs/assets/kibana_template_solution_nav.png b/dev_docs/assets/kibana_template_solution_nav.png new file mode 100644 index 0000000000000000000000000000000000000000..4ffec1990ed0a4e30cda9daa917d33fd982e599c GIT binary patch literal 142856 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ʋ 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
pP+ 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%0APfc(1(i{WbFIJ^!uHtN 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 XPWs>T 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%0DMjXK`kW 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 + {...} + +``` + + +![Screenshot of Stack Management empty state with a provided solution navigation shown on the left, outlined in pink.](../assets/kibana_template_solution_nav.png) + +![Screenshots of Stack Management page in mobile view. Menu closed on the left, menu open on the right.](../assets/kibana_template_solution_nav_mobile.png) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 7f2d0768a1fb9d..a585a0fc7542f5 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -9,7 +9,7 @@ pageLoadAssetSize: charts: 195358 cloud: 21076 console: 46091 - core: 414000 + core: 432925 crossClusterReplication: 65408 dashboard: 374194 dashboardEnhanced: 65646 diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss index 936b41e7682bb7..3a748f3ceb6fd3 100644 --- a/src/core/public/rendering/_base.scss +++ b/src/core/public/rendering/_base.scss @@ -49,6 +49,13 @@ top: $headerHeight; height: calc(100% - #{$headerHeight}); } + + @include euiBreakpoint('m', 'l', 'xl') { + .euiPageSideBar--sticky { + max-height: calc(100vh - #{$headerHeight}); + top: #{$headerHeight}; + } + } } .kbnBody { diff --git a/src/plugins/kibana_react/public/page_template/__snapshots__/page_template.test.tsx.snap b/src/plugins/kibana_react/public/page_template/__snapshots__/page_template.test.tsx.snap index 89fa05615a0391..a80e3a67fb2db0 100644 --- a/src/plugins/kibana_react/public/page_template/__snapshots__/page_template.test.tsx.snap +++ b/src/plugins/kibana_react/public/page_template/__snapshots__/page_template.test.tsx.snap @@ -2,6 +2,7 @@ exports[`KibanaPageTemplate render basic template 1`] = ` `; exports[`KibanaPageTemplate render custom empty prompt only 1`] = ` @@ -33,6 +45,7 @@ exports[`KibanaPageTemplate render custom empty prompt only 1`] = ` exports[`KibanaPageTemplate render custom empty prompt with page header 1`] = ` @@ -58,6 +76,12 @@ exports[`KibanaPageTemplate render custom empty prompt with page header 1`] = ` exports[`KibanaPageTemplate render default empty prompt 1`] = ` @@ -72,7 +96,76 @@ exports[`KibanaPageTemplate render default empty prompt 1`] = ` test

} + iconColor="" iconType="test" />
`; + +exports[`KibanaPageTemplate render solutionNav 1`] = ` + + } + pageSideBarProps={ + Object { + "className": "kbnPageTemplate__pageSideBar", + } + } + restrictWidth={true} +/> +`; diff --git a/src/plugins/kibana_react/public/page_template/page_template.scss b/src/plugins/kibana_react/public/page_template/page_template.scss new file mode 100644 index 00000000000000..4b8513311114d8 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/page_template.scss @@ -0,0 +1,15 @@ +$euiSideNavEmphasizedBackgroundColor: transparentize($euiColorLightShade, .7); + +.kbnPageTemplate__pageSideBar { + padding: $euiSizeL; + background: + linear-gradient(160deg, $euiSideNavEmphasizedBackgroundColor 0, $euiSideNavEmphasizedBackgroundColor $euiSizeXL, rgba(#FFF, 0) 0), + linear-gradient(175deg, $euiSideNavEmphasizedBackgroundColor 0, $euiSideNavEmphasizedBackgroundColor $euiSize, rgba(#FFF, 0) 0); +} + +@include euiBreakpoint('xs','s') { + .kbnPageTemplate__pageSideBar { + width: auto; + padding: 0; + } +} diff --git a/src/plugins/kibana_react/public/page_template/page_template.test.tsx b/src/plugins/kibana_react/public/page_template/page_template.test.tsx index 2ad9a81e7916c9..43f96a6c2b98cf 100644 --- a/src/plugins/kibana_react/public/page_template/page_template.test.tsx +++ b/src/plugins/kibana_react/public/page_template/page_template.test.tsx @@ -10,6 +10,46 @@ import React from 'react'; import { shallow } from 'enzyme'; import { KibanaPageTemplate } from './page_template'; import { EuiEmptyPrompt } from '@elastic/eui'; +import { KibanaPageTemplateSolutionNavProps } from './solution_nav'; + +const navItems: KibanaPageTemplateSolutionNavProps['items'] = [ + { + name: 'Ingest', + id: '1', + items: [ + { + name: 'Ingest Node Pipelines', + id: '1.1', + }, + { + name: 'Logstash Pipelines', + id: '1.2', + }, + { + name: 'Beats Central Management', + id: '1.3', + }, + ], + }, + { + name: 'Data', + id: '2', + items: [ + { + name: 'Index Management', + id: '2.1', + }, + { + name: 'Index Lifecycle Policies', + id: '2.2', + }, + { + name: 'Snapshot and Restore', + id: '2.3', + }, + ], + }, +]; describe('KibanaPageTemplate', () => { test('render default empty prompt', () => { @@ -66,4 +106,23 @@ describe('KibanaPageTemplate', () => { ); expect(component).toMatchSnapshot(); }); + + test('render solutionNav', () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); }); diff --git a/src/plugins/kibana_react/public/page_template/page_template.tsx b/src/plugins/kibana_react/public/page_template/page_template.tsx index eb834d00402ef0..0bbf97ca6ddb57 100644 --- a/src/plugins/kibana_react/public/page_template/page_template.tsx +++ b/src/plugins/kibana_react/public/page_template/page_template.tsx @@ -5,10 +5,21 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ +import './page_template.scss'; -import { EuiEmptyPrompt, EuiPageTemplate, EuiPageTemplateProps } from '@elastic/eui'; import React, { FunctionComponent } from 'react'; +import classNames from 'classnames'; + +import { EuiEmptyPrompt, EuiPageTemplate, EuiPageTemplateProps } from '@elastic/eui'; + +import { + KibanaPageTemplateSolutionNav, + KibanaPageTemplateSolutionNavProps, +} from './solution_nav/solution_nav'; +/** + * A thin wrapper around EuiPageTemplate with a few Kibana specific additions + */ export type KibanaPageTemplateProps = EuiPageTemplateProps & { /** * Changes the template type depending on other props provided. @@ -17,6 +28,10 @@ export type KibanaPageTemplateProps = EuiPageTemplateProps & { * With `pageHeader` and `children`: Uses `centeredContent` */ isEmptyState?: boolean; + /** + * Quick creation of EuiSideNav. Hooks up mobile instance too + */ + solutionNav?: KibanaPageTemplateSolutionNavProps; }; export const KibanaPageTemplate: FunctionComponent = ({ @@ -27,6 +42,8 @@ export const KibanaPageTemplate: FunctionComponent = ({ restrictWidth = true, bottomBar, bottomBarProps, + pageSideBar, + solutionNav, ...rest }) => { // Needed for differentiating between union types @@ -38,6 +55,13 @@ export const KibanaPageTemplate: FunctionComponent = ({ }; } + /** + * Create the solution nav component + */ + if (solutionNav) { + pageSideBar = ; + } + /** * An easy way to create the right content for empty pages */ @@ -48,6 +72,7 @@ export const KibanaPageTemplate: FunctionComponent = ({ children = ( {pageTitle} : undefined} body={description ?

{description}

: undefined} actions={rightSideItems} @@ -62,8 +87,14 @@ export const KibanaPageTemplate: FunctionComponent = ({ return ( diff --git a/src/plugins/kibana_react/public/page_template/solution_nav/__snapshots__/solution_nav.test.tsx.snap b/src/plugins/kibana_react/public/page_template/solution_nav/__snapshots__/solution_nav.test.tsx.snap new file mode 100644 index 00000000000000..02673577095348 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/solution_nav/__snapshots__/solution_nav.test.tsx.snap @@ -0,0 +1,238 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`KibanaPageTemplateSolutionNav accepts EuiSideNavProps 1`] = ` +
+ +

+ + Solution + +

+
+ + + + } + toggleOpenOnMobile={[Function]} + /> +
+`; + +exports[`KibanaPageTemplateSolutionNav renders 1`] = ` +
+ +

+ + Solution + +

+
+ + + + } + toggleOpenOnMobile={[Function]} + /> +
+`; + +exports[`KibanaPageTemplateSolutionNav renders with icon 1`] = ` +
+ +

+ + + Solution + +

+
+ + + + + } + toggleOpenOnMobile={[Function]} + /> +
+`; diff --git a/src/plugins/kibana_react/public/page_template/solution_nav/__snapshots__/solution_nav_avatar.test.tsx.snap b/src/plugins/kibana_react/public/page_template/solution_nav/__snapshots__/solution_nav_avatar.test.tsx.snap new file mode 100644 index 00000000000000..ede09c5652c310 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/solution_nav/__snapshots__/solution_nav_avatar.test.tsx.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`KibanaPageTemplateSolutionNavAvatar renders 1`] = ` + +`; diff --git a/src/plugins/kibana_react/public/page_template/solution_nav/index.ts b/src/plugins/kibana_react/public/page_template/solution_nav/index.ts new file mode 100644 index 00000000000000..abbcde9a084869 --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/solution_nav/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { KibanaPageTemplateSolutionNav, KibanaPageTemplateSolutionNavProps } from './solution_nav'; +export { + KibanaPageTemplateSolutionNavAvatar, + KibanaPageTemplateSolutionNavAvatarProps, +} from './solution_nav_avatar'; diff --git a/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.scss b/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.scss new file mode 100644 index 00000000000000..bdb88b2ab7baae --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.scss @@ -0,0 +1,22 @@ +.kbnPageTemplateSolutionNav__title { + margin-bottom: $euiSizeL; +} + +@include euiBreakpoint('xs','s') { + .kbnPageTemplateSolutionNav { + // TODO: Fix in EUI + .euiSideNav__mobileToggle { + height: auto; + font-size: $euiFontSizeM; + + .euiButtonEmpty__text { + overflow: visible; + } + } + } + + // Rely on the `mobileToggle` of the EuiSideNav component to title the navigation list + .kbnPageTemplateSolutionNav__title { + display: none; + } +} diff --git a/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.test.tsx b/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.test.tsx new file mode 100644 index 00000000000000..1ba6cc924cda1f --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { KibanaPageTemplateSolutionNav, KibanaPageTemplateSolutionNavProps } from './solution_nav'; + +const items: KibanaPageTemplateSolutionNavProps['items'] = [ + { + name: 'Ingest', + id: '1', + items: [ + { + name: 'Ingest Node Pipelines', + id: '1.1', + }, + { + name: 'Logstash Pipelines', + id: '1.2', + }, + { + name: 'Beats Central Management', + id: '1.3', + }, + ], + }, + { + name: 'Data', + id: '2', + items: [ + { + name: 'Index Management', + id: '2.1', + }, + { + name: 'Index Lifecycle Policies', + id: '2.2', + }, + { + name: 'Snapshot and Restore', + id: '2.3', + }, + ], + }, +]; + +describe('KibanaPageTemplateSolutionNav', () => { + test('renders', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + test('renders with icon', () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); + + test('accepts EuiSideNavProps', () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.tsx b/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.tsx new file mode 100644 index 00000000000000..4aa456f716dbda --- /dev/null +++ b/src/plugins/kibana_react/public/page_template/solution_nav/solution_nav.tsx @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import './solution_nav.scss'; + +import React, { FunctionComponent, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { EuiTitle, EuiSideNav, EuiSideNavProps, htmlIdGenerator } from '@elastic/eui'; + +import { + KibanaPageTemplateSolutionNavAvatar, + KibanaPageTemplateSolutionNavAvatarProps, +} from './solution_nav_avatar'; + +export type KibanaPageTemplateSolutionNavProps = EuiSideNavProps<{}> & { + /** + * Name of the solution, i.e. "Observability" + */ + name: KibanaPageTemplateSolutionNavAvatarProps['name']; + /** + * Solution logo, i.e. "logoObservability" + */ + icon?: KibanaPageTemplateSolutionNavAvatarProps['iconType']; +}; + +/** + * A wrapper around EuiSideNav but also creates the appropriate title with optional solution logo + */ +export const KibanaPageTemplateSolutionNav: FunctionComponent = ({ + name, + icon, + items, + ...rest +}) => { + const [isSideNavOpenOnMobile, setisSideNavOpenOnMobile] = useState(false); + const toggleOpenOnMobile = () => { + setisSideNavOpenOnMobile(!isSideNavOpenOnMobile); + }; + + /** + * Create the avatar. + */ + let solutionAvatar; + if (icon) { + solutionAvatar = ; + } + + /** + * Create the required title. + * a11y: Since the heading can't be nested inside `