diff --git a/docs/apm/apm-app-users.asciidoc b/docs/apm/apm-app-users.asciidoc index d766c866f87e4..3f0a42251304c 100644 --- a/docs/apm/apm-app-users.asciidoc +++ b/docs/apm/apm-app-users.asciidoc @@ -84,7 +84,7 @@ Here are two examples: | Allow the use of the the {beat_kib_app} | Spaces -| `Read` or `All` on Dashboards, Visualize, and Discover +| `Read` or `All` on Dashboards and Discover | Allow the user to view, edit, and create dashboards, as well as browse data. |==== diff --git a/docs/canvas/canvas-elements.asciidoc b/docs/canvas/canvas-elements.asciidoc new file mode 100644 index 0000000000000..782bae061b8c1 --- /dev/null +++ b/docs/canvas/canvas-elements.asciidoc @@ -0,0 +1,167 @@ +[role="xpack"] +[[add-canvas-elements]] +=== Add elements + +Create a story about your data by adding elements to your workpad that include images, text, charts, and more. You can create your own elements and connect them to your data sources, add saved objects, and add your own images. + +[float] +[[create-canvas-element]] +==== Create an element + +Choose the type of element you want to use, then connect it to your own data. + +. Click *Add element*, then select the element you want to use. ++ +[role="screenshot"] +image::images/canvas-element-select.gif[Canvas elements] + +. To familiarize yourself with the element, use the preconfigured data demo data. ++ +By default, most of the elements you create use demo data until you change the data source. The demo data includes a small data set that you can use to experiment with your element. + +. To connect the element to your data, select *Data*, then select one of the following data sources: + +* *{es} SQL* — Access your data in {es} using SQL syntax. For information about SQL syntax, refer to {ref}/sql-spec.html[SQL language]. + +* *{es} documents* — Access your data in {es} without using aggregations. To use, select an index and fields, and optionally enter a query using the <>. Use the *{es} documents* data source when you have low volume datasets, to view raw documents, or to plot exact, non-aggregated values on a chart. + +* *Timelion* — Access your time series data using <> queries. To use Timelion queries, you can enter a query using the <>. + +Each element can display a different data source. Pages and workpads often contain multiple data sources. + +[float] +[[canvas-add-object]] +==== Add a saved object + +Add <> to your workpad, such as maps and visualizations. + +. Click *Add element > Add from Visualize Library*. + +. Select the saved object you want to add. ++ +[role="screenshot"] +image::images/canvas-map-embed.gif[] + +. To use the customization options, click the panel menu, then select one of the following options: + +* *Edit map* — Opens <> or <> so that you can edit the original saved object. + +* *Edit panel title* — Adds a title to the saved object. + +* *Customize time range* — Exposes a time filter dedicated to the saved object. + +* *Inspect* — Allows you to drill down into the element data. + +[float] +[[canvas-add-image]] +==== Add your own image + +To personalize your workpad, add your own logos and graphics. + +. Click *Add element > Manage assets*. + +. On the *Manage workpad assets* window, drag and drop your images. + +. To add the image to the workpad, click the *Create image element* icon. ++ +[role="screenshot"] +image::images/canvas-add-image.gif[] + +[float] +[[move-canvas-elements]] +==== Organize elements + +Move and resize your elements to meet your design needs. + +* To move, click and hold the element, then drag to the new location. + +* To move by 1 pixel, select the element, press and hold Shift, then use your arrow keys. + +* To move by 10 pixels, select the element, then use your arrow keys. + +* To resize, click and drag the resize handles to the new dimensions. + +[float] +[[format-canvas-elements]] +==== Format elements + +For consistency and readability across your workpad pages, align, distribute, and reorder elements. + +To align two or more elements: + +. Press and hold Shift, then select the elements you want to align. + +. Click *Edit > Alignment*, then select the alignment option. + +To distribute three or more elements: + +. Press and hold Shift, then select the elements you want to distribute. + +. Click *Edit > Distribution*, then select the distribution option. + +To reorder elements: + +. Select the element you want to reorder. + +. Click *Edit > Order*, then select the order option. + +[float] +[[data-display]] +==== Change the element display options + +Each element has its own display options to fit your design needs. + +To choose the display options, click *Display*, then make your changes. + +To define the appearance of the container and border: + +. Next to *Element style*, click *+*, then select *Container style*. + +. Expand *Container style*. + +. Change the *Appearance* and *Border* options. + +To apply CSS overrides: + +. Next to *Element style*, click *+*, then select *CSS*. + +. Enter the *CSS*. ++ +For example, to center the Markdown element, enter: ++ +[source,text] +-------------------------------------------------- +.canvasRenderEl h1 { +text.align: center; +} +-------------------------------------------------- + +. Click *Apply stylesheet*. + +[float] +[[save-elements]] +==== Save elements + +To use the elements across all workpads, save the elements. + +When you're ready to save your element, select the element, then click *Edit > Save as new element*. + +[role="screenshot"] +image::images/canvas_save_element.png[] + +To save a group of elements, press and hold Shift, select the elements you want to save, then click *Edit > Save as new element*. + +To access your saved elements, click *Add element > My elements*. + +[float] +[[delete-elements]] +==== Delete elements + +When you no longer need an element, delete it from your workpad. + +. Select the element you want to delete. + +. Click *Edit > Delete*. ++ +[role="screenshot"] +image::images/canvas_element_options.png[] diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.dependencies_.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.dependencies_.md deleted file mode 100644 index 7475f0e3a4c1c..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.dependencies_.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) > [dependencies$](./kibana-plugin-core-server.statusservicesetup.dependencies_.md) - -## StatusServiceSetup.dependencies$ property - -Current status for all plugins this plugin depends on. Each key of the `Record` is a plugin id. - -Signature: - -```typescript -dependencies$: Observable>; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.derivedstatus_.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.derivedstatus_.md deleted file mode 100644 index 6c65e44270a06..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.derivedstatus_.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) > [derivedStatus$](./kibana-plugin-core-server.statusservicesetup.derivedstatus_.md) - -## StatusServiceSetup.derivedStatus$ property - -The status of this plugin as derived from its dependencies. - -Signature: - -```typescript -derivedStatus$: Observable; -``` - -## Remarks - -By default, plugins inherit this derived status from their dependencies. Calling overrides this default status. - -This may emit multliple times for a single status change event as propagates through the dependency tree - diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md index ba0645be4d26c..3d3b73ccda25f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md @@ -12,73 +12,10 @@ API for accessing status of Core and this plugin's dependencies as well as for c export interface StatusServiceSetup ``` -## Remarks - -By default, a plugin inherits it's current status from the most severe status level of any Core services and any plugins that it depends on. This default status is available on the API. - -Plugins may customize their status calculation by calling the API with an Observable. Within this Observable, a plugin may choose to only depend on the status of some of its dependencies, to ignore severe status levels of particular Core services they are not concerned with, or to make its status dependent on other external services. - -## Example 1 - -Customize a plugin's status to only depend on the status of SavedObjects: - -```ts -core.status.set( - core.status.core$.pipe( -. map((coreStatus) => { - return coreStatus.savedObjects; - }) ; - ); -); - -``` - -## Example 2 - -Customize a plugin's status to include an external service: - -```ts -const externalStatus$ = interval(1000).pipe( - switchMap(async () => { - const resp = await fetch(`https://myexternaldep.com/_healthz`); - const body = await resp.json(); - if (body.ok) { - return of({ level: ServiceStatusLevels.available, summary: 'External Service is up'}); - } else { - return of({ level: ServiceStatusLevels.available, summary: 'External Service is unavailable'}); - } - }), - catchError((error) => { - of({ level: ServiceStatusLevels.unavailable, summary: `External Service is down`, meta: { error }}) - }) -); - -core.status.set( - combineLatest([core.status.derivedStatus$, externalStatus$]).pipe( - map(([derivedStatus, externalStatus]) => { - if (externalStatus.level > derivedStatus) { - return externalStatus; - } else { - return derivedStatus; - } - }) - ) -); - -``` - ## Properties | Property | Type | Description | | --- | --- | --- | | [core$](./kibana-plugin-core-server.statusservicesetup.core_.md) | Observable<CoreStatus> | Current status for all Core services. | -| [dependencies$](./kibana-plugin-core-server.statusservicesetup.dependencies_.md) | Observable<Record<string, ServiceStatus>> | Current status for all plugins this plugin depends on. Each key of the Record is a plugin id. | -| [derivedStatus$](./kibana-plugin-core-server.statusservicesetup.derivedstatus_.md) | Observable<ServiceStatus> | The status of this plugin as derived from its dependencies. | | [overall$](./kibana-plugin-core-server.statusservicesetup.overall_.md) | Observable<ServiceStatus> | Overall system status for all of Kibana. | -## Methods - -| Method | Description | -| --- | --- | -| [set(status$)](./kibana-plugin-core-server.statusservicesetup.set.md) | Allows a plugin to specify a custom status dependent on its own criteria. Completely overrides the default inherited status. | - diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md deleted file mode 100644 index 143cd397c40ae..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md +++ /dev/null @@ -1,28 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) > [set](./kibana-plugin-core-server.statusservicesetup.set.md) - -## StatusServiceSetup.set() method - -Allows a plugin to specify a custom status dependent on its own criteria. Completely overrides the default inherited status. - -Signature: - -```typescript -set(status$: Observable): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| status$ | Observable<ServiceStatus> | | - -Returns: - -`void` - -## Remarks - -See the [StatusServiceSetup.derivedStatus$](./kibana-plugin-core-server.statusservicesetup.derivedstatus_.md) API for leveraging the default status calculation that is provided by Core. - diff --git a/docs/discover/search.asciidoc b/docs/discover/search.asciidoc index eef2a12a964b8..da58382deb89a 100644 --- a/docs/discover/search.asciidoc +++ b/docs/discover/search.asciidoc @@ -1,7 +1,7 @@ [[search]] == Search data Many Kibana apps embed a query bar for real-time search, including -*Discover*, *Visualize*, and *Dashboard*. +*Discover* and *Dashboard*. [float] === Search your data @@ -84,7 +84,7 @@ query language you can also submit queries using the {ref}/query-dsl.html[Elasti [[save-open-search]] === Save a search -A saved search persists your current view of Discover for later retrieval and reuse. You can reload a saved search into Discover, add it to a dashboard, and use it as the basis for a <>. +A saved search persists your current view of Discover for later retrieval and reuse. You can reload a saved search into Discover, add it to a dashboard, and use it as the basis for a visualization. A saved search includes the query text, filters, and optionally, the time filter. A saved search also includes the selected columns in the document table, the sort order, and the current index pattern. @@ -120,7 +120,7 @@ used for the saved search will also be automatically selected. [[save-load-delete-query]] === Save a query -A saved query is a portable collection of query text and filters that you can reuse in <>, <>, and <>. Save a query when you want to: +A saved query is a portable collection of query text and filters that you can reuse in <> and <>. Save a query when you want to: * Retrieve results from the same query at a later time without having to reenter the query text, add the filters or set the time filter * View the results of the same query in multiple apps @@ -148,7 +148,7 @@ image::discover/images/saved-query-save-form-default-filters.png["Example of the . Click *Save*. ==== Load a query -To load a saved query into Discover, Dashboard, or Visualize: +To load a saved query into Discover or Dashboard: . Click *#* in the search bar, next to the query text input. . Select the query you want to load. You might need to scroll down to find the query you are looking for. diff --git a/docs/drilldowns/explore-underlying-data.asciidoc b/docs/drilldowns/explore-underlying-data.asciidoc deleted file mode 100644 index c2bba599730d8..0000000000000 --- a/docs/drilldowns/explore-underlying-data.asciidoc +++ /dev/null @@ -1,41 +0,0 @@ -[[explore-underlying-data]] -== Explore the underlying data for a visualization - -++++ -Explore the underlying data -++++ - -Dashboard panels have an *Explore underlying data* action that navigates you to *Discover*, -where you can narrow your documents to the ones you'll most likely use in a visualization. -This action is available for visualizations backed by a single index pattern. - -You can access *Explore underlying data* in two ways: from the panel context -menu or from the menu that appears when you interact with the chart. - -[float] -[[explore-data-from-panel-context-menu]] -=== Explore data from panel context menu - -The *Explore underlying data* action in the panel menu navigates you to Discover, -carrying over the index pattern, filters, query, and time range for the visualization. - -[role="screenshot"] -image::images/explore_data_context_menu.png[Explore underlying data from panel context menu] - -[float] -[[explore-data-from-chart]] -=== Explore data from chart action - -Initiating *Explore underlying data* from the chart also navigates to Discover, -carrying over the current context for the visualization. In addition, this action -applies the filters and time range created by the events that triggered the action. - -[role="screenshot"] -image::images/explore_data_in_chart.png[Explore underlying data from chart] - -To enable this action add the following line to your `kibana.yml` config. - -["source","yml"] ------------ -xpack.discoverEnhanced.actions.exploreDataInChart.enabled: true ------------ diff --git a/docs/getting-started/add-sample-data.asciidoc b/docs/getting-started/add-sample-data.asciidoc deleted file mode 100644 index ab43431601888..0000000000000 --- a/docs/getting-started/add-sample-data.asciidoc +++ /dev/null @@ -1,28 +0,0 @@ -[[add-sample-data]] -== Add sample data - -{kib} has several sample data sets that you can use to explore {kib} before loading your own data. -These sample data sets showcase a variety of use cases: - -* *eCommerce orders* includes visualizations for product-related information, -such as cost, revenue, and price. -* *Flight data* enables you to view and interact with flight routes. -* *Web logs* lets you analyze website traffic. - -To get started, go to the {kib} home page and click the link underneath *Add sample data*. - -Once you've loaded a data set, click *View data* to view prepackaged -visualizations, dashboards, Canvas workpads, Maps, and Machine Learning jobs. - -[role="screenshot"] -image::images/add-sample-data.png[] - -NOTE: The timestamps in the sample data sets are relative to when they are installed. -If you uninstall and reinstall a data set, the timestamps will change to reflect the most recent installation. - -[float] -=== Next steps - -* Explore {kib} by following the <>. - -* Learn how to load data, define index patterns, and build visualizations by <>. diff --git a/docs/getting-started/images/gs_maps_time_filter.png b/docs/getting-started/images/gs_maps_time_filter.png new file mode 100644 index 0000000000000..83e20c279906e Binary files /dev/null and b/docs/getting-started/images/gs_maps_time_filter.png differ diff --git a/docs/getting-started/images/tutorial-discover-2.png b/docs/getting-started/images/tutorial-discover-2.png index 7190c90d8e5ba..681e4834de830 100644 Binary files a/docs/getting-started/images/tutorial-discover-2.png and b/docs/getting-started/images/tutorial-discover-2.png differ diff --git a/docs/getting-started/images/tutorial-pattern-1.png b/docs/getting-started/images/tutorial-pattern-1.png index 8a289f93fc66e..0026b18775518 100644 Binary files a/docs/getting-started/images/tutorial-pattern-1.png and b/docs/getting-started/images/tutorial-pattern-1.png differ diff --git a/docs/getting-started/images/tutorial-visualize-bar-1.5.png b/docs/getting-started/images/tutorial-visualize-bar-1.5.png index c02b9ca59dff5..009152f9407e4 100644 Binary files a/docs/getting-started/images/tutorial-visualize-bar-1.5.png and b/docs/getting-started/images/tutorial-visualize-bar-1.5.png differ diff --git a/docs/getting-started/images/tutorial-visualize-map-2.png b/docs/getting-started/images/tutorial-visualize-map-2.png index f4d1d0e47fe6a..ed2fd47cb27de 100644 Binary files a/docs/getting-started/images/tutorial-visualize-map-2.png and b/docs/getting-started/images/tutorial-visualize-map-2.png differ diff --git a/docs/getting-started/images/tutorial-visualize-md-2.png b/docs/getting-started/images/tutorial-visualize-md-2.png index 9e9a670ba196f..af56faa3b0516 100644 Binary files a/docs/getting-started/images/tutorial-visualize-md-2.png and b/docs/getting-started/images/tutorial-visualize-md-2.png differ diff --git a/docs/getting-started/images/tutorial-visualize-pie-2.png b/docs/getting-started/images/tutorial-visualize-pie-2.png index ef5d62b4ceee7..ca8f5e92146bc 100644 Binary files a/docs/getting-started/images/tutorial-visualize-pie-2.png and b/docs/getting-started/images/tutorial-visualize-pie-2.png differ diff --git a/docs/getting-started/images/tutorial-visualize-pie-3.png b/docs/getting-started/images/tutorial-visualize-pie-3.png index 6974c8d34b0dd..59fce360096c0 100644 Binary files a/docs/getting-started/images/tutorial-visualize-pie-3.png and b/docs/getting-started/images/tutorial-visualize-pie-3.png differ diff --git a/docs/getting-started/images/tutorial_index_patterns.png b/docs/getting-started/images/tutorial_index_patterns.png new file mode 100644 index 0000000000000..430baf898b612 Binary files /dev/null and b/docs/getting-started/images/tutorial_index_patterns.png differ diff --git a/docs/getting-started/tutorial-dashboard.asciidoc b/docs/getting-started/tutorial-dashboard.asciidoc deleted file mode 100644 index 2ee2d76024aed..0000000000000 --- a/docs/getting-started/tutorial-dashboard.asciidoc +++ /dev/null @@ -1,53 +0,0 @@ -[[tutorial-dashboard]] -=== Add the visualizations to a dashboard - -Build a dashboard that contains the visualizations and map that you saved during -this tutorial. - -. Open the menu, go to *Dashboard*, then click *Create dashboard*. -. Set the time filter to May 18, 2015 to May 20, 2015. -. Click *Add*, then select the following: - * *Bar Example* - * *Map Example* - * *Markdown Example* - * *Pie Example* -+ -Your sample dashboard looks like this: -+ -[role="screenshot"] -image::images/tutorial-dashboard.png[] - -. Try out the editing controls. -+ -You can rearrange the visualizations by clicking a the header of a -visualization and dragging. The gear icon in the top right of a visualization -displays controls for editing and deleting the visualization. A resize control -is on the lower right. - -. *Save* your dashboard. - -==== Inspect the data - -Seeing visualizations of your data is great, -but sometimes you need to look at the actual data to -understand what's really going on. You can inspect the data behind any visualization -and view the {es} query used to retrieve it. - -. Click the pie chart *Options* menu, then select *Inspect*. -+ -[role="screenshot"] -image::images/tutorial-full-inspect1.png[] - -. To look at the query used to fetch the data for the visualization, select *View > Requests*. - -[float] -=== Next steps - -Now that you have the basics, you're ready to start exploring -your own data with {kib}. - -* To learn about searching and filtering your data, refer to {kibana-ref}/discover.html[Discover]. -* To learn about the visualization types {kib} has to offer, refer to {kibana-ref}/visualize.html[Visualize]. -* To learn about configuring {kib} and managing your saved objects, refer to {kibana-ref}/management.html[Management]. -* To learn about the interactive console you can use to submit REST requests to {es}, refer to {kibana-ref}/console-kibana.html[Console]. - diff --git a/docs/getting-started/tutorial-define-index.asciidoc b/docs/getting-started/tutorial-define-index.asciidoc index 254befa55faea..fbe7450683dbc 100644 --- a/docs/getting-started/tutorial-define-index.asciidoc +++ b/docs/getting-started/tutorial-define-index.asciidoc @@ -1,7 +1,7 @@ [[tutorial-define-index]] === Define your index patterns -Index patterns tell Kibana which Elasticsearch indices you want to explore. +Index patterns tell {kib} which {es} indices you want to explore. An index pattern can match the name of a single index, or include a wildcard (*) to match multiple indices. @@ -10,28 +10,29 @@ series of indices in the format `logstash-YYYY.MMM.DD`. To explore all of the log data from May 2018, you could specify the index pattern `logstash-2018.05*`. - [float] -==== Create your first index pattern +==== Create the index patterns First you'll create index patterns for the Shakespeare data set, which has an index named `shakespeare,` and the accounts data set, which has an index named `bank`. These data sets don't contain time series data. . Open the menu, then go to *Stack Management > {kib} > Index Patterns*. + . If this is your first index pattern, the *Create index pattern* page opens. -Otherwise, click *Create index pattern*. -. In the *Index pattern field*, enter `shakes*`. + +. In the *Index pattern name* field, enter `shakes*`. + [role="screenshot"] -image::images/tutorial-pattern-1.png[] +image::images/tutorial-pattern-1.png[shakes* index patterns] . Click *Next step*. -. Select the *Time Filter field name*, then click *Create index pattern*. + +. On the *Configure settings* page, *Create index pattern*. + You’re presented a table of all fields and associated data types in the index. -. Return to the *Index patterns* page and create a second index pattern named `ba*`. +. Create a second index pattern named `ba*`. [float] ==== Create an index pattern for the time series data @@ -39,15 +40,12 @@ You’re presented a table of all fields and associated data types in the index. Create an index pattern for the Logstash index, which contains the time series data. -. Define an index pattern named `logstash*`. -. Click *Next step*. -. From the *Time Filter field name* dropdown, select *@timestamp*. -. Click *Create index pattern*. +. Create an index pattern named `logstash*`, then click *Next step*. -NOTE: When you define an index pattern, the indices that match that pattern must -exist in Elasticsearch and they must contain data. To check which indices are -available, open the menu, then go to *Dev Tools > Console* and enter `GET _cat/indices`. Alternately, use -`curl -XGET "http://localhost:9200/_cat/indices"`. +. From the *Time field* dropdown, select *@timestamp, then click *Create index pattern*. ++ +[role="screenshot"] +image::images/tutorial_index_patterns.png[All tutorial index patterns] diff --git a/docs/getting-started/tutorial-discovering.asciidoc b/docs/getting-started/tutorial-discovering.asciidoc index 31d77be1275ee..ec07a74b8ac0d 100644 --- a/docs/getting-started/tutorial-discovering.asciidoc +++ b/docs/getting-started/tutorial-discovering.asciidoc @@ -1,9 +1,8 @@ -[[tutorial-discovering]] -=== Discover your data +[[explore-your-data]] +=== Explore your data -Using *Discover*, enter -an {ref}/query-dsl-query-string-query.html#query-string-syntax[Elasticsearch -query] to search your data and filter the results. +With *Discover*, you use {ref}/query-dsl-query-string-query.html#query-string-syntax[Elasticsearch +queries] to explore your data and narrow the results with filters. . Open the menu, then go to *Discover*. + @@ -13,7 +12,7 @@ The `shakes*` index pattern appears. + By default, all fields are shown for each matching document. -. In the *Search* field, enter the following: +. In the *Search* field, enter the following, then click *Update*: + [source,text] account_number<100 AND balance>47500 @@ -32,3 +31,5 @@ account numbers. + [role="screenshot"] image::images/tutorial-discover-3.png[] + +Now that you know what your documents contain, it's time to gain insight into your data with visualizations. diff --git a/docs/getting-started/tutorial-full-experience.asciidoc b/docs/getting-started/tutorial-full-experience.asciidoc index e6f2de87905bf..1e6fe39dbd013 100644 --- a/docs/getting-started/tutorial-full-experience.asciidoc +++ b/docs/getting-started/tutorial-full-experience.asciidoc @@ -1,32 +1,23 @@ -[[tutorial-build-dashboard]] -== Build your own dashboard +[[create-your-own-dashboard]] +== Create your own dashboard -Want to load some data into Kibana and build a dashboard? This tutorial shows you how to: +Ready to add data to {kib} and create your own dashboard? In this tutorial, you'll use three types of data sets that'll help you learn to: -* <> -* <> -* <> -* <> -* <> - -When you complete this tutorial, you'll have a dashboard that looks like this. - -[role="screenshot"] -image::images/tutorial-dashboard.png[] +* <> +* <> +* <> +* <> [float] -[[tutorial-load-dataset]] -=== Load sample data +[[download-the-data]] +=== Download the data -This tutorial requires you to download three data sets: +To complete the tutorial, you'll download and use the following data sets: * The complete works of William Shakespeare, suitably parsed into fields -* A set of fictitious accounts with randomly generated data +* A set of fictitious bank accounts with randomly generated data * A set of randomly generated log files -[float] -==== Download the data sets - Create a new working directory where you want to download the files. From that directory, run the following commands: [source,shell] @@ -34,7 +25,7 @@ curl -O https://download.elastic.co/demos/kibana/gettingstarted/8.x/shakespeare. curl -O https://download.elastic.co/demos/kibana/gettingstarted/8.x/accounts.zip curl -O https://download.elastic.co/demos/kibana/gettingstarted/8.x/logs.jsonl.gz -Two of the data sets are compressed. To extract the files, use these commands: +Two of the data sets are compressed. To extract the files, use the following commands: [source,shell] unzip accounts.zip @@ -43,7 +34,7 @@ gunzip logs.jsonl.gz [float] ==== Structure of the data sets -The Shakespeare data set has this structure: +The Shakespeare data set has the following structure: [source,json] { @@ -55,7 +46,7 @@ The Shakespeare data set has this structure: "text_entry": "String", } -The accounts data set is structured as follows: +The accounts data set has the following structure: [source,json] { @@ -72,7 +63,7 @@ The accounts data set is structured as follows: "state": "String" } -The logs data set has dozens of different fields. Here are the notable fields for this tutorial: +The logs data set has dozens of different fields. The notable fields include the following: [source,json] { @@ -94,7 +85,7 @@ You must also have the `create`, `manage` `read`, `write,` and `delete` index privileges. See {ref}/security-privileges.html[Security privileges] for more information. -Open *Dev Tools*. On the *Console* page, set up a mapping for the Shakespeare data set: +Open the menu, then go to *Dev Tools*. On the *Console* page, set up a mapping for the Shakespeare data set: [source,js] PUT /shakespeare @@ -111,10 +102,11 @@ PUT /shakespeare //CONSOLE -This mapping specifies field characteristics for the data set: +The mapping specifies field characteristics for the data set: * The `speaker` and `play_name` fields are keyword fields. These fields are not analyzed. The strings are treated as a single unit even if they contain multiple words. + * The `line_id` and `speech_number` fields are integers. The logs data set requires a mapping to label the latitude and longitude pairs @@ -177,6 +169,7 @@ PUT /logstash-2015.05.20 The accounts data set doesn't require any mappings. [float] +[[load-the-data-sets]] ==== Load the data sets At this point, you're ready to use the Elasticsearch {ref}/docs-bulk.html[bulk] @@ -195,14 +188,20 @@ Invoke-RestMethod "http://:/_bulk?pretty" -Method Post -ContentType These commands might take some time to execute, depending on the available computing resources. -Verify successful loading: +When you define an index pattern, the indices that match the pattern must +exist in {es} and contain data. + +To verify the availability of the indices, open the menu, go to *Dev Tools > Console*, then enter: [source,js] GET /_cat/indices?v -//CONSOLE +Alternately, use: + +[source,shell] +`curl -XGET "http://localhost:9200/_cat/indices"`. -Your output should look similar to this: +The output should look similar to: [source,shell] health status index pri rep docs.count docs.deleted store.size pri.store.size diff --git a/docs/getting-started/tutorial-sample-data.asciidoc b/docs/getting-started/tutorial-sample-data.asciidoc index 2460a55e13293..18ef862272f85 100644 --- a/docs/getting-started/tutorial-sample-data.asciidoc +++ b/docs/getting-started/tutorial-sample-data.asciidoc @@ -1,207 +1,159 @@ -[[tutorial-sample-data]] +[[explore-kibana-using-sample-data]] == Explore {kib} using sample data -Ready to get some hands-on experience with Kibana? -In this tutorial, you’ll work -with Kibana sample data and learn to: +Ready to get some hands-on experience with {kib}? +In this tutorial, you’ll work with {kib} sample data and learn to: -* <> -* <> -* <> -* <> +* <> +* <> -NOTE: If security is enabled, you must have `read`, `write`, and `manage` privileges -on the `kibana_sample_data_*` indices. See -{ref}/security-privileges.html[Security privileges] for more information. +* <> +NOTE: If security is enabled, you must have `read`, `write`, and `manage` privileges +on the `kibana_sample_data_*` indices. For more information, refer to +{ref}/security-privileges.html[Security privileges]. [float] -=== Add sample data +[[add-the-sample-data]] +=== Add the sample data -Install the Flights sample data set, if you haven't already. +Add the *Sample flight data*. . On the home page, click *Load a data set and a {kib} dashboard*. + . On the *Sample flight data* card, click *Add data*. -. Once the data is added, click *View data > Dashboard*. -+ -You’re taken to the *Global Flight* dashboard, a collection of charts, graphs, -maps, and other visualizations of the the data in the `kibana_sample_data_flights` index. -+ -[role="screenshot"] -image::getting-started/images/tutorial-sample-dashboard.png[] [float] -[[tutorial-sample-filter]] -=== Filter and query the data +[[explore-the-data]] +=== Explore the data -You can use filters and queries to -narrow the view of the data. -For more detailed information on these actions, see -{ref}/query-filter-context.html[Query and filter context]. +Explore the documents in the index that +match the selected index pattern. The index pattern tells {kib} which {es} index you want to +explore. -[float] -==== Filter the data +. Open the menu, then go to *Discover*. -. In the *Controls* visualization, select an *Origin City* and a *Destination City*. -. Click *Apply changes*. +. Make sure `kibana_sample_data_flights` is the current index pattern. +You might need to click *New* in the {kib} toolbar to refresh the data. + -The `OriginCityName` and the `DestCityName` fields filter the data on the dasbhoard to match -the data you specified. +You'll see a histogram that shows the distribution of +documents over time. A table lists the fields for +each document that matches the index. By default, all fields are shown. + -For example, the following dashboard shows the data for flights from London to Milan. +[role="screenshot"] +image::getting-started/images/tutorial-sample-discover1.png[] + +. Hover over the list of *Available fields*, then click *Add* next +to each field you want explore in the table. + [role="screenshot"] -image::getting-started/images/tutorial-sample-filter.png[] +image::getting-started/images/tutorial-sample-discover2.png[] -. To add a filter manually, click *Add filter*, -then specify the data you want to view. +[float] +[[view-and-analyze-the-data]] +=== View and analyze the data -. When you are finished experimenting, remove all filters. +A _dashboard_ is a collection of panels that provide you with an overview of your data that you can +use to analyze your data. Panels contain everything you need, including visualizations, +interactive controls, Markdown, and more. + +To open the *Global Flight* dashboard, open the menu, then go to *Dashboard*. +[role="screenshot"] +image::getting-started/images/tutorial-sample-dashboard.png[] [float] -[[tutorial-sample-query]] -==== Query the data +[[change-the-panel-data]] +==== Change the panel data -. To find all flights out of Rome, enter this query in the query bar and click *Update*: -+ -[source,text] -OriginCityName:Rome +To gain insights into your data, change the appearance and behavior of the panels. +For example, edit the metric panel to find the airline that has the lowest average fares. -. For a more complex query with AND and OR, try this: -+ -[source,text] -OriginCityName:Rome AND (Carrier:JetBeats OR "Kibana Airlines") -+ -The dashboard updates to show data for the flights out of Rome on JetBeats and -{kib} Airlines. -+ -[role="screenshot"] -image::getting-started/images/tutorial-sample-query.png[] +. In the {kib} toolbar, click *Edit*. -. When you are finished exploring the dashboard, remove the query by -clearing the contents in the query bar and clicking *Update*. +. In the *Average Ticket Price* metric panel, open the panel menu, then select *Edit visualization*. -[float] -[[tutorial-sample-discover]] -=== Discover the data +. To change the data on the panel, use an {es} {ref}/search-aggregations.html[bucket aggregation], +which sorts the documents that match your search criteria into different categories or buckets. -In Discover, you have access to every document in every index that -matches the selected index pattern. The index pattern tells {kib} which {es} index you are currently -exploring. You can submit search queries, filter the -search results, and view document data. +.. In the *Buckets* pane, select *Add > Split group*. -. From the menu, click *Discover*. +.. From the *Aggregation* dropdown, select *Terms*. -. Ensure `kibana_sample_data_flights` is the current index pattern. -You might need to click *New* in the menu bar to refresh the data. +.. From the *Field* dropdown, select *Carrier*. + +.. Set *Descending* to *4*, then click *Update*. + -You'll see a histogram that shows the distribution of -documents over time. A table lists the fields for -each matching document. By default, all fields are shown. +The average ticket price for all four airlines appear in the visualization builder. + [role="screenshot"] -image::getting-started/images/tutorial-sample-discover1.png[] +image::getting-started/images/tutorial-sample-edit1.png[] -. To choose which fields to display, -hover the pointer over the list of *Available fields*, and then click *add* next -to each field you want include as a column in the table. -+ -For example, if you add the `DestAirportID` and `DestWeather` fields, -the display includes columns for those two fields. +. To save your changes, click *Save and return* in the {kib} toolbar. + +. To save the dashboard, click *Save* in the {kib} toolbar. + [role="screenshot"] -image::getting-started/images/tutorial-sample-discover2.png[] +image::getting-started/images/tutorial-sample-edit2.png[] [float] -[[tutorial-sample-edit]] -=== Edit a visualization - -You have edit permissions for the *Global Flight* dashboard, so you can change -the appearance and behavior of the visualizations. For example, you might want -to see which airline has the lowest average fares. - -. In the side navigation, click *Recently viewed* and open the *Global Flight Dashboard*. -. In the menu bar, click *Edit*. -. In the *Average Ticket Price* visualization, click the gear icon in -the upper right. -. From the *Options* menu, select *Edit visualization*. -+ -*Average Ticket Price* is a metric visualization. -To specify which groups to display -in this visualization, you use an {es} {ref}/search-aggregations.html[bucket aggregation]. -This aggregation sorts the documents that match your search criteria into different -categories, or buckets. +[[filter-and-query-the-data]] +==== Filter and query the data -[float] -==== Create a bucket aggregation +To focus in on the data you want to explore, use filters and queries. +For more information, refer to +{ref}/query-filter-context.html[Query and filter context]. + +To filter the data: -. In the *Buckets* pane, select *Add > Split group*. -. In the *Aggregation* dropdown, select *Terms*. -. In the *Field* dropdown, select *Carrier*. -. Set *Descending* to *4*. -. Click *Apply changes* image:images/apply-changes-button.png[]. +. In the *Controls* visualization, select an *Origin City* and *Destination City*, then click *Apply changes*. + -You now see the average ticket price for all four airlines. +The `OriginCityName` and the `DestCityName` fields filter the data in the panels. + -[role="screenshot"] -image::getting-started/images/tutorial-sample-edit1.png[] - -[float] -==== Save the visualization - -. In the menu bar, click *Save*. -. Leave the visualization name as is and confirm the save. -. Go to the *Global Flight* dashboard and scroll the *Average Ticket Price* visualization to see the four prices. -. Optionally, edit the dashboard. Resize the panel -for the *Average Ticket Price* visualization by dragging the -handle in the lower right. You can also rearrange the visualizations by clicking -the header and dragging. Be sure to save the dashboard. +For example, the following dashboard shows the data for flights from London to Milan. + [role="screenshot"] -image::getting-started/images/tutorial-sample-edit2.png[] +image::getting-started/images/tutorial-sample-filter.png[] -[float] -[[tutorial-sample-inspect]] -=== Inspect the data +. To manually add a filter, click *Add filter*, +then specify the data you want to view. -Seeing visualizations of your data is great, -but sometimes you need to look at the actual data to -understand what's really going on. You can inspect the data behind any visualization -and view the {es} query used to retrieve it. +. When you are finished experimenting, remove all filters. -. In the dashboard, hover the pointer over the pie chart, and then click the icon in the upper right. -. From the *Options* menu, select *Inspect*. +[[query-the-data]] +To query the data: + +. To view all flights out of Rome, enter the following in the *KQL* query bar, then click *Update*: + -The initial view shows the document count. +[source,text] +OriginCityName: Rome + +. For a more complex query with AND and OR, enter: ++ +[source,text] +OriginCityName:Rome AND (Carrier:JetBeats OR Carrier:"Kibana Airlines") ++ +The dashboard panels update to display the flights out of Rome on JetBeats and +{kib} Airlines. + [role="screenshot"] -image::getting-started/images/tutorial-sample-inspect1.png[] - -. To look at the query used to fetch the data for the visualization, select *View > Requests* -in the upper right of the Inspect pane. - -[float] -[[tutorial-sample-remove]] -=== Remove the sample data set -When you’re done experimenting with the sample data set, you can remove it. +image::getting-started/images/tutorial-sample-query.png[] -. Go to the *Sample data* page. -. On the *Sample flight data* card, click *Remove*. +. When you are finished exploring, remove the query by +clearing the contents in the *KQL* query bar, then click *Update*. [float] === Next steps -Now that you have a handle on the {kib} basics, you might be interested in the -tutorial <>, where you'll learn to: +Now that you know the {kib} basics, try out the <> tutorial, where you'll learn to: + +* Add a data set to {kib} -* Load data * Define an index pattern -* Discover and explore data -* Create visualizations -* Add visualizations to a dashboard +* Discover and explore data +* Create and add panels to a dashboard diff --git a/docs/getting-started/tutorial-visualizing.asciidoc b/docs/getting-started/tutorial-visualizing.asciidoc index 20b4e33583072..33a7035160247 100644 --- a/docs/getting-started/tutorial-visualizing.asciidoc +++ b/docs/getting-started/tutorial-visualizing.asciidoc @@ -1,47 +1,76 @@ [[tutorial-visualizing]] === Visualize your data -In *Visualize*, you can shape your data using a variety -of charts, tables, and maps, and more. In this tutorial, you'll create four -visualizations: +Shape your data using a variety +of {kib} supported visualizations, tables, and more. In this tutorial, you'll create four +visualizations that you'll use to create a dashboard. -* <> -* <> -* <> -* <> +To begin, open the menu, go to *Dashboard*, then click *Create new dashboard*. [float] -[[tutorial-visualize-pie]] -=== Pie chart +[[compare-the-number-of-speaking-parts-in-the-play]] +=== Compare the number of speaking parts in the plays -Use the pie chart to -gain insight into the account balances in the bank account data. +To visualize the Shakespeare data and compare the number of speaking parts in the plays, create a bar chart using *Lens*. -. Open then menu, then go to *Visualize*. -. Click *Create visualization*. +. Click *Create new*, then click *Lens* on the *New Visualization* window. + [role="screenshot"] -image::images/tutorial-visualize-wizard-step-1.png[] -. Click *Pie*. +image::images/tutorial-visualize-wizard-step-1.png[Bar chart] -. On the *Choose a source* window, select `ba*`. +. Make sure the index pattern is *shakes*. + +. Display the play data along the x-axis. + +.. From the *Available fields* list, drag and drop *play_name* to the *X-axis* field. + +.. Click *Top values of play_name*. + +.. From the *Order direction* dropdown, select *Ascending*. + +.. In the *Label* field, enter `Play Name`. + +. Display the number of speaking parts per play along the y-axis. + +.. From the *Available fields* list, drag and drop *speaker* to the *Y-axis* field. + +.. Click *Unique count of speaker*. + +.. In the *Label* field, enter `Speaking Parts`. ++ +[role="screenshot"] +image::images/tutorial-visualize-bar-1.5.png[Bar chart] + +. *Save* the chart with the name `Bar Example`. + -Initially, the pie contains a single "slice." -That's because the default search matches all documents. +To show a tooltip with the number of speaking parts for that play, hover over a bar. + -To specify which slices to display in the pie, you use an Elasticsearch -{ref}/search-aggregations.html[bucket aggregation]. This aggregation -sorts the documents that match your search criteria into different -categories. You'll use a bucket aggregation to establish -multiple ranges of account balances and find out how many accounts fall into -each range. +Notice how the individual play names show up as whole phrases, instead of +broken up into individual words. This is the result of the mapping +you did at the beginning of the tutorial, when you marked the `play_name` field +as `not analyzed`. -. In the *Buckets* pane, click *Add > Split slices.* +[float] +[[view-the-average-account-balance-by-age]] +=== View the average account balance by age + +To gain insight into the account balances in the bank account data, create a pie chart. In this tutorial, you'll use the {es} +{ref}/search-aggregations.html[bucket aggregation] to specify the pie slices to display. The bucket aggregation sorts the documents that match your search criteria into different +categories and establishes multiple ranges of account balances so that you can find how many accounts fall into each range. + +. Click *Create new*, then click *Pie* on the *New Visualization* window. + +. On the *Choose a source* window, select `ba*`. + +Since the default search matches all documents, the pie contains a single slice. + +. In the *Buckets* pane, click *Add > Split slices.* + .. From the *Aggregation* dropdown, select *Range*. + .. From the *Field* dropdown, select *balance*. -.. Click *Add range* four times to bring the total number of ranges to six. -.. Define the following ranges: + +.. Click *Add range* until there are six rows of fields, then define the following ranges: + [source,text] 0 999 @@ -53,80 +82,83 @@ each range. . Click *Update*. + -Now you can see what proportion of the 1000 accounts fall into each balance -range. +The pie chart displays the proportion of the 1,000 accounts that fall into each of the ranges. + [role="screenshot"] -image::images/tutorial-visualize-pie-2.png[] +image::images/tutorial-visualize-pie-2.png[Pie chart] -. Add another bucket aggregation that looks at the ages of the account -holders. +. Add another bucket aggregation that displays the ages of the account holders. .. In the *Buckets* pane, click *Add*, then click *Split slices*. + .. From the *Sub aggregation* dropdown, select *Terms*. -.. From the *Field* dropdown, select *age*. -. Click *Update*. +.. From the *Field* dropdown, select *age*, then click *Update*. + The break down of the ages of the account holders are displayed in a ring around the balance ranges. + [role="screenshot"] -image::images/tutorial-visualize-pie-3.png[] +image::images/tutorial-visualize-pie-3.png[Final pie chart] . Click *Save*, then enter `Pie Example` in the *Title* field. [float] -[[tutorial-visualize-bar]] -=== Bar chart +[role="xpack"] +[[visualize-geographic-information]] +=== Visualize geographic information -Use a bar chart to look at the Shakespeare data set and compare -the number of speaking parts in the plays. +To visualize geographic information in the log file data, use <>. -. Click *Create visualization > Vertical Bar*, then set the source to `shakes*`. +. Click *Create new*, then click *Maps* on the *New Visualization* window. + +. To change the time, use the time filter. + +.. Set the *Start date* to `May 18, 2015 @ 12:00:00.000`. + +.. Set the *End date* to `May 20, 2015 @ 12:00:00.000`. + -Initially, the chart is a single bar that shows the total count -of documents that match the default wildcard query. +[role="screenshot"] +image::images/gs_maps_time_filter.png[Time filter for Maps tutorial] -. Show the number of speaking parts per play along the y-axis. +.. Click *Update* + +. Map the geo coordinates from the log files. -.. In the *Metrics* pane, expand *Y-axis*. -.. From the *Aggregation* dropdown, select *Unique Count*. -.. From the *Field* dropdown, select *speaker*. -.. In the *Custom label* field, enter `Speaking Parts`. +.. Click *Add layer > Clusters and grids*. -. Click *Update*. +.. From the *Index pattern* dropdown, select *logstash*. -. Show the plays along the x-axis. +.. Click *Add layer*. -.. In the *Buckets* pane, click *Add > X-axis*. -.. From the *Aggregation* dropdown, select *Terms*. -.. From the *Field* dropdown, select *play_name*. -.. To list the plays alphabetically, select *Ascending* from the *Order* dropdown. -.. In the *Custom label* field, enter `Play Name`. +. Specify the *Layer Style*. -. Click *Update*. +.. From the *Fill color* dropdown, select the yellow to red color ramp. + +.. In the *Border width* field, enter `3`. + +.. From the *Border color* dropdown, select *#FFF*, then click *Save & close*. + [role="screenshot"] -image::images/tutorial-visualize-bar-1.5.png[] -. *Save* the chart with the name `Bar Example`. -+ -Hovering over a bar shows a tooltip with the number of speaking parts for -that play. -+ -Notice how the individual play names show up as whole phrases, instead of -broken into individual words. This is the result of the mapping -you did at the beginning of the tutorial, when you marked the `play_name` field -as `not analyzed`. +image::images/tutorial-visualize-map-2.png[Map] + +. Click *Save*, then enter `Map Example` in the *Title* field. + +. Add the map to your dashboard. + +.. Open the menu, go to *Dashboard*, then click *Add*. + +.. On the *Add panels* flyout, click *Map Example*. [float] [[tutorial-visualize-markdown]] -=== Markdown +=== Add context to your visualizations with Markdown -Add formatted text to your dashboard with a markdown tool. +Add context to your new visualizations with Markdown text. -. Click *Create visualization > Markdown*. -. In the text field, enter the following: +. Click *Create new*, then click *Markdown* on the *New Visualization* window. + +. In the *Markdown* text field, enter: + [source,markdown] # This is a tutorial dashboard! @@ -140,40 +172,22 @@ The Markdown renders in the preview pane. [role="screenshot"] image::images/tutorial-visualize-md-2.png[] -. *Save* the tool with the name `Markdown Example`. +. Click *Save*, then enter `Markdown Example` in the *Title* field. -[float] -[[tutorial-visualize-map]] -=== Map +[role="screenshot"] +image::images/tutorial-dashboard.png[] -Using <>, you can visualize geographic information in the log file sample data. +[float] +=== Next steps -. Click *Create visualization > Maps*. +Now that you have the basics, you're ready to start exploring your own system data with {kib}. -. Set the time. -.. In the time filter, click *Show dates*. -.. Click the start date, then *Absolute*. -.. Set the *Start date* to May 18, 2015. -.. Click *now*, then *Absolute*. -.. Set the *End date* to May 20, 2015. -.. Click *Update* +* To add your own data to {kib}, refer to <>. -. Map the geo coordinates from the log files. +* To search and filter your data, refer to {kibana-ref}/discover.html[Discover]. -.. Click *Add layer > Clusters and Grids*. -.. From the *Index pattern* dropdown, select *logstash*. -.. Click *Add layer*. +* To create a dashboard with your own data, refer to <>. -. Set the *Layer Style*. -.. From the *Fill color* dropdown, select the yellow to red color ramp. -.. From the *Border color* dropdown, select white. -.. Click *Save & close*. -+ -The map looks like this: -+ -[role="screenshot"] -image::images/tutorial-visualize-map-2.png[] +* To create maps that you can add to your dashboards, refer to <>. -. Navigate the map by clicking and dragging. Use the controls -to zoom the map and set filters. -. *Save* the map with the name `Map Example`. +* To create presentations of your live data, refer to <>. diff --git a/docs/glossary.asciidoc b/docs/glossary.asciidoc index 1edb33032418b..be24402170bbe 100644 --- a/docs/glossary.asciidoc +++ b/docs/glossary.asciidoc @@ -151,7 +151,7 @@ that you are interested in. A navigation path that retains context (time range and filters) from the source to the destination, so you can view the data from a new perspective. A dashboard that shows the overall status of multiple data centers -might have a drilldown to a dashboard for a single data center. See {kibana-ref}/drilldowns.html[Drilldowns]. +might have a drilldown to a dashboard for a single data center. See {kibana-ref}/dashboard.html[Drilldowns]. // end::drilldown-def[] @@ -238,7 +238,7 @@ support for scripted fields. See Enables you to build visualizations by dragging and dropping data fields. Lens makes makes smart visualization suggestions for your data, allowing you to switch between visualization types. -See {kibana-ref}/lens.html[Lens]. +See {kibana-ref}/dashboard.html[Lens]. // end::lens-def[] @@ -350,7 +350,7 @@ A {kib} control that constrains the search results to a particular time period. [[glossary-timelion]] Timelion :: // tag::timelion-def[] A tool for building a time series visualization that analyzes data in time order. -See {kibana-ref}/timelion.html[Timelion]. +See {kibana-ref}/dashboard.html[Timelion]. // end::timelion-def[] @@ -364,7 +364,7 @@ Timestamped data such as logs, metrics, and events that is indexed on an ongoing // tag::tsvb-def[] A time series data visualizer that allows you to combine an infinite number of aggregations to display complex data. -See {kibana-ref}/TSVB.html[TSVB]. +See {kibana-ref}/dashboard.html[TSVB]. // end::tsvb-def[] @@ -388,7 +388,7 @@ indices and guides you through resolving issues, including reindexing. See [[glossary-vega]] Vega :: // tag::vega-def[] A declarative language used to create interactive visualizations. -See {kibana-ref}/vega-graph.html[Vega]. +See {kibana-ref}/dashboard.html[Vega]. // end::vega-def[] [[glossary-vector]] vector data:: diff --git a/docs/management/index-patterns.asciidoc b/docs/management/index-patterns.asciidoc index 05036311c094c..7de2a042160e9 100644 --- a/docs/management/index-patterns.asciidoc +++ b/docs/management/index-patterns.asciidoc @@ -7,7 +7,7 @@ you want to work with. Once you create an index pattern, you're ready to: * Interactively explore your data in <>. -* Analyze your data in charts, tables, gauges, tag clouds, and more in <>. +* Analyze your data in charts, tables, gauges, tag clouds, and more in <>. * Show off your data in a <> workpad. * If your data includes geo data, visualize it with <>. diff --git a/docs/management/managing-saved-objects.asciidoc b/docs/management/managing-saved-objects.asciidoc index 51de5ad620b46..8c885ddca52e5 100644 --- a/docs/management/managing-saved-objects.asciidoc +++ b/docs/management/managing-saved-objects.asciidoc @@ -92,5 +92,5 @@ index pattern. This is useful if the index you were working with has been rename WARNING: Validation is not performed for object properties. Submitting an invalid change will render the object unusable. A more failsafe approach is to use -*Discover*, *Visualize*, or *Dashboard* to create new objects instead of +*Discover* or *Dashboard* to create new objects instead of directly editing an existing one. diff --git a/docs/management/numeral.asciidoc b/docs/management/numeral.asciidoc index 5d4d48ca785e1..a8834a3278a9e 100644 --- a/docs/management/numeral.asciidoc +++ b/docs/management/numeral.asciidoc @@ -10,7 +10,7 @@ Numeral formatting patterns are used in multiple places in {kib}, including: * <> * <> -* <> +* <> * <> The simplest pattern format is `0`, and the default {kib} pattern is `0,0.[000]`. diff --git a/docs/management/rollups/create_and_manage_rollups.asciidoc b/docs/management/rollups/create_and_manage_rollups.asciidoc index 831b536f8c1cb..8aa57f50fe94b 100644 --- a/docs/management/rollups/create_and_manage_rollups.asciidoc +++ b/docs/management/rollups/create_and_manage_rollups.asciidoc @@ -60,7 +60,7 @@ You can read more at {ref}/rollup-job-config.html[rollup job configuration]. === Try it: Create and visualize rolled up data This example creates a rollup job to capture log data from sample web logs. -To follow along, add the <>. +To follow along, add the <>. In this example, you want data that is older than 7 days in the target index pattern `kibana_sample_data_logs` to roll up once a day into the index `rollup_logstash`. You’ll bucket the @@ -145,7 +145,7 @@ is `rollup_logstash,kibana_sample_data_logs`. In this index pattern, `rollup_log matches the rolled up index pattern and `kibana_sample_data_logs` matches the index pattern for raw data. -. Go to *Visualize* and create a vertical bar chart. +. Go to *Dashboard* and create a vertical bar chart. . Choose `rollup_logstash,kibana_sample_data_logs` as your source to see both the raw and rolled up data. diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 1a20c1df582e6..42d1d89145d79 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -59,7 +59,7 @@ This page has moved. Please see <>. [role="exclude",id="add-sample-data"] == Add sample data -This page has moved. Please see <>. +This page has moved. Please see <>. [role="exclude",id="tilemap"] == Coordinate map @@ -95,3 +95,8 @@ More information on this new feature is available in <>. == Role-based access control This content has moved to the <> page. + +[role="exclude",id="TSVB"] +== TSVB + +This page was deleted. See <>. diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index f750784c47043..ea02afb8a9fda 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -11,7 +11,7 @@ To start working with your data in {kib}, you can: * Connect {kib} with existing {es} indices. -If you're not ready to use your own data, you can add a <> +If you're not ready to use your own data, you can add a <> to see all that you can do in {kib}. [float] diff --git a/docs/user/canvas.asciidoc b/docs/user/canvas.asciidoc index 317ec67dd7c0a..0b0eb7a318495 100644 --- a/docs/user/canvas.asciidoc +++ b/docs/user/canvas.asciidoc @@ -137,7 +137,7 @@ image::images/canvas-map-embed.gif[] . To use the customization options, click the panel menu, then select one of the following options: -* *Edit map* — Opens <> or <> so that you can edit the original saved object. +* *Edit map* — Opens <> or a visualization builder so that you can edit the original saved object. * *Edit panel title* — Adds a title to the saved object. diff --git a/docs/user/dashboard.asciidoc b/docs/user/dashboard.asciidoc deleted file mode 100644 index b812af7e981bf..0000000000000 --- a/docs/user/dashboard.asciidoc +++ /dev/null @@ -1,191 +0,0 @@ -[[dashboard]] -= Dashboard - -[partintro] --- - -A _dashboard_ is a collection of visualizations, searches, and -maps, typically in real-time. Dashboards provide -at-a-glance insights into your data and enable you to drill down into details. - -With *Dashboard*, you can: - -* Add visualizations, saved searches, and maps for side-by-side analysis - -* Arrange dashboard elements to display exactly how you want - -* Customize time ranges to display only the data you want - -* Inspect and edit dashboard elements to find out exactly what kind of data is displayed - -[role="screenshot"] -image:images/Dashboard_example.png[Example dashboard] - -[float] -[[dashboard-read-only-access]] -=== [xpack]#Read only access# -If you see -the read-only icon in the application header, -then you don't have sufficient privileges to create and save dashboards. The buttons to create and edit -dashboards are not visible. For more information, see <>. - -[role="screenshot"] -image::images/dashboard-read-only-badge.png[Example of Dashboard read only access indicator in Kibana header] - --- - -[[dashboard-create-new-dashboard]] -== Create a dashboard - -To create a dashboard, you must have data indexed into {es}, an index pattern -to retrieve the data from {es}, and -visualizations, saved searches, or maps. If these don't exist, you're prompted to -add them as you create the dashboard, or you can add -<>, -which include pre-built dashboards. - -To begin, open the menu, go to *Dashboard*, then click *Create dashboard.* - -[float] -[[dashboard-add-elements]] -=== Add elements - -The visualizations, saved searches, and maps are stored as elements in panels -that you can move and resize. - -You can add elements from multiple indices, and the same element can appear in -multiple dashboards. - -To create an element: - -. Click *Create new*. -. On the *New Visualization* window, click the visualization type. -+ -[role="screenshot"] -image:images/Dashboard_add_new_visualization.png[Example add new visualization to dashboard] -+ -For information on how to create visualizations, see <>. -+ -For information on how to create maps, see <>. - -To add an existing element: - -. Click *Add*. - -. On the *Add panels* flyout, select the panel. -+ -When a dashboard element has a stored query, -both queries are applied. -+ -[role="screenshot"] -image:images/Dashboard_add_visualization.png[Example add visualization to dashboard] - -[float] -[[customizing-your-dashboard]] -=== Arrange dashboard elements - -In *Edit* mode, you can move, resize, customize, and delete panels to suit your needs. - -[[moving-containers]] -* To move a panel, click and hold the panel header and drag to the new location. - -[[resizing-containers]] -* To resize a panel, click the resize control and drag -to the new dimensions. - -* To toggle the use of margins and panel titles, use the *Options* menu. - -* To delete a panel, open the panel menu and select *Delete from dashboard.* Deleting a panel from a -dashboard does *not* delete the saved visualization or search. - -[float] -[[cloning-a-panel]] -=== Clone dashboard elements - -In *Edit* mode, you can clone any panel on a dashboard. - -To clone an existing panel, open the panel menu of the element you wish to clone, then select *Clone panel*. - -* Cloned panels appear beside the original, and will move other panels down to make room if necessary. - -* Clones support all of the original panel's functionality, including renaming, editing, and cloning. - -* All cloned visualizations will appear in the visualization list. - -[role="screenshot"] -image:images/clone_panel.gif[clone panel] - - -[float] -[[viewing-detailed-information]] -=== Inspect and edit elements - -Many dashboard elements allow you to drill down into the data and requests -behind the element. - -From the panel menu, select *Inspect*. -The data that displays depends on the element that you inspect. - -[role="screenshot"] -image:images/Dashboard_inspect.png[Inspect in dashboard] - -To open an element for editing, put the dashboard in *Edit* mode, -and then select *Edit visualization* from the panel menu. The changes you make appear in -every dashboard that uses the element. - -[float] -[[dashboard-customize-filter]] -=== Customize time ranges - -You can configure each visualization, saved search, and map on your dashboard -for a specific time range. For example, you might want one visualization to show -the monthly trend for CPU usage and another to show the current CPU usage. - -From the panel menu, select *Customize time range* to expose a time filter -dedicated to that panel. Panels that are not restricted by a specific -time range are controlled by the -global time filter. - -[role="screenshot"] -image:images/time_range_per_panel.gif[Time range per dashboard panel] - -[float] -[[save-dashboards]] -=== Save the dashboard - -When you're finished adding and arranging the panels, save the dashboard. - -. In the {kib} toolbar, click *Save*. - -. Enter the dashboard *Title* and optional *Description*, then *Save* the dashboard. - -include::{kib-repo-dir}/drilldowns/drilldowns.asciidoc[] -include::{kib-repo-dir}/drilldowns/explore-underlying-data.asciidoc[] - -[[sharing-dashboards]] -== Share the dashboard - -[[embedding-dashboards]] -Share your dashboard outside of {kib}. - -From the *Share* menu, you can: - -* Embed the code in a web page. Users must have {kib} access -to view an embedded dashboard. -* Share a direct link to a {kib} dashboard -* Generate a PDF report -* Generate a PNG report - -TIP: To create a link to a dashboard by title, use: + -`${domain}/${basepath?}/app/dashboards#/list?title=${yourdashboardtitle}` - -TIP: When sharing a link to a dashboard snapshot, use the *Short URL*. Snapshot -URLs are long and can be problematic for Internet Explorer and other -tools. To create a short URL, you must have write access to {kib}. - -[float] -[[import-dashboards]] -=== Export the dashboard - -To export the dashboard, open the menu, then click *Stack Management > Saved Objects*. For more information, -refer to <>. diff --git a/docs/user/dashboard/aggregation-reference.asciidoc b/docs/user/dashboard/aggregation-reference.asciidoc new file mode 100644 index 0000000000000..1bcea3bb36aea --- /dev/null +++ b/docs/user/dashboard/aggregation-reference.asciidoc @@ -0,0 +1,242 @@ +[[aggregation-reference]] +== Aggregation reference + +{kib} supports many types of {ref}/search-aggregations.html[{es} aggregations] that you can use to build complex summaries of your data. + +By using a series of {es} aggregations to extract and process your data, you can create panels that tell a +story about the trends, patterns, and outliers in your data. + +[float] +[[bucket-aggregations]] +=== Bucket aggregations + +For information about Elasticsearch bucket aggregations, refer to {ref}/search-aggregations-bucket.html[Bucket aggregations]. + +[options="header"] +|=== + +| Type | Visualizations | Data table | Markdown | Lens | TSVB + +| Histogram +^| X +^| X +^| X +| +| + +| Date histogram +^| X +^| X +^| X +^| X +^| X + +| Date range +^| X +^| X +^| X +| +| + +| Filter +^| X +^| X +^| X +| +^| X + +| Filters +^| X +^| X +^| X +| +^| X + +| GeoHash grid +^| X +^| X +^| X +| +| + +| IP range +^| X +^| X +^| X +| +| + +| Range +^| X +^| X +^| X +| +| + +| Terms +^| X +^| X +^| X +^| X +^| X + +| Significant terms +^| X +^| X +^| X +| +^| X + +|=== + +[float] +[[metrics-aggregations]] +=== Metrics aggregations + +For information about Elasticsearch metrics aggregations, refer to {ref}/search-aggregations-metrics.html[Metrics aggregations]. + +[options="header"] +|=== + +| Type | Visualizations | Data table | Markdown | Lens | TSVB + +| Average +^| X +^| X +^| X +^| X +^| X + +| Sum +^| X +^| X +^| X +^| X +^| X + +| Unique count (Cardinality) +^| X +^| X +^| X +^| X +^| X + +| Max +^| X +^| X +^| X +^| X +^| X + +| Min +^| X +^| X +^| X +^| X +^| X + +| Percentiles +^| X +^| X +^| X +| +^| X + +| Percentiles Rank +^| X +^| X +^| X +| +^| X + +| Top hit +^| X +^| X +^| X +| +^| X + +| Value count +| +| +| +| +^| X + +|=== + +[float] +[[pipeline-aggregations]] +=== Pipeline aggregations + +For information about Elasticsearch pipeline aggregations, refer to {ref}/search-aggregations-pipeline.html[Pipeline aggregations]. + +[options="header"] +|=== + +| Type | Visualizations | Data table | Markdown | Lens | TSVB + +| Avg bucket +^| X +^| X +^| X +| +^| X + +| Derivative +^| X +^| X +^| X +| +^| X + +| Max bucket +^| X +^| X +^| X +| +^| X + +| Min bucket +^| X +^| X +^| X +| +^| X + +| Sum bucket +^| X +^| X +^| X +^| +^| X + +| Moving average +^| X +^| X +^| X +^| +^| X + +| Cumulative sum +^| X +^| X +^| X +^| +^| X + +| Bucket script +| +| +| +| +^| X + +| Serial differencing +^| X +^| X +^| X +| +^| X + +|=== diff --git a/docs/user/dashboard/dashboard.asciidoc b/docs/user/dashboard/dashboard.asciidoc new file mode 100644 index 0000000000000..0c0151cc3ace2 --- /dev/null +++ b/docs/user/dashboard/dashboard.asciidoc @@ -0,0 +1,472 @@ +[[dashboard]] += Dashboard + +[partintro] +-- + +A _dashboard_ is a collection of panels that you use to analyze your data. On a dashboard, you can add a variety of panels that +you can rearrange and tell a story about your data. Panels contain everything you need, including visualizations, +interactive controls, markdown, and more. + +With *Dashboard*s, you can: + +* Add multiple panels to see many aspects and views of your data in one place. + +* Arrange panels for analysis and comparison. + +* Add text and images to provide context to the panels and make them easy to consume. + +* Create and apply filters to focus on the data you want to display. + +* Control who can use your data, and share the dashboard with a small or large audience. + +* Generate reports based on your findings. + +To begin, open the menu, go to *Dashboard*, then click *Create dashboard*. + +[role="screenshot"] +image:images/Dashboard_example.png[Example dashboard] + +[float] +[[dashboard-read-only-access]] +=== [xpack]#Read only access# +If you see +the read-only icon in the application header, +then you don't have sufficient privileges to create and save dashboards. The buttons to create and edit +dashboards are not visible. For more information, see <>. + +[role="screenshot"] +image::images/dashboard-read-only-badge.png[Example of Dashboard read only access indicator in Kibana header] + +[float] +[[types-of-panels]] +== Types of panels + +Panels contain everything you need to tell a story about you data, including visualizations, +interactive controls, Markdown, and more. + +[cols="50, 50"] +|=== + +a| *Area* + +Displays data points, connected by a line, where the area between the line and axes are shaded. +Use area charts to compare two or more categories over time, and display the magnitude of trends. + +| image:images/area.png[Area chart] + +a| *Stacked area* + +Displays the evolution of the value of several data groups. The values of each group are displayed +on top of each other. Use stacked area charts to visualize part-to-whole relationships, and to show +how each category contributes to the cumulative total. + +| image:images/stacked_area.png[Stacked area chart] + +a| *Bar* + +Displays bars side-by-side where each bar represents a category. Use bar charts to compare data across a +large number of categories, display data that includes categories with negative values, and easily identify +the categories that represent the highest and lowest values. Kibana also supports horizontal bar charts. + +| image:images/bar.png[Bar chart] + +a| *Stacked bar* + +Displays numeric values across two or more categories. Use stacked bar charts to compare numeric values between +levels of a categorical value. Kibana also supports stacked horizontal bar charts. + +| image:images/stacked_bar.png[Stacked area chart] + + +a| *Line* + +Displays data points that are connected by a line. Use line charts to visualize a sequence of values, discover +trends over time, and forecast future values. + +| image:images/line.png[Line chart] + +a| *Pie* + +Displays slices that represent a data category, where the slice size is proportional to the quantity it represents. +Use pie charts to show comparisons between multiple categories, illustrate the dominance of one category over others, +and show percentage or proportional data. + +| image:images/pie.png[Pie chart] + +a| *Donut* + +Similar to the pie chart, but the central circle is removed. Use donut charts when you’d like to display multiple statistics at once. + +| image:images/donut.png[Donut chart] + + +a| *Tree map* + +Relates different segments of your data to the whole. Each rectangle is subdivided into smaller rectangles, or sub branches, based on +its proportion to the whole. Use treemaps to make efficient use of space to show percent total for each category. + +| image:images/treemap.png[Tree map] + + +a| *Heat map* + +Displays graphical representations of data where the individual values are represented by colors. Use heat maps when your data set includes +categorical data. For example, use a heat map to see the flights of origin countries compared to destination countries using the sample flight data. + +| image:images/heat_map.png[Heat map] + +a| *Goal* + +Displays how your metric progresses toward a fixed goal. Use the goal to display an easy to read visual of the status of your goal progression. + +| image:images/goal.png[Goal] + + +a| *Gauge* + +Displays your data along a scale that changes color according to where your data falls on the expected scale. Use the gauge to show how metric +values relate to reference threshold values, or determine how a specified field is performing versus how it is expected to perform. + +| image:images/gauge.png[Gauge] + + +a| *Metric* + +Displays a single numeric value for an aggregation. Use the metric visualization when you have a numeric value that is powerful enough to tell +a story about your data. + +| image:images/metric.png[Metric] + + +a| *Data table* + +Displays your raw data or aggregation results in a tabular format. Use data tables to display server configuration details, track counts, min, +or max values for a specific field, and monitor the status of key services. + +| image:images/data_table.png[Data table] + + +a| *Tag cloud* + +Graphical representations of how frequently a word appears in the source text. Use tag clouds to easily produce a summary of large documents and +create visual art for a specific topic. + +| image:images/tag_cloud.png[Tag cloud] + + +a| *Maps* + +For all your mapping needs, use <>. + +| image:images/maps.png[Maps] + + +|=== + +[float] +[[create-panels]] +== Create panels + +To create a panel, make sure you have {ref}/getting-started-index.html[data indexed into {es}] and an <> +to retrieve the data from {es}. If you aren’t ready to use your own data, {kib} comes with several pre-built dashboards that you can test out. For more information, +refer to <>. + +To begin, click *Create new*, then choose one of the following options on the +*New Visualization* window: + +* Click on the type of panel you want to create, then configure the options. + +* Select an editor to help you create the panel. + +[role="screenshot"] +image:images/Dashboard_add_new_visualization.png[Example add new visualization to dashboard] + +{kib} provides you with several editors that help you create panels. + +[float] +[[lens]] +=== Create panels with Lens + +*Lens* is the simplest and fastest way to create powerful visualizations of your data. To use *Lens*, you drag and drop as many data fields +as you want onto the visualization builder pane, and *Lens* uses heuristics to decide how to apply each field to the visualization. + +With *Lens*, you can: + +* Use the automatically generated suggestions to change the visualization type. +* Create visualizations with multiple layers and indices. +* Change the aggregation and labels to customize the data. + +[role="screenshot"] +image::images/lens_drag_drop.gif[Drag and drop] + +TIP: Drag-and-drop capabilities are available only when *Lens* knows how to use the data. If *Lens* is unable to automatically generate a +visualization, configure the customization options for your visualization. + +[float] +[[fiter-the-data-fields]] +==== Filter the data fields + +The data fields that are displayed are based on the selected <> and the <>. + +To view the data fields in a different index pattern, click the index pattern, then select a new one. The data fields automatically update. + +To filter the data fields: + +* Enter the name in the *Search field names*. +* Click *Field by type*, then select the filter. To show all fields in the index pattern, deselect *Only show fields with data*. + +[float] +[[view-data-summaries]] +==== View data summaries + +To help you decide exactly the data you want to display, get a quick summary of each field. The summary shows the distribution of +values within the specified time range. + +To view the data field summary information, navigate to the field, then click *i*. + +[role="screenshot"] +image::images/lens_data_info.png[Data summary window] + +[float] +[[change-the-visualization-type]] +==== Change the visualization type + +Use the automatically generated suggestions to change the visualization type, or manually select the type of visualization you want to view. + +*Suggestions* are shortcuts to alternative visualizations that *Lens* generates for you. + +[role="screenshot"] +image::images/lens_suggestions.gif[Visualization suggestions] + +If you’d like to use a visualization type outside of the suggestions, click the visualization type, then select a new one. + +[role="screenshot"] +image::images/lens_viz_types.png[] + +When there is an exclamation point (!) next to a visualization type, *Lens* is unable to transfer your data, but still allows you to make the change. + +[float] +[[customize-the-data]] +==== Customize the data + +For each visualization type, you can customize the aggregation and labels. The options available depend on the selected visualization type. + +. Click a data field name in the editor, or click *Drop a field here*. +. Change the options that appear. ++ +[role="screenshot"] +image::images/lens_aggregation_labels.png[Quick function options] + +[float] +[[add-layers-and-indices]] +==== Add layers and indices + +To compare and analyze data from different sources, you can visualize multiple data layers and indices. Multiple layers and indices are +supported in area, line, and bar charts. + +To add a layer, click *+*, then drag and drop the data fields for the new layer. + +[role="screenshot"] +image::images/lens_layers.png[Add layers] + +To view a different index, click the index name in the editor, then select a new one. + +[role="screenshot"] +image::images/lens_index_pattern.png[Add index pattern] + +Ready to try out *Lens*? Refer to the <>. + +[float] +[[tsvb]] +=== Create panels with TSVB + +*TSVB* is a time series data visualizer that allows you to use the full power of the Elasticsearch aggregation framework. To use *TSVB*, +you can combine an infinite number of <> to display your data. + +With *TSVB*, you can: + +* Create visualizations, data tables, and markdown panels. +* Create visualizations with multiple indices. +* Change the aggregation and labels to customize the data. ++ +[role="screenshot"] +image::images/tsvb.png[TSVB UI] + +[float] +[[configure-the-data]] +==== Configure the data + +With *TSVB*, you can add and display multiple data sets to compare and analyze. {kib} uses many types of <> that you can use to build +complex summaries of that data. + +. Select *Data*. If you are using *Table*, select *Columns*. +. From the *Aggregation* drop down, select the aggregation you want to visualize. ++ +If you don’t see any data, change the <>. ++ +To add multiple aggregations, click *+*. +. From the *Group by* drop down, select how you want to group or split the data. +. To add another data set, click *+*. ++ +When you have more than one aggregation, the last value is displayed, which is indicated by the eye icon. + +[float] +[[change-the-data-display]] +==== Change the data display + +To find the best way to display your data, *TSVB* supports several types of panels and charts. + +To change the *Time Series* chart type: + +. Click *Data > Options*. +. Select the *Chart type*. + +To change the panel type, click on the panel options: + +[role="screenshot"] +image::images/tsvb_change_display.gif[TSVB change the panel type] + +[float] +[[custommize-the-data]] +==== Customize the data + +View data in a different <>, and change the data label name and colors. The options available depend on the panel type. + +To change the index pattern, click *Panel options*, then enter the new *Index Pattern*. + +To override the index pattern for a data set, click *Data > Options*. Select *Yes* to override, then enter the new *Index pattern*. + +To change the data labels and colors: + +. Click *Data*. +. Enter the *Label* name, which *TSVB* uses on the legends and data labels. +. Click the color picker, then select the color for the data. ++ +[role="screenshot"] +image::images/tsvb_color_picker.png[TSVB color picker] + +[float] +[[add-annotations]] +==== Add annotations + +You can overlay annotation events on top of your *Time Series* charts. The options available depend on the data source. + +To begin, click *Annotations*, click *Add data source*, then configure the options. + +[role="screenshot"] +image::images/tsvb_annotations.png[TSVB annotations] + +[float] +[[filter-the-panel]] +==== Filter the panel + +The data that displays on the panel is based on the <> and <>. +You can filter the data on the panels using the <>. + +Click *Panel options*, then enter the syntax in the *Panel Filter* field. + +If you want to ignore filters from all of {kib}, select *Yes* for *Ignore global filter*. + +[float] +[[vega]] +=== Create custom panels with Vega + +Build custom visualizations using *Vega* and *Vega-Lite*, backed by one or more data sources including {es}, Elastic Map Service, +URL, or static data. Use the {kib} extensions to embed *Vega* in your dashboard, and add interactive tools. + +Use *Vega* and *Vega-Lite* when you want to create a visualization for: + +* Aggregations that use `nested` or `parent/child` mapping +* Aggregations without an index pattern +* Queries that use custom time filters +* Complex calculations +* Extracting data from _source instead of aggregations +* Scatter charts, sankey charts, and custom maps +* Using an unsupported visual theme + +[role="screenshot"] +image::images/vega.png[Vega UI] + +*Vega* and *Vega-Lite* are declarative formats that: + +* Create complex visualizations +* Use JSON and a different syntax for declaring visualizations +* Are not fully interchangeable + +For more information about *Vega* and *Vega-Lite*, refer to: + +* <> +* <> +* <> +* <> + +[float] +[[timelion]] +=== Create panels with Timelion + +*Timelion* is a time series data visualizer that enables you to combine independent data sources within a single visualization. + +*Timelion* is driven by a simple expression language that you use to: + +* Retrieve time series data +* Perform calculation to tease out the answers to complex questions +* Visualize the results + +[role="screenshot"] +image::images/timelion.png[Timelion UI] + +Ready to try out Timelion? For step-by-step tutorials, refer to: + +* <> +* <> +* <> + +[float] +[[save-panels]] +=== Save panels + +When you’ve finished making changes, save the panels. + +. Click *Save*. +. Add the *Title* and optional *Description*. +. Click *Save and return*. + +[float] +[[add-existing-panels]] +== Add existing panels + +Add panels that you’ve already created to your dashboard. + +On the dashboard, click *Add an existing*, then select the panel you want to add. + +When a panel contains a stored query, both queries are applied. + +[role="screenshot"] +image:images/Dashboard_add_visualization.png[Example add visualization to dashboard] + +To make changes to the panel, put the dashboard in *Edit* mode, then select the edit option from the panel menu. +The changes you make appear in every dashboard that uses the panel, except if you edit the panel title. Changes to the panel title appear only on the dashboard where you made the change. + +[float] +[[save-dashboards]] +== Save dashboards + +When you’ve finished adding the panels, save the dashboard. + +. In the toolbar, click *Save*. + +. Enter the dashboard *Title* and optional *Description*, then *Save* the dashboard. + +-- +include::edit-dashboards.asciidoc[] + +include::explore-dashboard-data.asciidoc[] + +include::share-dashboards.asciidoc[] + +include::tutorials.asciidoc[] + +include::aggregation-reference.asciidoc[] + +include::vega-reference.asciidoc[] diff --git a/docs/drilldowns/drilldowns.asciidoc b/docs/user/dashboard/drilldowns.asciidoc similarity index 93% rename from docs/drilldowns/drilldowns.asciidoc rename to docs/user/dashboard/drilldowns.asciidoc index e2dfaa5af39ce..5fca974d58135 100644 --- a/docs/drilldowns/drilldowns.asciidoc +++ b/docs/user/dashboard/drilldowns.asciidoc @@ -1,5 +1,6 @@ +[float] [[drilldowns]] -== Use drilldowns for dashboard actions +=== Use drilldowns for dashboard actions Drilldowns, also known as custom actions, allow you to configure a workflow for analyzing and troubleshooting your data. @@ -13,7 +14,7 @@ that shows a single data center or server. [float] [[how-drilldowns-work]] -=== How drilldowns work +==== How drilldowns work Drilldowns are user-configurable {kib} actions that are stored with the dashboard metadata. Drilldowns are specific to the dashboard panel @@ -35,7 +36,7 @@ to learn how to code drilldowns. [float] [[create-manage-drilldowns]] -=== Create and manage drilldowns +==== Create and manage drilldowns Your dashboard must be in *Edit* mode to create a drilldown. Once a panel has at least one drilldown, the menu also includes a *Manage drilldowns* action @@ -46,14 +47,13 @@ image::images/drilldown_menu.png[Panel menu with Create drilldown and Manage dri [float] [[drilldowns-example]] -=== Try it: Create a drilldown +==== Try it: Create a drilldown This example shows how to create the *Host Overview* drilldown shown earlier in this doc. -[float] -==== Set up the dashboards +*Set up the dashboards* -. Add the <> data set. +. Add the <> data set. . Create a new dashboard, called `Host Overview`, and include these visualizations from the sample data set: @@ -74,9 +74,7 @@ TIP: If you don’t see data for a panel, try changing the time range. Search: `extension.keyword:( “gz” or “css” or “deb”)` Filter: `geo.src : CN` -[float] -==== Create the drilldown - +*Create the drilldown* . In the dashboard menu bar, click *Edit*. diff --git a/docs/user/dashboard/edit-dashboards.asciidoc b/docs/user/dashboard/edit-dashboards.asciidoc new file mode 100644 index 0000000000000..7534ea1e9e9fb --- /dev/null +++ b/docs/user/dashboard/edit-dashboards.asciidoc @@ -0,0 +1,115 @@ +[[edit-dashboards]] +== Edit 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 dashboard. +To make your dashboard look the way you want, use the editing options. + +[float] +[[add-controls]] +=== Add controls + +To filter the data on your dashboard in real-time, add a *Controls* panel. + +You can add two types of *Controls*: + +* Options list — Filters content based on one or more specified options. The dropdown menu is dynamically populated with the results of a terms aggregation. +For example, use the options list on the sample flight dashboard when you want to filter the data by origin city and destination city. + +* Range slider — Filters data within a specified range of numbers. The minimum and maximum values are dynamically populated with the results of a +min and max aggregation. For example, use the range slider when you want to filter the sample flight dashboard by a specific average ticket price. + +[role="screenshot"] +image::images/dashboard-controls.png[] + +To configure *Controls* for your dashboard: + +. Click *Options*, then configure the following: + +* *Update Kibana 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*. + +* *Use time filter* — When selected, the aggregations that generate the options list and time range are connected to the <>. + +* *Pin filters to global state* — When selected, all filters created by interacting with the inputs are automatically pinned. + +. Click *Update*. + +[float] +[[add-markdown]] +=== Add Markdown + +*Markdown* is a text entry field that accepts GitHub-flavored Markdown text. When you enter the text, the tool populates the results on the dashboard. + +Use Markdown when you want to add context to the other panels on your dashboard, such as important information, instructions and images. + +For information about GitHub-flavored Markdown text, click *Help*. + +For example, when you enter: + +[role="screenshot"] +image::images/markdown_example_1.png[] + +The following instructions are displayed: + +[role="screenshot"] +image::images/markdown_example_2.png[] + +Or when you enter: + +[role="screenshot"] +image::images/markdown_example_3.png[] + +The following image is displayed: + +[role="screenshot"] +image::images/markdown_example_4.png[] + +[float] +[[arrange-panels]] +[[moving-containers]] +[[resizing-containers]] +=== Arrange panels + +To make your dashboard panels look exactly how you want, you can move, resize, customize, and delete them. + +Put the dashboard in *Edit* mode, then use the following options: + +* To move, click and hold the panel header, then drag to the new location. + +* To resize, click the resize control, then drag to the new dimensions. + +* To delete, open the panel menu, then select Delete from dashboard. When you delete a panel from the dashboard, the +visualization or saved search from the panel is still available in Kibana. + +[float] +[[clone-panels]] +=== Clone panels + +To duplicate a panel and its configured functionality, clone the panel. Cloned panels support all of the original functionality, +including renaming, editing, and cloning. + +. Put the dashboard in *Edit* mode. + +. For the panel you want to clone, open the panel menu, then select *Clone panel*. + +Cloned panels appear beside the original, and move other panels down to make room when necessary. +All cloned visualization panels appear in the visualization list. + +[role="screenshot"] +image:images/clone_panel.gif[clone panel] + +[float] +[[dashboard-customize-filter]] +=== Customize time ranges + +You can configure each visualization, saved search, and map on your dashboard +for a specific time range. For example, you might want one visualization to show +the monthly trend for CPU usage and another to show the current CPU usage. + +From the panel menu, select *Customize time range* to expose a time filter +dedicated to that panel. Panels that are not restricted by a specific +time range are controlled by the +<>. + +[role="screenshot"] +image:images/time_range_per_panel.gif[Time range per dashboard panel] diff --git a/docs/user/dashboard/explore-dashboard-data.asciidoc b/docs/user/dashboard/explore-dashboard-data.asciidoc new file mode 100644 index 0000000000000..a0564f5bceb3d --- /dev/null +++ b/docs/user/dashboard/explore-dashboard-data.asciidoc @@ -0,0 +1,20 @@ +[[explore-dashboard-data]] +== Explore dashboard data + +Get a closer look at your data by inspecting elements and using drilldown actions. + +[float] +[[viewing-detailed-information]] +=== Inspect elements + +To view the data and requests behind the visualizations and saved searches, you can drill down into the elements. + +From the panel menu, select *Inspect*. +The data that displays depends on the element that you inspect. + +[role="screenshot"] +image:images/Dashboard_inspect.png[Inspect in dashboard] + +include::explore-underlying-data.asciidoc[] + +include::drilldowns.asciidoc[] diff --git a/docs/user/dashboard/explore-underlying-data.asciidoc b/docs/user/dashboard/explore-underlying-data.asciidoc new file mode 100644 index 0000000000000..9b7be21dc45d2 --- /dev/null +++ b/docs/user/dashboard/explore-underlying-data.asciidoc @@ -0,0 +1,27 @@ +[float] +[[explore-the-underlying-data]] +=== Explore the underlying data for panels + +To explore the underlying data of the panels on your dashboard, {kib} opens *Discover*, +where you can view and filter the data in the visualization panel. When {kib} opens *Discover*, the index pattern, filters, query, and time range for the visualization continue to apply. + +TIP: The *Explore underlying data* option is available only for visualization panels with a single index pattern. + +To use the *Explore underlying data* option: + +* Click the from the panel menu, then click *Explore underlying data*. ++ +[role="screenshot"] +image::images/explore_data_context_menu.png[Explore underlying data from panel context menu] + +* Interact with the chart, then click *Explore underlying data* on the menu that appears. ++ +[role="screenshot"] +image::images/explore_data_in_chart.png[Explore underlying data from chart] ++ +To enable, open `kibana.yml`, then add the following: + +["source","yml"] +----------- +xpack.discoverEnhanced.actions.exploreDataInChart.enabled: true +----------- diff --git a/docs/user/dashboard/images/area.png b/docs/user/dashboard/images/area.png new file mode 100644 index 0000000000000..85d21a9e178c5 Binary files /dev/null and b/docs/user/dashboard/images/area.png differ diff --git a/docs/user/dashboard/images/bar.png b/docs/user/dashboard/images/bar.png new file mode 100644 index 0000000000000..f1db847655947 Binary files /dev/null and b/docs/user/dashboard/images/bar.png differ diff --git a/docs/user/dashboard/images/data_table.png b/docs/user/dashboard/images/data_table.png new file mode 100644 index 0000000000000..3e08ec526ba57 Binary files /dev/null and b/docs/user/dashboard/images/data_table.png differ diff --git a/docs/user/dashboard/images/donut.png b/docs/user/dashboard/images/donut.png new file mode 100644 index 0000000000000..a662f58ba553b Binary files /dev/null and b/docs/user/dashboard/images/donut.png differ diff --git a/docs/drilldowns/images/drilldown_create.png b/docs/user/dashboard/images/drilldown_create.png similarity index 100% rename from docs/drilldowns/images/drilldown_create.png rename to docs/user/dashboard/images/drilldown_create.png diff --git a/docs/drilldowns/images/drilldown_menu.png b/docs/user/dashboard/images/drilldown_menu.png similarity index 100% rename from docs/drilldowns/images/drilldown_menu.png rename to docs/user/dashboard/images/drilldown_menu.png diff --git a/docs/drilldowns/images/drilldown_on_panel.png b/docs/user/dashboard/images/drilldown_on_panel.png similarity index 100% rename from docs/drilldowns/images/drilldown_on_panel.png rename to docs/user/dashboard/images/drilldown_on_panel.png diff --git a/docs/drilldowns/images/drilldown_on_piechart.gif b/docs/user/dashboard/images/drilldown_on_piechart.gif similarity index 100% rename from docs/drilldowns/images/drilldown_on_piechart.gif rename to docs/user/dashboard/images/drilldown_on_piechart.gif diff --git a/docs/drilldowns/images/explore_data_context_menu.png b/docs/user/dashboard/images/explore_data_context_menu.png similarity index 100% rename from docs/drilldowns/images/explore_data_context_menu.png rename to docs/user/dashboard/images/explore_data_context_menu.png diff --git a/docs/drilldowns/images/explore_data_in_chart.png b/docs/user/dashboard/images/explore_data_in_chart.png similarity index 100% rename from docs/drilldowns/images/explore_data_in_chart.png rename to docs/user/dashboard/images/explore_data_in_chart.png diff --git a/docs/user/dashboard/images/gauge.png b/docs/user/dashboard/images/gauge.png new file mode 100644 index 0000000000000..c4aef7f5f6854 Binary files /dev/null and b/docs/user/dashboard/images/gauge.png differ diff --git a/docs/user/dashboard/images/goal.png b/docs/user/dashboard/images/goal.png new file mode 100644 index 0000000000000..967e64f722d74 Binary files /dev/null and b/docs/user/dashboard/images/goal.png differ diff --git a/docs/user/dashboard/images/heat_map.png b/docs/user/dashboard/images/heat_map.png new file mode 100644 index 0000000000000..d4a6502509f6f Binary files /dev/null and b/docs/user/dashboard/images/heat_map.png differ diff --git a/docs/user/dashboard/images/lens_aggregation_labels.png b/docs/user/dashboard/images/lens_aggregation_labels.png new file mode 100644 index 0000000000000..9dcf1d226a197 Binary files /dev/null and b/docs/user/dashboard/images/lens_aggregation_labels.png differ diff --git a/docs/user/dashboard/images/lens_data_info.png b/docs/user/dashboard/images/lens_data_info.png new file mode 100644 index 0000000000000..5ea6fc64a217d Binary files /dev/null and b/docs/user/dashboard/images/lens_data_info.png differ diff --git a/docs/user/dashboard/images/lens_drag_drop.gif b/docs/user/dashboard/images/lens_drag_drop.gif new file mode 100644 index 0000000000000..ca62115e7ea3a Binary files /dev/null and b/docs/user/dashboard/images/lens_drag_drop.gif differ diff --git a/docs/user/dashboard/images/lens_index_pattern.png b/docs/user/dashboard/images/lens_index_pattern.png new file mode 100644 index 0000000000000..90a34b7a5d225 Binary files /dev/null and b/docs/user/dashboard/images/lens_index_pattern.png differ diff --git a/docs/user/dashboard/images/lens_layers.png b/docs/user/dashboard/images/lens_layers.png new file mode 100644 index 0000000000000..7410425a6977e Binary files /dev/null and b/docs/user/dashboard/images/lens_layers.png differ diff --git a/docs/user/dashboard/images/lens_suggestions.gif b/docs/user/dashboard/images/lens_suggestions.gif new file mode 100644 index 0000000000000..3258e924cb205 Binary files /dev/null and b/docs/user/dashboard/images/lens_suggestions.gif differ diff --git a/docs/user/dashboard/images/lens_viz_types.png b/docs/user/dashboard/images/lens_viz_types.png new file mode 100644 index 0000000000000..2ecfa6bd0e0e3 Binary files /dev/null and b/docs/user/dashboard/images/lens_viz_types.png differ diff --git a/docs/user/dashboard/images/line.png b/docs/user/dashboard/images/line.png new file mode 100644 index 0000000000000..123fa74dc7e14 Binary files /dev/null and b/docs/user/dashboard/images/line.png differ diff --git a/docs/user/dashboard/images/maps.png b/docs/user/dashboard/images/maps.png new file mode 100644 index 0000000000000..65336451cc1c7 Binary files /dev/null and b/docs/user/dashboard/images/maps.png differ diff --git a/docs/user/dashboard/images/metric.png b/docs/user/dashboard/images/metric.png new file mode 100644 index 0000000000000..f8182d538a608 Binary files /dev/null and b/docs/user/dashboard/images/metric.png differ diff --git a/docs/user/dashboard/images/pie.png b/docs/user/dashboard/images/pie.png new file mode 100644 index 0000000000000..927fbb98adc07 Binary files /dev/null and b/docs/user/dashboard/images/pie.png differ diff --git a/docs/user/dashboard/images/stacked_area.png b/docs/user/dashboard/images/stacked_area.png new file mode 100644 index 0000000000000..ae66fc51176f9 Binary files /dev/null and b/docs/user/dashboard/images/stacked_area.png differ diff --git a/docs/user/dashboard/images/stacked_bar.png b/docs/user/dashboard/images/stacked_bar.png new file mode 100644 index 0000000000000..aa90ce3685cff Binary files /dev/null and b/docs/user/dashboard/images/stacked_bar.png differ diff --git a/docs/user/dashboard/images/tag_cloud.png b/docs/user/dashboard/images/tag_cloud.png new file mode 100644 index 0000000000000..976c456e4a1f1 Binary files /dev/null and b/docs/user/dashboard/images/tag_cloud.png differ diff --git a/docs/user/dashboard/images/timelion.png b/docs/user/dashboard/images/timelion.png new file mode 100644 index 0000000000000..a663791575077 Binary files /dev/null and b/docs/user/dashboard/images/timelion.png differ diff --git a/docs/user/dashboard/images/treemap.png b/docs/user/dashboard/images/treemap.png new file mode 100644 index 0000000000000..5df3c9526bfeb Binary files /dev/null and b/docs/user/dashboard/images/treemap.png differ diff --git a/docs/user/dashboard/images/tsvb.png b/docs/user/dashboard/images/tsvb.png new file mode 100644 index 0000000000000..09a3c7e86eb56 Binary files /dev/null and b/docs/user/dashboard/images/tsvb.png differ diff --git a/docs/user/dashboard/images/tsvb_annotations.png b/docs/user/dashboard/images/tsvb_annotations.png new file mode 100644 index 0000000000000..510f3c2672118 Binary files /dev/null and b/docs/user/dashboard/images/tsvb_annotations.png differ diff --git a/docs/user/dashboard/images/tsvb_change_display.gif b/docs/user/dashboard/images/tsvb_change_display.gif new file mode 100644 index 0000000000000..09d435b0a6b24 Binary files /dev/null and b/docs/user/dashboard/images/tsvb_change_display.gif differ diff --git a/docs/user/dashboard/images/tsvb_color_picker.png b/docs/user/dashboard/images/tsvb_color_picker.png new file mode 100644 index 0000000000000..4f033579d0005 Binary files /dev/null and b/docs/user/dashboard/images/tsvb_color_picker.png differ diff --git a/docs/user/dashboard/images/vega.png b/docs/user/dashboard/images/vega.png new file mode 100644 index 0000000000000..6a0d8cb772adf Binary files /dev/null and b/docs/user/dashboard/images/vega.png differ diff --git a/docs/user/dashboard/share-dashboards.asciidoc b/docs/user/dashboard/share-dashboards.asciidoc new file mode 100644 index 0000000000000..cfa146d60fdac --- /dev/null +++ b/docs/user/dashboard/share-dashboards.asciidoc @@ -0,0 +1,27 @@ +[[share-dashboards]] +== Share dashboards + +[[embedding-dashboards]] +Share your dashboard outside of {kib}. + +From the *Share* menu, you can: + +* Embed the code in a web page. Users must have {kib} access +to view an embedded dashboard. +* Share a direct link to a {kib} dashboard +* Generate a PDF report +* Generate a PNG report + +TIP: To create a link to a dashboard by title, use: + +`${domain}/${basepath?}/app/dashboards#/list?title=${yourdashboardtitle}` + +TIP: When sharing a link to a dashboard snapshot, use the *Short URL*. Snapshot +URLs are long and can be problematic for Internet Explorer and other +tools. To create a short URL, you must have write access to {kib}. + +[float] +[[import-dashboards]] +=== Export the dashboard + +To export the dashboard, open the menu, then click *Stack Management > Saved Objects*. For more information, +refer to <>. \ No newline at end of file diff --git a/docs/visualize/vega.asciidoc b/docs/user/dashboard/tutorials.asciidoc similarity index 60% rename from docs/visualize/vega.asciidoc rename to docs/user/dashboard/tutorials.asciidoc index b231159e86bde..931720ccbe257 100644 --- a/docs/visualize/vega.asciidoc +++ b/docs/user/dashboard/tutorials.asciidoc @@ -1,36 +1,79 @@ -[[vega-graph]] -== Vega +[[tutorials]] +== Tutorials -Build custom visualizations using Vega and Vega-Lite, backed by one or more -data sources including {es}, Elastic Map Service, URL, -or static data. Use the {kib} extensions to Vega to embed Vega into -your dashboard, and to add interactivity to the visualizations. +Learn how to use *Lens*, *Vega*, and *Timelion* by going through one of the step-by-step tutorials. -Vega and Vega-Lite are both declarative formats to create visualizations -using JSON. Both use a different syntax for declaring visualizations, -and are not fully interchangeable. +[[lens-tutorial]] +=== Compare sales over time with Lens + +Ready to create your own visualization with Lens? Use the following tutorial to create a visualization that lets you compare sales over time. + +[float] +[[lens-before-begin]] +==== Before you begin + +To start, you'll need to add the <>. + +[float] +==== Build the visualization + +Drag and drop your data onto the visualization builder pane. + +. Select the *kibana_sample_data_ecommerce* index pattern. + +. Click image:images/time-filter-calendar.png[], then click *Last 7 days*. ++ +The fields in the data panel update. + +. Drag and drop the *taxful_total_price* data field to the visualization builder pane. ++ +[role="screenshot"] +image::images/lens_tutorial_1.png[Lens tutorial] + +To display the average order prices over time, *Lens* automatically added in *order_date* field. + +To break down your data, drag the *category.keyword* field to the visualization builder pane. Lens +knows that you want to show the top categories and compare them across the dates, +and creates a chart that compares the sales for each of the top three categories: + +[role="screenshot"] +image::images/lens_tutorial_2.png[Lens tutorial] [float] -[[when-to-vega]] -=== When to use Vega - -Vega and Vega-Lite are capable of building most of the visualizations -that {kib} provides, but with higher complexity. The most common reason -to use Vega in {kib} is that {kib} is missing support for the query or -visualization, for example: - -* Aggregations using the `nested` or `parent/child` mapping -* Aggregations without a {kib} index pattern -* Queries using custom time filters -* Complex calculations -* Extracting data from _source instead of aggregation -* Scatter charts -* Sankey charts -* Custom maps -* Using a visual theme that {kib} does not provide - -[[vega-lite-tutorial]] -=== Tutorial: First visualization in Vega-Lite +[[customize-lens-visualization]] +==== Customize your visualization + +Make your visualization look exactly how you want with the customization options. + +. Click *Average of taxful_total_price*, then change the *Label* to `Sales`. ++ +[role="screenshot"] +image::images/lens_tutorial_3.1.png[Lens tutorial] + +. Click *Top values of category.keyword*, then change *Number of values* to `10`. ++ +[role="screenshot"] +image::images/lens_tutorial_3.2.png[Lens tutorial] ++ +The visualization updates to show there are only six available categories. ++ +Look at the *Suggestions*. An area chart is not an option, but for the sales data, a stacked area chart might be the best option. + +. To switch the chart type, click *Stacked bar chart* in the column, then click *Stacked area* from the *Select a visualizations* window. ++ +[role="screenshot"] +image::images/lens_tutorial_3.png[Lens tutorial] + +[float] +[[lens-tutorial-next-steps]] +==== Next steps + +Now that you've created your visualization, you can add it to a <> or <>. + +[[vega-lite-tutorial-create-your-first-visualizations]] +=== Create your first visualization with Vega-Lite + +experimental[] In this tutorial, you will learn about how to edit Vega-Lite in {kib} to create a stacked area chart from an {es} search query. It will give you a starting point @@ -65,6 +108,7 @@ which is similar to JSON but optimized for human editing. HJSON supports: * Multiline strings [float] +[[small-steps]] ==== Small steps Always work on Vega in the smallest steps possible, and save your work frequently. @@ -633,8 +677,10 @@ The final result of this tutorial is this spec: ==== -[[vega-tutorial]] -=== Tutorial: Updating {kib} filters from Vega +[[vega-tutorial-update-kibana-filters-from-vega]] +=== Update {kib} filters from Vega + +experimental[] In this tutorial you will build an area chart in Vega using an {es} search query, and add a click handler and drag handler to update {kib} filters. @@ -1225,415 +1271,486 @@ The final Vega spec for this tutorial is here: ---- ==== -[[vega-reference]] -=== Reference for {kib} extensions - -{kib} has extended Vega and Vega-Lite with extensions that support: - -* Default height and width -* Default theme to match {kib} -* Writing {es} queries using the time range and filters from dashboards -* Using the Elastic Map Service in Vega maps -* Additional tooltip styling -* Advanced setting to enable URL loading from any domain -* Limited debugging support using the browser dev tools -* (Vega only) Expression functions which can update the time range and dashboard filters - -[[vega-sizing-and-positioning]] -==== Default height and width +[[timelion-tutorial-create-time-series-visualizations]] +=== Create time series visualizations with Timelion -By default, Vega visualizations use the `autosize = { type: 'fit', contains: 'padding' }` layout. -`fit` uses all available space, ignores `width` and `height` values, -and respects the padding values. To override this behavior, change the -`autosize` value. +To compare the real-time percentage of CPU time spent in user space to the results offset by one hour, create a time series visualization. -[[vega-theme]] -==== Default theme to match {kib} - -{kib} registers a default https://vega.github.io/vega/docs/schemes/[Vega color scheme] -with the id `elastic`, and sets a default color for each `mark` type. -Override it by providing a different `stroke`, `fill`, or `color` (Vega-Lite) value. - -[[vega-queries]] -==== Writing {es} queries in Vega - -{kib} extends the Vega https://vega.github.io/vega/docs/data/[data] elements -with support for direct {es} queries specified as a `url`. - -Because of this, {kib} is **unable to support dynamically loaded data**, -which would otherwise work in Vega. All data is fetched before it's passed to -the Vega renderer. +[float] +[[define-the-functions]] +==== Define the functions -To define an {es} query in Vega, set the `url` to an object. {kib} will parse -the object looking for special tokens that allow your query to integrate with {kib}. -These tokens are: +To start tracking the real-time percentage of CPU, enter the following in the *Timelion Expression* field: -* `%context%: true`: Set at the top level, and replaces the `query` section with filters from dashboard -* `%timefield%: `: Set at the top level, integrates the query with the dashboard time filter -* `{%timefilter%: true}`: Replaced by an {es} range query with upper and lower bounds -* `{%timefilter%: "min" | "max"}`: Replaced only by the upper or lower bounds -* `{%timefilter: true, shift: -1, unit: 'hour'}`: Generates a time range query one hour in the past -* `{%autointerval%: true}`: Replaced by the string which contains the automatic {kib} time interval, such as `1h` -* `{%autointerval%: 10}`: Replaced by a string which is approximately dividing the time into 10 ranges, allowing - you to influence the automatic interval -* `"%dashboard_context-must_clause%"`: String replaced by object containing filters -* `"%dashboard_context-filter_clause%"`: String replaced by an object containing filters -* `"%dashboard_context-must_not_clause%"`: String replaced by an object containing filters +[source,text] +---------------------------------- +.es(index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct') +---------------------------------- -Putting this together, an example query that counts the number of documents in -a specific index: +[role="screenshot"] +image::images/timelion-create01.png[] +{nbsp} -[source,yaml] ----- -// An object instead of a string for the URL value -// is treated as a context-aware Elasticsearch query. -url: { - // Specify the time filter. - %timefield%: @timestamp - // Apply dashboard context filters when set - %context%: true - - // Which indexes to search - index: kibana_sample_data_logs - // The body element may contain "aggs" and "query" keys - body: { - aggs: { - time_buckets: { - date_histogram: { - // Use date histogram aggregation on @timestamp field - field: @timestamp <1> - // interval value will depend on the time filter - // Use an integer to set approximate bucket count - interval: { %autointerval%: true } - // Make sure we get an entire range, even if it has no data - extended_bounds: { - min: { %timefilter%: "min" } - max: { %timefilter%: "max" } - } - // Use this for linear (e.g. line, area) graphs - // Without it, empty buckets will not show up - min_doc_count: 0 - } - } - } - // Speed up the response by only including aggregation results - size: 0 - } -} ----- +[float] +[[compare-the-data]] +==== Compare the data -<1> `@timestamp` — Filters the time range and breaks it into histogram -buckets. +To compare the two data sets, add another series with data from the previous hour, separated by a comma: -The full result includes the following structure: +[source,text] +---------------------------------- +.es(index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct'), +.es(offset=-1h, <1> + index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct') +---------------------------------- -[source,yaml] ----- -{ - "aggregations": { - "time_buckets": { - "buckets": [{ - "key_as_string": "2015-11-30T22:00:00.000Z", - "key": 1448920800000,<1> - "doc_count": 28 - }, { - "key_as_string": "2015-11-30T23:00:00.000Z", - "key": 1448924400000, <1> - "doc_count": 330 - }, ... ----- +<1> `offset` offsets the data retrieval by a date expression. In this example, `-1h` offsets the data back by one hour. -<1> `"key"` — The unix timestamp you can use without conversions by the -Vega date expressions. +[role="screenshot"] +image::images/timelion-create02.png[] +{nbsp} -For most visualizations, you only need the list of bucket values. To focus on -only the data you need, use `format: {property: "aggregations.time_buckets.buckets"}`. +[float] +[[add-label-names]] +==== Add label names -Specify a query with individual range and dashboard context. The query is -equivalent to `"%context%": true, "%timefield%": "@timestamp"`, -except that the time range is shifted back by 10 minutes: +To easily distinguish between the two data sets, add the label names: -[source,yaml] ----- -{ - body: { - query: { - bool: { - must: [ - // This string will be replaced - // with the auto-generated "MUST" clause - "%dashboard_context-must_clause%" - { - range: { - // apply timefilter (upper right corner) - // to the @timestamp variable - @timestamp: { - // "%timefilter%" will be replaced with - // the current values of the time filter - // (from the upper right corner) - "%timefilter%": true - // Only work with %timefilter% - // Shift current timefilter by 10 units back - shift: 10 - // week, day (default), hour, minute, second - unit: minute - } - } - } - ] - must_not: [ - // This string will be replaced with - // the auto-generated "MUST-NOT" clause - "%dashboard_context-must_not_clause%" - ] - filter: [ - // This string will be replaced - // with the auto-generated "FILTER" clause - "%dashboard_context-filter_clause%" - ] - } - } - } -} ----- +[source,text] +---------------------------------- +.es(offset=-1h,index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct').label('last hour'), +.es(index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct').label('current hour') <1> +---------------------------------- -NOTE: When using `"%context%": true` or defining a value for `"%timefield%"` the body cannot contain a query. To customize the query within the VEGA specification (e.g. add an additional filter, or shift the timefilter), define your query and use the placeholders as in the example above. The placeholders will be replaced by the actual context of the dashboard or visualization once parsed. +<1> `.label()` adds custom labels to the visualization. -The `"%timefilter%"` can also be used to specify a single min or max -value. The date_histogram's `extended_bounds` can be set -with two values - min and max. Instead of hardcoding a value, you may -use `"min": {"%timefilter%": "min"}`, which will be replaced with the -beginning of the current time range. The `shift` and `unit` values are -also supported. The `"interval"` can also be set dynamically, depending -on the currently picked range: `"interval": {"%autointerval%": 10}` will -try to get about 10-15 data points (buckets). +[role="screenshot"] +image::images/timelion-create03.png[] +{nbsp} [float] -[[vega-esmfiles]] -=== Access Elastic Map Service files - -Access the Elastic Map Service files via the same mechanism: +[[add-a-title]] +==== Add a title + +Add a meaningful title: + +[source,text] +---------------------------------- +.es(offset=-1h, + index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct') + .label('last hour'), +.es(index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct') + .label('current hour') + .title('CPU usage over time') <1> +---------------------------------- + +<1> `.title()` adds a title with a meaningful name. Titles make is easier for unfamiliar users to understand the purpose of the visualization. -[source,yaml] ----- -url: { - // "type" defaults to "elasticsearch" otherwise - type: emsfile - // Name of the file, exactly as in the Region map visualization - name: World Countries -} -// The result is a geojson file, get its features to use -// this data source with the "shape" marks -// https://vega.github.io/vega/docs/marks/shape/ -format: {property: "features"} ----- - -To enable Maps, the graph must specify `type=map` in the host -configuration: - -[source,yaml] ----- -{ - "config": { - "kibana": { - "type": "map", +[role="screenshot"] +image::images/timelion-customize01.png[] +{nbsp} - // Initial map position - "latitude": 40.7, // default 0 - "longitude": -74, // default 0 - "zoom": 7, // default 2 +[float] +[[change-the-chart-type]] +==== Change the chart type + +To differentiate between the current hour data and the last hour data, change the chart type: + +[source,text] +---------------------------------- +.es(offset=-1h, + index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct') + .label('last hour') + .lines(fill=1,width=0.5), <1> +.es(index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct') + .label('current hour') + .title('CPU usage over time') +---------------------------------- + +<1> `.lines()` changes the appearance of the chart lines. In this example, `.lines(fill=1,width=0.5)` sets the fill level to `1`, and the border width to `0.5`. - // defaults to "default". Use false to disable base layer. - "mapStyle": false, +[role="screenshot"] +image::images/timelion-customize02.png[] +{nbsp} - // default 0 - "minZoom": 5, +[float] +[[change-the-line-colors]] +==== Change the line colors + +To make the current hour data stand out, change the line colors: + +[source,text] +---------------------------------- +.es(offset=-1h, + index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct') + .label('last hour') + .lines(fill=1,width=0.5) + .color(gray), <1> +.es(index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct') + .label('current hour') + .title('CPU usage over time') + .color(#1E90FF) +---------------------------------- + +<1> `.color()` changes the color of the data. Supported color types include standard color names, hexadecimal values, or a color schema for grouped data. In this example, `.color(gray)` represents the last hour, and `.color(#1E90FF)` represents the current hour. - // defaults to the maximum for the given style, - // or 25 when base is disabled - "maxZoom": 13, +[role="screenshot"] +image::images/timelion-customize03.png[] +{nbsp} - // defaults to true, shows +/- buttons to zoom in/out - "zoomControl": false, +[float] +[[make-adjustments-to-the-legend]] +==== Make adjustments to the legend + +Change the position and style of the legend: + +[source,text] +---------------------------------- +.es(offset=-1h, + index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct') + .label('last hour') + .lines(fill=1,width=0.5) + .color(gray), +.es(index=metricbeat-*, + timefield='@timestamp', + metric='avg:system.cpu.user.pct') + .label('current hour') + .title('CPU usage over time') + .color(#1E90FF) + .legend(columns=2, position=nw) <1> +---------------------------------- + +<1> `.legend()` sets the position and style of the legend. In this example, `.legend(columns=2, position=nw)` places the legend in the north west position of the visualization with two columns. - // Defaults to 'false', disables mouse wheel zoom. If set to - // 'true', map may zoom unexpectedly while scrolling dashboard - "scrollWheelZoom": false, +[role="screenshot"] +image::images/timelion-customize04.png[] +{nbsp} - // When false, repaints on each move frame. - // Makes the graph slower when moving the map - "delayRepaint": true, // default true - } - }, - /* the rest of Vega JSON */ -} ----- +[[timelion-tutorial-create-visualizations-with-mathematical-functions]] +=== Timelion tutorial: Create visualizations with mathematical functions -The visualization automatically injects a `"projection"`, which you can use to -calculate the position of all geo-aware marks. -Additionally, you can use `latitude`, `longitude`, and `zoom` signals. -These signals can be used in the graph, or can be updated to modify the -position of the map. +To create a visualization for inbound and outbound network traffic, use mathematical functions. [float] -[[vega-tooltip]] -==== Additional tooltip styling +[[mathematical-functions-define-functions]] +==== Define the functions -{kib} has installed the https://vega.github.io/vega-lite/docs/tooltip.html[Vega tooltip plugin], -so tooltips can be defined in the ways documented there. Beyond that, {kib} also supports -a configuration option for changing the tooltip position and padding: +To start tracking the inbound and outbound network traffic, enter the following in the *Timelion Expression* field: -```js -{ - config: { - kibana: { - tooltips: { - position: 'top', - padding: 15 - } - } - } -} -``` +[source,text] +---------------------------------- +.es(index=metricbeat*, + timefield=@timestamp, + metric=max:system.network.in.bytes) +---------------------------------- -[[vega-url-loading]] -==== Advanced setting to enable URL loading from any domain +[role="screenshot"] +image::images/timelion-math01.png[] +{nbsp} -Vega can load data from any URL, but this is disabled by default in {kib}. -To change this, set `vis_type_vega.enableExternalUrls: true` in `kibana.yml`, -then restart {kib}. +[float] +[[mathematical-functions-plot-change]] +==== Plot the rate of change -[[vega-inspector]] -==== Vega Inspector -Use the contextual *Inspect* tool to gain insights into different elements. -For Vega visualizations, there are two different views: *Request* and *Vega debug*. +Change how the data is displayed so that you can easily monitor the inbound traffic: -===== Inspect Elasticsearch requests +[source,text] +---------------------------------- +.es(index=metricbeat*, + timefield=@timestamp, + metric=max:system.network.in.bytes) + .derivative() <1> +---------------------------------- -Vega uses the {ref}/search-search.html[{es} search API] to get documents and aggregation -results from {es}. To troubleshoot these requests, click *Inspect*, which shows the most recent requests. -In case your specification has more than one request, you can switch between the views using the *View* dropdown. +<1> `.derivative` plots the change in values over time. [role="screenshot"] -image::visualize/images/vega_tutorial_inspect_requests.png[] - -===== Vega debugging - -With the *Vega debug* view, you can inspect the *Data sets* and *Signal Values* runtime data. - -The runtime data is read from the -https://vega.github.io/vega/docs/api/debugging/#scope[runtime scope]. +image::images/timelion-math02.png[] +{nbsp} + +Add a similar calculation for outbound traffic: + +[source,text] +---------------------------------- +.es(index=metricbeat*, + timefield=@timestamp, + metric=max:system.network.in.bytes) + .derivative(), +.es(index=metricbeat*, + timefield=@timestamp, + metric=max:system.network.out.bytes) + .derivative() + .multiply(-1) <1> +---------------------------------- + +<1> `.multiply()` multiplies the data series by a number, the result of a data series, or a list of data series. For this example, `.multiply(-1)` converts the outbound network traffic to a negative value since the outbound network traffic is leaving your machine. [role="screenshot"] -image::visualize/images/vega_tutorial_inspect_data_sets.png[] - -To debug more complex specs, access to the `view` variable. For more information, refer to -the <>. +image::images/timelion-math03.png[] +{nbsp} -===== Asking for help with a Vega spec - -Because of the dynamic nature of the data in {es}, it is hard to help you with -Vega specs unless you can share a dataset. To do this, click *Inspect*, select the *Vega debug* view, -then select the *Spec* tab: +[float] +[[mathematical-functions-convert-data]] +==== Change the data metric + +To make the visualization easier to analyze, change the data metric from bytes to megabytes: + +[source,text] +---------------------------------- +.es(index=metricbeat*, + timefield=@timestamp, + metric=max:system.network.in.bytes) + .derivative() + .divide(1048576), +.es(index=metricbeat*, + timefield=@timestamp, + metric=max:system.network.out.bytes) + .derivative() + .multiply(-1) + .divide(1048576) <1> +---------------------------------- + +<1> `.divide()` accepts the same input as `.multiply()`, then divides the data series by the defined divisor. [role="screenshot"] -image::visualize/images/vega_tutorial_getting_help.png[] +image::images/timelion-math04.png[] +{nbsp} -To copy the response, click *Copy to clipboard*. Paste the copied data to -https://gist.github.com/[gist.github.com], possibly with a .json extension. Use the [raw] button, -and share that when asking for help. +[float] +[[mathematical-functions-add-labels]] +==== Customize and format the visualization + +Customize and format the visualization using functions: + +[source,text] +---------------------------------- +.es(index=metricbeat*, + timefield=@timestamp, + metric=max:system.network.in.bytes) + .derivative() + .divide(1048576) + .lines(fill=2, width=1) + .color(green) + .label("Inbound traffic") <1> + .title("Network traffic (MB/s)"), <2> +.es(index=metricbeat*, + timefield=@timestamp, + metric=max:system.network.out.bytes) + .derivative() + .multiply(-1) + .divide(1048576) + .lines(fill=2, width=1) <3> + .color(blue) <4> + .label("Outbound traffic") + .legend(columns=2, position=nw) <5> +---------------------------------- + +<1> `.label()` adds custom labels to the visualization. +<2> `.title()` adds a title with a meaningful name. +<3> `.lines()` changes the appearance of the chart lines. In this example, `.lines(fill=2, width=1)` sets the fill level to `2`, and the border width to `1`. +<4> `.color()` changes the color of the data. Supported color types include standard color names, hexadecimal values, or a color schema for grouped data. In this example, `.color(green)` represents the inbound network traffic, and `.color(blue)` represents the outbound network traffic. +<5> `.legend()` sets the position and style of the legend. For this example, `legend(columns=2, position=nw)` places the legend in the north west position of the visualization with two columns. -[[vega-browser-debugging-console]] -==== Browser debugging console +[role="screenshot"] +image::images/timelion-math05.png[] +{nbsp} -experimental[] Use browser debugging tools (for example, F12 or Ctrl+Shift+J in Chrome) to -inspect the `VEGA_DEBUG` variable: +[[timelion-tutorial-create-visualizations-withconditional-logic-and-tracking-trends]] +=== Create visualizations with conditional logic and tracking trends using Timelion -* `view` — Access to the Vega View object. See https://vega.github.io/vega/docs/api/debugging/[Vega Debugging Guide] -on how to inspect data and signals at runtime. For Vega-Lite, -`VEGA_DEBUG.view.data('source_0')` gets the pre-transformed data, and `VEGA_DEBUG.view.data('data_0')` -gets the encoded data. For Vega, it uses the data name as defined in your Vega spec. +To easily detect outliers and discover patterns over time, modify time series data with conditional logic and create a trend with a moving average. -* `vega_spec` — Vega JSON graph specification after some modifications by {kib}. In case -of Vega-Lite, this is the output of the Vega-Lite compiler. +With Timelion conditional logic, you can use the following operator values to compare your data: -* `vegalite_spec` — If this is a Vega-Lite graph, JSON specification of the graph before -Vega-Lite compilation. +[horizontal] +`eq`:: equal +`ne`:: not equal +`lt`:: less than +`lte`:: less than or equal to +`gt`:: greater than +`gte`:: greater than or equal to [float] -[[vega-expression-functions]] -==== (Vega only) Expression functions which can update the time range and dashboard filters +[[conditional-define-functions]] +==== Define the functions -{kib} has extended the Vega expression language with these functions: - -```js -/** - * @param {object} query Elastic Query DSL snippet, as used in the query DSL editor - * @param {string} [index] as defined in Kibana, or default if missing - */ -kibanaAddFilter(query, index) - -/** - * @param {object} query Elastic Query DSL snippet, as used in the query DSL editor - * @param {string} [index] as defined in Kibana, or default if missing - */ -kibanaRemoveFilter(query, index) - -kibanaRemoveAllFilters() - -/** - * Update dashboard time filter to the new values - * @param {number|string|Date} start - * @param {number|string|Date} end - */ -kibanaSetTimeFilter(start, end) -``` +To chart the maximum value of `system.memory.actual.used.bytes`, enter the following in the *Timelion Expression* field: -[float] -[[vega-additional-configuration-options]] -==== Additional configuration options +[source,text] +---------------------------------- +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') +---------------------------------- -[source,yaml] ----- -{ - config: { - kibana: { - // Placement of the Vega-defined signal bindings. - // Can be `left`, `right`, `top`, or `bottom` (default). - controlsLocation: top - // Can be `vertical` or `horizontal` (default). - controlsDirection: vertical - // If true, hides most of Vega and Vega-Lite warnings - hideWarnings: true - // Vega renderer to use: `svg` or `canvas` (default) - renderer: canvas - } - } -} ----- +[role="screenshot"] +image::images/timelion-conditional01.png[] +{nbsp} +[float] +[[conditional-track-memory]] +==== Track used memory + +To track the amount of memory used, create two thresholds: + +[source,text] +---------------------------------- +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes'), +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') + .if(gt, <1> + 11300000000, <2> + .es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes'), + null) + .label('warning') + .color('#FFCC11'), +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') + .if(gt, + 11375000000, + .es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes'), + null) + .label('severe') + .color('red') +---------------------------------- + +<1> Timelion conditional logic for the _greater than_ operator. In this example, the warning threshold is 11.3GB (`11300000000`), and the severe threshold is 11.375GB (`11375000000`). If the threshold values are too high or low for your machine, adjust the values accordingly. +<2> `if()` compares each point to a number. If the condition evaluates to `true`, adjust the styling. If the condition evaluates to `false`, use the default styling. -[[vega-notes]] -[[vega-useful-links]] -=== Resources and examples +[role="screenshot"] +image::images/timelion-conditional02.png[] +{nbsp} -To learn more about Vega and Vega-Lite, refer to the resources and examples. +[float] +[[conditional-determine-trend]] +==== Determine the trend + +To determine the trend, create a new data series: + +[source,text] +---------------------------------- +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes'), +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') + .if(gt,11300000000, + .es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes'), + null) + .label('warning') + .color('#FFCC11'), +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') + .if(gt,11375000000, + .es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes'), + null). + label('severe') + .color('red'), +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') + .mvavg(10) <1> +---------------------------------- + +<1> `mvavg()` calculates the moving average over a specified period of time. In this example, `.mvavg(10)` creates a moving average with a window of 10 data points. -==== Vega editor -The https://vega.github.io/editor/[Vega Editor] includes examples for Vega & Vega-Lite, but does not support any -{kib}-specific features like {es} requests and interactive base maps. +[role="screenshot"] +image::images/timelion-conditional03.png[] +{nbsp} -==== Vega-Lite resources -* https://vega.github.io/vega-lite/tutorials/getting_started.html[Tutorials] -* https://vega.github.io/vega-lite/docs/[Docs] -* https://vega.github.io/vega-lite/examples/[Examples] +[float] +[[conditional-format-visualization]] +==== Customize and format the visualization + +Customize and format the visualization using functions: + +[source,text] +---------------------------------- +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') + .label('max memory') <1> + .title('Memory consumption over time'), <2> +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') + .if(gt, + 11300000000, + .es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes'), + null) + .label('warning') + .color('#FFCC11') <3> + .lines(width=5), <4> +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') + .if(gt, + 11375000000, + .es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes'), + null) + .label('severe') + .color('red') + .lines(width=5), +.es(index=metricbeat-*, + timefield='@timestamp', + metric='max:system.memory.actual.used.bytes') + .mvavg(10) + .label('mvavg') + .lines(width=2) + .color(#5E5E5E) + .legend(columns=4, position=nw) <5> +---------------------------------- + +<1> `.label()` adds custom labels to the visualization. +<2> `.title()` adds a title with a meaningful name. +<3> `.color()` changes the color of the data. Supported color types include standard color names, hexadecimal values, or a color schema for grouped data. +<4> `.lines()` changes the appearance of the chart lines. In this example, .lines(width=5) sets border width to `5`. +<5> `.legend()` sets the position and style of the legend. For this example, `(columns=4, position=nw)` places the legend in the north west position of the visualization with four columns. -==== Vega resources -* https://vega.github.io/vega/tutorials/[Tutorials] -* https://vega.github.io/vega/docs/[Docs] -* https://vega.github.io/vega/examples/[Examples] +[role="screenshot"] +image::images/timelion-conditional04.png[] +{nbsp} -TIP: When you use the examples in {kib}, you may -need to modify the "data" section to use absolute URL. For example, -replace `"url": "data/world-110m.json"` with -`"url": "https://vega.github.io/editor/data/world-110m.json"`. +For additional information on Timelion conditional capabilities, go to https://www.elastic.co/blog/timeseries-if-then-else-with-timelion[I have but one .condition()]. \ No newline at end of file diff --git a/docs/user/dashboard/vega-reference.asciidoc b/docs/user/dashboard/vega-reference.asciidoc new file mode 100644 index 0000000000000..eed8d9a35b874 --- /dev/null +++ b/docs/user/dashboard/vega-reference.asciidoc @@ -0,0 +1,437 @@ +[[vega-reference]] +== Vega reference + +experimental[] + +For additional *Vega* and *Vega-Lite* information, refer to the reference sections. + +[float] +[[reference-for-kibana-extensions]] +=== Reference for {kib} extensions + +{kib} has extended Vega and Vega-Lite with extensions that support: + +* Default height and width +* Default theme to match {kib} +* Writing {es} queries using the time range and filters from dashboards +* Using the Elastic Map Service in Vega maps +* Additional tooltip styling +* Advanced setting to enable URL loading from any domain +* Limited debugging support using the browser dev tools +* (Vega only) Expression functions which can update the time range and dashboard filters + +[float] +[[vega-sizing-and-positioning]] +==== Default height and width + +By default, Vega visualizations use the `autosize = { type: 'fit', contains: 'padding' }` layout. +`fit` uses all available space, ignores `width` and `height` values, +and respects the padding values. To override this behavior, change the +`autosize` value. + +[float] +[[vega-theme]] +==== Default theme to match {kib} + +{kib} registers a default https://vega.github.io/vega/docs/schemes/[Vega color scheme] +with the id `elastic`, and sets a default color for each `mark` type. +Override it by providing a different `stroke`, `fill`, or `color` (Vega-Lite) value. + +[float] +[[vega-queries]] +==== Writing {es} queries in Vega + +experimental[] {kib} extends the Vega https://vega.github.io/vega/docs/data/[data] elements +with support for direct {es} queries specified as a `url`. + +Because of this, {kib} is **unable to support dynamically loaded data**, +which would otherwise work in Vega. All data is fetched before it's passed to +the Vega renderer. + +To define an {es} query in Vega, set the `url` to an object. {kib} will parse +the object looking for special tokens that allow your query to integrate with {kib}. +These tokens are: + +* `%context%: true`: Set at the top level, and replaces the `query` section with filters from dashboard +* `%timefield%: `: Set at the top level, integrates the query with the dashboard time filter +* `{%timefilter%: true}`: Replaced by an {es} range query with upper and lower bounds +* `{%timefilter%: "min" | "max"}`: Replaced only by the upper or lower bounds +* `{%timefilter: true, shift: -1, unit: 'hour'}`: Generates a time range query one hour in the past +* `{%autointerval%: true}`: Replaced by the string which contains the automatic {kib} time interval, such as `1h` +* `{%autointerval%: 10}`: Replaced by a string which is approximately dividing the time into 10 ranges, allowing + you to influence the automatic interval +* `"%dashboard_context-must_clause%"`: String replaced by object containing filters +* `"%dashboard_context-filter_clause%"`: String replaced by an object containing filters +* `"%dashboard_context-must_not_clause%"`: String replaced by an object containing filters + +Putting this together, an example query that counts the number of documents in +a specific index: + +[source,yaml] +---- +// An object instead of a string for the URL value +// is treated as a context-aware Elasticsearch query. +url: { + // Specify the time filter. + %timefield%: @timestamp + // Apply dashboard context filters when set + %context%: true + + // Which indexes to search + index: kibana_sample_data_logs + // The body element may contain "aggs" and "query" keys + body: { + aggs: { + time_buckets: { + date_histogram: { + // Use date histogram aggregation on @timestamp field + field: @timestamp <1> + // interval value will depend on the time filter + // Use an integer to set approximate bucket count + interval: { %autointerval%: true } + // Make sure we get an entire range, even if it has no data + extended_bounds: { + min: { %timefilter%: "min" } + max: { %timefilter%: "max" } + } + // Use this for linear (e.g. line, area) graphs + // Without it, empty buckets will not show up + min_doc_count: 0 + } + } + } + // Speed up the response by only including aggregation results + size: 0 + } +} +---- + +<1> `@timestamp` — Filters the time range and breaks it into histogram +buckets. + +The full result includes the following structure: + +[source,yaml] +---- +{ + "aggregations": { + "time_buckets": { + "buckets": [{ + "key_as_string": "2015-11-30T22:00:00.000Z", + "key": 1448920800000,<1> + "doc_count": 28 + }, { + "key_as_string": "2015-11-30T23:00:00.000Z", + "key": 1448924400000, <1> + "doc_count": 330 + }, ... +---- + +<1> `"key"` — The unix timestamp you can use without conversions by the +Vega date expressions. + +For most visualizations, you only need the list of bucket values. To focus on +only the data you need, use `format: {property: "aggregations.time_buckets.buckets"}`. + +Specify a query with individual range and dashboard context. The query is +equivalent to `"%context%": true, "%timefield%": "@timestamp"`, +except that the time range is shifted back by 10 minutes: + +[source,yaml] +---- +{ + body: { + query: { + bool: { + must: [ + // This string will be replaced + // with the auto-generated "MUST" clause + "%dashboard_context-must_clause%" + { + range: { + // apply timefilter (upper right corner) + // to the @timestamp variable + @timestamp: { + // "%timefilter%" will be replaced with + // the current values of the time filter + // (from the upper right corner) + "%timefilter%": true + // Only work with %timefilter% + // Shift current timefilter by 10 units back + shift: 10 + // week, day (default), hour, minute, second + unit: minute + } + } + } + ] + must_not: [ + // This string will be replaced with + // the auto-generated "MUST-NOT" clause + "%dashboard_context-must_not_clause%" + ] + filter: [ + // This string will be replaced + // with the auto-generated "FILTER" clause + "%dashboard_context-filter_clause%" + ] + } + } + } +} +---- + +NOTE: When using `"%context%": true` or defining a value for `"%timefield%"` the body cannot contain a query. To customize the query within the VEGA specification (e.g. add an additional filter, or shift the timefilter), define your query and use the placeholders as in the example above. The placeholders will be replaced by the actual context of the dashboard or visualization once parsed. + +The `"%timefilter%"` can also be used to specify a single min or max +value. The date_histogram's `extended_bounds` can be set +with two values - min and max. Instead of hardcoding a value, you may +use `"min": {"%timefilter%": "min"}`, which will be replaced with the +beginning of the current time range. The `shift` and `unit` values are +also supported. The `"interval"` can also be set dynamically, depending +on the currently picked range: `"interval": {"%autointerval%": 10}` will +try to get about 10-15 data points (buckets). + +[float] +[[vega-esmfiles]] +=== Access Elastic Map Service files + +experimental[] Access the Elastic Map Service files via the same mechanism: + +[source,yaml] +---- +url: { + // "type" defaults to "elasticsearch" otherwise + type: emsfile + // Name of the file, exactly as in the Region map visualization + name: World Countries +} +// The result is a geojson file, get its features to use +// this data source with the "shape" marks +// https://vega.github.io/vega/docs/marks/shape/ +format: {property: "features"} +---- + +To enable Maps, the graph must specify `type=map` in the host +configuration: + +[source,yaml] +---- +{ + "config": { + "kibana": { + "type": "map", + + // Initial map position + "latitude": 40.7, // default 0 + "longitude": -74, // default 0 + "zoom": 7, // default 2 + + // defaults to "default". Use false to disable base layer. + "mapStyle": false, + + // default 0 + "minZoom": 5, + + // defaults to the maximum for the given style, + // or 25 when base is disabled + "maxZoom": 13, + + // defaults to true, shows +/- buttons to zoom in/out + "zoomControl": false, + + // Defaults to 'false', disables mouse wheel zoom. If set to + // 'true', map may zoom unexpectedly while scrolling dashboard + "scrollWheelZoom": false, + + // When false, repaints on each move frame. + // Makes the graph slower when moving the map + "delayRepaint": true, // default true + } + }, + /* the rest of Vega JSON */ +} +---- + +The visualization automatically injects a `"projection"`, which you can use to +calculate the position of all geo-aware marks. +Additionally, you can use `latitude`, `longitude`, and `zoom` signals. +These signals can be used in the graph, or can be updated to modify the +position of the map. + +[float] +[[vega-tooltip]] +==== Additional tooltip styling + +{kib} has installed the https://vega.github.io/vega-lite/docs/tooltip.html[Vega tooltip plugin], +so tooltips can be defined in the ways documented there. Beyond that, {kib} also supports +a configuration option for changing the tooltip position and padding: + +```js +{ + config: { + kibana: { + tooltips: { + position: 'top', + padding: 15 + } + } + } +} +``` + +[float] +[[vega-url-loading]] +==== Advanced setting to enable URL loading from any domain + +Vega can load data from any URL, but this is disabled by default in {kib}. +To change this, set `vis_type_vega.enableExternalUrls: true` in `kibana.yml`, +then restart {kib}. + +[float] +[[vega-inspector]] +==== Vega Inspector +Use the contextual *Inspect* tool to gain insights into different elements. +For Vega visualizations, there are two different views: *Request* and *Vega debug*. + +[float] +[[inspect-elasticsearch-requests]] +===== Inspect {es} requests + +Vega uses the {ref}/search-search.html[{es} search API] to get documents and aggregation +results from {es}. To troubleshoot these requests, click *Inspect*, which shows the most recent requests. +In case your specification has more than one request, you can switch between the views using the *View* dropdown. + +[role="screenshot"] +image::visualize/images/vega_tutorial_inspect_requests.png[] + +[float] +[[vega-debugging]] +===== Vega debugging + +With the *Vega debug* view, you can inspect the *Data sets* and *Signal Values* runtime data. + +The runtime data is read from the +https://vega.github.io/vega/docs/api/debugging/#scope[runtime scope]. + +[role="screenshot"] +image::visualize/images/vega_tutorial_inspect_data_sets.png[] + +To debug more complex specs, access to the `view` variable. For more information, refer to +the <>. + +[float] +[[asking-for-help-with-a-vega-spec]] +===== Asking for help with a Vega spec + +Because of the dynamic nature of the data in {es}, it is hard to help you with +Vega specs unless you can share a dataset. To do this, click *Inspect*, select the *Vega debug* view, +then select the *Spec* tab: + +[role="screenshot"] +image::visualize/images/vega_tutorial_getting_help.png[] + +To copy the response, click *Copy to clipboard*. Paste the copied data to +https://gist.github.com/[gist.github.com], possibly with a .json extension. Use the [raw] button, +and share that when asking for help. + +[float] +[[vega-browser-debugging-console]] +==== Browser debugging console + +experimental[] Use browser debugging tools (for example, F12 or Ctrl+Shift+J in Chrome) to +inspect the `VEGA_DEBUG` variable: + +* `view` — Access to the Vega View object. See https://vega.github.io/vega/docs/api/debugging/[Vega Debugging Guide] +on how to inspect data and signals at runtime. For Vega-Lite, +`VEGA_DEBUG.view.data('source_0')` gets the pre-transformed data, and `VEGA_DEBUG.view.data('data_0')` +gets the encoded data. For Vega, it uses the data name as defined in your Vega spec. + +* `vega_spec` — Vega JSON graph specification after some modifications by {kib}. In case +of Vega-Lite, this is the output of the Vega-Lite compiler. + +* `vegalite_spec` — If this is a Vega-Lite graph, JSON specification of the graph before +Vega-Lite compilation. + +[float] +[[vega-expression-functions]] +==== (Vega only) Expression functions which can update the time range and dashboard filters + +{kib} has extended the Vega expression language with these functions: + +```js +/** + * @param {object} query Elastic Query DSL snippet, as used in the query DSL editor + * @param {string} [index] as defined in Kibana, or default if missing + */ +kibanaAddFilter(query, index) + +/** + * @param {object} query Elastic Query DSL snippet, as used in the query DSL editor + * @param {string} [index] as defined in Kibana, or default if missing + */ +kibanaRemoveFilter(query, index) + +kibanaRemoveAllFilters() + +/** + * Update dashboard time filter to the new values + * @param {number|string|Date} start + * @param {number|string|Date} end + */ +kibanaSetTimeFilter(start, end) +``` + +[float] +[[vega-additional-configuration-options]] +==== Additional configuration options + +[source,yaml] +---- +{ + config: { + kibana: { + // Placement of the Vega-defined signal bindings. + // Can be `left`, `right`, `top`, or `bottom` (default). + controlsLocation: top + // Can be `vertical` or `horizontal` (default). + controlsDirection: vertical + // If true, hides most of Vega and Vega-Lite warnings + hideWarnings: true + // Vega renderer to use: `svg` or `canvas` (default) + renderer: canvas + } + } +} +---- + +[[vega-notes]] +[[resources-and-examples]] +=== Resources and examples + +experimental[] To learn more about Vega and Vega-Lite, refer to the resources and examples. + +[float] +[[vega-editor]] +==== Vega editor +The https://vega.github.io/editor/[Vega Editor] includes examples for Vega & Vega-Lite, but does not support any +{kib}-specific features like {es} requests and interactive base maps. + +[float] +[[vega-lite-resources]] +==== Vega-Lite resources +* https://vega.github.io/vega-lite/tutorials/getting_started.html[Tutorials] +* https://vega.github.io/vega-lite/docs/[Docs] +* https://vega.github.io/vega-lite/examples/[Examples] + +[float] +[[vega-resources]] +==== Vega resources +* https://vega.github.io/vega/tutorials/[Tutorials] +* https://vega.github.io/vega/docs/[Docs] +* https://vega.github.io/vega/examples/[Examples] + +TIP: When you use the examples in {kib}, you may +need to modify the "data" section to use absolute URL. For example, +replace `"url": "data/world-110m.json"` with +`"url": "https://vega.github.io/editor/data/world-110m.json"`. diff --git a/docs/user/getting-started.asciidoc b/docs/user/getting-started.asciidoc index 2ff3a09152df4..a877f6a66a79a 100644 --- a/docs/user/getting-started.asciidoc +++ b/docs/user/getting-started.asciidoc @@ -1,19 +1,19 @@ -[[getting-started]] +[[get-started]] = Get started [partintro] -- -Ready to try out {kib} and see what it can do? To quickest way to get started with {kib} is to set up on Cloud, then add a sample data set that helps you get a handle on the full range of {kib} features. +Ready to try out {kib} and see what it can do? The quickest way to get started with {kib} is to set up on Cloud, then add a sample data set to explore the full range of {kib} features. [float] -[[cloud-set-up]] +[[set-up-on-cloud]] == Set up on cloud include::{docs-root}/shared/cloud/ess-getting-started.asciidoc[] [float] -[[get-data-in]] +[[gs-get-data-into-kibana]] == Get data into {kib} The easiest way to get data into {kib} is to add a sample data set. @@ -42,12 +42,11 @@ NOTE: The timestamps in the sample data sets are relative to when they are insta If you uninstall and reinstall a data set, the timestamps change to reflect the most recent installation. [float] -[[getting-started-next-steps]] == Next steps -* To get a hands-on experience creating visualizations, follow the <> tutorial. +* To get a hands-on experience creating visualizations, follow the <> tutorial. -* If you're ready to load an actual data set and build a dashboard, follow the <> tutorial. +* If you're ready to load an actual data set and build a dashboard, follow the <> tutorial. -- @@ -60,5 +59,3 @@ include::{kib-repo-dir}/getting-started/tutorial-define-index.asciidoc[] include::{kib-repo-dir}/getting-started/tutorial-discovering.asciidoc[] include::{kib-repo-dir}/getting-started/tutorial-visualizing.asciidoc[] - -include::{kib-repo-dir}/getting-started/tutorial-dashboard.asciidoc[] diff --git a/docs/user/index.asciidoc b/docs/user/index.asciidoc index abbdbeb68d9cb..e909626c5779c 100644 --- a/docs/user/index.asciidoc +++ b/docs/user/index.asciidoc @@ -2,8 +2,6 @@ include::introduction.asciidoc[] include::whats-new.asciidoc[] -include::getting-started.asciidoc[] - include::setup.asciidoc[] include::monitoring/configuring-monitoring.asciidoc[leveloffset=+1] @@ -13,9 +11,11 @@ include::monitoring/monitoring-kibana.asciidoc[leveloffset=+2] include::security/securing-kibana.asciidoc[] +include::getting-started.asciidoc[] + include::discover.asciidoc[] -include::dashboard.asciidoc[] +include::dashboard/dashboard.asciidoc[] include::canvas.asciidoc[] @@ -25,8 +25,6 @@ include::ml/index.asciidoc[] include::graph/index.asciidoc[] -include::visualize.asciidoc[] - include::{kib-repo-dir}/observability/index.asciidoc[] include::{kib-repo-dir}/logs/index.asciidoc[] diff --git a/docs/user/introduction.asciidoc b/docs/user/introduction.asciidoc index ff936fb4d5569..079d183dd959d 100644 --- a/docs/user/introduction.asciidoc +++ b/docs/user/introduction.asciidoc @@ -83,12 +83,6 @@ image::images/intro-dashboard.png[] {kib} also offers these visualization features: -* <> allows you to display your data in -charts, graphs, and tables -(just to name a few). It's also home to Lens. -Visualize supports the ability to add interactive -controls to your dashboard, filter dashboard content in real time, and add your own images and logos for your brand. - * <> gives you the ability to present your data in a visually compelling, pixel-perfect report. Give your data the “wow” factor needed to impress your CEO or to captivate coworkers with a big-screen display. @@ -98,7 +92,7 @@ questions of your location-based data. Maps supports multiple layers and data sources, mapping of individual geo points and shapes, and dynamic client-side styling. -* <> allows you to combine +* <> allows you to combine an infinite number of aggregations to display complex data. With TSVB, you can analyze multiple index patterns and customize every aspect of your visualization. Choose your own date format and color @@ -161,6 +155,6 @@ and start exploring data in minutes. You can also <> — no code, no additional infrastructure required. -Our <> and in-product guidance can +Our <> and in-product guidance can help you get up and running, faster. Click the help icon image:images/intro-help-icon.png[] in the top navigation bar for help with questions or to provide feedback. diff --git a/docs/user/reporting/automating-report-generation.asciidoc b/docs/user/reporting/automating-report-generation.asciidoc index 3e227229ddcc5..371855deb2f3c 100644 --- a/docs/user/reporting/automating-report-generation.asciidoc +++ b/docs/user/reporting/automating-report-generation.asciidoc @@ -13,7 +13,7 @@ URL that triggers a report to generate. To create the POST URL for PDF reports: -. Go to *Visualize* or *Dashboard*, then open the visualization or dashboard. +. Go to *Dashboard*, then open the visualization or dashboard. + To specify a relative or absolute time period, use the time filter. diff --git a/docs/user/reporting/index.asciidoc b/docs/user/reporting/index.asciidoc index 4f4d59315fafa..50ae92382fb24 100644 --- a/docs/user/reporting/index.asciidoc +++ b/docs/user/reporting/index.asciidoc @@ -11,7 +11,7 @@ saved search, or Canvas workpad. Depending on the object type, you can export th a PDF, PNG, or CSV document, which you can keep for yourself, or share with others. Reporting is available from the *Share* menu -in *Discover*, *Visualize*, *Dashboard*, and *Canvas*. +in *Discover*, *Dashboard*, and *Canvas*. [role="screenshot"] image::user/reporting/images/share-button.png["Share"] diff --git a/docs/user/security/rbac_tutorial.asciidoc b/docs/user/security/rbac_tutorial.asciidoc index 3a4b2202201e2..cc4af9041bcd9 100644 --- a/docs/user/security/rbac_tutorial.asciidoc +++ b/docs/user/security/rbac_tutorial.asciidoc @@ -28,7 +28,7 @@ To complete this tutorial, you'll need the following: * **A space**: In this tutorial, use `Dev Mortgage` as the space name. See <> for details on creating a space. -* **Data**: You can use <> or +* **Data**: You can use <> or live data. In the following steps, Filebeat and Metricbeat data are used. [float] diff --git a/docs/user/security/reporting.asciidoc b/docs/user/security/reporting.asciidoc index 4e02759ce99cb..daf9720a0f1d8 100644 --- a/docs/user/security/reporting.asciidoc +++ b/docs/user/security/reporting.asciidoc @@ -47,7 +47,7 @@ image::user/security/images/reporting-privileges-example.png["Reporting privileg Reporting users typically save searches, create visualizations, and build dashboards. They require a space that provides read and write privileges in -*Discover*, *Visualize*, and *Dashboard*. +*Discover* and *Dashboard*. . Save your new role. diff --git a/docs/user/visualize.asciidoc b/docs/user/visualize.asciidoc deleted file mode 100644 index dc116962f9e96..0000000000000 --- a/docs/user/visualize.asciidoc +++ /dev/null @@ -1,142 +0,0 @@ -[[visualize]] -= Visualize - -[partintro] --- -_Visualize_ enables you to create visualizations of the data from your {es} indices, which you can then add to dashboards for analysis. - -{kib} visualizations are based on {es} queries. By using a series of {es} {ref}/search-aggregations.html[aggregations] to extract and process your data, you can create charts that show you the trends, spikes, and dips you need to know about. - -To begin, open the menu, go to *Visualize*, then click *Create visualization*. - -[float] -[[visualization-types]] -== Types of visualizations - -{kib} supports several types of visualizations. - -<>:: -Quickly build several types of basic visualizations by simply dragging and dropping the data fields you want to display. - -<>:: - -* *Line, area, and bar charts* — Compares different series in X/Y charts. - -* *Pie chart* — Displays each source contribution to a total. - -* *Data table* — Flattens aggregations into table format. - -* *Metric* — Displays a single number. - -* *Goal and gauge* — Displays a number with progress indicators. - -* *Tag cloud* — Displays words in a cloud, where the size of the word corresponds to its importance. - -<>:: Visualizes time series data using pipeline aggregations. - -<>:: Computes and combine data from multiple time series -data sets. - -<>:: -* *<>* — Displays geospatial data in {kib}. - -* <>:: Display shaded cells within a matrix. - -<>:: - -* *Markdown widget* — Displays free-form information or instructions. - -* *Controls* — Adds interactive inputs to a dashboard. - -<>:: Completes control over query and display. - -[float] -[[choose-your-data]] -== Choose your data - -Specify a search query to retrieve the data for your visualization, or used rolled up data. - -* To enter new search criteria, select the <> for the indices that -contain the data you want to visualize. The visualization builder opens -with a wildcard query that matches all of the documents in the selected -indices. - -* To build a visualization from a saved search, click the name of the saved -search you want to use. The visualization builder opens and loads the -selected query. -+ -NOTE: When you build a visualization from a saved search, any subsequent -modifications to the saved search are reflected in the -visualization. To disable automatic updates, delete the visualization -on the *Saved Object* page. - -* To build a visualization using <>, select -the index pattern that includes the data. Rolled up data is summarized into -time buckets that can be split into sub buckets for numeric field values or -terms. To lower granularity, use a time aggregation that uses and combines -several time buckets. For an example, refer to <>. - -[float] -[[vis-inspector]] -== Inspect visualizations - -Many visualizations allow you to inspect the query and data behind the visualization. - -. In the {kib} toolbar, click *Inspect*. -. To download the data, click *Download CSV*, then choose one of the following options: -* *Formatted CSV* - Downloads the data in table format. -* *Raw CSV* - Downloads the data as provided. -. To view the requests for collecting data, select *Requests* from the *View* -dropdown. - -[float] -[[save-visualize]] -== Save visualizations -To use your visualizations in <>, you must save them. - -. In the {kib} toolbar, click *Save*. -. Enter the visualization *Title* and optional *Description*, then *Save* the visualization. - -To access the saved visualization, go to *Management > {kib} > Saved Objects*. - -[float] -[[save-visualization-read-only-access]] -==== Read only access -When you have insufficient privileges to save visualizations, the following indicator is -displayed and the *Save* button is not visible. - -For more information, refer to <>. - -[role="screenshot"] -image::visualize/images/read-only-badge.png[Example of Visualize's read only access indicator in Kibana's header] - -[float] -[[visualize-share-options]] -== Share visualizations - -When you've finished your visualization, you can share it outside of {kib}. - -From the *Share* menu, you can: - -* Embed the code in a web page. Users must have {kib} access -to view an embedded visualization. -* Share a direct link to a {kib} visualization. -* Generate a PDF report. -* Generate a PNG report. - --- -include::{kib-repo-dir}/visualize/aggregations.asciidoc[] - -include::{kib-repo-dir}/visualize/lens.asciidoc[] - -include::{kib-repo-dir}/visualize/most-frequent.asciidoc[] - -include::{kib-repo-dir}/visualize/tsvb.asciidoc[] - -include::{kib-repo-dir}/visualize/timelion.asciidoc[] - -include::{kib-repo-dir}/visualize/tilemap.asciidoc[] - -include::{kib-repo-dir}/visualize/for-dashboard.asciidoc[] - -include::{kib-repo-dir}/visualize/vega.asciidoc[] diff --git a/docs/visualize/aggregations.asciidoc b/docs/visualize/aggregations.asciidoc deleted file mode 100644 index ef38f716f2303..0000000000000 --- a/docs/visualize/aggregations.asciidoc +++ /dev/null @@ -1,110 +0,0 @@ -[[supported-aggregations]] -== Supported aggregations - -Use the supported aggregations to build your visualizations. - -[float] -[[visualize-metric-aggregations]] -=== Metric aggregations - -Metric aggregations extract field from documents to generate data values. - -{ref}/search-aggregations-metrics-avg-aggregation.html[Average]:: The mean value. - -{ref}/search-aggregations-metrics-valuecount-aggregation.html[Count]:: The total number of documents that match the query, which allows you to visualize the number of documents in a bucket. Count is the default value. - -{ref}/search-aggregations-metrics-max-aggregation.html[Max]:: The highest value. - -{ref}/search-aggregations-metrics-percentile-aggregation.html[Median]:: The value that is in the 50% percentile. - -{ref}/search-aggregations-metrics-min-aggregation.html[Min]:: The lowest value. - -{ref}/search-aggregations-metrics-percentile-rank-aggregation.html[Percentile ranks]:: Returns the percentile rankings for the values in the specified numeric field. Select a numeric field from the drop-down, then specify one or more percentile rank values in the *Values* fields. - -{ref}/search-aggregations-metrics-percentile-aggregation.html[Percentiles]:: Divides the -values in a numeric field into specified percentile bands. Select a field from the drop-down, then specify one or more ranges in the *Percentiles* fields. - -Standard Deviation:: Requires a numeric field. Uses the {ref}/search-aggregations-metrics-extendedstats-aggregation.html[_extended stats_] aggregation. - -{ref}/search-aggregations-metrics-sum-aggregation.html[Sum]:: The total value. - -{ref}/search-aggregations-metrics-top-hits-aggregation.html[Top hit]:: Returns a sample of individual documents. When the Top Hit aggregation is matched to more than one document, you must choose a technique for combining the values. Techniques include average, minimum, maximum, and sum. - -Unique Count:: The {ref}/search-aggregations-metrics-cardinality-aggregation.html[Cardinality] of the field within the bucket. - -Alternatively, you can override the field values with a script using JSON input. For example: - -[source,shell] -{ "script" : "doc['grade'].value * 1.2" } - -The example implements a {es} {ref}/search-aggregations.html[Script Value Source], which replaces -the value in the metric. The options available depend on the aggregation you choose. - -[float] -[[visualize-parent-pipeline-aggregations]] -=== Parent pipeline aggregations - -Parent pipeline aggregations assume the bucket aggregations are ordered and are especially useful for time series data. For each parent pipeline aggregation, you must define a bucket aggregation and metric aggregation. - -You can also nest these aggregations. For example, if you want to produce a third derivative. - -{ref}/search-aggregations-pipeline-bucket-script-aggregation.html[Bucket script]:: Executes a script that performs computations for each bucket that specifies metrics in the parent multi-bucket aggregation. - -{ref}/search-aggregations-pipeline-cumulative-sum-aggregation.html[Cumulative sum]:: Calculates the cumulative sum of a specified metric in a parent histogram. - -{ref}/search-aggregations-pipeline-derivative-aggregation.html[Derivative]:: Calculates the derivative of specific metrics. - -{ref}/search-aggregations-pipeline-movavg-aggregation.html[Moving avg]:: Slides a window across the data and emits the average value of the window. - -{ref}/search-aggregations-pipeline-serialdiff-aggregation.html[Serial diff]:: Values in a time series are subtracted from itself at different time lags or periods. - -[float] -[[visualize-sibling-pipeline-aggregations]] -=== Sibling pipeline aggregations - -Sibling pipeline aggregations condense many buckets into one. For each sibling pipeline aggregation, you must define a bucket aggregations and metric aggregation. - -{ref}/search-aggregations-pipeline-avg-bucket-aggregation.html[Average bucket]:: Calculates the mean, or average, value of a specified metric in a sibling aggregation. - -{ref}/search-aggregations-pipeline-avg-bucket-aggregation.html[Max Bucket]:: Calculates the maximum value of a specified metric in a sibling aggregation. - -{ref}/search-aggregations-pipeline-avg-bucket-aggregation.html[Min Bucket]:: Calculates the minimum value of a specified metric in a sibling aggregation. - -{ref}/search-aggregations-pipeline-avg-bucket-aggregation.html[Sum Bucket]:: Calculates the sum of the values of a specified metric in a sibling aggregation. - -[float] -[[visualize-bucket-aggregations]] -=== Bucket aggregations - -Bucket aggregations sort documents into buckets, depending on the contents of the document. - -{ref}/search-aggregations-bucket-datehistogram-aggregation.html[Date histogram]:: Splits a date field into buckets by interval. If the date field is the primary time field for the index pattern, it chooses an automatic interval for you. Intervals are labeled at the start of the interval, using the date-key returned by {es}. For example, the tooltip for a monthly interval displays the first day of the month. - -{ref}/search-aggregations-bucket-daterange-aggregation.html[Date range]:: Reports values that are within a range of dates that you specify. You can specify the ranges for the dates using {ref}/common-options.html#date-math[_date math_] expressions. - -{ref}/search-aggregations-bucket-filter-aggregation.html[Filter]:: Each filter creates a bucket of documents. You can specify a filter as a -<> or <> query string. - -{ref}/search-aggregations-bucket-geohashgrid-aggregation.html[Geohash]:: Displays points based on a geohash. Supported by data table visualizations and <>. - -{ref}/search-aggregations-bucket-geotilegrid-aggregation.html[Geotile]:: Groups points based on web map tiling. Supported by data table visualizations and <>. - -{ref}/search-aggregations-bucket-histogram-aggregation.html[Histogram]:: Builds from a numeric field. - -{ref}/search-aggregations-bucket-iprange-aggregation.html[IPv4 range]:: Specify ranges of IPv4 addresses. - -{ref}/search-aggregations-bucket-range-aggregation.html[Range]:: Specify ranges of values for a numeric field. - -{ref}/search-aggregations-bucket-significantterms-aggregation.html[Significant terms]:: Returns interesting or unusual occurrences of terms in a set. Supports {es} {ref}/search-aggregations-bucket-terms-aggregation.html#_filtering_values_4[exclude and include patterns]. - -{ref}/search-aggregations-bucket-terms-aggregation.html[Terms]:: Specify the top or bottom _n_ elements of a given field to display, ordered by count or a custom metric. Supports {es} {ref}/search-aggregations-bucket-terms-aggregation.html#_filtering_values_4[exclude and include patterns]. - -{kib} filters string fields with only regular expression patterns, and does not filter numeric fields or match with arrays. - -For example: - -* You want to exclude the metricbeat process from your visualization of top processes: `metricbeat.*` -* You only want to show processes collecting beats: `.*beat` -* You want to exclude two specific values, the string `"empty"` and `"none"`: `empty|none` - -Patterns are case sensitive. diff --git a/docs/visualize/for-dashboard.asciidoc b/docs/visualize/for-dashboard.asciidoc deleted file mode 100644 index 400179e9ceae7..0000000000000 --- a/docs/visualize/for-dashboard.asciidoc +++ /dev/null @@ -1,67 +0,0 @@ -[[for-dashboard]] -== Dashboard tools - -Visualize comes with controls and Markdown tools that you can add to dashboards for an interactive experience. - -[float] -[[controls]] -=== Controls -experimental[] - -The controls tool enables you to add interactive inputs -on a dashboard. - -You can add two types of interactive inputs: - -* *Options list* — Filters content based on one or more specified options. The dropdown menu is dynamically populated with the results of a terms aggregation. For example, use the options list on the sample flight dashboard when you want to filter the data by origin city and destination city. - -* *Range slider* — Filters data within a specified range of numbers. The minimum and maximum values are dynamically populated with the results of a min and max aggregation. For example, use the range slider when you want to filter the sample flight dashboard by a specific average ticket price. - -[role="screenshot"] -image::images/dashboard-controls.png[] - -[float] -[[controls-options]] -==== Controls options - -Configure the settings that apply to the interactive inputs on a dashboard. - -. Click *Options*, then configure the following: - -* *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*. - -* *Use time filter* — When selected, the aggregations that generate the options list and time range are connected to the <>. - -* *Pin filters to global state* — When selected, all filters created by interacting with the inputs are automatically pinned. - -. Click *Update*. - -[float] -[[markdown-widget]] -=== Markdown - -The Markdown tool is a text entry field that accepts GitHub-flavored Markdown text. When you enter the text, the tool populates the results on the dashboard. - -Markdown is helpful when you want to include important information, instructions, and images on your dashboard. - -For information about GitHub-flavored Markdown text, click *Help*. - -For example, when you enter: - -[role="screenshot"] -image::images/markdown_example_1.png[] - -The following instructions are displayed: - -[role="screenshot"] -image::images/markdown_example_2.png[] - -Or when you enter: - -[role="screenshot"] -image::images/markdown_example_3.png[] - -The following image is displayed: - -[role="screenshot"] -image::images/markdown_example_4.png[] diff --git a/docs/visualize/images/lens_drag_drop.gif b/docs/visualize/images/lens_drag_drop.gif index ca62115e7ea3a..1f8580d462702 100644 Binary files a/docs/visualize/images/lens_drag_drop.gif and b/docs/visualize/images/lens_drag_drop.gif differ diff --git a/docs/visualize/lens.asciidoc b/docs/visualize/lens.asciidoc deleted file mode 100644 index 6e51433bca3f6..0000000000000 --- a/docs/visualize/lens.asciidoc +++ /dev/null @@ -1,173 +0,0 @@ -[role="xpack"] -[[lens]] -== Lens - -beta[] - -*Lens* is a simple and fast way to create visualizations of your {es} data. To create visualizations, -you drag and drop your data fields onto the visualization builder pane, and *Lens* automatically generates -a visualization that best displays your data. - -With Lens, you can: - -* Use the automatically generated visualization suggestions to change the visualization type. - -* Create visualizations with multiple layers and indices. - -* Add your visualizations to dashboards and Canvas workpads. - -To get started with *Lens*, select a field in the data panel, then drag and drop the field on a highlighted area. - -[role="screenshot"] -image::images/lens_drag_drop.gif[Drag and drop] - -You can incorporate many fields into your visualization, and Lens uses heuristics to decide how to apply each one to the visualization. - -TIP: Drag-and-drop capabilities are available only when Lens knows how to use the data. If *Lens* is unable to automatically generate a visualization, -you can still configure the customization options for your visualization. - -[float] -[[apply-lens-filters]] -==== Change the data panel fields - -The fields in the data panel are based on the selected <> and <>. - -To change the index pattern, click it, then select a new one. The fields in the data panel automatically update. - -To filter the fields in the data panel: - -* Enter the name in *Search field names*. - -* Click *Filter by type*, then select the filter. To show all of the fields in the index pattern, deselect *Only show fields with data*. - -[float] -[[view-data-summaries]] -==== Data summaries - -To help you decide exactly the data you want to display, get a quick summary of each field. The summary shows the distribution of values within the selected time range. - -To view the field summary information, navigate to the field, then click *i*. - -[role="screenshot"] -image::images/lens_data_info.png[Data summary window] - -[float] -[[change-the-visualization-type]] -==== Change the visualization type - -*Lens* enables you to switch between any supported visualization type at any time. - -*Suggestions* are shortcuts to alternate visualizations that *Lens* generates for you. - -[role="screenshot"] -image::images/lens_suggestions.gif[Visualization suggestions] - -If you'd like to use a visualization type that is not suggested, click the visualization type, -then select a new one. - -[role="screenshot"] -image::images/lens_viz_types.png[] - -When there is an exclamation point (!) -next to a visualization type, Lens is unable to transfer your data, but -still allows you to make the change. - -[float] -[[customize-operation]] -==== Change the aggregation and labels - -For each visualization, Lens allows some customizations of the data. - -. Click *Drop a field here* or the field name in the column. - -. Change the options that appear. Options vary depending on the type of field. -+ -[role="screenshot"] -image::images/lens_aggregation_labels.png[Quick function options] - -[float] -[[layers]] -==== Add layers and indices - -Area, line, and bar charts allow you to visualize multiple data layers and indices so that you can compare and analyze data from multiple sources. - -To add a layer, click *+*, then drag and drop the fields for the new layer. - -[role="screenshot"] -image::images/lens_layers.png[Add layers] - -To view a different index, click it, then select a new one. - -[role="screenshot"] -image::images/lens_index_pattern.png[Add index pattern] - -[float] -[[lens-tutorial]] -=== Lens tutorial - -Ready to create your own visualization with Lens? Use the following tutorial to create a visualization that -lets you compare sales over time. - -[float] -[[lens-before-begin]] -==== Before you begin - -To start, you'll need to add the <>. - -[float] -==== Build the visualization - -Drag and drop your data onto the visualization builder pane. - -. Select the *kibana_sample_data_ecommerce* index pattern. - -. Click image:images/time-filter-calendar.png[], then click *Last 7 days*. -+ -The fields in the data panel update. - -. Drag and drop the *taxful_total_price* data field to the visualization builder pane. -+ -[role="screenshot"] -image::images/lens_tutorial_1.png[Lens tutorial] - -To display the average order prices over time, *Lens* automatically added in *order_date* field. - -To break down your data, drag the *category.keyword* field to the visualization builder pane. Lens -knows that you want to show the top categories and compare them across the dates, -and creates a chart that compares the sales for each of the top three categories: - -[role="screenshot"] -image::images/lens_tutorial_2.png[Lens tutorial] - -[float] -[[customize-lens-visualization]] -==== Customize your visualization - -Make your visualization look exactly how you want with the customization options. - -. Click *Average of taxful_total_price*, then change the *Label* to `Sales`. -+ -[role="screenshot"] -image::images/lens_tutorial_3.1.png[Lens tutorial] - -. Click *Top values of category.keyword*, then change *Number of values* to `10`. -+ -[role="screenshot"] -image::images/lens_tutorial_3.2.png[Lens tutorial] -+ -The visualization updates to show there are only six available categories. -+ -Look at the *Suggestions*. An area chart is not an option, but for the sales data, a stacked area chart might be the best option. - -. To switch the chart type, click *Stacked bar chart* in the column, then click *Stacked area* from the *Select a visualizations* window. -+ -[role="screenshot"] -image::images/lens_tutorial_3.png[Lens tutorial] - -[float] -[[lens-tutorial-next-steps]] -==== Next steps - -Now that you've created your visualization, you can add it to a dashboard or Canvas workpad. - -For more information, refer to <> or <>. diff --git a/docs/visualize/most-frequent.asciidoc b/docs/visualize/most-frequent.asciidoc deleted file mode 100644 index f716930e7e65c..0000000000000 --- a/docs/visualize/most-frequent.asciidoc +++ /dev/null @@ -1,59 +0,0 @@ -[[most-frequent]] -== Most frequently used visualizations - -The most frequently used visualizations allow you to plot aggregated data from a <> or <>. - -The most frequently used visualizations include: - -* Line, area, and bar charts -* Pie chart -* Data table -* Metric, goal, and gauge -* Tag cloud - -[[metric-chart]] - -[float] -=== Configure your visualization - -You configure visualizations using the default editor. Each visualization supports different configurations of the metrics and buckets. - -For example, a bar chart allows you to add an x-axis: - -[role="screenshot"] -image::images/add-bucket.png["",height=478] - -A common configuration for the x-axis is to use a {es} {ref}/search-aggregations-bucket-datehistogram-aggregation.html[date histogram] aggregation: - -[role="screenshot"] -image::images/visualize-date-histogram.png[] - -To see your changes, click *Apply changes* image:images/apply-changes-button.png[] - -If it's supported by the visualization, you can add more buckets. In this example we have -added a -{es} {ref}/search-aggregations-bucket-terms-aggregation.html[terms] aggregation on the field -`geo.src` to show the top 5 sources of log traffic. - -[role="screenshot"] -image::images/visualize-date-histogram-split-1.png[] - -The new aggregation is added after the first one, so the result shows -the top 5 sources of traffic per 3 hours. If you want to change the aggregation order, you can do -so by dragging: - -[role="screenshot"] -image::images/visualize-drag-reorder.png["",width=366] - -The visualization -now shows the top 5 sources of traffic overall, and compares them in 3 hour increments: - -[role="screenshot"] -image::images/visualize-date-histogram-split-2.png[] - -For more information about how aggregations are used in visualizations, see <>. - -Each visualization also has its own customization options. Most visualizations allow you to customize the color of a specific series: - -[role="screenshot"] -image::images/color-picker.png[An array of color dots that users can select,height=267] diff --git a/docs/visualize/tilemap.asciidoc b/docs/visualize/tilemap.asciidoc deleted file mode 100644 index c889bd0bb6ca0..0000000000000 --- a/docs/visualize/tilemap.asciidoc +++ /dev/null @@ -1,27 +0,0 @@ -[[heat-map]] -== Heat map - -Display graphical representations of data where the individual values are represented by colors. Use heat maps when your data set includes categorical data. For example, use a heat map to see the flights of origin countries compared to destination countries using the sample flight data. - -[role="screenshot"] -image::images/visualize_heat_map_example.png[] - -[float] -[[navigate-heatmap]] -=== Change the color ranges - -When only one color displays on the heat map, you might need to change the color ranges. - -To specify the number of color ranges: - -. Click *Options*. - -. Enter the *Number of colors* to display. - -To specify custom ranges: - -. Click *Options*. - -. Select *Use custom ranges*. - -. Enter the ranges to display. diff --git a/docs/visualize/timelion.asciidoc b/docs/visualize/timelion.asciidoc deleted file mode 100644 index 4869664fab0a4..0000000000000 --- a/docs/visualize/timelion.asciidoc +++ /dev/null @@ -1,547 +0,0 @@ -[[timelion]] -== Timelion - -Timelion is a time series data visualizer that enables you to combine totally -independent data sources within a single visualization. It's driven by a simple -expression language you use to retrieve time series data, perform calculations -to tease out the answers to complex questions, and visualize the results. - -For example, Timelion enables you to easily get the answers to questions like: - -* <> -* <> -* <> - -[float] -[[time-series-before-you-begin]] -=== Before you begin - -In this tutorial, you'll use the time series data from https://www.elastic.co/guide/en/beats/metricbeat/current/index.html[Metricbeat]. To ingest the data locally, link:https://www.elastic.co/downloads/beats/metricbeat[download Metricbeat]. - -[float] -[[time-series-intro]] -=== Create time series visualizations - -To compare the real-time percentage of CPU time spent in user space to the results offset by one hour, create a time series visualization. - -[float] -[[time-series-define-functions]] -==== Define the functions - -To start tracking the real-time percentage of CPU, enter the following in the *Timelion Expression* field: - -[source,text] ----------------------------------- -.es(index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct') ----------------------------------- - -[role="screenshot"] -image::images/timelion-create01.png[] -{nbsp} - -[float] -[[time-series-compare-data]] -==== Compare the data - -To compare the two data sets, add another series with data from the previous hour, separated by a comma: - -[source,text] ----------------------------------- -.es(index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct'), -.es(offset=-1h, <1> - index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct') ----------------------------------- - -<1> `offset` offsets the data retrieval by a date expression. In this example, `-1h` offsets the data back by one hour. - -[role="screenshot"] -image::images/timelion-create02.png[] -{nbsp} - -[float] -[[time-series-add-labels]] -==== Add label names - -To easily distinguish between the two data sets, add the label names: - -[source,text] ----------------------------------- -.es(offset=-1h,index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct').label('last hour'), -.es(index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct').label('current hour') <1> ----------------------------------- - -<1> `.label()` adds custom labels to the visualization. - -[role="screenshot"] -image::images/timelion-create03.png[] -{nbsp} - -[float] -[[time-series-title]] -==== Add a title - -Add a meaningful title: - -[source,text] ----------------------------------- -.es(offset=-1h, - index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct') - .label('last hour'), -.es(index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct') - .label('current hour') - .title('CPU usage over time') <1> ----------------------------------- - -<1> `.title()` adds a title with a meaningful name. Titles make is easier for unfamiliar users to understand the purpose of the visualization. - -[role="screenshot"] -image::images/timelion-customize01.png[] -{nbsp} - -[float] -[[time-series-change-chart-type]] -==== Change the chart type - -To differentiate between the current hour data and the last hour data, change the chart type: - -[source,text] ----------------------------------- -.es(offset=-1h, - index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct') - .label('last hour') - .lines(fill=1,width=0.5), <1> -.es(index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct') - .label('current hour') - .title('CPU usage over time') ----------------------------------- - -<1> `.lines()` changes the appearance of the chart lines. In this example, `.lines(fill=1,width=0.5)` sets the fill level to `1`, and the border width to `0.5`. - -[role="screenshot"] -image::images/timelion-customize02.png[] -{nbsp} - -[float] -[[time-series-change-color]] -==== Change the line colors - -To make the current hour data stand out, change the line colors: - -[source,text] ----------------------------------- -.es(offset=-1h, - index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct') - .label('last hour') - .lines(fill=1,width=0.5) - .color(gray), <1> -.es(index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct') - .label('current hour') - .title('CPU usage over time') - .color(#1E90FF) ----------------------------------- - -<1> `.color()` changes the color of the data. Supported color types include standard color names, hexadecimal values, or a color schema for grouped data. In this example, `.color(gray)` represents the last hour, and `.color(#1E90FF)` represents the current hour. - -[role="screenshot"] -image::images/timelion-customize03.png[] -{nbsp} - -[float] -[[time-series-adjust-legend]] -==== Make adjustments to the legend - -Change the position and style of the legend: - -[source,text] ----------------------------------- -.es(offset=-1h, - index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct') - .label('last hour') - .lines(fill=1,width=0.5) - .color(gray), -.es(index=metricbeat-*, - timefield='@timestamp', - metric='avg:system.cpu.user.pct') - .label('current hour') - .title('CPU usage over time') - .color(#1E90FF) - .legend(columns=2, position=nw) <1> ----------------------------------- - -<1> `.legend()` sets the position and style of the legend. In this example, `.legend(columns=2, position=nw)` places the legend in the north west position of the visualization with two columns. - -[role="screenshot"] -image::images/timelion-customize04.png[] -{nbsp} - -[float] -[[mathematical-functions-intro]] -=== Create visualizations with mathematical functions - -To create a visualization for inbound and outbound network traffic, use mathematical functions. - -[float] -[[mathematical-functions-define-functions]] -==== Define the functions - -To start tracking the inbound and outbound network traffic, enter the following in the *Timelion Expression* field: - -[source,text] ----------------------------------- -.es(index=metricbeat*, - timefield=@timestamp, - metric=max:system.network.in.bytes) ----------------------------------- - -[role="screenshot"] -image::images/timelion-math01.png[] -{nbsp} - -[float] -[[mathematical-functions-plot-change]] -==== Plot the rate of change - -Change how the data is displayed so that you can easily monitor the inbound traffic: - -[source,text] ----------------------------------- -.es(index=metricbeat*, - timefield=@timestamp, - metric=max:system.network.in.bytes) - .derivative() <1> ----------------------------------- - -<1> `.derivative` plots the change in values over time. - -[role="screenshot"] -image::images/timelion-math02.png[] -{nbsp} - -Add a similar calculation for outbound traffic: - -[source,text] ----------------------------------- -.es(index=metricbeat*, - timefield=@timestamp, - metric=max:system.network.in.bytes) - .derivative(), -.es(index=metricbeat*, - timefield=@timestamp, - metric=max:system.network.out.bytes) - .derivative() - .multiply(-1) <1> ----------------------------------- - -<1> `.multiply()` multiplies the data series by a number, the result of a data series, or a list of data series. For this example, `.multiply(-1)` converts the outbound network traffic to a negative value since the outbound network traffic is leaving your machine. - -[role="screenshot"] -image::images/timelion-math03.png[] -{nbsp} - -[float] -[[mathematical-functions-convert-data]] -==== Change the data metric - -To make the visualization easier to analyze, change the data metric from bytes to megabytes: - -[source,text] ----------------------------------- -.es(index=metricbeat*, - timefield=@timestamp, - metric=max:system.network.in.bytes) - .derivative() - .divide(1048576), -.es(index=metricbeat*, - timefield=@timestamp, - metric=max:system.network.out.bytes) - .derivative() - .multiply(-1) - .divide(1048576) <1> ----------------------------------- - -<1> `.divide()` accepts the same input as `.multiply()`, then divides the data series by the defined divisor. - -[role="screenshot"] -image::images/timelion-math04.png[] -{nbsp} - -[float] -[[mathematical-functions-add-labels]] -==== Customize and format the visualization - -Customize and format the visualization using functions: - -[source,text] ----------------------------------- -.es(index=metricbeat*, - timefield=@timestamp, - metric=max:system.network.in.bytes) - .derivative() - .divide(1048576) - .lines(fill=2, width=1) - .color(green) - .label("Inbound traffic") <1> - .title("Network traffic (MB/s)"), <2> -.es(index=metricbeat*, - timefield=@timestamp, - metric=max:system.network.out.bytes) - .derivative() - .multiply(-1) - .divide(1048576) - .lines(fill=2, width=1) <3> - .color(blue) <4> - .label("Outbound traffic") - .legend(columns=2, position=nw) <5> ----------------------------------- - -<1> `.label()` adds custom labels to the visualization. -<2> `.title()` adds a title with a meaningful name. -<3> `.lines()` changes the appearance of the chart lines. In this example, `.lines(fill=2, width=1)` sets the fill level to `2`, and the border width to `1`. -<4> `.color()` changes the color of the data. Supported color types include standard color names, hexadecimal values, or a color schema for grouped data. In this example, `.color(green)` represents the inbound network traffic, and `.color(blue)` represents the outbound network traffic. -<5> `.legend()` sets the position and style of the legend. For this example, `legend(columns=2, position=nw)` places the legend in the north west position of the visualization with two columns. - -[role="screenshot"] -image::images/timelion-math05.png[] -{nbsp} - -[float] -[[timelion-conditional-intro]] -=== Create visualizations with conditional logic and tracking trends - -To easily detect outliers and discover patterns over time, modify time series data with conditional logic and create a trend with a moving average. - -With Timelion conditional logic, you can use the following operator values to compare your data: - -[horizontal] -`eq`:: equal -`ne`:: not equal -`lt`:: less than -`lte`:: less than or equal to -`gt`:: greater than -`gte`:: greater than or equal to - -[float] -[[conditional-define-functions]] -==== Define the functions - -To chart the maximum value of `system.memory.actual.used.bytes`, enter the following in the *Timelion Expression* field: - -[source,text] ----------------------------------- -.es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes') ----------------------------------- - -[role="screenshot"] -image::images/timelion-conditional01.png[] -{nbsp} - -[float] -[[conditional-track-memory]] -==== Track used memory - -To track the amount of memory used, create two thresholds: - -[source,text] ----------------------------------- -.es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes'), -.es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes') - .if(gt, <1> - 11300000000, <2> - .es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes'), - null) - .label('warning') - .color('#FFCC11'), -.es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes') - .if(gt, - 11375000000, - .es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes'), - null) - .label('severe') - .color('red') ----------------------------------- - -<1> Timelion conditional logic for the _greater than_ operator. In this example, the warning threshold is 11.3GB (`11300000000`), and the severe threshold is 11.375GB (`11375000000`). If the threshold values are too high or low for your machine, adjust the values accordingly. -<2> `if()` compares each point to a number. If the condition evaluates to `true`, adjust the styling. If the condition evaluates to `false`, use the default styling. - -[role="screenshot"] -image::images/timelion-conditional02.png[] -{nbsp} - -[float] -[[conditional-determine-trend]] -==== Determine the trend - -To determine the trend, create a new data series: - -[source,text] ----------------------------------- -.es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes'), -.es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes') - .if(gt,11300000000, - .es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes'), - null) - .label('warning') - .color('#FFCC11'), -.es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes') - .if(gt,11375000000, - .es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes'), - null). - label('severe') - .color('red'), -.es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes') - .mvavg(10) <1> ----------------------------------- - -<1> `mvavg()` calculates the moving average over a specified period of time. In this example, `.mvavg(10)` creates a moving average with a window of 10 data points. - -[role="screenshot"] -image::images/timelion-conditional03.png[] -{nbsp} - -[float] -[[conditional-format-visualization]] -==== Customize and format the visualization - -Customize and format the visualization using functions: - -[source,text] ----------------------------------- -.es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes') - .label('max memory') <1> - .title('Memory consumption over time'), <2> -.es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes') - .if(gt, - 11300000000, - .es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes'), - null) - .label('warning') - .color('#FFCC11') <3> - .lines(width=5), <4> -.es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes') - .if(gt, - 11375000000, - .es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes'), - null) - .label('severe') - .color('red') - .lines(width=5), -.es(index=metricbeat-*, - timefield='@timestamp', - metric='max:system.memory.actual.used.bytes') - .mvavg(10) - .label('mvavg') - .lines(width=2) - .color(#5E5E5E) - .legend(columns=4, position=nw) <5> ----------------------------------- - -<1> `.label()` adds custom labels to the visualization. -<2> `.title()` adds a title with a meaningful name. -<3> `.color()` changes the color of the data. Supported color types include standard color names, hexadecimal values, or a color schema for grouped data. -<4> `.lines()` changes the appearance of the chart lines. In this example, .lines(width=5) sets border width to `5`. -<5> `.legend()` sets the position and style of the legend. For this example, `(columns=4, position=nw)` places the legend in the north west position of the visualization with four columns. - -[role="screenshot"] -image::images/timelion-conditional04.png[] -{nbsp} - -For additional information on Timelion conditional capabilities, go to https://www.elastic.co/blog/timeseries-if-then-else-with-timelion[I have but one .condition()]. - -[float] -[[timelion-deprecation]] -=== Timelion App deprecation - -Deprecated since 7.0, the Timelion app will be removed in 8.0. If you have any Timelion worksheets, you must migrate them to a dashboard. - -NOTE: Only the Timelion app is deprecated. {kib} continues to support Timelion visualizations on dashboards, in Visualize, and in Canvas. - -[float] -[[timelion-app-to-vis]] -==== Create a dashboard from a Timelion worksheet - -To replace a Timelion worksheet with a dashboard, follow the same process for adding a visualization. -In addition, you must migrate the Timelion graphs to Visualize. - -. Open the menu, click **Dashboard**, then click **Create dashboard**. - -. On the dashboard, click **Create New**, then select the Timelion visualization. -+ -[role="screenshot"] -image::images/timelion-create-new-dashboard.png[] -+ -The only thing you need is the Timelion expression for each graph. - -. Open the Timelion app on a new tab, select the chart you want to copy, and copy its expression. -+ -[role="screenshot"] -image::images/timelion-copy-expression.png[] - -. Return to the other tab and paste the copied expression to the *Timelion Expression* field and click **Update**. -+ -[role="screenshot"] -image::images/timelion-vis-paste-expression.png[] - -. Save the new visualization, give it a name, and click **Save and Return**. -+ -Your Timelion visualization will appear on the dashboard. Repeat this for all your charts on each worksheet. -+ -[role="screenshot"] -image::images/timelion-dashboard.png[] diff --git a/docs/visualize/tsvb.asciidoc b/docs/visualize/tsvb.asciidoc deleted file mode 100644 index 9a1e81670b654..0000000000000 --- a/docs/visualize/tsvb.asciidoc +++ /dev/null @@ -1,138 +0,0 @@ -[[TSVB]] -== TSVB - -TSVB is a time series data visualizer that allows you to use the full power of the -Elasticsearch aggregation framework. With TSVB, you can combine an infinite -number of aggregations to display complex data. - -NOTE: In Elasticsearch version 7.3.0 and later, the time series data visualizer is now referred to as TSVB instead of Time Series Visual Builder. - -[float] -[[tsvb-visualization-types]] -=== Types of TSVB visualizations - -TSVB comes with these types of visualizations: - -Time Series:: A histogram visualization that supports area, line, bar, and steps along with multiple y-axis. - -[role="screenshot"] -image:images/tsvb-screenshot.png["Time series visualization"] - -Metric:: A metric that displays the latest number in a data series. - -[role="screenshot"] -image:images/tsvb-metric.png["Metric visualization"] - -Top N:: A horizontal bar chart where the y-axis is based on a series of metrics, and the x-axis is the latest value in the series. - -[role="screenshot"] -image:images/tsvb-top-n.png["Top N visualization"] - -Gauge:: A single value gauge visualization based on the latest value in a series. - -[role="screenshot"] -image:images/tsvb-gauge.png["Gauge visualization"] - -Markdown:: Edit the data using using Markdown text and Mustache template syntax. - -[role="screenshot"] -image:images/tsvb-markdown.png["Markdown visualization"] - -Table:: Display data from multiple time series by defining the field group to show in the rows, and the columns of data to display. - -[role="screenshot"] -image:images/tsvb-table.png["Table visualization"] - -[float] -[[create-tsvb-visualization]] -=== Create TSVB visualizations - -To create a TSVB visualization, choose the data series you want to display, then choose how you want to display the data. The options available are dependent on the visualization. - -[float] -[[tsvb-data-series-options]] -==== Configure the data series - -To create a single metric, add multiple data series with multiple aggregations. - -. Select the visualization type. - -. Specify the data series labels and colors. - -.. Select *Data*. -+ -If you are using the *Table* visualization, select *Columns*. - -.. In the *Label* field, enter a name for the data series, which is used on legends and titles. -+ -For series that are grouped by a term, you can specify a mustache variable of `{{key}}` to substitute the term. - -.. If supported by the visualization, click the swatch and choose a color for the data series. - -.. To add another data series, click *+*, then repeat the steps to specify the labels and colors. - -. Specify the data series metrics. - -.. Select *Metrics*. - -.. From the dropdown lists, choose your options. - -.. To add another metric, click *+*. -+ -When you add more than one metric, the last metric value is displayed, which is indicated by the eye icon. - -. To specify the format and display options, select *Options*. - -. To specify how to group or split the data, choose an option from the *Group by* drop down list. -+ -By default, the data series are grouped by everything. - -[float] -[[tsvb-panel-options]] -==== Configure the panel - -Change the data that you want to display and choose the style options for the panel. - -. Select *Panel options*. - -. Under *Data*, specify how much of the data that you want to display in the visualization. - -. Under *Style*, specify how you want the visualization to look. - -[float] -[[tsvb-add-annotations]] -==== Add annotations - -If you are using the Time Series visualization, add annotation data sources. - -. Select *Annotations*. - -. Click *Add data source*, then specify the options. - -[float] -[[tsvb-enter-markdown]] -==== Enter Markdown text - -Edit the source for the Markdown visualization. - -. Select *Markdown*. - -. In the editor, enter enter your Markdown text, then press Enter. - -. To insert the mustache template variable into the editor, click the variable name. -+ -The http://mustache.github.io/mustache.5.html[mustache syntax] uses the Handlebar.js processor, which is an extended version of the Mustache template language. - -[float] -[[tsvb-style-markdown]] -==== Style Markdown text - -Style your Markdown visualization using http://lesscss.org/features/[less syntax]. - -. Select *Markdown*. - -. Select *Panel options*. - -. Enter styling rules in *Custom CSS* section -+ -Less in TSVB does not support custom plugins or inline JavaScript. diff --git a/package.json b/package.json index cbf8fd6bc3bd1..ff487510f7a32 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "**/istanbul-instrumenter-loader/schema-utils": "1.0.0", "**/image-diff/gm/debug": "^2.6.9", "**/load-grunt-config/lodash": "^4.17.20", + "**/node-jose/node-forge": "^0.10.0", "**/react-dom": "^16.12.0", "**/react": "^16.12.0", "**/react-test-renderer": "^16.12.0", @@ -191,7 +192,7 @@ "moment-timezone": "^0.5.27", "mustache": "2.3.2", "node-fetch": "1.7.3", - "node-forge": "^0.9.1", + "node-forge": "^0.10.0", "opn": "^5.5.0", "oppsy": "^2.0.0", "p-map": "^4.0.0", @@ -305,7 +306,7 @@ "@types/moment-timezone": "^0.5.12", "@types/mustache": "^0.8.31", "@types/node": ">=10.17.17 <10.20.0", - "@types/node-forge": "^0.9.0", + "@types/node-forge": "^0.9.5", "@types/normalize-path": "^3.0.0", "@types/opn": "^5.1.0", "@types/pegjs": "^0.10.1", diff --git a/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js b/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js index d4e234e3a6a2e..60a03ae8a104e 100755 --- a/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js +++ b/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js @@ -27,11 +27,7 @@ exports.getWebpackConfig = function (kibanaPath) { mainFields: ['browser', 'main'], modules: ['node_modules', resolve(kibanaPath, 'node_modules')], alias: { - // Kibana defaults https://github.com/elastic/kibana/blob/6998f074542e8c7b32955db159d15661aca253d7/src/legacy/ui/ui_bundler_env.js#L30-L36 - ui: resolve(kibanaPath, 'src/legacy/ui/public'), - // Dev defaults for test bundle https://github.com/elastic/kibana/blob/6998f074542e8c7b32955db159d15661aca253d7/src/core_plugins/tests_bundle/index.js#L73-L78 - ng_mock$: resolve(kibanaPath, 'src/test_utils/public/ng_mock'), fixtures: resolve(kibanaPath, 'src/fixtures'), test_utils: resolve(kibanaPath, 'src/test_utils/public'), }, diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts index 96fd525efa3ec..2e40aeec4f43d 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts @@ -21,7 +21,6 @@ import { ToolingLog } from '@kbn/dev-utils'; import { defaultsDeep } from 'lodash'; import { Config } from './config'; -import { transformDeprecations } from './transform_deprecations'; const cache = new WeakMap(); @@ -52,8 +51,7 @@ async function getSettingsFromFile(log: ToolingLog, path: string, settingOverrid await cache.get(configProvider)! ); - const logDeprecation = (error: string | Error) => log.error(error); - return transformDeprecations(settingsWithDefaults, logDeprecation); + return settingsWithDefaults; } export async function readConfigFile(log: ToolingLog, path: string, settingOverrides: any = {}) { diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/transform_deprecations.ts b/packages/kbn-test/src/functional_test_runner/lib/config/transform_deprecations.ts deleted file mode 100644 index 08dfc4a4f61f4..0000000000000 --- a/packages/kbn-test/src/functional_test_runner/lib/config/transform_deprecations.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// @ts-ignore -import { createTransform, Deprecations } from '../../../../../../src/legacy/deprecation'; - -type DeprecationTransformer = ( - settings: object, - log: (msg: string) => void -) => { - [key: string]: any; -}; - -export const transformDeprecations: DeprecationTransformer = createTransform([ - Deprecations.unused('servers.webdriver'), -]); diff --git a/rfcs/text/0010_service_status.md b/rfcs/text/0010_service_status.md index 76195c4f1ab89..ded594930a367 100644 --- a/rfcs/text/0010_service_status.md +++ b/rfcs/text/0010_service_status.md @@ -137,7 +137,7 @@ interface StatusSetup { * Current status for all dependencies of the current plugin. * Each key of the `Record` is a plugin id. */ - dependencies$: Observable>; + plugins$: Observable>; /** * The status of this plugin as derived from its dependencies. diff --git a/src/core/server/legacy/config/get_unused_config_keys.test.ts b/src/core/server/legacy/config/get_unused_config_keys.test.ts index 2106a0748d814..f8506b5744030 100644 --- a/src/core/server/legacy/config/get_unused_config_keys.test.ts +++ b/src/core/server/legacy/config/get_unused_config_keys.test.ts @@ -217,34 +217,4 @@ describe('getUnusedConfigKeys', () => { }) ).toEqual([]); }); - - describe('using deprecation', () => { - it('should use the plugin deprecations provider', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - pluginSpecs: [ - ({ - getDeprecationsProvider() { - return async ({ rename }: any) => [rename('foo1', 'foo2')]; - }, - getConfigPrefix: () => 'foo', - } as unknown) as LegacyPluginSpec, - ], - disabledPluginSpecs: [], - settings: { - foo: { - foo: 'dolly', - foo1: 'bar', - }, - }, - legacyConfig: getConfig({ - foo: { - foo2: 'bar', - }, - }), - }) - ).toEqual(['foo.foo']); - }); - }); }); diff --git a/src/core/server/legacy/config/get_unused_config_keys.ts b/src/core/server/legacy/config/get_unused_config_keys.ts index 354bf9af042cf..d10c574f04974 100644 --- a/src/core/server/legacy/config/get_unused_config_keys.ts +++ b/src/core/server/legacy/config/get_unused_config_keys.ts @@ -17,10 +17,7 @@ * under the License. */ -import { set } from '@elastic/safer-lodash-set'; -import { difference, get } from 'lodash'; -// @ts-expect-error -import { getTransform } from '../../../../legacy/deprecation/index'; +import { difference } from 'lodash'; import { unset } from '../../../../legacy/utils'; import { getFlattenedObject } from '../../../utils'; import { hasConfigPathIntersection } from '../../config'; @@ -41,21 +38,6 @@ export async function getUnusedConfigKeys({ settings: LegacyVars; legacyConfig: LegacyConfig; }) { - // transform deprecated plugin settings - for (let i = 0; i < pluginSpecs.length; i++) { - const spec = pluginSpecs[i]; - const transform = await getTransform(spec); - const prefix = spec.getConfigPrefix(); - - // nested plugin prefixes (a.b) translate to nested objects - const pluginSettings = get(settings, prefix); - if (pluginSettings) { - // flattened settings are expected to be converted to nested objects - // a.b = true => { a: { b: true }} - set(settings, prefix, transform(pluginSettings)); - } - } - // remove config values from disabled plugins for (const spec of disabledPluginSpecs) { unset(settings, spec.getConfigPrefix()); diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 7d5557be92b30..adfdecdd7c976 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -323,9 +323,6 @@ export class LegacyService implements CoreService { status: { core$: setupDeps.core.status.core$, overall$: setupDeps.core.status.overall$, - set: setupDeps.core.status.plugins.set.bind(null, 'legacy'), - dependencies$: setupDeps.core.status.plugins.getDependenciesStatus$('legacy'), - derivedStatus$: setupDeps.core.status.plugins.getDerivedStatus$('legacy'), }, uiSettings: { register: setupDeps.core.uiSettings.register, diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index eb31b2380d177..fa2659ca130a0 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -185,9 +185,6 @@ export function createPluginSetupContext( status: { core$: deps.status.core$, overall$: deps.status.overall$, - set: deps.status.plugins.set.bind(null, plugin.name), - dependencies$: deps.status.plugins.getDependenciesStatus$(plugin.name), - derivedStatus$: deps.status.plugins.getDerivedStatus$(plugin.name), }, uiSettings: { register: deps.uiSettings.register, diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts index 71ac31db13f92..7af77491df1ab 100644 --- a/src/core/server/plugins/plugins_system.test.ts +++ b/src/core/server/plugins/plugins_system.test.ts @@ -100,27 +100,15 @@ test('getPluginDependencies returns dependency tree of symbols', () => { pluginsSystem.addPlugin(createPlugin('no-dep')); expect(pluginsSystem.getPluginDependencies()).toMatchInlineSnapshot(` - Object { - "asNames": Map { - "plugin-a" => Array [ - "no-dep", - ], - "plugin-b" => Array [ - "plugin-a", - "no-dep", - ], - "no-dep" => Array [], - }, - "asOpaqueIds": Map { - Symbol(plugin-a) => Array [ - Symbol(no-dep), - ], - Symbol(plugin-b) => Array [ - Symbol(plugin-a), - Symbol(no-dep), - ], - Symbol(no-dep) => Array [], - }, + Map { + Symbol(plugin-a) => Array [ + Symbol(no-dep), + ], + Symbol(plugin-b) => Array [ + Symbol(plugin-a), + Symbol(no-dep), + ], + Symbol(no-dep) => Array [], } `); }); diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index b2acd9a6fd04b..f5c1b35d678a3 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -20,11 +20,10 @@ import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { PluginWrapper } from './plugin'; -import { DiscoveredPlugin, PluginName } from './types'; +import { DiscoveredPlugin, PluginName, PluginOpaqueId } from './types'; import { createPluginSetupContext, createPluginStartContext } from './plugin_context'; import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; import { withTimeout } from '../../utils'; -import { PluginDependencies } from '.'; const Sec = 1000; /** @internal */ @@ -46,19 +45,9 @@ export class PluginsSystem { * @returns a ReadonlyMap of each plugin and an Array of its available dependencies * @internal */ - public getPluginDependencies(): PluginDependencies { - const asNames = new Map( - [...this.plugins].map(([name, plugin]) => [ - plugin.name, - [ - ...new Set([ - ...plugin.requiredPlugins, - ...plugin.optionalPlugins.filter((optPlugin) => this.plugins.has(optPlugin)), - ]), - ].map((depId) => this.plugins.get(depId)!.name), - ]) - ); - const asOpaqueIds = new Map( + public getPluginDependencies(): ReadonlyMap { + // Return dependency map of opaque ids + return new Map( [...this.plugins].map(([name, plugin]) => [ plugin.opaqueId, [ @@ -69,8 +58,6 @@ export class PluginsSystem { ].map((depId) => this.plugins.get(depId)!.opaqueId), ]) ); - - return { asNames, asOpaqueIds }; } public async setupPlugins(deps: PluginsServiceSetupDeps) { diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index 517261b5bc9bb..eb2a9ca3daf5f 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -93,12 +93,6 @@ export type PluginName = string; /** @public */ export type PluginOpaqueId = symbol; -/** @internal */ -export interface PluginDependencies { - asNames: ReadonlyMap; - asOpaqueIds: ReadonlyMap; -} - /** * Describes the set of required and optional properties plugin can define in its * mandatory JSON manifest file. diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 2128eb077211f..1123433e30ac5 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2858,17 +2858,10 @@ export type SharedGlobalConfig = RecursiveReadonly<{ // @public export type StartServicesAccessor = () => Promise<[CoreStart, TPluginsStart, TStart]>; -// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ServiceStatusSetup" -// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ServiceStatusSetup" -// // @public export interface StatusServiceSetup { core$: Observable; - dependencies$: Observable>; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "StatusSetup" - derivedStatus$: Observable; overall$: Observable; - set(status$: Observable): void; } // @public @@ -2961,8 +2954,8 @@ export const validBodyOutput: readonly ["data", "stream"]; // src/core/server/legacy/types.ts:165:3 - (ae-forgotten-export) The symbol "LegacyNavLinkSpec" needs to be exported by the entry point index.d.ts // src/core/server/legacy/types.ts:166:3 - (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts // src/core/server/legacy/types.ts:167:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:272:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:272:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:274:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:268:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 1bd364c2f87b7..417f66a2988c2 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -41,7 +41,6 @@ import { Server } from './server'; import { getEnvOptions } from './config/__mocks__/env'; import { loggingSystemMock } from './logging/logging_system.mock'; import { rawConfigServiceMock } from './config/raw_config_service.mock'; -import { PluginName } from './plugins'; const env = new Env('.', getEnvOptions()); const logger = loggingSystemMock.create(); @@ -50,7 +49,7 @@ const rawConfigService = rawConfigServiceMock.create({}); beforeEach(() => { mockConfigService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); mockPluginsService.discover.mockResolvedValue({ - pluginTree: { asOpaqueIds: new Map(), asNames: new Map() }, + pluginTree: new Map(), uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() }, }); }); @@ -99,7 +98,7 @@ test('injects legacy dependency to context#setup()', async () => { [pluginB, [pluginA]], ]); mockPluginsService.discover.mockResolvedValue({ - pluginTree: { asOpaqueIds: pluginDependencies, asNames: new Map() }, + pluginTree: pluginDependencies, uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() }, }); @@ -114,31 +113,6 @@ test('injects legacy dependency to context#setup()', async () => { }); }); -test('injects legacy dependency to status#setup()', async () => { - const server = new Server(rawConfigService, env, logger); - - const pluginDependencies = new Map([ - ['a', []], - ['b', ['a']], - ]); - mockPluginsService.discover.mockResolvedValue({ - pluginTree: { asOpaqueIds: new Map(), asNames: pluginDependencies }, - uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() }, - }); - - await server.setup(); - - expect(mockStatusService.setup).toHaveBeenCalledWith({ - elasticsearch: expect.any(Object), - savedObjects: expect.any(Object), - pluginDependencies: new Map([ - ['a', []], - ['b', ['a']], - ['legacy', ['a', 'b']], - ]), - }); -}); - test('runs services on "start"', async () => { const server = new Server(rawConfigService, env, logger); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index e2f77f0551f34..cc6d8171e7a03 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -121,13 +121,10 @@ export class Server { const contextServiceSetup = this.context.setup({ // We inject a fake "legacy plugin" with dependencies on every plugin so that legacy plugins: - // 1) Can access context from any KP plugin + // 1) Can access context from any NP plugin // 2) Can register context providers that will only be available to other legacy plugins and will not leak into // New Platform plugins. - pluginDependencies: new Map([ - ...pluginTree.asOpaqueIds, - [this.legacy.legacyId, [...pluginTree.asOpaqueIds.keys()]], - ]), + pluginDependencies: new Map([...pluginTree, [this.legacy.legacyId, [...pluginTree.keys()]]]), }); const auditTrailSetup = this.auditTrail.setup(); @@ -157,12 +154,6 @@ export class Server { const statusSetup = await this.status.setup({ elasticsearch: elasticsearchServiceSetup, - // We inject a fake "legacy plugin" with dependencies on every plugin so that legacy can access plugin status from - // any KP plugin - pluginDependencies: new Map([ - ...pluginTree.asNames, - ['legacy', [...pluginTree.asNames.keys()]], - ]), savedObjects: savedObjectsSetup, }); diff --git a/src/core/server/status/get_summary_status.test.ts b/src/core/server/status/get_summary_status.test.ts index d97083162b502..7516e82ee784d 100644 --- a/src/core/server/status/get_summary_status.test.ts +++ b/src/core/server/status/get_summary_status.test.ts @@ -94,38 +94,6 @@ describe('getSummaryStatus', () => { describe('summary', () => { describe('when a single service is at highest level', () => { it('returns all information about that single service', () => { - expect( - getSummaryStatus( - Object.entries({ - s1: degraded, - s2: { - level: ServiceStatusLevels.unavailable, - summary: 'Lorem ipsum', - meta: { - custom: { data: 'here' }, - }, - }, - }) - ) - ).toEqual({ - level: ServiceStatusLevels.unavailable, - summary: '[s2]: Lorem ipsum', - detail: 'See the status page for more information', - meta: { - affectedServices: { - s2: { - level: ServiceStatusLevels.unavailable, - summary: 'Lorem ipsum', - meta: { - custom: { data: 'here' }, - }, - }, - }, - }, - }); - }); - - it('allows the single service to override the detail and documentationUrl fields', () => { expect( getSummaryStatus( Object.entries({ @@ -147,17 +115,7 @@ describe('getSummaryStatus', () => { detail: 'Vivamus pulvinar sem ac luctus ultrices.', documentationUrl: 'http://helpmenow.com/problem1', meta: { - affectedServices: { - s2: { - level: ServiceStatusLevels.unavailable, - summary: 'Lorem ipsum', - detail: 'Vivamus pulvinar sem ac luctus ultrices.', - documentationUrl: 'http://helpmenow.com/problem1', - meta: { - custom: { data: 'here' }, - }, - }, - }, + custom: { data: 'here' }, }, }); }); diff --git a/src/core/server/status/get_summary_status.ts b/src/core/server/status/get_summary_status.ts index 1dc92839e8261..748a54f0bf8bb 100644 --- a/src/core/server/status/get_summary_status.ts +++ b/src/core/server/status/get_summary_status.ts @@ -23,10 +23,7 @@ import { ServiceStatus, ServiceStatusLevels, ServiceStatusLevel } from './types' * Returns a single {@link ServiceStatus} that summarizes the most severe status level from a group of statuses. * @param statuses */ -export const getSummaryStatus = ( - statuses: Array<[string, ServiceStatus]>, - { allAvailableSummary = `All services are available` }: { allAvailableSummary?: string } = {} -): ServiceStatus => { +export const getSummaryStatus = (statuses: Array<[string, ServiceStatus]>): ServiceStatus => { const grouped = groupByLevel(statuses); const highestSeverityLevel = getHighestSeverityLevel(grouped.keys()); const highestSeverityGroup = grouped.get(highestSeverityLevel)!; @@ -34,18 +31,13 @@ export const getSummaryStatus = ( if (highestSeverityLevel === ServiceStatusLevels.available) { return { level: ServiceStatusLevels.available, - summary: allAvailableSummary, + summary: `All services are available`, }; } else if (highestSeverityGroup.size === 1) { const [serviceName, status] = [...highestSeverityGroup.entries()][0]; return { ...status, summary: `[${serviceName}]: ${status.summary!}`, - // TODO: include URL to status page - detail: status.detail ?? `See the status page for more information`, - meta: { - affectedServices: { [serviceName]: status }, - }, }; } else { return { diff --git a/src/core/server/status/plugins_status.test.ts b/src/core/server/status/plugins_status.test.ts deleted file mode 100644 index b2d2ac8a5ef90..0000000000000 --- a/src/core/server/status/plugins_status.test.ts +++ /dev/null @@ -1,338 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { PluginName } from '../plugins'; -import { PluginsStatusService } from './plugins_status'; -import { of, Observable, BehaviorSubject } from 'rxjs'; -import { ServiceStatusLevels, CoreStatus, ServiceStatus } from './types'; -import { first } from 'rxjs/operators'; -import { ServiceStatusLevelSnapshotSerializer } from './test_utils'; - -expect.addSnapshotSerializer(ServiceStatusLevelSnapshotSerializer); - -describe('PluginStatusService', () => { - const coreAllAvailable$: Observable = of({ - elasticsearch: { level: ServiceStatusLevels.available, summary: 'elasticsearch avail' }, - savedObjects: { level: ServiceStatusLevels.available, summary: 'savedObjects avail' }, - }); - const coreOneDegraded$: Observable = of({ - elasticsearch: { level: ServiceStatusLevels.available, summary: 'elasticsearch avail' }, - savedObjects: { level: ServiceStatusLevels.degraded, summary: 'savedObjects degraded' }, - }); - const coreOneCriticalOneDegraded$: Observable = of({ - elasticsearch: { level: ServiceStatusLevels.critical, summary: 'elasticsearch critical' }, - savedObjects: { level: ServiceStatusLevels.degraded, summary: 'savedObjects degraded' }, - }); - const pluginDependencies: Map = new Map([ - ['a', []], - ['b', ['a']], - ['c', ['a', 'b']], - ]); - - describe('getDerivedStatus$', () => { - it(`defaults to core's most severe status`, async () => { - const serviceAvailable = new PluginsStatusService({ - core$: coreAllAvailable$, - pluginDependencies, - }); - expect(await serviceAvailable.getDerivedStatus$('a').pipe(first()).toPromise()).toEqual({ - level: ServiceStatusLevels.available, - summary: 'All dependencies are available', - }); - - const serviceDegraded = new PluginsStatusService({ - core$: coreOneDegraded$, - pluginDependencies, - }); - expect(await serviceDegraded.getDerivedStatus$('a').pipe(first()).toPromise()).toEqual({ - level: ServiceStatusLevels.degraded, - summary: '[savedObjects]: savedObjects degraded', - detail: 'See the status page for more information', - meta: expect.any(Object), - }); - - const serviceCritical = new PluginsStatusService({ - core$: coreOneCriticalOneDegraded$, - pluginDependencies, - }); - expect(await serviceCritical.getDerivedStatus$('a').pipe(first()).toPromise()).toEqual({ - level: ServiceStatusLevels.critical, - summary: '[elasticsearch]: elasticsearch critical', - detail: 'See the status page for more information', - meta: expect.any(Object), - }); - }); - - it(`provides a summary status when core and dependencies are at same severity level`, async () => { - const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies }); - service.set('a', of({ level: ServiceStatusLevels.degraded, summary: 'a is degraded' })); - expect(await service.getDerivedStatus$('b').pipe(first()).toPromise()).toEqual({ - level: ServiceStatusLevels.degraded, - summary: '[2] services are degraded', - detail: 'See the status page for more information', - meta: expect.any(Object), - }); - }); - - it(`allows dependencies status to take precedence over lower severity core statuses`, async () => { - const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies }); - service.set('a', of({ level: ServiceStatusLevels.unavailable, summary: 'a is not working' })); - expect(await service.getDerivedStatus$('b').pipe(first()).toPromise()).toEqual({ - level: ServiceStatusLevels.unavailable, - summary: '[a]: a is not working', - detail: 'See the status page for more information', - meta: expect.any(Object), - }); - }); - - it(`allows core status to take precedence over lower severity dependencies statuses`, async () => { - const service = new PluginsStatusService({ - core$: coreOneCriticalOneDegraded$, - pluginDependencies, - }); - service.set('a', of({ level: ServiceStatusLevels.unavailable, summary: 'a is not working' })); - expect(await service.getDerivedStatus$('b').pipe(first()).toPromise()).toEqual({ - level: ServiceStatusLevels.critical, - summary: '[elasticsearch]: elasticsearch critical', - detail: 'See the status page for more information', - meta: expect.any(Object), - }); - }); - - it(`allows a severe dependency status to take precedence over a less severe dependency status`, async () => { - const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies }); - service.set('a', of({ level: ServiceStatusLevels.degraded, summary: 'a is degraded' })); - service.set('b', of({ level: ServiceStatusLevels.unavailable, summary: 'b is not working' })); - expect(await service.getDerivedStatus$('c').pipe(first()).toPromise()).toEqual({ - level: ServiceStatusLevels.unavailable, - summary: '[b]: b is not working', - detail: 'See the status page for more information', - meta: expect.any(Object), - }); - }); - }); - - describe('getAll$', () => { - it('defaults to empty record if no plugins', async () => { - const service = new PluginsStatusService({ - core$: coreAllAvailable$, - pluginDependencies: new Map(), - }); - expect(await service.getAll$().pipe(first()).toPromise()).toEqual({}); - }); - - it('defaults to core status when no plugin statuses are set', async () => { - const serviceAvailable = new PluginsStatusService({ - core$: coreAllAvailable$, - pluginDependencies, - }); - expect(await serviceAvailable.getAll$().pipe(first()).toPromise()).toEqual({ - a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, - b: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, - c: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, - }); - - const serviceDegraded = new PluginsStatusService({ - core$: coreOneDegraded$, - pluginDependencies, - }); - expect(await serviceDegraded.getAll$().pipe(first()).toPromise()).toEqual({ - a: { - level: ServiceStatusLevels.degraded, - summary: '[savedObjects]: savedObjects degraded', - detail: 'See the status page for more information', - meta: expect.any(Object), - }, - b: { - level: ServiceStatusLevels.degraded, - summary: '[2] services are degraded', - detail: 'See the status page for more information', - meta: expect.any(Object), - }, - c: { - level: ServiceStatusLevels.degraded, - summary: '[3] services are degraded', - detail: 'See the status page for more information', - meta: expect.any(Object), - }, - }); - - const serviceCritical = new PluginsStatusService({ - core$: coreOneCriticalOneDegraded$, - pluginDependencies, - }); - expect(await serviceCritical.getAll$().pipe(first()).toPromise()).toEqual({ - a: { - level: ServiceStatusLevels.critical, - summary: '[elasticsearch]: elasticsearch critical', - detail: 'See the status page for more information', - meta: expect.any(Object), - }, - b: { - level: ServiceStatusLevels.critical, - summary: '[2] services are critical', - detail: 'See the status page for more information', - meta: expect.any(Object), - }, - c: { - level: ServiceStatusLevels.critical, - summary: '[3] services are critical', - detail: 'See the status page for more information', - meta: expect.any(Object), - }, - }); - }); - - it('uses the manually set status level if plugin specifies one', async () => { - const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies }); - service.set('a', of({ level: ServiceStatusLevels.available, summary: 'a status' })); - - expect(await service.getAll$().pipe(first()).toPromise()).toEqual({ - a: { level: ServiceStatusLevels.available, summary: 'a status' }, // a is available depsite savedObjects being degraded - b: { - level: ServiceStatusLevels.degraded, - summary: '[savedObjects]: savedObjects degraded', - detail: 'See the status page for more information', - meta: expect.any(Object), - }, - c: { - level: ServiceStatusLevels.degraded, - summary: '[2] services are degraded', - detail: 'See the status page for more information', - meta: expect.any(Object), - }, - }); - }); - - it('updates when a new plugin status observable is set', async () => { - const service = new PluginsStatusService({ - core$: coreAllAvailable$, - pluginDependencies: new Map([['a', []]]), - }); - const statusUpdates: Array> = []; - const subscription = service - .getAll$() - .subscribe((pluginStatuses) => statusUpdates.push(pluginStatuses)); - - service.set('a', of({ level: ServiceStatusLevels.degraded, summary: 'a degraded' })); - service.set('a', of({ level: ServiceStatusLevels.unavailable, summary: 'a unavailable' })); - service.set('a', of({ level: ServiceStatusLevels.available, summary: 'a available' })); - subscription.unsubscribe(); - - expect(statusUpdates).toEqual([ - { a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' } }, - { a: { level: ServiceStatusLevels.degraded, summary: 'a degraded' } }, - { a: { level: ServiceStatusLevels.unavailable, summary: 'a unavailable' } }, - { a: { level: ServiceStatusLevels.available, summary: 'a available' } }, - ]); - }); - }); - - describe('getDependenciesStatus$', () => { - it('only includes dependencies of specified plugin', async () => { - const service = new PluginsStatusService({ - core$: coreAllAvailable$, - pluginDependencies, - }); - expect(await service.getDependenciesStatus$('a').pipe(first()).toPromise()).toEqual({}); - expect(await service.getDependenciesStatus$('b').pipe(first()).toPromise()).toEqual({ - a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, - }); - expect(await service.getDependenciesStatus$('c').pipe(first()).toPromise()).toEqual({ - a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, - b: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, - }); - }); - - it('uses the manually set status level if plugin specifies one', async () => { - const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies }); - service.set('a', of({ level: ServiceStatusLevels.available, summary: 'a status' })); - - expect(await service.getDependenciesStatus$('c').pipe(first()).toPromise()).toEqual({ - a: { level: ServiceStatusLevels.available, summary: 'a status' }, // a is available depsite savedObjects being degraded - b: { - level: ServiceStatusLevels.degraded, - summary: '[savedObjects]: savedObjects degraded', - detail: 'See the status page for more information', - meta: expect.any(Object), - }, - }); - }); - - it('throws error if unknown plugin passed', () => { - const service = new PluginsStatusService({ core$: coreAllAvailable$, pluginDependencies }); - expect(() => { - service.getDependenciesStatus$('dont-exist'); - }).toThrowError(); - }); - - it('debounces events in quick succession', async () => { - const service = new PluginsStatusService({ - core$: coreAllAvailable$, - pluginDependencies, - }); - const available: ServiceStatus = { - level: ServiceStatusLevels.available, - summary: 'a available', - }; - const degraded: ServiceStatus = { - level: ServiceStatusLevels.degraded, - summary: 'a degraded', - }; - const pluginA$ = new BehaviorSubject(available); - service.set('a', pluginA$); - - const statusUpdates: Array> = []; - const subscription = service - .getDependenciesStatus$('b') - .subscribe((status) => statusUpdates.push(status)); - const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - - pluginA$.next(degraded); - pluginA$.next(available); - pluginA$.next(degraded); - pluginA$.next(available); - pluginA$.next(degraded); - pluginA$.next(available); - pluginA$.next(degraded); - // Waiting for the debounce timeout should cut a new update - await delay(100); - pluginA$.next(available); - await delay(100); - subscription.unsubscribe(); - - expect(statusUpdates).toMatchInlineSnapshot(` - Array [ - Object { - "a": Object { - "level": degraded, - "summary": "a degraded", - }, - }, - Object { - "a": Object { - "level": available, - "summary": "a available", - }, - }, - ] - `); - }); - }); -}); diff --git a/src/core/server/status/plugins_status.ts b/src/core/server/status/plugins_status.ts deleted file mode 100644 index df6f13eeec4e5..0000000000000 --- a/src/core/server/status/plugins_status.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { BehaviorSubject, Observable, combineLatest, of } from 'rxjs'; -import { map, distinctUntilChanged, switchMap, debounceTime } from 'rxjs/operators'; -import { isDeepStrictEqual } from 'util'; - -import { PluginName } from '../plugins'; -import { ServiceStatus, CoreStatus } from './types'; -import { getSummaryStatus } from './get_summary_status'; - -interface Deps { - core$: Observable; - pluginDependencies: ReadonlyMap; -} - -export class PluginsStatusService { - private readonly pluginStatuses = new Map>(); - private readonly update$ = new BehaviorSubject(true); - constructor(private readonly deps: Deps) {} - - public set(plugin: PluginName, status$: Observable) { - this.pluginStatuses.set(plugin, status$); - this.update$.next(true); // trigger all existing Observables to update from the new source Observable - } - - public getAll$(): Observable> { - return this.getPluginStatuses$([...this.deps.pluginDependencies.keys()]); - } - - public getDependenciesStatus$(plugin: PluginName): Observable> { - const dependencies = this.deps.pluginDependencies.get(plugin); - if (!dependencies) { - throw new Error(`Unknown plugin: ${plugin}`); - } - - return this.getPluginStatuses$(dependencies).pipe( - // Prevent many emissions at once from dependency status resolution from making this too noisy - debounceTime(100) - ); - } - - public getDerivedStatus$(plugin: PluginName): Observable { - return combineLatest([this.deps.core$, this.getDependenciesStatus$(plugin)]).pipe( - map(([coreStatus, pluginStatuses]) => { - return getSummaryStatus( - [...Object.entries(coreStatus), ...Object.entries(pluginStatuses)], - { - allAvailableSummary: `All dependencies are available`, - } - ); - }) - ); - } - - private getPluginStatuses$(plugins: PluginName[]): Observable> { - if (plugins.length === 0) { - return of({}); - } - - return this.update$.pipe( - switchMap(() => { - const pluginStatuses = plugins - .map( - (depName) => - [depName, this.pluginStatuses.get(depName) ?? this.getDerivedStatus$(depName)] as [ - PluginName, - Observable - ] - ) - .map(([pName, status$]) => - status$.pipe(map((status) => [pName, status] as [PluginName, ServiceStatus])) - ); - - return combineLatest(pluginStatuses).pipe( - map((statuses) => Object.fromEntries(statuses)), - distinctUntilChanged(isDeepStrictEqual) - ); - }) - ); - } -} diff --git a/src/core/server/status/status_service.mock.ts b/src/core/server/status/status_service.mock.ts index 42b3eecdca310..47ef8659b4079 100644 --- a/src/core/server/status/status_service.mock.ts +++ b/src/core/server/status/status_service.mock.ts @@ -40,9 +40,6 @@ const createSetupContractMock = () => { const setupContract: jest.Mocked = { core$: new BehaviorSubject(availableCoreStatus), overall$: new BehaviorSubject(available), - set: jest.fn(), - dependencies$: new BehaviorSubject({}), - derivedStatus$: new BehaviorSubject(available), }; return setupContract; @@ -53,11 +50,6 @@ const createInternalSetupContractMock = () => { core$: new BehaviorSubject(availableCoreStatus), overall$: new BehaviorSubject(available), isStatusPageAnonymous: jest.fn().mockReturnValue(false), - plugins: { - set: jest.fn(), - getDependenciesStatus$: jest.fn(), - getDerivedStatus$: jest.fn(), - }, }; return setupContract; diff --git a/src/core/server/status/status_service.test.ts b/src/core/server/status/status_service.test.ts index 341c40a86bf77..863fe34e8ecea 100644 --- a/src/core/server/status/status_service.test.ts +++ b/src/core/server/status/status_service.test.ts @@ -34,7 +34,6 @@ describe('StatusService', () => { service = new StatusService(mockCoreContext.create()); }); - const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const available: ServiceStatus = { level: ServiceStatusLevels.available, summary: 'Available', @@ -54,7 +53,6 @@ describe('StatusService', () => { savedObjects: { status$: of(degraded), }, - pluginDependencies: new Map(), }); expect(await setup.core$.pipe(first()).toPromise()).toEqual({ elasticsearch: available, @@ -70,7 +68,6 @@ describe('StatusService', () => { savedObjects: { status$: of(degraded), }, - pluginDependencies: new Map(), }); const subResult1 = await setup.core$.pipe(first()).toPromise(); const subResult2 = await setup.core$.pipe(first()).toPromise(); @@ -99,7 +96,6 @@ describe('StatusService', () => { savedObjects: { status$: savedObjects$, }, - pluginDependencies: new Map(), }); const statusUpdates: CoreStatus[] = []; @@ -162,7 +158,6 @@ describe('StatusService', () => { savedObjects: { status$: of(degraded), }, - pluginDependencies: new Map(), }); expect(await setup.overall$.pipe(first()).toPromise()).toMatchObject({ level: ServiceStatusLevels.degraded, @@ -178,7 +173,6 @@ describe('StatusService', () => { savedObjects: { status$: of(degraded), }, - pluginDependencies: new Map(), }); const subResult1 = await setup.overall$.pipe(first()).toPromise(); const subResult2 = await setup.overall$.pipe(first()).toPromise(); @@ -207,95 +201,26 @@ describe('StatusService', () => { savedObjects: { status$: savedObjects$, }, - pluginDependencies: new Map(), }); const statusUpdates: ServiceStatus[] = []; const subscription = setup.overall$.subscribe((status) => statusUpdates.push(status)); - // Wait for timers to ensure that duplicate events are still filtered out regardless of debouncing. elasticsearch$.next(available); - await delay(100); elasticsearch$.next(available); - await delay(100); elasticsearch$.next({ level: ServiceStatusLevels.available, summary: `Wow another summary`, }); - await delay(100); savedObjects$.next(degraded); - await delay(100); savedObjects$.next(available); - await delay(100); savedObjects$.next(available); - await delay(100); subscription.unsubscribe(); expect(statusUpdates).toMatchInlineSnapshot(` Array [ Object { - "detail": "See the status page for more information", "level": degraded, - "meta": Object { - "affectedServices": Object { - "savedObjects": Object { - "level": degraded, - "summary": "This is degraded!", - }, - }, - }, - "summary": "[savedObjects]: This is degraded!", - }, - Object { - "level": available, - "summary": "All services are available", - }, - ] - `); - }); - - it('debounces events in quick succession', async () => { - const savedObjects$ = new BehaviorSubject(available); - const setup = await service.setup({ - elasticsearch: { - status$: new BehaviorSubject(available), - }, - savedObjects: { - status$: savedObjects$, - }, - pluginDependencies: new Map(), - }); - - const statusUpdates: ServiceStatus[] = []; - const subscription = setup.overall$.subscribe((status) => statusUpdates.push(status)); - - // All of these should debounced into a single `available` status - savedObjects$.next(degraded); - savedObjects$.next(available); - savedObjects$.next(degraded); - savedObjects$.next(available); - savedObjects$.next(degraded); - savedObjects$.next(available); - savedObjects$.next(degraded); - // Waiting for the debounce timeout should cut a new update - await delay(100); - savedObjects$.next(available); - await delay(100); - subscription.unsubscribe(); - - expect(statusUpdates).toMatchInlineSnapshot(` - Array [ - Object { - "detail": "See the status page for more information", - "level": degraded, - "meta": Object { - "affectedServices": Object { - "savedObjects": Object { - "level": degraded, - "summary": "This is degraded!", - }, - }, - }, "summary": "[savedObjects]: This is degraded!", }, Object { diff --git a/src/core/server/status/status_service.ts b/src/core/server/status/status_service.ts index 59e81343597c9..aea335e64babf 100644 --- a/src/core/server/status/status_service.ts +++ b/src/core/server/status/status_service.ts @@ -18,7 +18,7 @@ */ import { Observable, combineLatest } from 'rxjs'; -import { map, distinctUntilChanged, shareReplay, take, debounceTime } from 'rxjs/operators'; +import { map, distinctUntilChanged, shareReplay, take } from 'rxjs/operators'; import { isDeepStrictEqual } from 'util'; import { CoreService } from '../../types'; @@ -26,16 +26,13 @@ import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { InternalElasticsearchServiceSetup } from '../elasticsearch'; import { InternalSavedObjectsServiceSetup } from '../saved_objects'; -import { PluginName } from '../plugins'; import { config, StatusConfigType } from './status_config'; import { ServiceStatus, CoreStatus, InternalStatusServiceSetup } from './types'; import { getSummaryStatus } from './get_summary_status'; -import { PluginsStatusService } from './plugins_status'; interface SetupDeps { elasticsearch: Pick; - pluginDependencies: ReadonlyMap; savedObjects: Pick; } @@ -43,29 +40,17 @@ export class StatusService implements CoreService { private readonly logger: Logger; private readonly config$: Observable; - private pluginsStatus?: PluginsStatusService; - constructor(coreContext: CoreContext) { this.logger = coreContext.logger.get('status'); this.config$ = coreContext.configService.atPath(config.path); } - public async setup({ elasticsearch, pluginDependencies, savedObjects }: SetupDeps) { + public async setup(core: SetupDeps) { const statusConfig = await this.config$.pipe(take(1)).toPromise(); - const core$ = this.setupCoreStatus({ elasticsearch, savedObjects }); - this.pluginsStatus = new PluginsStatusService({ core$, pluginDependencies }); - - const overall$: Observable = combineLatest( - core$, - this.pluginsStatus.getAll$() - ).pipe( - // Prevent many emissions at once from dependency status resolution from making this too noisy - debounceTime(100), - map(([coreStatus, pluginsStatus]) => { - const summary = getSummaryStatus([ - ...Object.entries(coreStatus), - ...Object.entries(pluginsStatus), - ]); + const core$ = this.setupCoreStatus(core); + const overall$: Observable = core$.pipe( + map((coreStatus) => { + const summary = getSummaryStatus(Object.entries(coreStatus)); this.logger.debug(`Recalculated overall status`, { status: summary }); return summary; }), @@ -75,11 +60,6 @@ export class StatusService implements CoreService { return { core$, overall$, - plugins: { - set: this.pluginsStatus.set.bind(this.pluginsStatus), - getDependenciesStatus$: this.pluginsStatus.getDependenciesStatus$.bind(this.pluginsStatus), - getDerivedStatus$: this.pluginsStatus.getDerivedStatus$.bind(this.pluginsStatus), - }, isStatusPageAnonymous: () => statusConfig.allowAnonymous, }; } @@ -88,10 +68,7 @@ export class StatusService implements CoreService { public stop() {} - private setupCoreStatus({ - elasticsearch, - savedObjects, - }: Pick): Observable { + private setupCoreStatus({ elasticsearch, savedObjects }: SetupDeps): Observable { return combineLatest([elasticsearch.status$, savedObjects.status$]).pipe( map(([elasticsearchStatus, savedObjectsStatus]) => ({ elasticsearch: elasticsearchStatus, diff --git a/src/core/server/status/types.ts b/src/core/server/status/types.ts index f884b80316fa8..2ecf11deb2960 100644 --- a/src/core/server/status/types.ts +++ b/src/core/server/status/types.ts @@ -19,7 +19,6 @@ import { Observable } from 'rxjs'; import { deepFreeze } from '../../utils'; -import { PluginName } from '../plugins'; /** * The current status of a service at a point in time. @@ -117,60 +116,6 @@ export interface CoreStatus { /** * API for accessing status of Core and this plugin's dependencies as well as for customizing this plugin's status. - * - * @remarks - * By default, a plugin inherits it's current status from the most severe status level of any Core services and any - * plugins that it depends on. This default status is available on the - * {@link ServiceStatusSetup.derivedStatus$ | core.status.derviedStatus$} API. - * - * Plugins may customize their status calculation by calling the {@link ServiceStatusSetup.set | core.status.set} API - * with an Observable. Within this Observable, a plugin may choose to only depend on the status of some of its - * dependencies, to ignore severe status levels of particular Core services they are not concerned with, or to make its - * status dependent on other external services. - * - * @example - * Customize a plugin's status to only depend on the status of SavedObjects: - * ```ts - * core.status.set( - * core.status.core$.pipe( - * . map((coreStatus) => { - * return coreStatus.savedObjects; - * }) ; - * ); - * ); - * ``` - * - * @example - * Customize a plugin's status to include an external service: - * ```ts - * const externalStatus$ = interval(1000).pipe( - * switchMap(async () => { - * const resp = await fetch(`https://myexternaldep.com/_healthz`); - * const body = await resp.json(); - * if (body.ok) { - * return of({ level: ServiceStatusLevels.available, summary: 'External Service is up'}); - * } else { - * return of({ level: ServiceStatusLevels.available, summary: 'External Service is unavailable'}); - * } - * }), - * catchError((error) => { - * of({ level: ServiceStatusLevels.unavailable, summary: `External Service is down`, meta: { error }}) - * }) - * ); - * - * core.status.set( - * combineLatest([core.status.derivedStatus$, externalStatus$]).pipe( - * map(([derivedStatus, externalStatus]) => { - * if (externalStatus.level > derivedStatus) { - * return externalStatus; - * } else { - * return derivedStatus; - * } - * }) - * ) - * ); - * ``` - * * @public */ export interface StatusServiceSetup { @@ -189,43 +134,9 @@ export interface StatusServiceSetup { * only depend on the statuses of {@link StatusServiceSetup.core$ | Core} or their dependencies. */ overall$: Observable; - - /** - * Allows a plugin to specify a custom status dependent on its own criteria. - * Completely overrides the default inherited status. - * - * @remarks - * See the {@link StatusServiceSetup.derivedStatus$} API for leveraging the default status - * calculation that is provided by Core. - */ - set(status$: Observable): void; - - /** - * Current status for all plugins this plugin depends on. - * Each key of the `Record` is a plugin id. - */ - dependencies$: Observable>; - - /** - * The status of this plugin as derived from its dependencies. - * - * @remarks - * By default, plugins inherit this derived status from their dependencies. - * Calling {@link StatusSetup.set} overrides this default status. - * - * This may emit multliple times for a single status change event as propagates - * through the dependency tree - */ - derivedStatus$: Observable; } /** @internal */ -export interface InternalStatusServiceSetup extends Pick { +export interface InternalStatusServiceSetup extends StatusServiceSetup { isStatusPageAnonymous: () => boolean; - // Namespaced under `plugins` key to improve clarity that these are APIs for plugins specifically. - plugins: { - set(plugin: PluginName, status$: Observable): void; - getDependenciesStatus$(plugin: PluginName): Observable>; - getDerivedStatus$(plugin: PluginName): Observable; - }; } diff --git a/src/legacy/deprecation/__tests__/create_transform.js b/src/legacy/deprecation/__tests__/create_transform.js deleted file mode 100644 index d3838da5c3399..0000000000000 --- a/src/legacy/deprecation/__tests__/create_transform.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { createTransform } from '../create_transform'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -describe('deprecation', function () { - describe('createTransform', function () { - it(`doesn't modify settings parameter`, function () { - const settings = { - original: true, - }; - const deprecations = [ - (settings) => { - settings.original = false; - }, - ]; - createTransform(deprecations)(settings); - expect(settings.original).to.be(true); - }); - - it('calls single deprecation in array', function () { - const deprecations = [sinon.spy()]; - createTransform(deprecations)({}); - expect(deprecations[0].calledOnce).to.be(true); - }); - - it('calls multiple deprecations in array', function () { - const deprecations = [sinon.spy(), sinon.spy()]; - createTransform(deprecations)({}); - expect(deprecations[0].calledOnce).to.be(true); - expect(deprecations[1].calledOnce).to.be(true); - }); - - it('passes log function to deprecation', function () { - const deprecation = sinon.spy(); - const log = function () {}; - createTransform([deprecation])({}, log); - expect(deprecation.args[0][1]).to.be(log); - }); - }); -}); diff --git a/src/legacy/deprecation/create_transform.js b/src/legacy/deprecation/create_transform.js deleted file mode 100644 index 72e8e153ed819..0000000000000 --- a/src/legacy/deprecation/create_transform.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { deepCloneWithBuffers as clone } from '../utils'; -import { forEach, noop } from 'lodash'; - -export function createTransform(deprecations) { - return (settings, log = noop) => { - const result = clone(settings); - - forEach(deprecations, (deprecation) => { - deprecation(result, log); - }); - - return result; - }; -} diff --git a/src/legacy/deprecation/deprecations/__tests__/rename.js b/src/legacy/deprecation/deprecations/__tests__/rename.js deleted file mode 100644 index 47c6b3257ff69..0000000000000 --- a/src/legacy/deprecation/deprecations/__tests__/rename.js +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import { rename } from '../rename'; -import sinon from 'sinon'; - -describe('deprecation/deprecations', function () { - describe('rename', function () { - it('should rename simple property', function () { - const value = 'value'; - const settings = { - before: value, - }; - - rename('before', 'after')(settings); - expect(settings.before).to.be(undefined); - expect(settings.after).to.be(value); - }); - - it('should rename nested property', function () { - const value = 'value'; - const settings = { - someObject: { - before: value, - }, - }; - - rename('someObject.before', 'someObject.after')(settings); - expect(settings.someObject.before).to.be(undefined); - expect(settings.someObject.after).to.be(value); - }); - - it('should rename property, even when the value is null', function () { - const value = null; - const settings = { - before: value, - }; - - rename('before', 'after')(settings); - expect(settings.before).to.be(undefined); - expect(settings.after).to.be(null); - }); - - it(`shouldn't log when a rename doesn't occur`, function () { - const settings = { - exists: true, - }; - - const log = sinon.spy(); - rename('doesntExist', 'alsoDoesntExist')(settings, log); - expect(log.called).to.be(false); - }); - - it('should log when a rename does occur', function () { - const settings = { - exists: true, - }; - - const log = sinon.spy(); - rename('exists', 'alsoExists')(settings, log); - - expect(log.calledOnce).to.be(true); - expect(log.args[0][0]).to.match(/exists.+deprecated/); - }); - }); -}); diff --git a/src/legacy/deprecation/deprecations/__tests__/unused.js b/src/legacy/deprecation/deprecations/__tests__/unused.js deleted file mode 100644 index 4907c2b166989..0000000000000 --- a/src/legacy/deprecation/deprecations/__tests__/unused.js +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import sinon from 'sinon'; -import { unused } from '../unused'; - -describe('deprecation/deprecations', function () { - describe('unused', function () { - it('should remove unused setting', function () { - const settings = { - old: true, - }; - - unused('old')(settings); - expect(settings.old).to.be(undefined); - }); - - it(`shouldn't remove used setting`, function () { - const value = 'value'; - const settings = { - new: value, - }; - - unused('old')(settings); - expect(settings.new).to.be(value); - }); - - it('should remove unused setting, even when null', function () { - const settings = { - old: null, - }; - - unused('old')(settings); - expect(settings.old).to.be(undefined); - }); - - it('should log when removing unused setting', function () { - const settings = { - old: true, - }; - - const log = sinon.spy(); - unused('old')(settings, log); - - expect(log.calledOnce).to.be(true); - expect(log.args[0][0]).to.match(/old.+deprecated/); - }); - - it(`shouldn't log when no setting is unused`, function () { - const settings = { - new: true, - }; - - const log = sinon.spy(); - unused('old')(settings, log); - expect(log.called).to.be(false); - }); - }); -}); diff --git a/src/legacy/deprecation/deprecations/index.js b/src/legacy/deprecation/deprecations/index.js deleted file mode 100644 index 527c99309ba80..0000000000000 --- a/src/legacy/deprecation/deprecations/index.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { rename } from './rename'; -export { unused } from './unused'; diff --git a/src/legacy/deprecation/deprecations/rename.js b/src/legacy/deprecation/deprecations/rename.js deleted file mode 100644 index c96b9146b4e2c..0000000000000 --- a/src/legacy/deprecation/deprecations/rename.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { set } from '@elastic/safer-lodash-set'; -import { get, isUndefined, noop } from 'lodash'; -import { unset } from '../../utils'; - -export function rename(oldKey, newKey) { - return (settings, log = noop) => { - const value = get(settings, oldKey); - if (isUndefined(value)) { - return; - } - - unset(settings, oldKey); - set(settings, newKey, value); - - log(`Config key "${oldKey}" is deprecated. It has been replaced with "${newKey}"`); - }; -} diff --git a/src/legacy/deprecation/deprecations/unused.js b/src/legacy/deprecation/deprecations/unused.js deleted file mode 100644 index 4291063dc482b..0000000000000 --- a/src/legacy/deprecation/deprecations/unused.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { get, isUndefined, noop } from 'lodash'; -import { unset } from '../../utils'; - -export function unused(oldKey) { - return (settings, log = noop) => { - const value = get(settings, oldKey); - if (isUndefined(value)) { - return; - } - - unset(settings, oldKey); - log(`${oldKey} is deprecated and is no longer used`); - }; -} diff --git a/src/legacy/deprecation/get_transform.js b/src/legacy/deprecation/get_transform.js deleted file mode 100644 index bf286901af62c..0000000000000 --- a/src/legacy/deprecation/get_transform.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { noop } from 'lodash'; - -import { createTransform } from './create_transform'; -import { rename, unused } from './deprecations'; - -export async function getTransform(spec) { - const deprecationsProvider = spec.getDeprecationsProvider() || noop; - if (!deprecationsProvider) return; - const transforms = (await deprecationsProvider({ rename, unused })) || []; - return createTransform(transforms); -} diff --git a/src/legacy/deprecation/index.js b/src/legacy/deprecation/index.js deleted file mode 100644 index 787563e7353ce..0000000000000 --- a/src/legacy/deprecation/index.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { rename, unused } from './deprecations'; - -export { createTransform } from './create_transform'; -export { getTransform } from './get_transform'; -export const Deprecations = { rename, unused }; diff --git a/src/legacy/plugin_discovery/plugin_config/__tests__/extend_config_service.js b/src/legacy/plugin_discovery/plugin_config/__tests__/extend_config_service.js index a74bfb872e99c..40f84f6f54b3b 100644 --- a/src/legacy/plugin_discovery/plugin_config/__tests__/extend_config_service.js +++ b/src/legacy/plugin_discovery/plugin_config/__tests__/extend_config_service.js @@ -45,10 +45,6 @@ describe('plugin discovery/extend config service', () => { enabled: Joi.boolean().default(true), test: Joi.string().default('bonk'), }).default(), - - deprecations({ rename }) { - return [rename('oldTest', 'test')]; - }, }), }) .getPluginSpecs() @@ -74,16 +70,14 @@ describe('plugin discovery/extend config service', () => { getConfigPrefix: sandbox.stub().returns(configPrefix), }; - const logDeprecation = sandbox.stub(); - const getSettings = sandbox.stub(SettingsNS, 'getSettings').returns(rootSettings.foo.bar); const getSchema = sandbox.stub(SchemaNS, 'getSchema').returns(schema); - await extendConfigService(pluginSpec, config, rootSettings, logDeprecation); + await extendConfigService(pluginSpec, config, rootSettings); sinon.assert.calledOnce(getSettings); - sinon.assert.calledWithExactly(getSettings, pluginSpec, rootSettings, logDeprecation); + sinon.assert.calledWithExactly(getSettings, pluginSpec, rootSettings); sinon.assert.calledOnce(getSchema); sinon.assert.calledWithExactly(getSchema, pluginSpec); @@ -145,41 +139,6 @@ describe('plugin discovery/extend config service', () => { expect(error.message).to.contain('"test" must be a string'); } }); - - it('calls logDeprecation() with deprecation messages', async () => { - const config = Config.withDefaultSchema(); - const logDeprecation = sinon.stub(); - await extendConfigService( - pluginSpec, - config, - { - foo: { - bar: { - baz: { - oldTest: '123', - }, - }, - }, - }, - logDeprecation - ); - sinon.assert.calledOnce(logDeprecation); - sinon.assert.calledWithExactly(logDeprecation, sinon.match('"oldTest" is deprecated')); - }); - - it('uses settings after transforming deprecations', async () => { - const config = Config.withDefaultSchema(); - await extendConfigService(pluginSpec, config, { - foo: { - bar: { - baz: { - oldTest: '123', - }, - }, - }, - }); - expect(config.get('foo.bar.baz.test')).to.be('123'); - }); }); describe('disableConfigExtension()', () => { diff --git a/src/legacy/plugin_discovery/plugin_config/__tests__/settings.js b/src/legacy/plugin_discovery/plugin_config/__tests__/settings.js index 2a26e29dfd63a..750c5ee6c6f50 100644 --- a/src/legacy/plugin_discovery/plugin_config/__tests__/settings.js +++ b/src/legacy/plugin_discovery/plugin_config/__tests__/settings.js @@ -18,7 +18,6 @@ */ import expect from '@kbn/expect'; -import sinon from 'sinon'; import { PluginPack } from '../../plugin_pack'; import { getSettings } from '../settings'; @@ -33,7 +32,6 @@ describe('plugin_discovery/settings', () => { provider: ({ Plugin }) => new Plugin({ configPrefix: 'a.b.c', - deprecations: ({ rename }) => [rename('foo', 'bar')], }), }) .getPluginSpecs() @@ -59,28 +57,5 @@ describe('plugin_discovery/settings', () => { it('allows rootSettings to be undefined', async () => { expect(await getSettings(pluginSpec)).to.eql(undefined); }); - - it('resolves deprecations', async () => { - const logDeprecation = sinon.stub(); - expect( - await getSettings( - pluginSpec, - { - a: { - b: { - c: { - foo: true, - }, - }, - }, - }, - logDeprecation - ) - ).to.eql({ - bar: true, - }); - - sinon.assert.calledOnce(logDeprecation); - }); }); }); diff --git a/src/legacy/plugin_discovery/plugin_config/extend_config_service.js b/src/legacy/plugin_discovery/plugin_config/extend_config_service.js index 9257227c0d62b..a6d5d4ae5f990 100644 --- a/src/legacy/plugin_discovery/plugin_config/extend_config_service.js +++ b/src/legacy/plugin_discovery/plugin_config/extend_config_service.js @@ -30,8 +30,8 @@ import { getSchema, getStubSchema } from './schema'; * @param {Function} [logDeprecation] * @return {Promise} */ -export async function extendConfigService(spec, config, rootSettings, logDeprecation) { - const settings = await getSettings(spec, rootSettings, logDeprecation); +export async function extendConfigService(spec, config, rootSettings) { + const settings = await getSettings(spec, rootSettings); const schema = await getSchema(spec); config.extendSchema(schema, settings, spec.getConfigPrefix()); } diff --git a/src/legacy/plugin_discovery/plugin_config/settings.js b/src/legacy/plugin_discovery/plugin_config/settings.js index 44ecb5718fe21..e6a4741d76eca 100644 --- a/src/legacy/plugin_discovery/plugin_config/settings.js +++ b/src/legacy/plugin_discovery/plugin_config/settings.js @@ -19,20 +19,16 @@ import { get } from 'lodash'; -import { getTransform } from '../../deprecation'; - /** * Get the settings for a pluginSpec from the raw root settings while * optionally calling logDeprecation() with warnings about deprecated * settings that were used * @param {PluginSpec} spec * @param {Object} rootSettings - * @param {Function} [logDeprecation] * @return {Promise} */ -export async function getSettings(spec, rootSettings, logDeprecation) { +export async function getSettings(spec, rootSettings) { const prefix = spec.getConfigPrefix(); const rawSettings = get(rootSettings, prefix); - const transform = await getTransform(spec); - return transform(rawSettings, logDeprecation); + return rawSettings; } diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 627e9f4f86bc3..69fb63fbbd87f 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -50,10 +50,6 @@ import { HomeServerPluginSetup } from '../../plugins/home/server'; // lot of legacy code was assuming this type only had these two methods export type KibanaConfig = Pick; -export interface UiApp { - getId(): string; -} - // Extend the defaults with the plugins and server methods we need. declare module 'hapi' { interface PluginProperties { @@ -66,13 +62,6 @@ declare module 'hapi' { interface Server { config: () => KibanaConfig; savedObjects: SavedObjectsLegacyService; - injectUiAppVars: (pluginName: string, getAppVars: () => { [key: string]: any }) => void; - getHiddenUiAppById(appId: string): UiApp; - addScopedTutorialContextFactory: ( - scopedTutorialContextFactory: (...args: any[]) => any - ) => void; - getInjectedUiAppVars: (pluginName: string) => { [key: string]: any }; - getUiNavLinks(): Array<{ _id: string }>; logWithMetadata: (tags: string[], message: string, meta: Record) => void; newPlatform: KbnServer['newPlatform']; } @@ -82,10 +71,6 @@ declare module 'hapi' { getBasePath(): string; getUiSettingsService(): IUiSettingsClient; } - - interface ResponseToolkit { - renderAppWithDefaultConfig(app: UiApp): ResponseObject; - } } type KbnMixinFunc = (kbnServer: KbnServer, server: Server, config: any) => Promise | void; diff --git a/src/legacy/ui/ui_apps/__snapshots__/ui_apps_mixin.test.js.snap b/src/legacy/ui/ui_apps/__snapshots__/ui_apps_mixin.test.js.snap deleted file mode 100644 index cb2b6cda26a81..0000000000000 --- a/src/legacy/ui/ui_apps/__snapshots__/ui_apps_mixin.test.js.snap +++ /dev/null @@ -1,92 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`UiAppsMixin creates kbnServer.uiApps from uiExports 1`] = ` -Array [ - StubUiApp { - "_hidden": true, - "_id": "foo", - }, - StubUiApp { - "_hidden": false, - "_id": "bar", - }, -] -`; - -exports[`UiAppsMixin decorates server with methods 1`] = ` -Array [ - Array [ - "server", - "getAllUiApps", - [Function], - ], - Array [ - "server", - "getUiAppById", - [Function], - ], - Array [ - "server", - "getHiddenUiAppById", - [Function], - ], - Array [ - "server", - "injectUiAppVars", - [Function], - ], - Array [ - "server", - "getInjectedUiAppVars", - [Function], - ], -] -`; - -exports[`UiAppsMixin server.getAllUiApps() returns hidden and non-hidden apps 1`] = ` -Array [ - StubUiApp { - "_hidden": true, - "_id": "foo", - }, - StubUiApp { - "_hidden": false, - "_id": "bar", - }, -] -`; - -exports[`UiAppsMixin server.getHiddenUiAppById() returns hidden apps when requested, undefined for non-hidden and unknown apps 1`] = ` -StubUiApp { - "_hidden": true, - "_id": "foo", -} -`; - -exports[`UiAppsMixin server.getUiAppById() returns non-hidden apps when requested, undefined for non-hidden and unknown apps 1`] = ` -StubUiApp { - "_hidden": false, - "_id": "bar", -} -`; - -exports[`UiAppsMixin server.injectUiAppVars()/server.getInjectedUiAppVars() merges injected vars provided by multiple providers in the order they are registered: foo 1`] = ` -Object { - "bar": false, - "baz": 1, - "box": true, - "foo": true, -} -`; - -exports[`UiAppsMixin server.injectUiAppVars()/server.getInjectedUiAppVars() stored injectVars provider and returns provider result when requested: bar 1`] = ` -Object { - "thisIsFoo": false, -} -`; - -exports[`UiAppsMixin server.injectUiAppVars()/server.getInjectedUiAppVars() stored injectVars provider and returns provider result when requested: foo 1`] = ` -Object { - "thisIsFoo": true, -} -`; diff --git a/src/legacy/ui/ui_apps/__tests__/ui_app.js b/src/legacy/ui/ui_apps/__tests__/ui_app.js deleted file mode 100644 index bb4bcfe2d7443..0000000000000 --- a/src/legacy/ui/ui_apps/__tests__/ui_app.js +++ /dev/null @@ -1,306 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import sinon from 'sinon'; -import expect from '@kbn/expect'; - -import { UiApp } from '../ui_app'; -import { UiNavLink } from '../../ui_nav_links'; - -function createStubUiAppSpec(extraParams) { - return { - id: 'uiapp-test', - main: 'main.js', - title: 'UIApp Test', - order: 9000, - icon: 'ui_app_test.svg', - linkToLastSubUrl: true, - hidden: false, - listed: false, - ...extraParams, - }; -} - -function createStubKbnServer(extraParams) { - return { - plugins: [], - config: { - get: sinon.stub().withArgs('server.basePath').returns(''), - }, - server: {}, - ...extraParams, - }; -} - -function createUiApp(spec = createStubUiAppSpec(), kbnServer = createStubKbnServer()) { - return new UiApp(kbnServer, spec); -} - -describe('ui apps / UiApp', () => { - describe('constructor', () => { - it('throws an exception if an ID is not given', () => { - const spec = {}; // should have id property - expect(() => createUiApp(spec)).to.throwException(); - }); - - describe('defaults', () => { - const spec = { id: 'uiapp-test-defaults' }; - const app = createUiApp(spec); - - it('has the ID from the spec', () => { - expect(app.getId()).to.be(spec.id); - }); - - it('has no plugin ID', () => { - expect(app.getPluginId()).to.be(undefined); - }); - - it('is not hidden', () => { - expect(app.isHidden()).to.be(false); - }); - - it('is listed', () => { - expect(app.isListed()).to.be(true); - }); - - it('has a navLink', () => { - expect(app.getNavLink()).to.be.a(UiNavLink); - }); - - it('has no main module', () => { - expect(app.getMainModuleId()).to.be(undefined); - }); - - it('has a mostly empty JSON representation', () => { - expect(JSON.parse(JSON.stringify(app))).to.eql({ - id: spec.id, - navLink: { - id: 'uiapp-test-defaults', - order: 0, - url: '/app/uiapp-test-defaults', - subUrlBase: '/app/uiapp-test-defaults', - linkToLastSubUrl: true, - hidden: false, - disabled: false, - tooltip: '', - }, - }); - }); - }); - - describe('mock spec', () => { - const spec = createStubUiAppSpec(); - const app = createUiApp(spec); - - it('has the ID from the spec', () => { - expect(app.getId()).to.be(spec.id); - }); - - it('has no plugin ID', () => { - expect(app.getPluginId()).to.be(undefined); - }); - - it('is not hidden', () => { - expect(app.isHidden()).to.be(false); - }); - - it('is also not listed', () => { - expect(app.isListed()).to.be(false); - }); - - it('has no navLink', () => { - expect(app.getNavLink()).to.be(undefined); - }); - - it('has a main module', () => { - expect(app.getMainModuleId()).to.be('main.js'); - }); - - it('has spec values in JSON representation', () => { - expect(JSON.parse(JSON.stringify(app))).to.eql({ - id: spec.id, - title: spec.title, - icon: spec.icon, - main: spec.main, - linkToLastSubUrl: spec.linkToLastSubUrl, - navLink: { - id: 'uiapp-test', - title: 'UIApp Test', - order: 9000, - url: '/app/uiapp-test', - subUrlBase: '/app/uiapp-test', - icon: 'ui_app_test.svg', - linkToLastSubUrl: true, - hidden: false, - disabled: false, - tooltip: '', - }, - }); - }); - }); - - /* - * The "hidden" and "listed" flags have an bound relationship. The "hidden" - * flag gets cast to a boolean value, and the "listed" flag is dependent on - * "hidden" - */ - describe('hidden flag', () => { - describe('is cast to boolean value', () => { - it('when undefined', () => { - const kbnServer = createStubKbnServer(); - const spec = { - id: 'uiapp-test', - }; - const newApp = new UiApp(kbnServer, spec); - expect(newApp.isHidden()).to.be(false); - }); - - it('when null', () => { - const kbnServer = createStubKbnServer(); - const spec = { - id: 'uiapp-test', - hidden: null, - }; - const newApp = new UiApp(kbnServer, spec); - expect(newApp.isHidden()).to.be(false); - }); - - it('when 0', () => { - const kbnServer = createStubKbnServer(); - const spec = { - id: 'uiapp-test', - hidden: 0, - }; - const newApp = new UiApp(kbnServer, spec); - expect(newApp.isHidden()).to.be(false); - }); - - it('when true', () => { - const kbnServer = createStubKbnServer(); - const spec = { - id: 'uiapp-test', - hidden: true, - }; - const newApp = new UiApp(kbnServer, spec); - expect(newApp.isHidden()).to.be(true); - }); - - it('when 1', () => { - const kbnServer = createStubKbnServer(); - const spec = { - id: 'uiapp-test', - hidden: 1, - }; - const newApp = new UiApp(kbnServer, spec); - expect(newApp.isHidden()).to.be(true); - }); - }); - }); - - describe('listed flag', () => { - describe('defaults to the opposite value of hidden', () => { - it(`when it's null and hidden is true`, () => { - const kbnServer = createStubKbnServer(); - const spec = { - id: 'uiapp-test', - hidden: true, - listed: null, - }; - const newApp = new UiApp(kbnServer, spec); - expect(newApp.isListed()).to.be(false); - }); - - it(`when it's null and hidden is false`, () => { - const kbnServer = createStubKbnServer(); - const spec = { - id: 'uiapp-test', - hidden: false, - listed: null, - }; - const newApp = new UiApp(kbnServer, spec); - expect(newApp.isListed()).to.be(true); - }); - - it(`when it's undefined and hidden is false`, () => { - const kbnServer = createStubKbnServer(); - const spec = { - id: 'uiapp-test', - hidden: false, - }; - const newApp = new UiApp(kbnServer, spec); - expect(newApp.isListed()).to.be(true); - }); - - it(`when it's undefined and hidden is true`, () => { - const kbnServer = createStubKbnServer(); - const spec = { - id: 'uiapp-test', - hidden: true, - }; - const newApp = new UiApp(kbnServer, spec); - expect(newApp.isListed()).to.be(false); - }); - }); - - it(`is set to true when it's passed as true`, () => { - const kbnServer = createStubKbnServer(); - const spec = { - id: 'uiapp-test', - listed: true, - }; - const newApp = new UiApp(kbnServer, spec); - expect(newApp.isListed()).to.be(true); - }); - - it(`is set to false when it's passed as false`, () => { - const kbnServer = createStubKbnServer(); - const spec = { - id: 'uiapp-test', - listed: false, - }; - const newApp = new UiApp(kbnServer, spec); - expect(newApp.isListed()).to.be(false); - }); - }); - }); - - describe('pluginId', () => { - describe('does not match a kbnServer plugin', () => { - it('throws an error at instantiation', () => { - expect(() => { - createUiApp(createStubUiAppSpec({ pluginId: 'foo' })); - }).to.throwException((error) => { - expect(error.message).to.match(/Unknown plugin id/); - }); - }); - }); - }); - - describe('#getMainModuleId', () => { - it('returns undefined by default', () => { - const app = createUiApp({ id: 'foo' }); - expect(app.getMainModuleId()).to.be(undefined); - }); - - it('returns main module id', () => { - const app = createUiApp({ id: 'foo', main: 'bar' }); - expect(app.getMainModuleId()).to.be('bar'); - }); - }); -}); diff --git a/src/legacy/ui/ui_apps/index.js b/src/legacy/ui/ui_apps/index.js deleted file mode 100644 index d64848b2c1a92..0000000000000 --- a/src/legacy/ui/ui_apps/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { uiAppsMixin } from './ui_apps_mixin'; diff --git a/src/legacy/ui/ui_apps/ui_app.js b/src/legacy/ui/ui_apps/ui_app.js deleted file mode 100644 index 7da9e39394deb..0000000000000 --- a/src/legacy/ui/ui_apps/ui_app.js +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { UiNavLink } from '../ui_nav_links'; - -export class UiApp { - constructor(kbnServer, spec) { - const { - pluginId, - id = pluginId, - main, - title, - order = 0, - icon, - euiIconType, - hidden, - linkToLastSubUrl, - disableSubUrlTracking, - listed, - category, - url = `/app/${id}`, - } = spec; - - if (!id) { - throw new Error('Every app must specify an id'); - } - - this._id = id; - this._main = main; - this._title = title; - this._order = order; - this._icon = icon; - this._euiIconType = euiIconType; - this._linkToLastSubUrl = linkToLastSubUrl; - this._disableSubUrlTracking = disableSubUrlTracking; - this._category = category; - this._hidden = hidden; - this._listed = listed; - this._url = url; - this._pluginId = pluginId; - this._kbnServer = kbnServer; - - if (this._pluginId && !this._getPlugin()) { - throw new Error(`Unknown plugin id "${this._pluginId}"`); - } - - if (!this.isHidden()) { - // unless an app is hidden it gets a navlink, but we only respond to `getNavLink()` - // if the app is also listed. This means that all apps in the kibanaPayload will - // have a navLink property since that list includes all normally accessible apps - this._navLink = new UiNavLink({ - id: this._id, - title: this._title, - order: this._order, - icon: this._icon, - euiIconType: this._euiIconType, - url: this._url, - linkToLastSubUrl: this._linkToLastSubUrl, - disableSubUrlTracking: this._disableSubUrlTracking, - category: this._category, - }); - } - } - - getId() { - return this._id; - } - - getPluginId() { - const plugin = this._getPlugin(); - return plugin ? plugin.id : undefined; - } - - isHidden() { - return !!this._hidden; - } - - isListed() { - return !this.isHidden() && (this._listed == null || !!this._listed); - } - - getNavLink() { - if (this.isListed()) { - return this._navLink; - } - } - - getMainModuleId() { - return this._main; - } - - _getPlugin() { - const pluginId = this._pluginId; - const { plugins } = this._kbnServer; - - return pluginId ? plugins.find((plugin) => plugin.id === pluginId) : undefined; - } - - toJSON() { - return { - id: this._id, - title: this._title, - icon: this._icon, - euiIconType: this._euiIconType, - main: this._main, - navLink: this._navLink, - linkToLastSubUrl: this._linkToLastSubUrl, - disableSubUrlTracking: this._disableSubUrlTracking, - category: this._category, - }; - } -} diff --git a/src/legacy/ui/ui_apps/ui_apps_mixin.js b/src/legacy/ui/ui_apps/ui_apps_mixin.js deleted file mode 100644 index c80b12a46bee3..0000000000000 --- a/src/legacy/ui/ui_apps/ui_apps_mixin.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { UiApp } from './ui_app'; - -/** - * @typedef {import('../../server/kbn_server').default} KbnServer - */ - -/** - * - * @param {KbnServer} kbnServer - * @param {KbnServer['server']} server - */ -export function uiAppsMixin(kbnServer, server) { - const { uiAppSpecs = [] } = kbnServer.uiExports; - const existingIds = new Set(); - const appsById = new Map(); - const hiddenAppsById = new Map(); - - kbnServer.uiApps = uiAppSpecs.map((spec) => { - const app = new UiApp(kbnServer, spec); - const id = app.getId(); - - if (!existingIds.has(id)) { - existingIds.add(id); - } else { - throw new Error(`Unable to create two apps with the id ${id}.`); - } - - if (app.isHidden()) { - hiddenAppsById.set(id, app); - } else { - appsById.set(id, app); - } - - return app; - }); - - server.decorate('server', 'getAllUiApps', () => kbnServer.uiApps.slice(0)); - server.decorate('server', 'getUiAppById', (id) => appsById.get(id)); - server.decorate('server', 'getHiddenUiAppById', (id) => hiddenAppsById.get(id)); - server.decorate('server', 'injectUiAppVars', (appId, provider) => - kbnServer.newPlatform.__internals.legacy.injectUiAppVars(appId, provider) - ); - server.decorate( - 'server', - 'getInjectedUiAppVars', - async (appId) => await kbnServer.newPlatform.__internals.legacy.getInjectedUiAppVars(appId) - ); -} diff --git a/src/legacy/ui/ui_apps/ui_apps_mixin.test.js b/src/legacy/ui/ui_apps/ui_apps_mixin.test.js deleted file mode 100644 index 048358edfc10b..0000000000000 --- a/src/legacy/ui/ui_apps/ui_apps_mixin.test.js +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { LegacyInternals } from '../../../core/server'; - -import { uiAppsMixin } from './ui_apps_mixin'; - -jest.mock('./ui_app', () => ({ - UiApp: class StubUiApp { - constructor(kbnServer, spec) { - this._id = spec.id; - this._hidden = !!spec.hidden; - } - getId() { - return this._id; - } - isHidden() { - return this._hidden; - } - }, -})); - -describe('UiAppsMixin', () => { - let kbnServer; - let server; - let uiExports; - - beforeEach(() => { - uiExports = { - uiAppSpecs: [ - { - id: 'foo', - hidden: true, - }, - { - id: 'bar', - hidden: false, - }, - ], - }; - server = { - decorate: jest.fn((type, name, value) => { - if (type !== 'server') { - return; - } - - server[name] = value; - }), - }; - kbnServer = { - uiExports, - newPlatform: { - __internals: { - legacy: new LegacyInternals(uiExports, {}, server), - }, - }, - }; - - uiAppsMixin(kbnServer, server); - }); - - it('creates kbnServer.uiApps from uiExports', () => { - expect(kbnServer.uiApps).toMatchSnapshot(); - }); - - it('decorates server with methods', () => { - expect(server.decorate.mock.calls).toMatchSnapshot(); - }); - - describe('server.getAllUiApps()', () => { - it('returns hidden and non-hidden apps', () => { - expect(server.getAllUiApps()).toMatchSnapshot(); - }); - }); - - describe('server.getUiAppById()', () => { - it('returns non-hidden apps when requested, undefined for non-hidden and unknown apps', () => { - expect(server.getUiAppById('foo')).toBe(undefined); - expect(server.getUiAppById('bar')).toMatchSnapshot(); - expect(server.getUiAppById('baz')).toBe(undefined); - }); - }); - - describe('server.getHiddenUiAppById()', () => { - it('returns hidden apps when requested, undefined for non-hidden and unknown apps', () => { - expect(server.getHiddenUiAppById('foo')).toMatchSnapshot(); - expect(server.getHiddenUiAppById('bar')).toBe(undefined); - expect(server.getHiddenUiAppById('baz')).toBe(undefined); - }); - }); - - describe('server.injectUiAppVars()/server.getInjectedUiAppVars()', () => { - it('stored injectVars provider and returns provider result when requested', async () => { - server.injectUiAppVars('foo', () => ({ - thisIsFoo: true, - })); - - server.injectUiAppVars('bar', async () => ({ - thisIsFoo: false, - })); - - await expect(server.getInjectedUiAppVars('foo')).resolves.toMatchSnapshot('foo'); - await expect(server.getInjectedUiAppVars('bar')).resolves.toMatchSnapshot('bar'); - await expect(server.getInjectedUiAppVars('baz')).resolves.toEqual({}); - }); - - it('merges injected vars provided by multiple providers in the order they are registered', async () => { - server.injectUiAppVars('foo', () => ({ - foo: true, - bar: true, - baz: true, - })); - - server.injectUiAppVars('foo', async () => ({ - bar: false, - box: true, - })); - - server.injectUiAppVars('foo', async () => ({ - baz: 1, - })); - - await expect(server.getInjectedUiAppVars('foo')).resolves.toMatchSnapshot('foo'); - await expect(server.getInjectedUiAppVars('bar')).resolves.toEqual({}); - await expect(server.getInjectedUiAppVars('baz')).resolves.toEqual({}); - }); - }); -}); diff --git a/src/legacy/ui/ui_exports/README.md b/src/legacy/ui/ui_exports/README.md index ab81febe67993..7fb117b1c25b9 100644 --- a/src/legacy/ui/ui_exports/README.md +++ b/src/legacy/ui/ui_exports/README.md @@ -88,7 +88,6 @@ This reducer format was chosen so that it will be easier to look back at these r The [`ui_exports/ui_export_defaults`][UiExportDefaults] module defines the default shape of the uiExports object produced by `collectUiExports()`. The defaults generally describe the `uiExports` from the UI System itself, like default visTypes and such. -[UiApp]: ../ui_apps/ui_app.js "UiApp class definition" [UiExportDefaults]: ./ui_export_defaults.js "uiExport defaults definition" [UiExportTypes]: ./ui_export_types/index.js "Index of default ui_export_types module" [UiAppExportType]: ./ui_export_types/ui_apps.js "UiApp extension type definition" diff --git a/src/legacy/ui/ui_mixin.js b/src/legacy/ui/ui_mixin.js index 54da001d20669..caa0ca4890661 100644 --- a/src/legacy/ui/ui_mixin.js +++ b/src/legacy/ui/ui_mixin.js @@ -17,10 +17,8 @@ * under the License. */ -import { uiAppsMixin } from './ui_apps'; import { uiRenderMixin } from './ui_render'; export async function uiMixin(kbnServer) { - await kbnServer.mixin(uiAppsMixin); await kbnServer.mixin(uiRenderMixin); } diff --git a/src/legacy/ui/ui_nav_links/__tests__/ui_nav_link.js b/src/legacy/ui/ui_nav_links/__tests__/ui_nav_link.js deleted file mode 100644 index 42368722f11ff..0000000000000 --- a/src/legacy/ui/ui_nav_links/__tests__/ui_nav_link.js +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; - -import { UiNavLink } from '../ui_nav_link'; - -describe('UiNavLink', () => { - describe('constructor', () => { - it('initializes the object properties as expected', () => { - const spec = { - id: 'discover', - title: 'Discover', - order: -1003, - url: '/app/discover#/', - euiIconType: 'discoverApp', - hidden: true, - disabled: true, - }; - - const link = new UiNavLink(spec); - expect(link.toJSON()).to.eql({ - id: spec.id, - title: spec.title, - order: spec.order, - url: spec.url, - subUrlBase: spec.url, - icon: spec.icon, - euiIconType: spec.euiIconType, - hidden: spec.hidden, - disabled: spec.disabled, - category: undefined, - - // defaults - linkToLastSubUrl: true, - disableSubUrlTracking: undefined, - tooltip: '', - }); - }); - - it('initializes the order property to 0 when order is not specified in the spec', () => { - const spec = { - id: 'discover', - title: 'Discover', - url: '/app/discover#/', - }; - const link = new UiNavLink(spec); - - expect(link.toJSON()).to.have.property('order', 0); - }); - - it('initializes the linkToLastSubUrl property to false when false is specified in the spec', () => { - const spec = { - id: 'discover', - title: 'Discover', - order: -1003, - url: '/app/discover#/', - linkToLastSubUrl: false, - }; - const link = new UiNavLink(spec); - - expect(link.toJSON()).to.have.property('linkToLastSubUrl', false); - }); - - it('initializes the linkToLastSubUrl property to true by default', () => { - const spec = { - id: 'discover', - title: 'Discover', - order: -1003, - url: '/app/discover#/', - }; - const link = new UiNavLink(spec); - - expect(link.toJSON()).to.have.property('linkToLastSubUrl', true); - }); - - it('initializes the hidden property to false by default', () => { - const spec = { - id: 'discover', - title: 'Discover', - order: -1003, - url: '/app/discover#/', - }; - const link = new UiNavLink(spec); - - expect(link.toJSON()).to.have.property('hidden', false); - }); - - it('initializes the disabled property to false by default', () => { - const spec = { - id: 'discover', - title: 'Discover', - order: -1003, - url: '/app/discover#/', - }; - const link = new UiNavLink(spec); - - expect(link.toJSON()).to.have.property('disabled', false); - }); - - it('initializes the tooltip property to an empty string by default', () => { - const spec = { - id: 'discover', - title: 'Discover', - order: -1003, - url: '/app/discover#/', - }; - const link = new UiNavLink(spec); - - expect(link.toJSON()).to.have.property('tooltip', ''); - }); - }); -}); diff --git a/src/legacy/ui/ui_nav_links/index.js b/src/legacy/ui/ui_nav_links/index.js deleted file mode 100644 index e725a77ee1183..0000000000000 --- a/src/legacy/ui/ui_nav_links/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { UiNavLink } from './ui_nav_link'; diff --git a/src/legacy/ui/ui_nav_links/ui_nav_link.js b/src/legacy/ui/ui_nav_links/ui_nav_link.js deleted file mode 100644 index f56809f7ebb80..0000000000000 --- a/src/legacy/ui/ui_nav_links/ui_nav_link.js +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export class UiNavLink { - constructor(spec) { - const { - id, - title, - order = 0, - url, - subUrlBase, - disableSubUrlTracking, - icon, - euiIconType, - linkToLastSubUrl = true, - hidden = false, - disabled = false, - tooltip = '', - category, - } = spec; - - this._id = id; - this._title = title; - this._order = order; - this._url = url; - this._subUrlBase = subUrlBase || url; - this._disableSubUrlTracking = disableSubUrlTracking; - this._icon = icon; - this._euiIconType = euiIconType; - this._linkToLastSubUrl = linkToLastSubUrl; - this._hidden = hidden; - this._disabled = disabled; - this._tooltip = tooltip; - this._category = category; - } - - getOrder() { - return this._order; - } - - toJSON() { - return { - id: this._id, - title: this._title, - order: this._order, - url: this._url, - subUrlBase: this._subUrlBase, - icon: this._icon, - euiIconType: this._euiIconType, - linkToLastSubUrl: this._linkToLastSubUrl, - disableSubUrlTracking: this._disableSubUrlTracking, - hidden: this._hidden, - disabled: this._disabled, - tooltip: this._tooltip, - category: this._category, - }; - } -} diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 8cc2cd1321a62..cd8dcf5aff71d 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -182,22 +182,16 @@ export function uiRenderMixin(kbnServer, server, config) { path: '/app/{id}/{any*}', method: 'GET', async handler(req, h) { - const id = req.params.id; - const app = server.getUiAppById(id); try { - return await h.renderApp(app); + return await h.renderApp(); } catch (err) { throw Boom.boomify(err); } }, }); - async function renderApp( - h, - app = { getId: () => 'core' }, - includeUserSettings = true, - overrides = {} - ) { + async function renderApp(h) { + const app = { getId: () => 'core' }; const { http } = kbnServer.newPlatform.setup.core; const { rendering, @@ -209,22 +203,17 @@ export function uiRenderMixin(kbnServer, server, config) { ); const vars = await legacy.getVars(app.getId(), h.request, { apmConfig: getApmConfig(h.request.path), - ...overrides, }); const content = await rendering.render(h.request, uiSettings, { app, - includeUserSettings, + includeUserSettings: true, vars, }); return h.response(content).type('text/html').header('content-security-policy', http.csp.header); } - server.decorate('toolkit', 'renderApp', function (app, overrides) { - return renderApp(this, app, true, overrides); - }); - - server.decorate('toolkit', 'renderAppWithDefaultConfig', function (app) { - return renderApp(this, app, false); + server.decorate('toolkit', 'renderApp', function () { + return renderApp(this); }); } diff --git a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts index 4c31ccee1243a..37657912deb95 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts @@ -248,11 +248,11 @@ export const getSavedObjects = (): SavedObject[] => [ version: '1', migrationVersion: {}, attributes: { - title: i18n.translate('home.sampleData.ecommerceSpec.averageSalesPerRegionTitle', { - defaultMessage: '[eCommerce] Average Sales Per Region', + title: i18n.translate('home.sampleData.ecommerceSpec.salesCountMapTitle', { + defaultMessage: '[eCommerce] Sales Count Map', }), visState: - '{"title":"[eCommerce] Average Sales Per Region","type":"region_map","params":{"legendPosition":"bottomright","addTooltip":true,"colorSchema":"Blues","selectedLayer":{"attribution":"

Made with NaturalEarth|Elastic Maps Service

","weight":1,"name":"World Countries","url":"https://vector.maps.elastic.co/blob/5659313586569216?elastic_tile_service_tos=agree&my_app_version=7.0.0-alpha1&license=f6c534b8-91b9-4499-8804-a2e9789ecc95","format":{"type":"geojson"},"fields":[{"name":"iso2","description":"Two letter abbreviation"},{"name":"name","description":"Country name"},{"name":"iso3","description":"Three letter abbreviation"}],"created_at":"2017-04-26T17:12:15.978370","tags":[],"id":5659313586569216,"layerId":"elastic_maps_service.World Countries","isEMS":true},"emsHotLink":"https://maps.elastic.co/v2#file/World Countries","selectedJoinField":{"name":"iso2","description":"Two letter abbreviation"},"isDisplayWarning":true,"wms":{"enabled":false,"options":{"format":"image/png","transparent":true}},"mapZoom":2,"mapCenter":[0,0],"outlineWeight":1,"showAllShapes":true},"aggs":[{"id":"1","enabled":true,"type":"avg","schema":"metric","params":{"field":"taxful_total_price","customLabel":"Average Sale"}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"geoip.country_iso_code","size":100,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + '{"title":"[eCommerce] Sales Count Map","type":"vega","aggs":[],"params":{"spec":"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\"map\\", latitude: 25, longitude: -40, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_ecommerce\\n %context%: true\\n %timefield%: order_date\\n body: {\\n size: 0\\n aggs: {\\n gridSplit: {\\n geotile_grid: {field: \\"geoip.location\\", precision: 4, size: 10000}\\n aggs: {\\n gridCentroid: {\\n geo_centroid: {\\n field: \\"geoip.location\\"\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\"aggregations.gridSplit.buckets\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n gridCentroid.location.lon\\n gridCentroid.location.lat\\n ]\\n }\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: gridSize\\n type: linear\\n domain: {data: \\"table\\", field: \\"doc_count\\"}\\n range: [\\n 50\\n 1000\\n ]\\n }\\n ]\\n marks: [\\n {\\n name: gridMarker\\n type: symbol\\n from: {data: \\"table\\"}\\n encode: {\\n update: {\\n size: {scale: \\"gridSize\\", field: \\"doc_count\\"}\\n xc: {signal: \\"datum.x\\"}\\n yc: {signal: \\"datum.y\\"}\\n }\\n }\\n },\\n {\\n name: gridLabel\\n type: text\\n from: {data: \\"table\\"}\\n encode: {\\n enter: {\\n fill: {value: \\"firebrick\\"}\\n text: {signal: \\"datum.doc_count\\"}\\n }\\n update: {\\n x: {signal: \\"datum.x\\"}\\n y: {signal: \\"datum.y\\"}\\n dx: {value: -6}\\n dy: {value: 6}\\n fontSize: {value: 18}\\n fontWeight: {value: \\"bold\\"}\\n }\\n }\\n }\\n ]\\n}"}}', uiStateJSON: '{}', description: '', version: 1, diff --git a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts index c1f7fcb75b149..6f701d75e7d52 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/flights/saved_objects.ts @@ -281,11 +281,10 @@ export const getSavedObjects = (): SavedObject[] => [ version: '1', migrationVersion: {}, attributes: { - title: i18n.translate('home.sampleData.flightsSpec.originCountryTicketPricesTitle', { - defaultMessage: '[Flights] Origin Country Ticket Prices', + title: i18n.translate('home.sampleData.flightsSpec.departuresCountMapTitle', { + defaultMessage: '[Flights] Departures Count Map', }), - visState: - '{"title":"[Flights] Origin Country Ticket Prices","type":"region_map","params":{"legendPosition":"bottomright","addTooltip":true,"colorSchema":"Blues","selectedLayer":{"attribution":"

Made with NaturalEarth | Elastic Maps Service

","name":"World Countries","weight":1,"format":{"type":"geojson"},"url":"https://vector.maps.elastic.co/blob/5659313586569216?elastic_tile_service_tos=agree&my_app_version=6.3.0&license=686f9ec6-d775-44f0-b334-38caf85da617","fields":[{"name":"iso2","description":"Two letter abbreviation"},{"name":"name","description":"Country name"},{"name":"iso3","description":"Three letter abbreviation"}],"created_at":"2017-04-26T17:12:15.978370","tags":[],"id":5659313586569216,"layerId":"elastic_maps_service.World Countries"},"selectedJoinField":{"name":"iso2","description":"Two letter abbreviation"},"isDisplayWarning":false,"wms":{"enabled":false,"options":{"format":"image/png","transparent":true},"baseLayersAreLoaded":{},"tmsLayers":[{"id":"road_map","url":"https://tiles.maps.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=6.3.0&license=686f9ec6-d775-44f0-b334-38caf85da617","minZoom":0,"maxZoom":18,"attribution":"

© OpenStreetMap contributors | Elastic Maps Service

","subdomains":[]}],"selectedTmsLayer":{"id":"road_map","url":"https://tiles.maps.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=6.3.0&license=686f9ec6-d775-44f0-b334-38caf85da617","minZoom":0,"maxZoom":18,"attribution":"

© OpenStreetMap contributors | Elastic Maps Service

","subdomains":[]}},"mapZoom":2,"mapCenter":[0,0],"outlineWeight":1,"showAllShapes":true},"aggs":[{"id":"1","enabled":true,"type":"avg","schema":"metric","params":{"field":"AvgTicketPrice"}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"OriginCountry","size":100,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + visState: '{\"title\":\"[Flights] Departure Count Map\",\"type\":\"vega\",\"aggs\":[],\"params\":{\"spec\":\"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\\"map\\\", latitude: 25, longitude: -40, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_flights\\n %context%: true\\n %timefield%: timestamp\\n body: {\\n size: 0\\n aggs: {\\n gridSplit: {\\n geotile_grid: {field: \\\"OriginLocation\\\", precision: 4, size: 10000}\\n aggs: {\\n gridCentroid: {\\n geo_centroid: {\\n field: \\\"OriginLocation\\\"\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\\"aggregations.gridSplit.buckets\\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n gridCentroid.location.lon\\n gridCentroid.location.lat\\n ]\\n }\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: gridSize\\n type: linear\\n domain: {data: \\\"table\\\", field: \\\"doc_count\\\"}\\n range: [\\n 50\\n 1000\\n ]\\n }\\n ]\\n marks: [\\n {\\n name: gridMarker\\n type: symbol\\n from: {data: \\\"table\\\"}\\n encode: {\\n update: {\\n size: {scale: \\\"gridSize\\\", field: \\\"doc_count\\\"}\\n xc: {signal: \\\"datum.x\\\"}\\n yc: {signal: \\\"datum.y\\\"}\\n tooltip: {\\n signal: \\\"{flights: datum.doc_count}\\\"\\n }\\n }\\n }\\n }\\n ]\\n}\"}}', uiStateJSON: '{}', description: '', version: 1, diff --git a/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts index 97258c21bc8f0..f8d39e6689fa8 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/logs/saved_objects.ts @@ -51,11 +51,11 @@ export const getSavedObjects = (): SavedObject[] => [ version: '1', migrationVersion: {}, attributes: { - title: i18n.translate('home.sampleData.logsSpec.uniqueVisitorsByCountryTitle', { - defaultMessage: '[Logs] Unique Visitors by Country', + title: i18n.translate('home.sampleData.logsSpec.visitorsMapTitle', { + defaultMessage: '[Logs] Visitors Map', }), visState: - '{"title":"[Logs] Unique Visitors by Country","type":"region_map","params":{"legendPosition":"bottomright","addTooltip":true,"colorSchema":"Reds","selectedLayer":{"attribution":"

Made with NaturalEarth | Elastic Maps Service

","name":"World Countries","weight":1,"format":{"type":"geojson"},"url":"https://vector.maps.elastic.co/blob/5659313586569216?elastic_tile_service_tos=agree&my_app_version=6.2.3&license=77ab0ecf-a521-499d-bd52-fbd740bb81d0","fields":[{"name":"iso2","description":"Two letter abbreviation"},{"name":"name","description":"Country name"},{"name":"iso3","description":"Three letter abbreviation"}],"created_at":"2017-04-26T17:12:15.978370","tags":[],"id":5659313586569216,"layerId":"elastic_maps_service.World Countries"},"selectedJoinField":{"name":"iso2","description":"Two letter abbreviation"},"isDisplayWarning":false,"wms":{"enabled":false,"options":{"format":"image/png","transparent":true},"baseLayersAreLoaded":{},"tmsLayers":[{"id":"road_map","url":"https://tiles.maps.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=6.2.3&license=77ab0ecf-a521-499d-bd52-fbd740bb81d0","minZoom":0,"maxZoom":18,"attribution":"

© OpenStreetMap contributors | Elastic Maps Service

","subdomains":[]}],"selectedTmsLayer":{"id":"road_map","url":"https://tiles.maps.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=6.2.3&license=77ab0ecf-a521-499d-bd52-fbd740bb81d0","minZoom":0,"maxZoom":18,"attribution":"

© OpenStreetMap contributors | Elastic Maps Service

","subdomains":[]}},"mapZoom":2,"mapCenter":[0,0],"outlineWeight":1,"showAllShapes":true,"emsHotLink":null},"aggs":[{"id":"1","enabled":true,"type":"cardinality","schema":"metric","params":{"field":"clientip","customLabel":"Unique Visitors"}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"geo.src","size":50,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}', + '{"title":"[Logs] Visitors Map","type":"vega","aggs":[],"params":{"spec":"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\"map\\", latitude: 30, longitude: -120, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_logs\\n %context%: true\\n %timefield%: timestamp\\n body: {\\n size: 0\\n aggs: {\\n gridSplit: {\\n geotile_grid: {field: \\"geo.coordinates\\", precision: 5, size: 10000}\\n aggs: {\\n gridCentroid: {\\n geo_centroid: {\\n field: \\"geo.coordinates\\"\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\"aggregations.gridSplit.buckets\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n gridCentroid.location.lon\\n gridCentroid.location.lat\\n ]\\n }\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: gridSize\\n type: linear\\n domain: {data: \\"table\\", field: \\"doc_count\\"}\\n range: [\\n 50\\n 1000\\n ]\\n }\\n {\\n name: bubbleColor\\n type: linear\\n domain: {\\n data: table\\n field: doc_count\\n }\\n range: [\\"rgb(249, 234, 197)\\",\\"rgb(243, 200, 154)\\",\\"rgb(235, 166, 114)\\", \\"rgb(231, 102, 76)\\"]\\n }\\n ]\\n marks: [\\n {\\n name: gridMarker\\n type: symbol\\n from: {data: \\"table\\"}\\n encode: {\\n update: {\\n fill: {\\n scale: bubbleColor\\n field: doc_count\\n }\\n size: {scale: \\"gridSize\\", field: \\"doc_count\\"}\\n xc: {signal: \\"datum.x\\"}\\n yc: {signal: \\"datum.y\\"}\\n tooltip: {\\n signal: \\"{flights: datum.doc_count}\\"\\n }\\n }\\n }\\n }\\n ]\\n}"}}', uiStateJSON: '{}', description: '', version: 1, diff --git a/src/plugins/ui_actions/public/triggers/select_range_trigger.ts b/src/plugins/ui_actions/public/triggers/select_range_trigger.ts index f6d5547f62481..312e75314bd92 100644 --- a/src/plugins/ui_actions/public/triggers/select_range_trigger.ts +++ b/src/plugins/ui_actions/public/triggers/select_range_trigger.ts @@ -27,6 +27,6 @@ export const selectRangeTrigger: Trigger<'SELECT_RANGE_TRIGGER'> = { defaultMessage: 'Range selection', }), description: i18n.translate('uiActions.triggers.selectRangeDescription', { - defaultMessage: 'Select a group of values', + defaultMessage: 'A range of values on the visualization', }), }; diff --git a/src/plugins/ui_actions/public/triggers/value_click_trigger.ts b/src/plugins/ui_actions/public/triggers/value_click_trigger.ts index e1e7b6507d82b..e63ff28f42d96 100644 --- a/src/plugins/ui_actions/public/triggers/value_click_trigger.ts +++ b/src/plugins/ui_actions/public/triggers/value_click_trigger.ts @@ -27,6 +27,6 @@ export const valueClickTrigger: Trigger<'VALUE_CLICK_TRIGGER'> = { defaultMessage: 'Single click', }), description: i18n.translate('uiActions.triggers.valueClickDescription', { - defaultMessage: 'A single point clicked on a visualization', + defaultMessage: 'A single point on the visualization', }), }; diff --git a/src/test_utils/expect_deep_equal.js b/src/test_utils/expect_deep_equal.js deleted file mode 100644 index e3e24cbdf5dc9..0000000000000 --- a/src/test_utils/expect_deep_equal.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { isEqual } from 'lodash'; -import expect from '@kbn/expect'; - -// expect.js's `eql` method provides nice error messages but sometimes misses things -// since it only tests loose (==) equality. This function uses lodash's `isEqual` as a -// second sanity check since it checks for strict equality. -export function expectDeepEqual(actual, expected) { - expect(actual).to.eql(expected); - expect(isEqual(actual, expected)).to.be(true); -} diff --git a/src/test_utils/public/image_comparator.js b/src/test_utils/public/image_comparator.js deleted file mode 100644 index f31a3e9cd646d..0000000000000 --- a/src/test_utils/public/image_comparator.js +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import pixelmatch from 'pixelmatch'; - -/** - * Utility to compare pixels of two images - * Adds the snapshots and comparison to the corners of the HTML-body to help with human inspection. - */ -export class ImageComparator { - constructor() { - this._expectCanvas = document.createElement('canvas'); - this._expectCanvas.style.position = 'fixed'; - this._expectCanvas.style.right = 0; - this._expectCanvas.style.top = 0; - this._expectCanvas.style.border = '1px solid green'; - document.body.appendChild(this._expectCanvas); - - this._diffCanvas = document.createElement('canvas'); - this._diffCanvas.style.position = 'fixed'; - this._diffCanvas.style.right = 0; - this._diffCanvas.style.bottom = 0; - this._diffCanvas.style.border = '1px solid red'; - document.body.appendChild(this._diffCanvas); - - this._actualCanvas = document.createElement('canvas'); - this._actualCanvas.style.position = 'fixed'; - this._actualCanvas.style.left = 0; - this._actualCanvas.style.bottom = 0; - this._actualCanvas.style.border = '1px solid yellow'; - document.body.appendChild(this._actualCanvas); - } - - async compareDOMContents( - domContentsText, - sourceWidth, - sourceHeight, - expectedImageSourcePng, - threshold - ) { - const sourceCanvas = document.createElement('canvas'); - sourceCanvas.width = sourceWidth; - sourceCanvas.height = sourceHeight; - sourceCanvas.style.position = 'fixed'; - sourceCanvas.style.left = 0; - sourceCanvas.style.top = 0; - sourceCanvas.style.border = '1px solid blue'; - const sourceContext2d = sourceCanvas.getContext('2d'); - document.body.appendChild(sourceCanvas); - - const sourceData = ` - - ${domContentsText} - - `; - - const sourceImage = new Image(); - return new Promise((resolve, reject) => { - sourceImage.onload = async () => { - sourceContext2d.drawImage(sourceImage, 0, 0); - const mismatch = await this.compareImage(sourceCanvas, expectedImageSourcePng, threshold); - document.body.removeChild(sourceCanvas); - resolve(mismatch); - }; - sourceImage.onerror = (e) => { - reject(e.message); - }; - sourceImage.src = 'data:image/svg+xml;base64,' + btoa(sourceData); - }); - } - - /** - * Do pixel-comparison of two images - * @param actualCanvasFromUser HTMl5 canvas - * @param expectedImageSourcePng Img to compare to - * @param threshold number between 0-1. A lower number indicates a lower tolerance for pixel-differences. - * @return number - */ - async compareImage(actualCanvasFromUser, expectedImageSourcePng, threshold) { - return new Promise((resolve, reject) => { - window.setTimeout(() => { - const actualContextFromUser = actualCanvasFromUser.getContext('2d'); - const actualImageDataFromUser = actualContextFromUser.getImageData( - 0, - 0, - actualCanvasFromUser.width, - actualCanvasFromUser.height - ); - const actualContext = this._actualCanvas.getContext('2d'); - this._actualCanvas.width = actualCanvasFromUser.width; - this._actualCanvas.height = actualCanvasFromUser.height; - actualContext.putImageData(actualImageDataFromUser, 0, 0); - - // convert expect PNG into pixel data by drawing in new canvas element - this._expectCanvas.width = this._actualCanvas.width; - this._expectCanvas.height = this._actualCanvas.height; - - const expectedImage = new Image(); - expectedImage.onload = () => { - const expectCtx = this._expectCanvas.getContext('2d'); - expectCtx.drawImage( - expectedImage, - 0, - 0, - this._actualCanvas.width, - this._actualCanvas.height - ); // draw reference image to size of generated image - - const expectImageData = expectCtx.getImageData( - 0, - 0, - this._actualCanvas.width, - this._actualCanvas.height - ); - - // compare live map vs expected pixel data - const diffImage = expectCtx.createImageData( - this._actualCanvas.width, - this._actualCanvas.height - ); - const mismatchedPixels = pixelmatch( - actualImageDataFromUser.data, - expectImageData.data, - diffImage.data, - this._actualCanvas.width, - this._actualCanvas.height, - { threshold: threshold } - ); - - const diffContext = this._diffCanvas.getContext('2d'); - this._diffCanvas.width = this._actualCanvas.width; - this._diffCanvas.height = this._actualCanvas.height; - diffContext.putImageData(diffImage, 0, 0); - - resolve(mismatchedPixels); - }; - - expectedImage.onerror = (e) => { - reject(e.message); - }; - - expectedImage.src = expectedImageSourcePng; - }); - }); - } - - destroy() { - document.body.removeChild(this._expectCanvas); - document.body.removeChild(this._diffCanvas); - document.body.removeChild(this._actualCanvas); - } -} diff --git a/src/test_utils/public/mocks/intl.js b/src/test_utils/public/mocks/intl.js deleted file mode 100644 index e75b7d71f5fa6..0000000000000 --- a/src/test_utils/public/mocks/intl.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -/* global jest */ - -export const intl = { - formatMessage: jest.fn().mockImplementation(({ defaultMessage }) => defaultMessage), - formatDate: jest.fn().mockImplementation((value) => value), - formatTime: jest.fn().mockImplementation((value) => value), - formatRelative: jest.fn().mockImplementation((value) => value), - formatNumber: jest.fn().mockImplementation((value) => value), - formatPlural: jest.fn().mockImplementation((value) => value), - formatHTMLMessage: jest.fn().mockImplementation(({ defaultMessage }) => defaultMessage), - now: jest.fn().mockImplementation(() => new Date(1531834573179)), - textComponent: 'span', -}; diff --git a/src/test_utils/public/ng_mock.js b/src/test_utils/public/ng_mock.js deleted file mode 100644 index 01bab4ce0a872..0000000000000 --- a/src/test_utils/public/ng_mock.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import angular from 'angular'; -import 'angular-mocks'; -import 'mocha'; - -if (angular.mocks) { - throw new Error( - "Don't require angular-mocks directly or the tests " + - "can't setup correctly, use the ngMock module instead." - ); -} - -export default angular.mock; diff --git a/src/test_utils/public/simulate_keys.js b/src/test_utils/public/simulate_keys.js deleted file mode 100644 index 460a75486169a..0000000000000 --- a/src/test_utils/public/simulate_keys.js +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import $ from 'jquery'; -import _ from 'lodash'; -import Bluebird from 'bluebird'; -import { keyMap } from './key_map'; -const reverseKeyMap = _.mapValues(_.invert(keyMap), _.ary(_.parseInt, 1)); - -/** - * Simulate keyboard events in an element. This allows testing the way that - * elements respond to keyboard input. - * - * # sequence style - * keyboard events occur in a sequence, this array of events describe that sequence. - * - * ## event - * an object with a type property, or a string which will be turned into a single press - * - * ## event types - * ### press - * represents a key press - * - `key`: the key for the button pressed - * - `events`: optional list of events that occur before this press completes - * - * ### wait - * represents a pause in a sequence - * - `ms`: the number of milliseconds that the pause takes - * - * ### repeat - * represents a key being repeated because it is held down. Should only exist as a - * sub event of `press` events. - * - `count`: the number of times the repeat occurs - * - * @param {element} $el - jQuery element where events should occur - * @param {[type]} sequence - an array of events - * @async - */ -export default function ($el, sequence) { - const modifierState = { - ctrlKey: false, - shiftKey: false, - altKey: false, - metaKey: false, - }; - - return doList(_.clone(sequence)); - - function setModifier(key, state) { - const name = key + 'Key'; - if (modifierState.hasOwnProperty(name)) { - modifierState[name] = !!state; - } - } - - function doList(list) { - return Bluebird.try(function () { - if (!list || !list.length) return; - - let event = list[0]; - if (_.isString(event)) { - event = { type: 'press', key: event }; - } - - switch (event.type) { - case 'press': - return Bluebird.resolve() - .then(_.partial(fire, 'keydown', event.key)) - .then(_.partial(fire, 'keypress', event.key)) - .then(_.partial(doList, event.events)) - .then(_.partial(fire, 'keyup', event.key)); - - case 'wait': - return Bluebird.delay(event.ms); - - case 'repeat': - return (function again(remaining) { - if (!remaining) return Bluebird.resolve(); - remaining = remaining - 1; - return Bluebird.resolve() - .then(_.partial(fire, 'keydown', event.key, true)) - .then(_.partial(fire, 'keypress', event.key, true)) - .then(_.partial(again, remaining)); - })(event.count); - - default: - throw new TypeError('invalid event type "' + event.type + '"'); - } - }).then(function () { - if (_.size(list) > 1) return doList(list.slice(1)); - }); - } - - function fire(type, key) { - const keyCode = reverseKeyMap[key]; - if (!keyCode) throw new TypeError('invalid key "' + key + '"'); - - if (type === 'keydown') setModifier(key, true); - if (type === 'keyup') setModifier(key, false); - - const $target = _.isFunction($el) ? $el() : $el; - const $event = new $.Event(type, _.defaults({ keyCode: keyCode }, modifierState)); - $target.trigger($event); - } -} diff --git a/src/test_utils/public/static_html_id_generator.js b/src/test_utils/public/static_html_id_generator.js deleted file mode 100644 index 07626f6a49688..0000000000000 --- a/src/test_utils/public/static_html_id_generator.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * Import this test utility in your jest test (and only there!) if you want the - * htmlIdGenerator from EUI to generate static ids. That will be needed if you - * want to use snapshot tests for a component, that uses the htmlIdGenerator. - * By default every test run would result in different ids and thus not be comparable. - * You can solve this by just importing this file. It will mock the htmlIdGenerator - * for the test file that imported it to produce static, but therefore potentially - * duplicate ids. - * - * import 'test_utils/html_id_generator'; - */ - -/* global jest */ -jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ - htmlIdGenerator: (prefix = 'staticGenerator') => { - return (suffix = 'staticId') => `${prefix}_${suffix}`; - }, -})); diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js index 94a271987ecdf..faf272daba097 100644 --- a/test/functional/apps/discover/_discover.js +++ b/test/functional/apps/discover/_discover.js @@ -224,9 +224,7 @@ export default function ({ getService, getPageObjects }) { await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'America/Phoenix' }); await PageObjects.common.navigateToApp('discover'); await PageObjects.header.awaitKibanaChrome(); - await queryBar.setQuery(''); - // To remove focus of the of the search bar so date/time picker can show - await PageObjects.discover.selectIndexPattern(defaultSettings.defaultIndex); + await queryBar.clearQuery(); await PageObjects.timePicker.setDefaultAbsoluteRange(); log.debug( diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index 06828e8e98cce..bfe0da7a5b24f 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -48,8 +48,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visualBuilder.checkVisualBuilderIsPresent(); }); - // FLAKY: https://github.com/elastic/kibana/issues/75127 - describe.skip('metric', () => { + describe('metric', () => { beforeEach(async () => { await PageObjects.visualBuilder.resetPage(); await PageObjects.visualBuilder.clickMetric(); diff --git a/test/functional/apps/visualize/input_control_vis/chained_controls.js b/test/functional/apps/visualize/input_control_vis/chained_controls.js index 035245b50d436..e1a58e1da34f3 100644 --- a/test/functional/apps/visualize/input_control_vis/chained_controls.js +++ b/test/functional/apps/visualize/input_control_vis/chained_controls.js @@ -26,8 +26,7 @@ export default function ({ getService, getPageObjects }) { const find = getService('find'); const comboBox = getService('comboBox'); - // FLAKY: https://github.com/elastic/kibana/issues/68472 - describe.skip('chained controls', function () { + describe('chained controls', function () { this.tags('includeFirefox'); before(async () => { diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index ce7a3f9e132f7..31f4e393f019e 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -346,6 +346,10 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo await browser.pressKeys(browser.keys.ENTER); } + async pressTabKey() { + await browser.pressKeys(browser.keys.TAB); + } + // Pause the browser at a certain place for debugging // Not meant for usage in CI, only for dev-usage async pause() { diff --git a/test/functional/services/query_bar.ts b/test/functional/services/query_bar.ts index 7c7fd2d81f170..8cd63fb2f4a51 100644 --- a/test/functional/services/query_bar.ts +++ b/test/functional/services/query_bar.ts @@ -54,6 +54,11 @@ export function QueryBarProvider({ getService, getPageObjects }: FtrProviderCont }); } + public async clearQuery(): Promise { + await this.setQuery(''); + await PageObjects.common.pressTabKey(); + } + public async submitQuery(): Promise { log.debug('QueryBar.submitQuery'); await testSubjects.click('queryInput'); diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx deleted file mode 100644 index 58916f26121d4..0000000000000 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx +++ /dev/null @@ -1,142 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { EuiCallOut, EuiFieldText, EuiFormRow, EuiSpacer, EuiSwitch } from '@elastic/eui'; -import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/public'; -import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public'; -import { ChartActionContext } from '../../../../../src/plugins/embeddable/public'; -import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../src/plugins/kibana_utils/public'; -import { - SELECT_RANGE_TRIGGER, - VALUE_CLICK_TRIGGER, -} from '../../../../../src/plugins/ui_actions/public'; -import { ActionExecutionContext } from '../../../../../src/plugins/ui_actions/public'; - -function isValidUrl(url: string) { - try { - new URL(url); - return true; - } catch { - return false; - } -} - -export type ActionContext = ChartActionContext; - -export interface Config { - url: string; - openInNewTab: boolean; -} - -type UrlTrigger = typeof VALUE_CLICK_TRIGGER | typeof SELECT_RANGE_TRIGGER; - -export type CollectConfigProps = CollectConfigPropsBase; - -const SAMPLE_DASHBOARD_TO_URL_DRILLDOWN = 'SAMPLE_DASHBOARD_TO_URL_DRILLDOWN'; - -export class DashboardToUrlDrilldown implements Drilldown { - public readonly id = SAMPLE_DASHBOARD_TO_URL_DRILLDOWN; - - public readonly order = 8; - - readonly minimalLicense = 'gold'; // example of minimal license support - readonly licenseFeatureName = 'Sample URL Drilldown'; - - public readonly getDisplayName = () => 'Go to URL (example)'; - - public readonly euiIcon = 'link'; - - supportedTriggers(): UrlTrigger[] { - return [VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER]; - } - - private readonly ReactCollectConfig: React.FC = ({ - config, - onConfig, - context, - }) => ( - <> - -

- This is an example drilldown. It is meant as a starting point for developers, so they can - grab this code and get started. It does not provide a complete working functionality but - serves as a getting started example. -

-

- Implementation of the actual Go to URL drilldown is tracked in{' '} - #55324 -

-
- - - onConfig({ ...config, url: event.target.value })} - onBlur={() => { - if (!config.url) return; - if (/https?:\/\//.test(config.url)) return; - onConfig({ ...config, url: 'https://' + config.url }); - }} - /> - - - onConfig({ ...config, openInNewTab: !config.openInNewTab })} - /> - - - - {/* just demo how can access selected triggers*/} -

Will be attached to triggers: {JSON.stringify(context.triggers)}

-
- - ); - - public readonly CollectConfig = reactToUiComponent(this.ReactCollectConfig); - - public readonly createConfig = () => ({ - url: '', - openInNewTab: false, - }); - - public readonly isConfigValid = (config: Config): config is Config => { - if (!config.url) return false; - return isValidUrl(config.url); - }; - - /** - * `getHref` is need to support mouse middle-click and Cmd + Click behavior - * to open a link in new tab. - */ - public readonly getHref = async (config: Config, context: ActionContext) => { - return config.url; - }; - - public readonly execute = async ( - config: Config, - context: ActionExecutionContext - ) => { - // Just for showcasing: - // we can get trigger a which caused this drilldown execution - // eslint-disable-next-line no-console - console.log(context.trigger?.id); - - const url = await this.getHref(config, context); - - if (config.openInNewTab) { - window.open(url, '_blank'); - } else { - window.location.href = url; - } - }; -} diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts b/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts index 7f2c9a9b3bbc8..3f0b64a2ac9ed 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts +++ b/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts @@ -11,7 +11,6 @@ import { AdvancedUiActionsStart, } from '../../../../x-pack/plugins/ui_actions_enhanced/public'; import { DashboardHelloWorldDrilldown } from './dashboard_hello_world_drilldown'; -import { DashboardToUrlDrilldown } from './dashboard_to_url_drilldown'; import { DashboardToDiscoverDrilldown } from './dashboard_to_discover_drilldown'; import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public'; import { DiscoverSetup, DiscoverStart } from '../../../../src/plugins/discover/public'; @@ -39,7 +38,6 @@ export class UiActionsEnhancedExamplesPlugin uiActions.registerDrilldown(new DashboardHelloWorldDrilldown()); uiActions.registerDrilldown(new DashboardHelloWorldOnlyRangeSelectDrilldown()); - uiActions.registerDrilldown(new DashboardToUrlDrilldown()); uiActions.registerDrilldown(new DashboardToDiscoverDrilldown({ start })); } diff --git a/x-pack/package.json b/x-pack/package.json index bf093fed30747..899eca1095923 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -159,7 +159,7 @@ "copy-to-clipboard": "^3.0.8", "copy-webpack-plugin": "^6.0.2", "cronstrue": "^1.51.0", - "cypress": "4.11.0", + "cypress": "5.0.0", "cypress-multi-reporters": "^1.2.3", "d3": "3.5.17", "d3-scale": "1.0.7", diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index d5843bd531d84..b16ded9fb5c91 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -152,13 +152,14 @@ export class AlertingPlugin { ); } + this.eventLogger = plugins.eventLog.getLogger({ + event: { provider: EVENT_LOG_PROVIDER }, + }); + setupSavedObjects(core.savedObjects, plugins.encryptedSavedObjects); this.eventLogService = plugins.eventLog; plugins.eventLog.registerProviderActions(EVENT_LOG_PROVIDER, Object.values(EVENT_LOG_ACTIONS)); - this.eventLogger = plugins.eventLog.getLogger({ - event: { provider: EVENT_LOG_PROVIDER }, - }); const alertTypeRegistry = new AlertTypeRegistry({ taskManager: plugins.taskManager, diff --git a/x-pack/plugins/alerts/server/saved_objects/index.ts b/x-pack/plugins/alerts/server/saved_objects/index.ts index c98d9bcbd9ae5..06ce8d673e6b7 100644 --- a/x-pack/plugins/alerts/server/saved_objects/index.ts +++ b/x-pack/plugins/alerts/server/saved_objects/index.ts @@ -6,6 +6,7 @@ import { SavedObjectsServiceSetup } from 'kibana/server'; import mappings from './mappings.json'; +import { getMigrations } from './migrations'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; export function setupSavedObjects( @@ -16,6 +17,7 @@ export function setupSavedObjects( name: 'alert', hidden: true, namespaceType: 'single', + migrations: getMigrations(encryptedSavedObjects), mappings: mappings.alert, }); diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts new file mode 100644 index 0000000000000..46fa2bcd512ff --- /dev/null +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import uuid from 'uuid'; +import { getMigrations } from './migrations'; +import { RawAlert } from '../types'; +import { SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; +import { migrationMocks } from 'src/core/server/mocks'; + +const { log } = migrationMocks.createContext(); +const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); + +describe('7.10.0', () => { + beforeEach(() => { + jest.resetAllMocks(); + encryptedSavedObjectsSetup.createMigration.mockImplementation( + (shouldMigrateWhenPredicate, migration) => migration + ); + }); + + test('changes nothing on alerts by other plugins', () => { + const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; + const alert = getMockData({}); + expect(migration710(alert, { log })).toMatchObject(alert); + + expect(encryptedSavedObjectsSetup.createMigration).toHaveBeenCalledWith( + expect.any(Function), + expect.any(Function) + ); + }); + + test('migrates the consumer for metrics', () => { + const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; + const alert = getMockData({ + consumer: 'metrics', + }); + expect(migration710(alert, { log })).toMatchObject({ + ...alert, + attributes: { + ...alert.attributes, + consumer: 'infrastructure', + }, + }); + }); + + test('migrates the consumer for alerting', () => { + const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; + const alert = getMockData({ + consumer: 'alerting', + }); + expect(migration710(alert, { log })).toMatchObject({ + ...alert, + attributes: { + ...alert.attributes, + consumer: 'alerts', + }, + }); + }); +}); + +describe('7.10.0 migrates with failure', () => { + beforeEach(() => { + jest.resetAllMocks(); + encryptedSavedObjectsSetup.createMigration.mockRejectedValueOnce( + new Error(`Can't migrate!`) as never + ); + }); + + test('should show the proper exception', () => { + const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0']; + const alert = getMockData({ + consumer: 'alerting', + }); + const res = migration710(alert, { log }); + expect(res).toMatchObject({ + ...alert, + attributes: { + ...alert.attributes, + }, + }); + expect(log.error).toHaveBeenCalledWith( + `encryptedSavedObject migration failed for alert ${alert.id} with error: migrationFunc is not a function`, + { + alertDocument: { + ...alert, + attributes: { + ...alert.attributes, + }, + }, + } + ); + }); +}); + +function getMockData( + overwrites: Record = {} +): SavedObjectUnsanitizedDoc { + return { + attributes: { + enabled: true, + name: 'abc', + tags: ['foo'], + alertTypeId: '123', + consumer: 'bar', + apiKey: '', + apiKeyOwner: '', + schedule: { interval: '10s' }, + throttle: null, + params: { + bar: true, + }, + muteAll: false, + mutedInstanceIds: [], + createdBy: new Date().toISOString(), + updatedBy: new Date().toISOString(), + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: '1', + actionTypeId: '1', + params: { + foo: true, + }, + }, + ], + ...overwrites, + }, + id: uuid.v4(), + type: 'alert', + }; +} diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.ts new file mode 100644 index 0000000000000..30570eeb0a453 --- /dev/null +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { + SavedObjectMigrationMap, + SavedObjectUnsanitizedDoc, + SavedObjectMigrationFn, + SavedObjectMigrationContext, +} from '../../../../../src/core/server'; +import { RawAlert } from '../types'; +import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; + +export function getMigrations( + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup +): SavedObjectMigrationMap { + const alertsMigration = changeAlertingConsumer(encryptedSavedObjects, 'alerting', 'alerts'); + + const infrastructureMigration = changeAlertingConsumer( + encryptedSavedObjects, + 'metrics', + 'infrastructure' + ); + + return { + '7.10.0': (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => { + if (doc.attributes.consumer === 'alerting') { + return executeMigration(doc, context, alertsMigration); + } else if (doc.attributes.consumer === 'metrics') { + return executeMigration(doc, context, infrastructureMigration); + } + return doc; + }, + }; +} + +function executeMigration( + doc: SavedObjectUnsanitizedDoc, + context: SavedObjectMigrationContext, + migrationFunc: SavedObjectMigrationFn +) { + try { + return migrationFunc(doc, context); + } catch (ex) { + context.log.error( + `encryptedSavedObject migration failed for alert ${doc.id} with error: ${ex.message}`, + { alertDocument: doc } + ); + } + return doc; +} + +function changeAlertingConsumer( + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup, + from: string, + to: string +): SavedObjectMigrationFn { + return encryptedSavedObjects.createMigration( + function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { + return doc.attributes.consumer === from; + }, + (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => { + const { + attributes: { consumer }, + } = doc; + return { + ...doc, + attributes: { + ...doc.attributes, + consumer: consumer === from ? to : consumer, + }, + }; + } + ); +} diff --git a/x-pack/plugins/apm/e2e/cypress/support/index.ts b/x-pack/plugins/apm/e2e/cypress/support/index.ts index 8a7a9f64cc461..c7e31b095e5ac 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/index.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/index.ts @@ -21,7 +21,7 @@ import './commands'; -// @ts-ignore +// @ts-expect-error import { register } from '@cypress/snapshot'; register(); diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx index ecdd52e31730c..3b118bcd91ff1 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx @@ -14,7 +14,7 @@ import { mean } from 'lodash'; import React from 'react'; import { asRelativeDateTimeRange } from '../../../../utils/formatters'; import { getTimezoneOffsetInMs } from '../../../shared/charts/CustomPlot/getTimezoneOffsetInMs'; -// @ts-ignore +// @ts-expect-error import Histogram from '../../../shared/charts/Histogram'; import { EmptyMessage } from '../../../shared/EmptyMessage'; diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx index d99dc4d5cd37a..7a00840daa3c5 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx @@ -17,7 +17,7 @@ export function EditAgentConfigurationRouteHandler() { // typescript complains because `pageStop` does not exist in `APMQueryParams` // Going forward we should move away from globally declared query params and this is a first step - // @ts-ignore + // @ts-expect-error const { name, environment, pageStep } = toQuery(search); const res = useFetcher( @@ -45,7 +45,7 @@ export function CreateAgentConfigurationRouteHandler() { const { search } = history.location; // Ignoring here because we specifically DO NOT want to add the query params to the global route handler - // @ts-ignore + // @ts-expect-error const { pageStep } = toQuery(search); return ( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownFilter.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownFilter.tsx index 7e5e7cdc53c55..12d8efdbd27f3 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownFilter.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownFilter.tsx @@ -5,64 +5,85 @@ */ import React from 'react'; -import { BreakdownGroup } from './BreakdownGroup'; -import { BreakdownItem } from '../../../../../typings/ui_filters'; +import { EuiSuperSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { CLIENT_GEO_COUNTRY_ISO_CODE, USER_AGENT_DEVICE, USER_AGENT_NAME, USER_AGENT_OS, } from '../../../../../common/elasticsearch_fieldnames'; +import { BreakdownItem } from '../../../../../typings/ui_filters'; interface Props { - id: string; - selectedBreakdowns: BreakdownItem[]; - onBreakdownChange: (values: BreakdownItem[]) => void; + selectedBreakdown: BreakdownItem | null; + onBreakdownChange: (value: BreakdownItem | null) => void; } export function BreakdownFilter({ - id, - selectedBreakdowns, + selectedBreakdown, onBreakdownChange, }: Props) { - const categories: BreakdownItem[] = [ + const NO_BREAKDOWN = 'noBreakdown'; + + const items: BreakdownItem[] = [ { - name: 'Browser', + name: i18n.translate('xpack.apm.csm.breakDownFilter.noBreakdown', { + defaultMessage: 'No breakdown', + }), + fieldName: NO_BREAKDOWN, type: 'category', - count: 0, - selected: selectedBreakdowns.some(({ name }) => name === 'Browser'), - fieldName: USER_AGENT_NAME, }, { - name: 'OS', + name: i18n.translate('xpack.apm.csm.breakdownFilter.browser', { + defaultMessage: 'Browser', + }), + fieldName: USER_AGENT_NAME, type: 'category', - count: 0, - selected: selectedBreakdowns.some(({ name }) => name === 'OS'), - fieldName: USER_AGENT_OS, }, { - name: 'Device', + name: i18n.translate('xpack.apm.csm.breakdownFilter.os', { + defaultMessage: 'OS', + }), + fieldName: USER_AGENT_OS, type: 'category', - count: 0, - selected: selectedBreakdowns.some(({ name }) => name === 'Device'), - fieldName: USER_AGENT_DEVICE, }, { - name: 'Location', + name: i18n.translate('xpack.apm.csm.breakdownFilter.device', { + defaultMessage: 'Device', + }), + fieldName: USER_AGENT_DEVICE, type: 'category', - count: 0, - selected: selectedBreakdowns.some(({ name }) => name === 'Location'), + }, + { + name: i18n.translate('xpack.apm.csm.breakdownFilter.location', { + defaultMessage: 'Location', + }), fieldName: CLIENT_GEO_COUNTRY_ISO_CODE, + type: 'category', }, ]; + const options = items.map(({ name, fieldName }) => ({ + inputDisplay: fieldName === NO_BREAKDOWN ? name : {name}, + value: fieldName, + dropdownDisplay: name, + })); + + const onOptionChange = (value: string) => { + if (value === NO_BREAKDOWN) { + onBreakdownChange(null); + } + onBreakdownChange(items.find(({ fieldName }) => fieldName === value)!); + }; + return ( - { - onBreakdownChange(selValues); - }} + onOptionChange(value)} /> ); } diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownGroup.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownGroup.tsx deleted file mode 100644 index d4f80667ce98b..0000000000000 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Breakdowns/BreakdownGroup.tsx +++ /dev/null @@ -1,100 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - EuiPopover, - EuiFilterButton, - EuiFilterGroup, - EuiPopoverTitle, - EuiFilterSelectItem, -} from '@elastic/eui'; -import React, { MouseEvent, useCallback, useEffect, useState } from 'react'; -import { BreakdownItem } from '../../../../../typings/ui_filters'; -import { I18LABELS } from '../translations'; - -export interface BreakdownGroupProps { - id: string; - disabled?: boolean; - items: BreakdownItem[]; - onChange: (values: BreakdownItem[]) => void; -} - -export function BreakdownGroup({ - id, - disabled, - onChange, - items, -}: BreakdownGroupProps) { - const [isOpen, setIsOpen] = useState(false); - - const [activeItems, setActiveItems] = useState(items); - - useEffect(() => { - setActiveItems(items); - }, [items]); - - const getSelItems = () => activeItems.filter((item) => item.selected); - - const onFilterItemClick = useCallback( - (name: string) => (_event: MouseEvent) => { - setActiveItems((prevItems) => - prevItems.map((item) => ({ - ...item, - selected: name === item.name ? !item.selected : item.selected, - })) - ); - }, - [] - ); - - return ( - - 0} - numFilters={activeItems.length} - numActiveFilters={getSelItems().length} - hasActiveFilters={getSelItems().length !== 0} - iconType="arrowDown" - onClick={() => { - setIsOpen(!isOpen); - }} - size="s" - > - {I18LABELS.breakdown} - - } - closePopover={() => { - setIsOpen(false); - onChange(getSelItems()); - }} - data-cy={`breakdown-popover_${id}`} - id={id} - isOpen={isOpen} - ownFocus={true} - withTitle - zIndex={10000} - > - {I18LABELS.selectBreakdown} -
- {activeItems.map(({ name, count, selected }) => ( - 0} - > - {name} - - ))} -
-
-
- ); -} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx index 33573052dbcbb..c832ec9fcc0d0 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageLoadDistChart.tsx @@ -43,7 +43,7 @@ interface PageLoadData { interface Props { onPercentileChange: (min: number, max: number) => void; data?: PageLoadData | null; - breakdowns: BreakdownItem[]; + breakdown: BreakdownItem | null; percentileRange: PercentileRange; loading: boolean; } @@ -57,7 +57,7 @@ const PageLoadChart = styled(Chart)` export function PageLoadDistChart({ onPercentileChange, data, - breakdowns, + breakdown, loading, percentileRange, }: Props) { @@ -122,17 +122,17 @@ export function PageLoadDistChart({ data={data?.pageLoadDistribution ?? []} curve={CurveType.CURVE_CATMULL_ROM} /> - {breakdowns.map(({ name, type }) => ( + {breakdown && ( { setBreakdownLoading(bLoading); }} /> - ))} + )} )} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx index 53f2d5ae238c5..3e35f15254937 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx @@ -34,7 +34,7 @@ export function PageLoadDistribution() { max: null, }); - const [breakdowns, setBreakdowns] = useState([]); + const [breakdown, setBreakdown] = useState(null); const { data, status } = useFetcher( (callApmApi) => { @@ -94,11 +94,10 @@ export function PageLoadDistribution() { {I18LABELS.resetZoom} - + @@ -107,7 +106,7 @@ export function PageLoadDistribution() { data={data} onPercentileChange={onPercentileChange} loading={status !== 'success'} - breakdowns={breakdowns} + breakdown={breakdown} percentileRange={{ max: percentileRange.max || data?.maxDuration, min: percentileRange.min || data?.minDuration, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx index 0f43c0ddf540d..a67f6dd8e3cb5 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx @@ -18,7 +18,7 @@ export function PageViewsTrend() { const { start, end, serviceName } = urlParams; - const [breakdowns, setBreakdowns] = useState([]); + const [breakdown, setBreakdown] = useState(null); const { data, status } = useFetcher( (callApmApi) => { @@ -30,9 +30,9 @@ export function PageViewsTrend() { start, end, uiFilters: JSON.stringify(uiFilters), - ...(breakdowns.length > 0 + ...(breakdown ? { - breakdowns: JSON.stringify(breakdowns), + breakdowns: JSON.stringify(breakdown), } : {}), }, @@ -41,13 +41,9 @@ export function PageViewsTrend() { } return Promise.resolve(undefined); }, - [end, start, serviceName, uiFilters, breakdowns] + [end, start, serviceName, uiFilters, breakdown] ); - const onBreakdownChange = (values: BreakdownItem[]) => { - setBreakdowns(values); - }; - return (
@@ -56,11 +52,10 @@ export function PageViewsTrend() {

{I18LABELS.pageViews}

- +
diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx index c8f586240471f..baba592e5886e 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx @@ -45,7 +45,7 @@ function doZoom( ) { if (cy) { const level = cy.zoom() + increment; - // @ts-ignore `.position()` _does_ work on a NodeCollection. It returns the position of the first element in the collection. + // @ts-expect-error `.position()` _does_ work on a NodeCollection. It returns the position of the first element in the collection. const primaryCenter = cy.nodes('.primary').position(); const { x1, y1, w, h } = cy.nodes().boundingBox({}); const graphCenter = { x: x1 + w / 2, y: y1 + h / 2 }; @@ -67,7 +67,7 @@ function useDebugDownloadUrl(cy?: cytoscape.Core) { // Handle elements changes to update the download URL useEffect(() => { const elementsHandler: cytoscape.EventHandler = (event) => { - // @ts-ignore The `true` argument to `cy.json` is to flatten the elements + // @ts-expect-error The `true` argument to `cy.json` is to flatten the elements // (instead of having them broken into nodes/edges.) DefinitelyTyped has // this wrong. const elementsJson = event.cy.json(true)?.elements.map((element) => ({ diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx index 4911d7f147d7c..197bc94c62603 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx @@ -31,7 +31,7 @@ interface ContentsProps { // This method of detecting IE is from a Stack Overflow answer: // https://stackoverflow.com/a/21825207 // -// @ts-ignore `documentMode` is not recognized as a valid property of `document`. +// @ts-expect-error `documentMode` is not recognized as a valid property of `document`. const isIE11 = !!window.MSInputMethodContext && !!document.documentMode; function FlexColumnGroup(props: { diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts index 9d58ed142dab7..9fedcc70bbbcf 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -87,14 +87,14 @@ function getBorderWidth(el: cytoscape.NodeSingular) { // This method of detecting IE is from a Stack Overflow answer: // https://stackoverflow.com/a/21825207 // -// @ts-ignore `documentMode` is not recognized as a valid property of `document`. +// @ts-expect-error `documentMode` is not recognized as a valid property of `document`. const isIE11 = !!window.MSInputMethodContext && !!document.documentMode; export const getAnimationOptions = ( theme: EuiTheme ): cytoscape.AnimationOptions => ({ duration: parseInt(theme.eui.euiAnimSpeedNormal, 10), - // @ts-ignore The cubic-bezier options here are not recognized by the cytoscape types + // @ts-expect-error The cubic-bezier options here are not recognized by the cytoscape types easing: theme.eui.euiAnimSlightBounce, }); @@ -119,8 +119,6 @@ const getStyle = (theme: EuiTheme): cytoscape.Stylesheet[] => { 'background-color': theme.eui.euiColorGhost, // The DefinitelyTyped definitions don't specify that a function can be // used here. - // - // @ts-ignore 'background-image': isIE11 ? undefined : (el: cytoscape.NodeSingular) => iconForNode(el) ?? defaultIcon, @@ -176,7 +174,7 @@ const getStyle = (theme: EuiTheme): cytoscape.Stylesheet[] => { // The DefinitelyTyped definitions don't specify this property since it's // fairly new. // - // @ts-ignore + // @ts-expect-error 'target-distance-from-node': isIE11 ? undefined : theme.eui.paddingSizes.xs, @@ -191,7 +189,7 @@ const getStyle = (theme: EuiTheme): cytoscape.Stylesheet[] => { 'source-arrow-shape': isIE11 ? 'none' : 'triangle', 'source-arrow-color': lineColor, 'target-arrow-shape': isIE11 ? 'none' : 'triangle', - // @ts-ignore + // @ts-expect-error 'source-distance-from-node': isIE11 ? undefined : parseInt(theme.eui.paddingSizes.xs, 10), @@ -202,7 +200,7 @@ const getStyle = (theme: EuiTheme): cytoscape.Stylesheet[] => { }, { selector: 'edge[isInverseEdge]', - // @ts-ignore DefinitelyTyped says visibility is "none" but it's + // @ts-expect-error DefinitelyTyped says visibility is "none" but it's // actually "hidden" style: { visibility: 'hidden' }, }, @@ -210,7 +208,6 @@ const getStyle = (theme: EuiTheme): cytoscape.Stylesheet[] => { selector: 'edge.nodeHover', style: { width: 4, - // @ts-ignore 'z-index': zIndexEdgeHover, 'line-color': theme.eui.euiColorDarkShade, 'source-arrow-color': theme.eui.euiColorDarkShade, @@ -230,7 +227,6 @@ const getStyle = (theme: EuiTheme): cytoscape.Stylesheet[] => { 'line-color': theme.eui.euiColorPrimary, 'source-arrow-color': theme.eui.euiColorPrimary, 'target-arrow-color': theme.eui.euiColorPrimary, - // @ts-ignore 'z-index': zIndexEdgeHighlight, }, }, diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts index c211ef3abab1d..2f4cc0d39d71c 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts @@ -111,7 +111,7 @@ function getSpanIcon(type?: string, subtype?: string) { // This method of detecting IE is from a Stack Overflow answer: // https://stackoverflow.com/a/21825207 // -// @ts-ignore `documentMode` is not recognized as a valid property of `document`. +// @ts-expect-error `documentMode` is not recognized as a valid property of `document`. const isIE11 = !!window.MSInputMethodContext && !!document.documentMode; export function iconForNode(node: cytoscape.NodeSingular) { diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx index 78d75feb72f1f..d9c5ff5130df6 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx @@ -61,7 +61,7 @@ const httpGet = jest.fn(); describe('Service Overview -> View', () => { beforeEach(() => { - // @ts-ignore + // @ts-expect-error global.sessionStorage = new SessionStorageMock(); // mock urlParams diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx index 9b4f2175731dc..069c4468d206b 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx @@ -16,7 +16,7 @@ import { TransactionDistributionAPIResponse } from '../../../../../server/lib/tr import { IBucket } from '../../../../../server/lib/transactions/distribution/get_buckets/transform'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { getDurationFormatter } from '../../../../utils/formatters'; -// @ts-ignore +// @ts-expect-error import Histogram from '../../../shared/charts/Histogram'; import { EmptyMessage } from '../../../shared/EmptyMessage'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts index 5d8c5bc9ec6cf..b85171308d745 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts @@ -27,7 +27,7 @@ export const getErrorMarks = ( return errorItems.map((error) => ({ type: 'errorMark', - offset: error.offset + error.skew, + offset: Math.max(error.offset + error.skew, 0), verticalLine: false, id: error.doc.error.id, error: error.doc, diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx index 519d0b476d769..19d8063846a06 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx @@ -9,7 +9,6 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import styled from 'styled-components'; import { px, unit } from '../../../../../style/variables'; -// @ts-ignore import { Legend } from '../../../../shared/charts/Legend'; import { IServiceColors } from './Waterfall/waterfall_helpers/waterfall_helpers'; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx index 7bceeb9ac1652..b1228646595f3 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx @@ -8,13 +8,13 @@ import { EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { tint } from 'polished'; import React, { Fragment } from 'react'; -// @ts-ignore +// @ts-expect-error import sql from 'react-syntax-highlighter/dist/languages/sql'; import SyntaxHighlighter, { registerLanguage, - // @ts-ignore + // @ts-expect-error } from 'react-syntax-highlighter/dist/light'; -// @ts-ignore +// @ts-expect-error import { xcode } from 'react-syntax-highlighter/dist/styles'; import styled from 'styled-components'; import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx index b2301298cbdfe..0958f99b5bd98 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx @@ -9,7 +9,6 @@ import { i18n } from '@kbn/i18n'; import { History, Location } from 'history'; import React, { useState } from 'react'; import { useHistory } from 'react-router-dom'; -// @ts-ignore import { StickyContainer } from 'react-sticky'; import styled from 'styled-components'; import { px } from '../../../../../../style/variables'; diff --git a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx index 1fa9a0baa7265..48d8bb2b41644 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx @@ -5,7 +5,7 @@ */ import { Transaction } from '../../../../../../typings/es_schemas/ui/transaction'; -// @ts-ignore +// @ts-expect-error import configureStore from '../../../../../store/config/configureStore'; import { getDiscoverQuery } from '../DiscoverTransactionLink'; diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx index 8cabb820ed7f3..50f87184f8ee7 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx @@ -12,7 +12,6 @@ import { px, unit, units } from '../../../style/variables'; import { Stacktrace } from '.'; import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; -// @ts-ignore Styled Components has trouble inferring the types of the default props here. const Accordion = styled(EuiAccordion)` border-top: ${({ theme }) => theme.eui.euiBorderThin}; margin-top: ${px(units.half)}; diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx index 72fdc93de9545..020137fc31672 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx @@ -8,17 +8,17 @@ import { size } from 'lodash'; import { tint } from 'polished'; import React from 'react'; // TODO add dependency for @types/react-syntax-highlighter -// @ts-ignore +// @ts-expect-error import javascript from 'react-syntax-highlighter/dist/languages/javascript'; -// @ts-ignore +// @ts-expect-error import python from 'react-syntax-highlighter/dist/languages/python'; -// @ts-ignore +// @ts-expect-error import ruby from 'react-syntax-highlighter/dist/languages/ruby'; -// @ts-ignore +// @ts-expect-error import SyntaxHighlighter from 'react-syntax-highlighter/dist/light'; -// @ts-ignore +// @ts-expect-error import { registerLanguage } from 'react-syntax-highlighter/dist/light'; -// @ts-ignore +// @ts-expect-error import { xcode } from 'react-syntax-highlighter/dist/styles'; import styled from 'styled-components'; import { StackframeWithLineContext } from '../../../../typings/es_schemas/raw/fields/stackframe'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/CustomPlot.stories.tsx b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/CustomPlot.stories.tsx index 48e83763cb9df..e70c53108cb0e 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/CustomPlot.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/CustomPlot.stories.tsx @@ -5,7 +5,7 @@ */ import { storiesOf } from '@storybook/react'; import React from 'react'; -// @ts-ignore +// @ts-expect-error import CustomPlot from './'; storiesOf('shared/charts/CustomPlot', module).add( diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.test.ts b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.test.ts index 7081286ecf3f2..935895022931c 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.test.ts +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.test.ts @@ -12,7 +12,7 @@ describe('getTimezoneOffsetInMs', () => { let originalTimezone: moment.MomentZone | null; beforeAll(() => { - // @ts-ignore moment types do not define defaultZone but it's there + // @ts-expect-error moment types do not define defaultZone but it's there originalTimezone = moment.defaultZone; }); diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.ts b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.ts index 178707bfd3c48..a442450a192ab 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.ts +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.ts @@ -6,9 +6,9 @@ import moment from 'moment-timezone'; export function getTimezoneOffsetInMs(time: number) { - // @ts-ignore moment types don't define defaultZone but it's there + // @ts-expect-error moment types don't define defaultZone but it's there const zone = moment.defaultZone ? moment.defaultZone.name : moment.tz.guess(); - // @ts-ignore + // @ts-expect-error return moment.tz.zone(zone).parse(time) * 60000; } diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts index 048a7306348f3..117ec26446de8 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore import * as plotUtils from './plotUtils'; import { TimeSeries, Coordinate } from '../../../../../typings/timeseries'; diff --git a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx index 1676d3f68b57c..1a91e398cec76 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/ErroneousTransactionsRateChart/index.tsx @@ -14,7 +14,7 @@ import { useChartsSync } from '../../../../hooks/useChartsSync'; import { useFetcher } from '../../../../hooks/useFetcher'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { callApmApi } from '../../../../services/rest/createCallApmApi'; -// @ts-ignore +// @ts-expect-error import CustomPlot from '../CustomPlot'; const tickFormatY = (y?: number) => { diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx index 5d55a6ff5bd8f..09e6b0e43945f 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx @@ -7,7 +7,7 @@ import React, { useCallback } from 'react'; import { Coordinate, TimeSeries } from '../../../../../../typings/timeseries'; import { useChartsSync } from '../../../../../hooks/useChartsSync'; -// @ts-ignore +// @ts-expect-error import CustomPlot from '../../CustomPlot'; interface Props { diff --git a/x-pack/plugins/apm/public/services/__test__/callApi.test.ts b/x-pack/plugins/apm/public/services/__test__/callApi.test.ts index 76cc275aa8c23..f82201bbd4de8 100644 --- a/x-pack/plugins/apm/public/services/__test__/callApi.test.ts +++ b/x-pack/plugins/apm/public/services/__test__/callApi.test.ts @@ -23,7 +23,7 @@ describe('callApi', () => { }), } as unknown) as HttpMock; - // @ts-ignore + // @ts-expect-error global.sessionStorage = new SessionStorageMock(); }); diff --git a/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts b/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts index 2e2f6c9a4c8b6..75b0471424e79 100644 --- a/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts @@ -5,9 +5,9 @@ */ import moment from 'moment'; -// @ts-ignore +// @ts-expect-error import { calculateAuto } from './calculate_auto'; -// @ts-ignore +// @ts-expect-error import { unitToSeconds } from './unit_to_seconds'; export function getBucketSize(start: number, end: number, interval: string) { diff --git a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap index 22b8c226e9026..2cb28d378e8fd 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/rum_client/__snapshots__/queries.test.ts.snap @@ -148,7 +148,7 @@ Object { "body": Object { "aggs": Object { "pageViews": Object { - "aggs": Object {}, + "aggs": undefined, "auto_date_histogram": Object { "buckets": 50, "field": "@timestamp", diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts index 23169ddaca534..114137e9fad17 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts @@ -11,7 +11,6 @@ import { SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; -import { AggregationInputMap } from '../../../typings/elasticsearch/aggregations'; import { BreakdownItem } from '../../../typings/ui_filters'; export async function getPageViewTrends({ @@ -24,18 +23,9 @@ export async function getPageViewTrends({ const projection = getRumOverviewProjection({ setup, }); - const breakdownAggs: AggregationInputMap = {}; + let breakdownItem: BreakdownItem | null = null; if (breakdowns) { - const breakdownList: BreakdownItem[] = JSON.parse(breakdowns); - breakdownList.forEach(({ name, type, fieldName }) => { - breakdownAggs[name] = { - terms: { - field: fieldName, - size: 9, - missing: 'Other', - }, - }; - }); + breakdownItem = JSON.parse(breakdowns); } const params = mergeProjection(projection, { @@ -50,7 +40,17 @@ export async function getPageViewTrends({ field: '@timestamp', buckets: 50, }, - aggs: breakdownAggs, + aggs: breakdownItem + ? { + breakdown: { + terms: { + field: breakdownItem.fieldName, + size: 9, + missing: 'Other', + }, + }, + } + : undefined, }, }, }, @@ -68,19 +68,18 @@ export async function getPageViewTrends({ x: xVal, y: bCount, }; - - Object.keys(breakdownAggs).forEach((bKey) => { - const categoryBuckets = (bucket[bKey] as any).buckets; + if (breakdownItem) { + const categoryBuckets = (bucket.breakdown as any).buckets; categoryBuckets.forEach( ({ key, doc_count: docCount }: { key: string; doc_count: number }) => { if (key === 'Other') { - res[key + `(${bKey})`] = docCount; + res[key + `(${breakdownItem?.name})`] = docCount; } else { res[key] = docCount; } } ); - }); + } return res; }); diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts index 7e4bcfdda7382..e529198e717d3 100644 --- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts @@ -72,7 +72,6 @@ describe('transformServiceMapResponses', () => { (element) => 'source' in element.data && 'target' in element.data ); - // @ts-ignore expect(connection?.data.target).toBe('opbeans-node'); expect( @@ -149,9 +148,9 @@ describe('transformServiceMapResponses', () => { const nodejsNode = nodes.find((node) => node.data.id === '>opbeans-node'); - // @ts-ignore + // @ts-expect-error expect(nodejsNode?.data[SPAN_TYPE]).toBe('external'); - // @ts-ignore + // @ts-expect-error expect(nodejsNode?.data[SPAN_SUBTYPE]).toBe('aa'); }); diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts index e943214b0b517..34863c64f9804 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.test.ts @@ -79,7 +79,7 @@ describe('getTransactionBreakdown', () => { }); it('should not include more KPIs than MAX_KPIs', async () => { - // @ts-ignore + // @ts-expect-error constants.MAX_KPIS = 2; const response = await getTransactionBreakdown({ diff --git a/x-pack/plugins/apm/server/routes/create_api/index.ts b/x-pack/plugins/apm/server/routes/create_api/index.ts index 92f52dd1552d6..42eebc51463db 100644 --- a/x-pack/plugins/apm/server/routes/create_api/index.ts +++ b/x-pack/plugins/apm/server/routes/create_api/index.ts @@ -140,7 +140,7 @@ export function createApi() { // Only return values for parameters that have runtime types, // but always include query as _debug is always set even if // it's not defined in the route. - // @ts-ignore + // @ts-expect-error params: pick(parsedParams, ...Object.keys(params), 'query'), config, logger, diff --git a/x-pack/plugins/apm/typings/ui_filters.ts b/x-pack/plugins/apm/typings/ui_filters.ts index 2a727dda7241d..efba6919778bb 100644 --- a/x-pack/plugins/apm/typings/ui_filters.ts +++ b/x-pack/plugins/apm/typings/ui_filters.ts @@ -14,7 +14,6 @@ export type UIFilters = { export interface BreakdownItem { name: string; - count: number; type: string; fieldName: string; selected?: boolean; diff --git a/x-pack/plugins/canvas/storybook/webpack.config.js b/x-pack/plugins/canvas/storybook/webpack.config.js index c9817de649c25..d8434bd5d9080 100644 --- a/x-pack/plugins/canvas/storybook/webpack.config.js +++ b/x-pack/plugins/canvas/storybook/webpack.config.js @@ -184,8 +184,6 @@ module.exports = async ({ config: storybookConfig }) => { __dirname, '../tasks/mocks/uiAbsoluteToParsedUrl' ), - ui: path.resolve(KIBANA_ROOT, 'src/legacy/ui/public'), - ng_mock$: path.resolve(KIBANA_ROOT, 'src/test_utils/public/ng_mock'), }, }, }; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx index b5e5e248eaeb1..607a11a76a066 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx @@ -80,6 +80,7 @@ export class FlyoutCreateDrilldownAction implements ActionByType ), { diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx index 6dfda93db7155..30de62d0d28dd 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx @@ -64,6 +64,7 @@ export class FlyoutEditDrilldownAction implements ActionByType ), { diff --git a/x-pack/plugins/embeddable_enhanced/kibana.json b/x-pack/plugins/embeddable_enhanced/kibana.json index 5663671de7bd9..acada946fe0d1 100644 --- a/x-pack/plugins/embeddable_enhanced/kibana.json +++ b/x-pack/plugins/embeddable_enhanced/kibana.json @@ -3,5 +3,6 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": ["embeddable", "uiActionsEnhanced"] + "requiredPlugins": ["embeddable", "kibanaReact", "uiActions", "uiActionsEnhanced"], + "requiredBundles": ["kibanaUtils"] } diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.d.ts b/x-pack/plugins/embeddable_enhanced/public/drilldowns/index.ts similarity index 84% rename from x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.d.ts rename to x-pack/plugins/embeddable_enhanced/public/drilldowns/index.ts index 086ba67703122..a8d5a179dbac1 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.d.ts +++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export declare const addAllExtensions: any; +export * from './url_drilldown'; diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/README.md b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/README.md new file mode 100644 index 0000000000000..996723ccb914d --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/README.md @@ -0,0 +1,24 @@ +# Basic url drilldown implementation + +Url drilldown allows navigating to external URL or to internal kibana URL. +By using variables in url template result url can be dynamic and depend on user's interaction. + +URL drilldown has 3 sources for variables: + +- Global static variables like, for example, `kibanaUrl`. Such variables won’t change depending on a place where url drilldown is used. +- Context variables are dynamic and different depending on where drilldown is created and used. +- Event variables depend on a trigger context. These variables are dynamically extracted from the action context when drilldown is executed. + +Difference between `event` and `context` variables, is that real `context` variables are available during drilldown creation (e.g. embeddable panel), +but `event` variables mapped from trigger context. Since there is no trigger context during drilldown creation, we have to provide some _mock_ variables for validating and previewing the URL. + +In current implementation url drilldown has to be used inside the embeddable and with `ValueClickTrigger` or `RangeSelectTrigger`. + +- `context` variables extracted from `embeddable` +- `event` variables extracted from `trigger` context + +In future this basic url drilldown implementation would allow injecting more variables into `context` (e.g. `dashboard` app specific variables) and would allow providing support for new trigger types from outside. +This extensibility improvements are tracked here: https://github.com/elastic/kibana/issues/55324 + +In case a solution app has a use case for url drilldown that has to be different from current basic implementation and +just extending variables list is not enough, then recommendation is to create own custom url drilldown and reuse building blocks from `ui_actions_enhanced`. diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/i18n.ts b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/i18n.ts new file mode 100644 index 0000000000000..748f6f4cecedd --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/i18n.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const txtUrlDrilldownDisplayName = i18n.translate( + 'xpack.embeddableEnhanced.drilldowns.urlDrilldownDisplayName', + { + defaultMessage: 'Go to URL', + } +); diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/index.ts b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/index.ts new file mode 100644 index 0000000000000..61406f7d84318 --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { UrlDrilldown } from './url_drilldown'; diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.test.ts b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.test.ts new file mode 100644 index 0000000000000..6a11663ea6c3d --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.test.ts @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UrlDrilldown, ActionContext, Config } from './url_drilldown'; +import { coreMock } from '../../../../../../src/core/public/mocks'; +import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public/lib/embeddables'; + +const mockDataPoints = [ + { + table: { + columns: [ + { + name: 'test', + id: '1-1', + meta: { + type: 'histogram', + indexPatternId: 'logstash-*', + aggConfigParams: { + field: 'bytes', + interval: 30, + otherBucket: true, + }, + }, + }, + ], + rows: [ + { + '1-1': '2048', + }, + ], + }, + column: 0, + row: 0, + value: 'test', + }, +]; + +const mockEmbeddable = ({ + getInput: () => ({ + filters: [], + timeRange: { from: 'now-15m', to: 'now' }, + query: { query: 'test', language: 'kuery' }, + }), + getOutput: () => ({}), +} as unknown) as IEmbeddable; + +const mockNavigateToUrl = jest.fn(() => Promise.resolve()); + +describe('UrlDrilldown', () => { + const urlDrilldown = new UrlDrilldown({ + getGlobalScope: () => ({ kibanaUrl: 'http://localhost:5601/' }), + getOpenModal: () => Promise.resolve(coreMock.createStart().overlays.openModal), + getSyntaxHelpDocsLink: () => 'http://localhost:5601/docs', + navigateToUrl: mockNavigateToUrl, + }); + + test('license', () => { + expect(urlDrilldown.minimalLicense).toBe('gold'); + }); + + describe('isCompatible', () => { + test('throws if no embeddable', async () => { + const config: Config = { + url: { + template: `https://elasti.co/?{{event.value}}`, + }, + openInNewTab: false, + }; + + const context: ActionContext = { + data: { + data: mockDataPoints, + }, + }; + + await expect(urlDrilldown.isCompatible(config, context)).rejects.toThrowError(); + }); + + test('compatible if url is valid', async () => { + const config: Config = { + url: { + template: `https://elasti.co/?{{event.value}}&{{rison context.panel.query}}`, + }, + openInNewTab: false, + }; + + const context: ActionContext = { + data: { + data: mockDataPoints, + }, + embeddable: mockEmbeddable, + }; + + await expect(urlDrilldown.isCompatible(config, context)).resolves.toBe(true); + }); + + test('not compatible if url is invalid', async () => { + const config: Config = { + url: { + template: `https://elasti.co/?{{event.value}}&{{rison context.panel.somethingFake}}`, + }, + openInNewTab: false, + }; + + const context: ActionContext = { + data: { + data: mockDataPoints, + }, + embeddable: mockEmbeddable, + }; + + await expect(urlDrilldown.isCompatible(config, context)).resolves.toBe(false); + }); + }); + + describe('getHref & execute', () => { + beforeEach(() => { + mockNavigateToUrl.mockReset(); + }); + + test('valid url', async () => { + const config: Config = { + url: { + template: `https://elasti.co/?{{event.value}}&{{rison context.panel.query}}`, + }, + openInNewTab: false, + }; + + const context: ActionContext = { + data: { + data: mockDataPoints, + }, + embeddable: mockEmbeddable, + }; + + const url = await urlDrilldown.getHref(config, context); + expect(url).toMatchInlineSnapshot(`"https://elasti.co/?test&(language:kuery,query:test)"`); + + await urlDrilldown.execute(config, context); + expect(mockNavigateToUrl).toBeCalledWith(url); + }); + + test('invalid url', async () => { + const config: Config = { + url: { + template: `https://elasti.co/?{{event.value}}&{{rison context.panel.invalid}}`, + }, + openInNewTab: false, + }; + + const context: ActionContext = { + data: { + data: mockDataPoints, + }, + embeddable: mockEmbeddable, + }; + + await expect(urlDrilldown.getHref(config, context)).rejects.toThrowError(); + await expect(urlDrilldown.execute(config, context)).rejects.toThrowError(); + expect(mockNavigateToUrl).not.toBeCalled(); + }); + }); +}); diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx new file mode 100644 index 0000000000000..d5ab095fdd287 --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { OverlayStart } from 'kibana/public'; +import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public'; +import { ChartActionContext, IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; +import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../../src/plugins/kibana_utils/public'; +import { + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, +} from '../../../../../../src/plugins/ui_actions/public'; +import { + UiActionsEnhancedDrilldownDefinition as Drilldown, + UrlDrilldownGlobalScope, + UrlDrilldownConfig, + UrlDrilldownCollectConfig, + urlDrilldownValidateUrlTemplate, + urlDrilldownBuildScope, + urlDrilldownCompileUrl, + UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext, +} from '../../../../ui_actions_enhanced/public'; +import { getContextScope, getEventScope, getMockEventScope } from './url_drilldown_scope'; +import { txtUrlDrilldownDisplayName } from './i18n'; + +interface UrlDrilldownDeps { + getGlobalScope: () => UrlDrilldownGlobalScope; + navigateToUrl: (url: string) => Promise; + getOpenModal: () => Promise; + getSyntaxHelpDocsLink: () => string; +} + +export type ActionContext = ChartActionContext; +export type Config = UrlDrilldownConfig; +export type UrlTrigger = typeof VALUE_CLICK_TRIGGER | typeof SELECT_RANGE_TRIGGER; +export interface ActionFactoryContext extends BaseActionFactoryContext { + embeddable?: IEmbeddable; +} +export type CollectConfigProps = CollectConfigPropsBase; + +const URL_DRILLDOWN = 'URL_DRILLDOWN'; + +export class UrlDrilldown implements Drilldown { + public readonly id = URL_DRILLDOWN; + + constructor(private deps: UrlDrilldownDeps) {} + + public readonly order = 8; + + readonly minimalLicense = 'gold'; + readonly licenseFeatureName = 'URL drilldown'; + + public readonly getDisplayName = () => txtUrlDrilldownDisplayName; + + public readonly euiIcon = 'link'; + + supportedTriggers(): UrlTrigger[] { + return [VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER]; + } + + private readonly ReactCollectConfig: React.FC = ({ + config, + onConfig, + context, + }) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const scope = React.useMemo(() => this.buildEditorScope(context), [context]); + return ( + + ); + }; + + public readonly CollectConfig = reactToUiComponent(this.ReactCollectConfig); + + public readonly createConfig = () => ({ + url: { template: '' }, + openInNewTab: false, + }); + + public readonly isConfigValid = ( + config: Config, + context: ActionFactoryContext + ): config is Config => { + const { isValid } = urlDrilldownValidateUrlTemplate(config.url, this.buildEditorScope(context)); + return isValid; + }; + + public readonly isCompatible = async (config: Config, context: ActionContext) => { + const { isValid, error } = urlDrilldownValidateUrlTemplate( + config.url, + await this.buildRuntimeScope(context) + ); + + if (!isValid) { + // eslint-disable-next-line no-console + console.warn( + `UrlDrilldown [${config.url.template}] is not valid. Error [${error}]. Skipping execution.` + ); + } + + return Promise.resolve(isValid); + }; + + public readonly getHref = async (config: Config, context: ActionContext) => + urlDrilldownCompileUrl(config.url.template, await this.buildRuntimeScope(context)); + + public readonly execute = async (config: Config, context: ActionContext) => { + const url = await urlDrilldownCompileUrl( + config.url.template, + await this.buildRuntimeScope(context, { allowPrompts: true }) + ); + if (config.openInNewTab) { + window.open(url, '_blank', 'noopener'); + } else { + await this.deps.navigateToUrl(url); + } + }; + + private buildEditorScope = (context: ActionFactoryContext) => { + return urlDrilldownBuildScope({ + globalScope: this.deps.getGlobalScope(), + contextScope: getContextScope(context), + eventScope: getMockEventScope(context.triggers), + }); + }; + + private buildRuntimeScope = async ( + context: ActionContext, + opts: { allowPrompts: boolean } = { allowPrompts: false } + ) => { + return urlDrilldownBuildScope({ + globalScope: this.deps.getGlobalScope(), + contextScope: getContextScope(context), + eventScope: await getEventScope(context, this.deps, opts), + }); + }; +} diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.tsx b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.tsx new file mode 100644 index 0000000000000..d3e3510f1b24e --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.tsx @@ -0,0 +1,319 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * This file contains all the logic for mapping from trigger's context and action factory context to variables for URL drilldown scope, + * Please refer to ./README.md for explanation of different scope sources + */ + +import React from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiRadioGroup, +} from '@elastic/eui'; +import uniqBy from 'lodash/uniqBy'; +import { FormattedMessage } from '@kbn/i18n/react'; +import type { Query, Filter, TimeRange } from '../../../../../../src/plugins/data/public'; +import { + IEmbeddable, + isRangeSelectTriggerContext, + isValueClickTriggerContext, + RangeSelectContext, + ValueClickContext, +} from '../../../../../../src/plugins/embeddable/public'; +import type { ActionContext, ActionFactoryContext, UrlTrigger } from './url_drilldown'; +import { SELECT_RANGE_TRIGGER } from '../../../../../../src/plugins/ui_actions/public'; +import { OverlayStart } from '../../../../../../src/core/public'; +import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; + +type ContextScopeInput = ActionContext | ActionFactoryContext; + +/** + * Part of context scope extracted from an embeddable + * Expose on the scope as: `{{context.panel.id}}`, `{{context.panel.filters.[0]}}` + */ +interface EmbeddableUrlDrilldownContextScope { + id: string; + title?: string; + query?: Query; + filters?: Filter[]; + timeRange?: TimeRange; + savedObjectId?: string; + /** + * In case panel supports only 1 index patterns + */ + indexPatternId?: string; + /** + * In case panel supports more then 1 index patterns + */ + indexPatternIds?: string[]; +} + +/** + * Url drilldown context scope + * `{{context.$}}` + */ +interface UrlDrilldownContextScope { + panel?: EmbeddableUrlDrilldownContextScope; +} + +export function getContextScope(contextScopeInput: ContextScopeInput): UrlDrilldownContextScope { + function hasEmbeddable(val: unknown): val is { embeddable: IEmbeddable } { + if (val && typeof val === 'object' && 'embeddable' in val) return true; + return false; + } + if (!hasEmbeddable(contextScopeInput)) + throw new Error( + "UrlDrilldown [getContextScope] can't build scope because embeddable object is missing in context" + ); + + const embeddable = contextScopeInput.embeddable; + const input = embeddable.getInput(); + const output = embeddable.getOutput(); + function hasSavedObjectId(obj: Record): obj is { savedObjectId: string } { + return 'savedObjectId' in obj && typeof obj.savedObjectId === 'string'; + } + function getIndexPatternIds(): string[] { + function hasIndexPatterns( + _output: Record + ): _output is { indexPatterns: Array<{ id?: string }> } { + return ( + 'indexPatterns' in _output && + Array.isArray(_output.indexPatterns) && + _output.indexPatterns.length > 0 + ); + } + return hasIndexPatterns(output) + ? (output.indexPatterns.map((ip) => ip.id).filter(Boolean) as string[]) + : []; + } + const indexPatternsIds = getIndexPatternIds(); + return { + panel: cleanEmptyKeys({ + id: input.id, + title: output.title ?? input.title, + savedObjectId: + output.savedObjectId ?? (hasSavedObjectId(input) ? input.savedObjectId : undefined), + query: input.query, + timeRange: input.timeRange, + filters: input.filters, + indexPatternIds: indexPatternsIds.length > 1 ? indexPatternsIds : undefined, + indexPatternId: indexPatternsIds.length === 1 ? indexPatternsIds[0] : undefined, + }), + }; +} + +/** + * URL drilldown event scope, + * available as: {{event.key}}, {{event.from}} + */ +type UrlDrilldownEventScope = ValueClickTriggerEventScope | RangeSelectTriggerEventScope; +type EventScopeInput = ActionContext; +interface ValueClickTriggerEventScope { + key?: string; + value?: string | number | boolean; + negate: boolean; +} +interface RangeSelectTriggerEventScope { + key: string; + from?: string | number; + to?: string | number; +} + +export async function getEventScope( + eventScopeInput: EventScopeInput, + deps: { getOpenModal: () => Promise }, + opts: { allowPrompts: boolean } = { allowPrompts: false } +): Promise { + if (isRangeSelectTriggerContext(eventScopeInput)) { + return getEventScopeFromRangeSelectTriggerContext(eventScopeInput); + } else if (isValueClickTriggerContext(eventScopeInput)) { + return getEventScopeFromValueClickTriggerContext(eventScopeInput, deps, opts); + } else { + throw new Error("UrlDrilldown [getEventScope] can't build scope from not supported trigger"); + } +} + +async function getEventScopeFromRangeSelectTriggerContext( + eventScopeInput: RangeSelectContext +): Promise { + const { table, column: columnIndex, range } = eventScopeInput.data; + const column = table.columns[columnIndex]; + return cleanEmptyKeys({ + key: toPrimitiveOrUndefined(column?.meta?.aggConfigParams?.field) as string, + from: toPrimitiveOrUndefined(range[0]) as string | number | undefined, + to: toPrimitiveOrUndefined(range[range.length - 1]) as string | number | undefined, + }); +} + +async function getEventScopeFromValueClickTriggerContext( + eventScopeInput: ValueClickContext, + deps: { getOpenModal: () => Promise }, + opts: { allowPrompts: boolean } = { allowPrompts: false } +): Promise { + const negate = eventScopeInput.data.negate ?? false; + const point = await getSingleValue(eventScopeInput.data.data, deps, opts); + const { key, value } = getKeyValueFromPoint(point); + return cleanEmptyKeys({ + key, + value, + negate, + }); +} + +/** + * @remarks + * Difference between `event` and `context` variables, is that real `context` variables are available during drilldown creation (e.g. embeddable panel) + * `event` variables are mapped from trigger context. Since there is no trigger context during drilldown creation, we have to provide some _mock_ variables for validating and previewing the URL + */ +export function getMockEventScope([trigger]: UrlTrigger[]): UrlDrilldownEventScope { + if (trigger === SELECT_RANGE_TRIGGER) { + return { + key: 'event.key', + from: new Date(Date.now() - 15 * 60 * 1000).toISOString(), // 15 minutes ago + to: new Date().toISOString(), + }; + } else { + return { + key: 'event.key', + value: 'event.value', + negate: false, + }; + } +} + +function getKeyValueFromPoint( + point: ValueClickContext['data']['data'][0] +): Pick { + const { table, column: columnIndex, value } = point; + const column = table.columns[columnIndex]; + return { + key: toPrimitiveOrUndefined(column?.meta?.aggConfigParams?.field) as string | undefined, + value: toPrimitiveOrUndefined(value), + }; +} + +function toPrimitiveOrUndefined(v: unknown): string | number | boolean | undefined { + if (typeof v === 'number' || typeof v === 'boolean' || typeof v === 'string') return v; + if (typeof v === 'object' && v instanceof Date) return v.toISOString(); + if (typeof v === 'undefined' || v === null) return undefined; + return String(v); +} + +function cleanEmptyKeys>(obj: T): T { + Object.keys(obj).forEach((key) => { + if (obj[key] === undefined) { + delete obj[key]; + } + }); + return obj; +} + +/** + * VALUE_CLICK_TRIGGER could have multiple data points + * Prompt user which data point to use in a drilldown + */ +async function getSingleValue( + data: ValueClickContext['data']['data'], + deps: { getOpenModal: () => Promise }, + opts: { allowPrompts: boolean } = { allowPrompts: false } +): Promise { + data = uniqBy(data.filter(Boolean), (point) => { + const { key, value } = getKeyValueFromPoint(point); + return `${key}:${value}`; + }); + if (data.length === 0) + throw new Error(`[trigger = "VALUE_CLICK_TRIGGER"][getSingleValue] no value to pick from`); + if (data.length === 1) return Promise.resolve(data[0]); + if (!opts.allowPrompts) return Promise.resolve(data[0]); + return new Promise(async (resolve, reject) => { + const openModal = await deps.getOpenModal(); + const overlay = openModal( + toMountPoint( + overlay.close()} + onSubmit={(point) => { + if (point) { + resolve(point); + } + overlay.close(); + }} + data={data} + /> + ) + ); + overlay.onClose.then(() => reject()); + }); +} + +function GetSingleValuePopup({ + data, + onCancel, + onSubmit, +}: { + data: ValueClickContext['data']['data']; + onCancel: () => void; + onSubmit: (value: ValueClickContext['data']['data'][0]) => void; +}) { + const values = data + .map((point) => { + const { key, value } = getKeyValueFromPoint(point); + return { + point, + id: key ?? '', + label: `${key}:${value}`, + }; + }) + .filter((value) => Boolean(value.id)); + + const [selectedValueId, setSelectedValueId] = React.useState(values[0].id); + + return ( + + + + + + + + + setSelectedValueId(id)} + name="drilldownValues" + /> + + + + + + + onSubmit(values.find((v) => v.id === selectedValueId)?.point!)} + data-test-subj="applySingleValuePopoverButton" + fill + > + + + + + ); +} diff --git a/x-pack/plugins/embeddable_enhanced/public/plugin.ts b/x-pack/plugins/embeddable_enhanced/public/plugin.ts index fd0bcc2023269..37e102b40131d 100644 --- a/x-pack/plugins/embeddable_enhanced/public/plugin.ts +++ b/x-pack/plugins/embeddable_enhanced/public/plugin.ts @@ -28,8 +28,11 @@ import { UiActionsEnhancedDynamicActionManager as DynamicActionManager, AdvancedUiActionsSetup, AdvancedUiActionsStart, + urlDrilldownGlobalScopeProvider, } from '../../ui_actions_enhanced/public'; import { PanelNotificationsAction, ACTION_PANEL_NOTIFICATIONS } from './actions'; +import { UrlDrilldown } from './drilldowns'; +import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public'; declare module '../../../../src/plugins/ui_actions/public' { export interface ActionContextMapping { @@ -61,11 +64,21 @@ export class EmbeddableEnhancedPlugin public setup(core: CoreSetup, plugins: SetupDependencies): SetupContract { this.setCustomEmbeddableFactoryProvider(plugins); - + const startServices = createStartServicesGetter(core.getStartServices); const panelNotificationAction = new PanelNotificationsAction(); plugins.uiActionsEnhanced.registerAction(panelNotificationAction); plugins.uiActionsEnhanced.attachAction(PANEL_NOTIFICATION_TRIGGER, panelNotificationAction.id); + plugins.uiActionsEnhanced.registerDrilldown( + new UrlDrilldown({ + getGlobalScope: urlDrilldownGlobalScopeProvider({ core }), + navigateToUrl: (url: string) => + core.getStartServices().then(([{ application }]) => application.navigateToUrl(url)), + getOpenModal: () => core.getStartServices().then(([{ overlays }]) => overlays.openModal), + getSyntaxHelpDocsLink: () => startServices().core.docLinks.links.dashboard.drilldowns, // TODO: replace with docs https://github.com/elastic/kibana/issues/69414 + }) + ); + return {}; } diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap similarity index 89% rename from x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap rename to x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap index 39eb54b941ac4..59d1723c39488 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap +++ b/x-pack/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.tsx.snap @@ -134,6 +134,7 @@ exports[`extend index management ilm summary extension should return extension w }, "step_time_millis": 1544187776208, }, + "isFrozen": false, "name": "testy3", "primary": "1", "primary_size": "6.5kb", @@ -326,6 +327,82 @@ exports[`extend index management ilm summary extension should return extension w className="euiSpacer euiSpacer--s" /> + + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="stackPopover" + isOpen={false} + ownFocus={false} + panelPaddingSize="m" + > + +
+
+ + + +
+
+
+
@@ -588,6 +665,7 @@ exports[`extend index management ilm summary extension should return extension w "step": "complete", "step_time_millis": 1544187775867, }, + "isFrozen": false, "name": "testy3", "primary": "1", "primary_size": "6.5kb", diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx similarity index 88% rename from x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js rename to x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx index 4fa1838115840..17573cb81c408 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js +++ b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx @@ -8,7 +8,8 @@ import moment from 'moment-timezone'; import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; -import { mountWithIntl } from '../../../test_utils/enzyme_helpers'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/public/mocks'; import { retryLifecycleActionExtension, removeLifecyclePolicyActionExtension, @@ -19,19 +20,22 @@ import { } from '../public/extend_index_management'; import { init as initHttp } from '../public/application/services/http'; import { init as initUiMetric } from '../public/application/services/ui_metric'; +import { Index } from '../public/application/services/policies/types'; // We need to init the http with a mock for any tests that depend upon the http service. // For example, add_lifecycle_confirm_modal makes an API request in its componentDidMount // lifecycle method. If we don't mock this, CI will fail with "Call retries were exceeded". -initHttp(axios.create({ adapter: axiosXhrAdapter }), (path) => path); -initUiMetric({ reportUiStats: () => {} }); +// This expects HttpSetup but we're giving it AxiosInstance. +// @ts-ignore +initHttp(axios.create({ adapter: axiosXhrAdapter })); +initUiMetric(usageCollectionPluginMock.createSetupContract()); jest.mock('../../../plugins/index_management/public', async () => { - const { indexManagementMock } = await import('../../../plugins/index_management/public/mocks.ts'); + const { indexManagementMock } = await import('../../../plugins/index_management/public/mocks'); return indexManagementMock.createSetup(); }); -const indexWithoutLifecyclePolicy = { +const indexWithoutLifecyclePolicy: Index = { health: 'yellow', status: 'open', name: 'noPolicy', @@ -43,13 +47,14 @@ const indexWithoutLifecyclePolicy = { size: '3.4kb', primary_size: '3.4kb', aliases: 'none', + isFrozen: false, ilm: { index: 'testy1', managed: false, }, }; -const indexWithLifecyclePolicy = { +const indexWithLifecyclePolicy: Index = { health: 'yellow', status: 'open', name: 'testy3', @@ -61,6 +66,7 @@ const indexWithLifecyclePolicy = { size: '6.5kb', primary_size: '6.5kb', aliases: 'none', + isFrozen: false, ilm: { index: 'testy3', managed: true, @@ -87,6 +93,7 @@ const indexWithLifecycleError = { size: '6.5kb', primary_size: '6.5kb', aliases: 'none', + isFrozen: false, ilm: { index: 'testy3', managed: true, @@ -115,10 +122,12 @@ const indexWithLifecycleError = { moment.tz.setDefault('utc'); -const getUrlForApp = (appId, options) => { +const getUrlForApp = (appId: string, options: any) => { return appId + '/' + (options ? options.path : ''); }; +const reloadIndices = () => {}; + describe('extend index management', () => { describe('retry lifecycle action extension', () => { test('should return null when no indices have index lifecycle policy', () => { @@ -153,6 +162,7 @@ describe('extend index management', () => { test('should return null when no indices have index lifecycle policy', () => { const extension = removeLifecyclePolicyActionExtension({ indices: [indexWithoutLifecyclePolicy], + reloadIndices, }); expect(extension).toBeNull(); }); @@ -160,6 +170,7 @@ describe('extend index management', () => { test('should return null when some indices have index lifecycle policy', () => { const extension = removeLifecyclePolicyActionExtension({ indices: [indexWithoutLifecyclePolicy, indexWithLifecyclePolicy], + reloadIndices, }); expect(extension).toBeNull(); }); @@ -167,6 +178,7 @@ describe('extend index management', () => { test('should return extension when all indices have lifecycle policy', () => { const extension = removeLifecyclePolicyActionExtension({ indices: [indexWithLifecycleError, indexWithLifecycleError], + reloadIndices, }); expect(extension).toBeDefined(); expect(extension).toMatchSnapshot(); @@ -175,16 +187,18 @@ describe('extend index management', () => { describe('add lifecycle policy action extension', () => { test('should return null when index has index lifecycle policy', () => { - const extension = addLifecyclePolicyActionExtension( - { indices: [indexWithLifecyclePolicy] }, - getUrlForApp - ); + const extension = addLifecyclePolicyActionExtension({ + indices: [indexWithLifecyclePolicy], + reloadIndices, + getUrlForApp, + }); expect(extension).toBeNull(); }); test('should return null when more than one index is passed', () => { const extension = addLifecyclePolicyActionExtension({ indices: [indexWithoutLifecyclePolicy, indexWithoutLifecyclePolicy], + reloadIndices, getUrlForApp, }); expect(extension).toBeNull(); @@ -193,10 +207,11 @@ describe('extend index management', () => { test('should return extension when one index is passed and it does not have lifecycle policy', () => { const extension = addLifecyclePolicyActionExtension({ indices: [indexWithoutLifecyclePolicy], + reloadIndices, getUrlForApp, }); - expect(extension.renderConfirmModal).toBeDefined; - const component = extension.renderConfirmModal(jest.fn()); + expect(extension?.renderConfirmModal).toBeDefined(); + const component = extension!.renderConfirmModal(jest.fn()); const rendered = mountWithIntl(component); expect(rendered.exists('.euiModal--confirmation')); }); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts index b80e9e70c54fa..e9365bfe06ea4 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts @@ -12,7 +12,7 @@ import { UIM_POLICY_ATTACH_INDEX_TEMPLATE, UIM_POLICY_DETACH_INDEX, UIM_INDEX_RETRY_STEP, -} from '../constants/ui_metric'; +} from '../constants'; import { trackUiMetric } from './ui_metric'; import { sendGet, sendPost, sendDelete, useRequest } from './http'; @@ -78,7 +78,11 @@ export const removeLifecycleForIndex = async (indexNames: string[]) => { return response; }; -export const addLifecyclePolicyToIndex = async (body: GenericObject) => { +export const addLifecyclePolicyToIndex = async (body: { + indexName: string; + policyName: string; + alias: string; +}) => { const response = await sendPost(`index/add`, body); // Only track successful actions. trackUiMetric(METRIC_TYPE.COUNT, UIM_POLICY_ATTACH_INDEX); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts index c191f82cf05cc..0e00b5a02b71d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Index as IndexInterface } from '../../../../../index_management/public'; + export interface SerializedPolicy { name: string; phases: Phases; @@ -169,3 +171,36 @@ export interface FrozenPhase export interface DeletePhase extends CommonPhaseSettings, PhaseWithMinAge { waitForSnapshotPolicy: string; } + +export interface IndexLifecyclePolicy { + index: string; + managed: boolean; + action?: string; + action_time_millis?: number; + age?: string; + failed_step?: string; + failed_step_retry_count?: number; + is_auto_retryable_error?: boolean; + lifecycle_date_millis?: number; + phase?: string; + phase_execution?: { + policy: string; + modified_date_in_millis: number; + version: number; + phase_definition: SerializedPhase; + }; + phase_time_millis?: number; + policy?: string; + step?: string; + step_info?: { + reason?: string; + stack_trace?: string; + type?: string; + message?: string; + }; + step_time_millis?: number; +} + +export interface Index extends IndexInterface { + ilm: IndexLifecyclePolicy; +} diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx similarity index 88% rename from x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.js rename to x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx index 0bd313c9a9f8d..060b208006bf3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.js +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx @@ -8,6 +8,8 @@ import React, { Component, Fragment } from 'react'; import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { ApplicationStart } from 'kibana/public'; + import { EuiLink, EuiSelect, @@ -26,9 +28,25 @@ import { import { loadPolicies, addLifecyclePolicyToIndex } from '../../application/services/api'; import { showApiError } from '../../application/services/api_errors'; import { toasts } from '../../application/services/notification'; +import { Index, PolicyFromES } from '../../application/services/policies/types'; + +interface Props { + indexName: string; + closeModal: () => void; + index: Index; + reloadIndices: () => void; + getUrlForApp: ApplicationStart['getUrlForApp']; +} + +interface State { + selectedPolicyName: string; + selectedAlias: string; + policies: PolicyFromES[]; + policyErrorMessage?: string; +} -export class AddLifecyclePolicyConfirmModal extends Component { - constructor(props) { +export class AddLifecyclePolicyConfirmModal extends Component { + constructor(props: Props) { super(props); this.state = { policies: [], @@ -41,7 +59,7 @@ export class AddLifecyclePolicyConfirmModal extends Component { const { selectedPolicyName, selectedAlias } = this.state; if (!selectedPolicyName) { this.setState({ - policyError: i18n.translate( + policyErrorMessage: i18n.translate( 'xpack.indexLifecycleMgmt.indexManagementTable.addLifecyclePolicyConfirmModal.noPolicySelectedErrorMessage', { defaultMessage: 'You must select a policy.' } ), @@ -81,7 +99,7 @@ export class AddLifecyclePolicyConfirmModal extends Component { ); } }; - renderAliasFormElement = (selectedPolicy) => { + renderAliasFormElement = (selectedPolicy?: PolicyFromES) => { const { selectedAlias } = this.state; const { index } = this.props; const showAliasSelect = @@ -109,7 +127,7 @@ export class AddLifecyclePolicyConfirmModal extends Component { defaultMessage="Policy {policyName} is configured for rollover, but index {indexName} does not have an alias, which is required for rollover." values={{ - policyName: selectedPolicy.name, + policyName: selectedPolicy?.name, indexName: index.name, }} /> @@ -117,7 +135,7 @@ export class AddLifecyclePolicyConfirmModal extends Component { ); } - const aliasOptions = aliases.map((alias) => { + const aliasOptions = (aliases as string[]).map((alias: string) => { return { text: alias, value: alias, @@ -152,10 +170,10 @@ export class AddLifecyclePolicyConfirmModal extends Component { ); }; renderForm() { - const { policies, selectedPolicyName, policyError } = this.state; + const { policies, selectedPolicyName, policyErrorMessage } = this.state; const selectedPolicy = selectedPolicyName ? policies.find((policy) => policy.name === selectedPolicyName) - : null; + : undefined; const options = policies.map(({ name }) => { return { @@ -175,8 +193,8 @@ export class AddLifecyclePolicyConfirmModal extends Component { return ( { - this.setState({ policyError: null, selectedPolicyName: e.target.value }); + this.setState({ policyErrorMessage: undefined, selectedPolicyName: e.target.value }); }} /> @@ -198,7 +216,7 @@ export class AddLifecyclePolicyConfirmModal extends Component { } async componentDidMount() { try { - const policies = await loadPolicies(false, this.props.httpClient); + const policies = await loadPolicies(false); this.setState({ policies }); } catch (err) { showApiError( diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx similarity index 69% rename from x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.js rename to x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx index 9e9dc009e4c40..02e4595a333bc 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.js +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx @@ -24,44 +24,71 @@ import { EuiPopoverTitle, } from '@elastic/eui'; +import { ApplicationStart } from 'kibana/public'; import { getPolicyPath } from '../../application/services/navigation'; +import { Index, IndexLifecyclePolicy } from '../../application/services/policies/types'; -const getHeaders = () => { - return { - policy: i18n.translate( - 'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.lifecyclePolicyHeader', - { - defaultMessage: 'Lifecycle policy', - } - ), - phase: i18n.translate( - 'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.currentPhaseHeader', - { - defaultMessage: 'Current phase', - } - ), - action: i18n.translate( - 'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.currentActionHeader', - { - defaultMessage: 'Current action', - } - ), - action_time_millis: i18n.translate( - 'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.currentActionTimeHeader', - { - defaultMessage: 'Current action time', - } - ), - failed_step: i18n.translate( - 'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.failedStepHeader', - { - defaultMessage: 'Failed step', - } - ), - }; +const getHeaders = (): Array<[keyof IndexLifecyclePolicy, string]> => { + return [ + [ + 'policy', + i18n.translate( + 'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.lifecyclePolicyHeader', + { + defaultMessage: 'Lifecycle policy', + } + ), + ], + [ + 'phase', + i18n.translate( + 'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.currentPhaseHeader', + { + defaultMessage: 'Current phase', + } + ), + ], + [ + 'action', + i18n.translate( + 'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.currentActionHeader', + { + defaultMessage: 'Current action', + } + ), + ], + [ + 'action_time_millis', + i18n.translate( + 'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.currentActionTimeHeader', + { + defaultMessage: 'Current action time', + } + ), + ], + [ + 'failed_step', + i18n.translate( + 'xpack.indexLifecycleMgmt.indexLifecycleMgmtSummary.headers.failedStepHeader', + { + defaultMessage: 'Failed step', + } + ), + ], + ]; }; -export class IndexLifecycleSummary extends Component { - constructor(props) { + +interface Props { + index: Index; + getUrlForApp: ApplicationStart['getUrlForApp']; +} +interface State { + showStackPopover: boolean; + showPhaseExecutionPopover: boolean; +} + +export class IndexLifecycleSummary extends Component { + constructor(props: Props) { super(props); this.state = { showStackPopover: false, @@ -80,8 +107,8 @@ export class IndexLifecycleSummary extends Component { closePhaseExecutionPopover = () => { this.setState({ showPhaseExecutionPopover: false }); }; - renderStackPopoverButton(ilm) { - if (!ilm.stack_trace) { + renderStackPopoverButton(ilm: IndexLifecyclePolicy) { + if (!ilm.step_info!.stack_trace) { return null; } const button = ( @@ -100,15 +127,12 @@ export class IndexLifecycleSummary extends Component { closePopover={this.closeStackPopover} >
-
{ilm.step_info.stack_trace}
+
{ilm.step_info!.stack_trace}
); } - renderPhaseExecutionPopoverButton(ilm) { - if (!ilm.phase_execution) { - return null; - } + renderPhaseExecutionPopoverButton(ilm: IndexLifecyclePolicy) { const button = ( { - const value = ilm[fieldName]; + headers.forEach(([fieldName, label], arrayIndex) => { + const value: any = ilm[fieldName]; let content; if (fieldName === 'action_time_millis') { content = moment(value).format('YYYY-MM-DD HH:mm:ss'); @@ -176,34 +203,38 @@ export class IndexLifecycleSummary extends Component { content = value; } content = content || '-'; - const cell = [ - - {headers[fieldName]} - , - - {content} - , - ]; + const cell = ( + <> + + {label} + + + {content} + + + ); if (arrayIndex % 2 === 0) { rows.left.push(cell); } else { rows.right.push(cell); } }); - rows.right.push(this.renderPhaseExecutionPopoverButton(ilm)); + if (ilm.phase_execution) { + rows.right.push(this.renderPhaseExecutionPopoverButton(ilm)); + } return rows; } render() { const { - index: { ilm = {} }, + index: { ilm }, } = this.props; if (!ilm.managed) { return null; } const { left, right } = this.buildRows(); return ( - + <>

{ilm.step_info && ilm.step_info.type ? ( - + <> {this.renderStackPopoverButton(ilm)} - + ) : null} - {ilm.step_info && ilm.step_info.message && !ilm.step_info.stack_trace ? ( - + {ilm.step_info && ilm.step_info!.message && !ilm.step_info!.stack_trace ? ( + <> } > - {ilm.step_info.message} + {ilm.step_info!.message} - + ) : null} @@ -256,7 +287,7 @@ export class IndexLifecycleSummary extends Component { {right} - + ); } } diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.tsx similarity index 81% rename from x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js rename to x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.tsx index 8d01f4a4c200e..bb5642cf3a476 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.js +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.tsx @@ -8,22 +8,27 @@ import React from 'react'; import { get, every, some } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiSearchBar } from '@elastic/eui'; +import { ApplicationStart } from 'kibana/public'; + +import { IndexManagementPluginSetup } from '../../../index_management/public'; import { retryLifecycleForIndex } from '../application/services/api'; import { IndexLifecycleSummary } from './components/index_lifecycle_summary'; + import { AddLifecyclePolicyConfirmModal } from './components/add_lifecycle_confirm_modal'; import { RemoveLifecyclePolicyConfirmModal } from './components/remove_lifecycle_confirm_modal'; +import { Index } from '../application/services/policies/types'; const stepPath = 'ilm.step'; -export const retryLifecycleActionExtension = ({ indices }) => { +export const retryLifecycleActionExtension = ({ indices }: { indices: Index[] }) => { const allHaveErrors = every(indices, (index) => { return index.ilm && index.ilm.failed_step; }); if (!allHaveErrors) { return null; } - const indexNames = indices.map(({ name }) => name); + const indexNames = indices.map(({ name }: Index) => name); return { requestMethod: retryLifecycleForIndex, icon: 'play', @@ -35,22 +40,28 @@ export const retryLifecycleActionExtension = ({ indices }) => { 'xpack.indexLifecycleMgmt.retryIndexLifecycleAction.retriedLifecycleMessage', { defaultMessage: 'Called retry lifecycle step for: {indexNames}', - values: { indexNames: indexNames.map((indexName) => `"${indexName}"`).join(', ') }, + values: { indexNames: indexNames.map((indexName: string) => `"${indexName}"`).join(', ') }, } ), }; }; -export const removeLifecyclePolicyActionExtension = ({ indices, reloadIndices }) => { +export const removeLifecyclePolicyActionExtension = ({ + indices, + reloadIndices, +}: { + indices: Index[]; + reloadIndices: () => void; +}) => { const allHaveIlm = every(indices, (index) => { return index.ilm && index.ilm.managed; }); if (!allHaveIlm) { return null; } - const indexNames = indices.map(({ name }) => name); + const indexNames = indices.map(({ name }: Index) => name); return { - renderConfirmModal: (closeModal) => { + renderConfirmModal: (closeModal: () => void) => { return ( { +export const addLifecyclePolicyActionExtension = ({ + indices, + reloadIndices, + getUrlForApp, +}: { + indices: Index[]; + reloadIndices: () => void; + getUrlForApp: ApplicationStart['getUrlForApp']; +}) => { if (indices.length !== 1) { return null; } @@ -79,7 +98,7 @@ export const addLifecyclePolicyActionExtension = ({ indices, reloadIndices, getU } const indexName = index.name; return { - renderConfirmModal: (closeModal) => { + renderConfirmModal: (closeModal: () => void) => { return ( { +export const ilmBannerExtension = (indices: Index[]) => { const { Query } = EuiSearchBar; if (!indices.length) { return null; } - const indicesWithLifecycleErrors = indices.filter((index) => { + const indicesWithLifecycleErrors = indices.filter((index: Index) => { return get(index, stepPath) === 'ERROR'; }); const numIndicesWithLifecycleErrors = indicesWithLifecycleErrors.length; @@ -124,11 +143,14 @@ export const ilmBannerExtension = (indices) => { }; }; -export const ilmSummaryExtension = (index, getUrlForApp) => { +export const ilmSummaryExtension = ( + index: Index, + getUrlForApp: ApplicationStart['getUrlForApp'] +) => { return ; }; -export const ilmFilterExtension = (indices) => { +export const ilmFilterExtension = (indices: Index[]) => { const hasIlm = some(indices, (index) => index.ilm && index.ilm.managed); if (!hasIlm) { return []; @@ -200,7 +222,9 @@ export const ilmFilterExtension = (indices) => { } }; -export const addAllExtensions = (extensionsService) => { +export const addAllExtensions = ( + extensionsService: IndexManagementPluginSetup['extensionsService'] +) => { extensionsService.addAction(retryLifecycleActionExtension); extensionsService.addAction(removeLifecyclePolicyActionExtension); extensionsService.addAction(addLifecyclePolicyActionExtension); diff --git a/x-pack/plugins/index_management/common/types/indices.ts b/x-pack/plugins/index_management/common/types/indices.ts index ecf5ba21fe60c..354e4fe67cd19 100644 --- a/x-pack/plugins/index_management/common/types/indices.ts +++ b/x-pack/plugins/index_management/common/types/indices.ts @@ -41,3 +41,18 @@ export interface IndexSettings { analysis?: AnalysisModule; [key: string]: any; } + +export interface Index { + health: string; + status: string; + name: string; + uuid: string; + primary: string; + replica: string; + documents: any; + size: any; + isFrozen: boolean; + aliases: string | string[]; + data_stream?: string; + [key: string]: any; +} diff --git a/x-pack/plugins/index_management/public/index.ts b/x-pack/plugins/index_management/public/index.ts index a2e9a41feb165..538dcaf25c47d 100644 --- a/x-pack/plugins/index_management/public/index.ts +++ b/x-pack/plugins/index_management/public/index.ts @@ -14,3 +14,5 @@ export const plugin = () => { export { IndexManagementPluginSetup }; export { getIndexListUri } from './application/services/routing'; + +export { Index } from '../common'; diff --git a/x-pack/plugins/index_management/server/index.ts b/x-pack/plugins/index_management/server/index.ts index 4d9409e4a516c..bf52d8a09c84c 100644 --- a/x-pack/plugins/index_management/server/index.ts +++ b/x-pack/plugins/index_management/server/index.ts @@ -18,5 +18,5 @@ export const config = { /** @public */ export { Dependencies } from './types'; export { IndexManagementPluginSetup } from './plugin'; -export { Index } from './types'; +export { Index } from '../common'; export { IndexManagementConfig } from './config'; diff --git a/x-pack/plugins/index_management/server/lib/fetch_indices.ts b/x-pack/plugins/index_management/server/lib/fetch_indices.ts index ae10629e069e8..e9eaec3e22428 100644 --- a/x-pack/plugins/index_management/server/lib/fetch_indices.ts +++ b/x-pack/plugins/index_management/server/lib/fetch_indices.ts @@ -5,7 +5,8 @@ */ import { CatIndicesParams } from 'elasticsearch'; import { IndexDataEnricher } from '../services'; -import { Index, CallAsCurrentUser } from '../types'; +import { CallAsCurrentUser } from '../types'; +import { Index } from '../index'; interface Hit { health: string; @@ -44,7 +45,9 @@ async function fetchIndicesCall( // This call retrieves alias and settings (incl. hidden status) information about indices const indices: GetIndicesResponse = await callAsCurrentUser('transport.request', { method: 'GET', - path: `/${indexNamesString}`, + // transport.request doesn't do any URI encoding, unlike other JS client APIs. This enables + // working with Logstash indices with names like %{[@metadata][beat]}-%{[@metadata][version]}. + path: `/${encodeURIComponent(indexNamesString)}`, query: { expand_wildcards: 'hidden,all', }, diff --git a/x-pack/plugins/index_management/server/services/index_data_enricher.ts b/x-pack/plugins/index_management/server/services/index_data_enricher.ts index 7a62ce9f7a3c3..80bdf76820f28 100644 --- a/x-pack/plugins/index_management/server/services/index_data_enricher.ts +++ b/x-pack/plugins/index_management/server/services/index_data_enricher.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Index, CallAsCurrentUser } from '../types'; +import { CallAsCurrentUser } from '../types'; +import { Index } from '../index'; export type Enricher = (indices: Index[], callAsCurrentUser: CallAsCurrentUser) => Promise; diff --git a/x-pack/plugins/index_management/server/types.ts b/x-pack/plugins/index_management/server/types.ts index cd3eb5dfecd40..fce0414dee936 100644 --- a/x-pack/plugins/index_management/server/types.ts +++ b/x-pack/plugins/index_management/server/types.ts @@ -26,19 +26,4 @@ export interface RouteDependencies { }; } -export interface Index { - health: string; - status: string; - name: string; - uuid: string; - primary: string; - replica: string; - documents: any; - size: any; - isFrozen: boolean; - aliases: string | string[]; - data_stream?: string; - [key: string]: any; -} - export type CallAsCurrentUser = LegacyScopedClusterClient['callAsCurrentUser']; diff --git a/x-pack/plugins/ingest_manager/package.json b/x-pack/plugins/ingest_manager/package.json index 8826ed57ab106..871972729b9f3 100644 --- a/x-pack/plugins/ingest_manager/package.json +++ b/x-pack/plugins/ingest_manager/package.json @@ -5,6 +5,7 @@ "private": true, "license": "Elastic-License", "dependencies": { - "abort-controller": "^3.0.0" + "abort-controller": "^3.0.0", + "ajv": "^6.12.4" } } diff --git a/x-pack/plugins/ingest_manager/server/errors.test.ts b/x-pack/plugins/ingest_manager/server/errors.test.ts new file mode 100644 index 0000000000000..70e3a3b4150ad --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/errors.test.ts @@ -0,0 +1,191 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { httpServerMock } from 'src/core/server/mocks'; +import { createAppContextStartContractMock } from './mocks'; + +import { + IngestManagerError, + RegistryError, + PackageNotFoundError, + defaultIngestErrorHandler, +} from './errors'; +import { appContextService } from './services'; + +describe('defaultIngestErrorHandler', () => { + let mockContract: ReturnType; + beforeEach(async () => { + // prevents `Logger not set.` and other appContext errors + mockContract = createAppContextStartContractMock(); + appContextService.start(mockContract); + }); + + afterEach(async () => { + jest.clearAllMocks(); + appContextService.stop(); + }); + + describe('IngestManagerError', () => { + it('502: RegistryError', async () => { + const error = new RegistryError('xyz'); + const response = httpServerMock.createResponseFactory(); + + await defaultIngestErrorHandler({ error, response }); + + // response + expect(response.ok).toHaveBeenCalledTimes(0); + expect(response.customError).toHaveBeenCalledTimes(1); + expect(response.customError).toHaveBeenCalledWith({ + statusCode: 502, + body: { message: error.message }, + }); + + // logging + expect(mockContract.logger?.error).toHaveBeenCalledTimes(1); + expect(mockContract.logger?.error).toHaveBeenCalledWith(error.message); + }); + + it('404: PackageNotFoundError', async () => { + const error = new PackageNotFoundError('123'); + const response = httpServerMock.createResponseFactory(); + + await defaultIngestErrorHandler({ error, response }); + + // response + expect(response.ok).toHaveBeenCalledTimes(0); + expect(response.customError).toHaveBeenCalledTimes(1); + expect(response.customError).toHaveBeenCalledWith({ + statusCode: 404, + body: { message: error.message }, + }); + + // logging + expect(mockContract.logger?.error).toHaveBeenCalledTimes(1); + expect(mockContract.logger?.error).toHaveBeenCalledWith(error.message); + }); + + it('400: IngestManagerError', async () => { + const error = new IngestManagerError('123'); + const response = httpServerMock.createResponseFactory(); + + await defaultIngestErrorHandler({ error, response }); + + // response + expect(response.ok).toHaveBeenCalledTimes(0); + expect(response.customError).toHaveBeenCalledTimes(1); + expect(response.customError).toHaveBeenCalledWith({ + statusCode: 400, + body: { message: error.message }, + }); + + // logging + expect(mockContract.logger?.error).toHaveBeenCalledTimes(1); + expect(mockContract.logger?.error).toHaveBeenCalledWith(error.message); + }); + }); + + describe('Boom', () => { + it('500: constructor - one arg', async () => { + const error = new Boom('bam'); + const response = httpServerMock.createResponseFactory(); + + await defaultIngestErrorHandler({ error, response }); + + // response + expect(response.ok).toHaveBeenCalledTimes(0); + expect(response.customError).toHaveBeenCalledTimes(1); + expect(response.customError).toHaveBeenCalledWith({ + statusCode: 500, + body: { message: 'An internal server error occurred' }, + }); + + // logging + expect(mockContract.logger?.error).toHaveBeenCalledTimes(1); + expect(mockContract.logger?.error).toHaveBeenCalledWith('An internal server error occurred'); + }); + + it('custom: constructor - 2 args', async () => { + const error = new Boom('Problem doing something', { + statusCode: 456, + }); + const response = httpServerMock.createResponseFactory(); + + await defaultIngestErrorHandler({ error, response }); + + // response + expect(response.ok).toHaveBeenCalledTimes(0); + expect(response.customError).toHaveBeenCalledTimes(1); + expect(response.customError).toHaveBeenCalledWith({ + statusCode: 456, + body: { message: error.message }, + }); + + // logging + expect(mockContract.logger?.error).toHaveBeenCalledTimes(1); + expect(mockContract.logger?.error).toHaveBeenCalledWith('Problem doing something'); + }); + + it('400: Boom.badRequest', async () => { + const error = Boom.badRequest('nope'); + const response = httpServerMock.createResponseFactory(); + + await defaultIngestErrorHandler({ error, response }); + + // response + expect(response.ok).toHaveBeenCalledTimes(0); + expect(response.customError).toHaveBeenCalledTimes(1); + expect(response.customError).toHaveBeenCalledWith({ + statusCode: 400, + body: { message: error.message }, + }); + + // logging + expect(mockContract.logger?.error).toHaveBeenCalledTimes(1); + expect(mockContract.logger?.error).toHaveBeenCalledWith('nope'); + }); + + it('404: Boom.notFound', async () => { + const error = Boom.notFound('sorry'); + const response = httpServerMock.createResponseFactory(); + + await defaultIngestErrorHandler({ error, response }); + + // response + expect(response.ok).toHaveBeenCalledTimes(0); + expect(response.customError).toHaveBeenCalledTimes(1); + expect(response.customError).toHaveBeenCalledWith({ + statusCode: 404, + body: { message: error.message }, + }); + + // logging + expect(mockContract.logger?.error).toHaveBeenCalledTimes(1); + expect(mockContract.logger?.error).toHaveBeenCalledWith('sorry'); + }); + }); + + describe('all other errors', () => { + it('500', async () => { + const error = new Error('something'); + const response = httpServerMock.createResponseFactory(); + + await defaultIngestErrorHandler({ error, response }); + + // response + expect(response.ok).toHaveBeenCalledTimes(0); + expect(response.customError).toHaveBeenCalledTimes(1); + expect(response.customError).toHaveBeenCalledWith({ + statusCode: 500, + body: { message: error.message }, + }); + + // logging + expect(mockContract.logger?.error).toHaveBeenCalledTimes(1); + expect(mockContract.logger?.error).toHaveBeenCalledWith(error); + }); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/errors.ts b/x-pack/plugins/ingest_manager/server/errors.ts index e6ef4a51284b0..9829a4de23d7b 100644 --- a/x-pack/plugins/ingest_manager/server/errors.ts +++ b/x-pack/plugins/ingest_manager/server/errors.ts @@ -5,6 +5,26 @@ */ /* eslint-disable max-classes-per-file */ +import Boom, { isBoom } from 'boom'; +import { + RequestHandlerContext, + KibanaRequest, + IKibanaResponse, + KibanaResponseFactory, +} from 'src/core/server'; +import { appContextService } from './services'; + +type IngestErrorHandler = ( + params: IngestErrorHandlerParams +) => IKibanaResponse | Promise; + +interface IngestErrorHandlerParams { + error: IngestManagerError | Boom | Error; + response: KibanaResponseFactory; + request?: KibanaRequest; + context?: RequestHandlerContext; +} + export class IngestManagerError extends Error { constructor(message?: string) { super(message); @@ -12,7 +32,7 @@ export class IngestManagerError extends Error { } } -export const getHTTPResponseCode = (error: IngestManagerError): number => { +const getHTTPResponseCode = (error: IngestManagerError): number => { if (error instanceof RegistryError) { return 502; // Bad Gateway } @@ -23,6 +43,40 @@ export const getHTTPResponseCode = (error: IngestManagerError): number => { return 400; // Bad Request }; +export const defaultIngestErrorHandler: IngestErrorHandler = async ({ + error, + response, +}: IngestErrorHandlerParams): Promise => { + const logger = appContextService.getLogger(); + + // our "expected" errors + if (error instanceof IngestManagerError) { + // only log the message + logger.error(error.message); + return response.customError({ + statusCode: getHTTPResponseCode(error), + body: { message: error.message }, + }); + } + + // handle any older Boom-based errors or the few places our app uses them + if (isBoom(error)) { + // only log the message + logger.error(error.output.payload.message); + return response.customError({ + statusCode: error.output.statusCode, + body: { message: error.output.payload.message }, + }); + } + + // not sure what type of error this is. log as much as possible + logger.error(error); + return response.customError({ + statusCode: 500, + body: { message: error.message }, + }); +}; + export class RegistryError extends IngestManagerError {} export class RegistryConnectionError extends RegistryError {} export class RegistryResponseError extends RegistryError {} diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts index 564f5d03e9450..b0439b30e8973 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts @@ -7,19 +7,13 @@ // handlers that handle events from agents in response to actions received import { RequestHandler } from 'kibana/server'; -import { TypeOf } from '@kbn/config-schema'; -import { PostAgentAcksRequestSchema } from '../../types/rest_spec'; import { AcksService } from '../../services/agents'; import { AgentEvent } from '../../../common/types/models'; -import { PostAgentAcksResponse } from '../../../common/types/rest_spec'; +import { PostAgentAcksRequest, PostAgentAcksResponse } from '../../../common/types/rest_spec'; export const postAgentAcksHandlerBuilder = function ( ackService: AcksService -): RequestHandler< - TypeOf, - undefined, - TypeOf -> { +): RequestHandler { return async (context, request, response) => { try { const soClient = ackService.getSavedObjectsClientContract(request); diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts index 2bce8daa6637e..605e4db230ce5 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts @@ -15,6 +15,7 @@ import { PostAgentEnrollResponse, GetAgentStatusResponse, PutAgentReassignResponse, + PostAgentEnrollRequest, } from '../../../common/types'; import { GetAgentsRequestSchema, @@ -22,8 +23,7 @@ import { UpdateAgentRequestSchema, DeleteAgentRequestSchema, GetOneAgentEventsRequestSchema, - PostAgentCheckinRequestSchema, - PostAgentEnrollRequestSchema, + PostAgentCheckinRequest, GetAgentStatusRequestSchema, PutAgentReassignRequestSchema, } from '../../types'; @@ -159,9 +159,9 @@ export const updateAgentHandler: RequestHandler< }; export const postAgentCheckinHandler: RequestHandler< - TypeOf, + PostAgentCheckinRequest['params'], undefined, - TypeOf + PostAgentCheckinRequest['body'] > = async (context, request, response) => { try { const soClient = appContextService.getInternalUserSOClient(request); @@ -218,7 +218,7 @@ export const postAgentCheckinHandler: RequestHandler< export const postAgentEnrollHandler: RequestHandler< undefined, undefined, - TypeOf + PostAgentEnrollRequest['body'] > = async (context, request, response) => { try { const soClient = appContextService.getInternalUserSOClient(request); diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts index a84b0f8d0a35a..a2e5c742ad6b5 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts @@ -9,7 +9,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from 'src/core/server'; +import { IRouter, RouteValidationResultFactory } from 'src/core/server'; +import Ajv from 'ajv'; import { PLUGIN_ID, AGENT_API_ROUTES, LIMITED_CONCURRENCY_ROUTE_TAG } from '../../constants'; import { GetAgentsRequestSchema, @@ -17,13 +18,15 @@ import { GetOneAgentEventsRequestSchema, UpdateAgentRequestSchema, DeleteAgentRequestSchema, - PostAgentCheckinRequestSchema, - PostAgentEnrollRequestSchema, - PostAgentAcksRequestSchema, + PostAgentCheckinRequestBodyJSONSchema, + PostAgentCheckinRequestParamsJSONSchema, + PostAgentAcksRequestParamsJSONSchema, + PostAgentAcksRequestBodyJSONSchema, PostAgentUnenrollRequestSchema, GetAgentStatusRequestSchema, PostNewAgentActionRequestSchema, PutAgentReassignRequestSchema, + PostAgentEnrollRequestBodyJSONSchema, } from '../../types'; import { getAgentsHandler, @@ -43,6 +46,29 @@ import { appContextService } from '../../services'; import { postAgentsUnenrollHandler } from './unenroll_handler'; import { IngestManagerConfigType } from '../..'; +const ajv = new Ajv({ + coerceTypes: true, + useDefaults: true, + removeAdditional: true, + allErrors: false, + nullable: true, +}); + +function schemaErrorsText(errors: Ajv.ErrorObject[], dataVar: any) { + return errors.map((e) => `${dataVar + (e.dataPath || '')} ${e.message}`).join(', '); +} + +function makeValidator(jsonSchema: any) { + const validator = ajv.compile(jsonSchema); + return function validateWithAJV(data: any, r: RouteValidationResultFactory) { + if (validator(data)) { + return r.ok(data); + } + + return r.badRequest(schemaErrorsText(validator.errors || [], data)); + }; +} + export const registerRoutes = (router: IRouter, config: IngestManagerConfigType) => { // Get one router.get( @@ -86,7 +112,10 @@ export const registerRoutes = (router: IRouter, config: IngestManagerConfigType) router.post( { path: AGENT_API_ROUTES.CHECKIN_PATTERN, - validate: PostAgentCheckinRequestSchema, + validate: { + params: makeValidator(PostAgentCheckinRequestParamsJSONSchema), + body: makeValidator(PostAgentCheckinRequestBodyJSONSchema), + }, options: { tags: [], ...(pollingRequestTimeout @@ -105,7 +134,9 @@ export const registerRoutes = (router: IRouter, config: IngestManagerConfigType) router.post( { path: AGENT_API_ROUTES.ENROLL_PATTERN, - validate: PostAgentEnrollRequestSchema, + validate: { + body: makeValidator(PostAgentEnrollRequestBodyJSONSchema), + }, options: { tags: [LIMITED_CONCURRENCY_ROUTE_TAG] }, }, postAgentEnrollHandler @@ -115,7 +146,10 @@ export const registerRoutes = (router: IRouter, config: IngestManagerConfigType) router.post( { path: AGENT_API_ROUTES.ACKS_PATTERN, - validate: PostAgentAcksRequestSchema, + validate: { + params: makeValidator(PostAgentAcksRequestParamsJSONSchema), + body: makeValidator(PostAgentAcksRequestBodyJSONSchema), + }, options: { tags: [LIMITED_CONCURRENCY_ROUTE_TAG] }, }, postAgentAcksHandlerBuilder({ diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts index 6400d6e215f9b..6d7252ffec41a 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -32,7 +32,7 @@ import { getLimitedPackages, getInstallationObject, } from '../../services/epm/packages'; -import { IngestManagerError, getHTTPResponseCode } from '../../errors'; +import { IngestManagerError, defaultIngestErrorHandler } from '../../errors'; import { splitPkgKey } from '../../services/epm/registry'; export const getCategoriesHandler: RequestHandler< @@ -45,11 +45,8 @@ export const getCategoriesHandler: RequestHandler< response: res, }; return response.ok({ body }); - } catch (e) { - return response.customError({ - statusCode: 500, - body: { message: e.message }, - }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); } }; @@ -69,11 +66,8 @@ export const getListHandler: RequestHandler< return response.ok({ body, }); - } catch (e) { - return response.customError({ - statusCode: 500, - body: { message: e.message }, - }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); } }; @@ -87,11 +81,8 @@ export const getLimitedListHandler: RequestHandler = async (context, request, re return response.ok({ body, }); - } catch (e) { - return response.customError({ - statusCode: 500, - body: { message: e.message }, - }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); } }; @@ -112,11 +103,8 @@ export const getFileHandler: RequestHandler { const soClient = context.core.savedObjects.client; @@ -46,11 +46,8 @@ export const getFleetStatusHandler: RequestHandler = async (context, request, re return response.ok({ body, }); - } catch (e) { - return response.customError({ - statusCode: 500, - body: { message: e.message }, - }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); } }; @@ -70,44 +67,22 @@ export const createFleetSetupHandler: RequestHandler< return response.ok({ body: { isInitialized: true }, }); - } catch (e) { - return response.customError({ - statusCode: 500, - body: { message: e.message }, - }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); } }; export const ingestManagerSetupHandler: RequestHandler = async (context, request, response) => { const soClient = context.core.savedObjects.client; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; - const logger = appContextService.getLogger(); + try { const body: PostIngestSetupResponse = { isInitialized: true }; await setupIngestManager(soClient, callCluster); return response.ok({ body, }); - } catch (e) { - if (e instanceof IngestManagerError) { - logger.error(e.message); - return response.customError({ - statusCode: getHTTPResponseCode(e), - body: { message: e.message }, - }); - } - if (e.isBoom) { - logger.error(e.output.payload.message); - return response.customError({ - statusCode: e.output.statusCode, - body: { message: e.output.payload.message }, - }); - } - logger.error(e.message); - logger.error(e.stack); - return response.customError({ - statusCode: 500, - body: { message: e.message }, - }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); } }; diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx index aabe4bd3e3597..e01568cfbb3c9 100644 --- a/x-pack/plugins/ingest_manager/server/types/index.tsx +++ b/x-pack/plugins/ingest_manager/server/types/index.tsx @@ -63,6 +63,9 @@ export { IndexTemplateMappings, Settings, SettingsSOAttributes, + // Agent Request types + PostAgentEnrollRequest, + PostAgentCheckinRequest, } from '../../common'; export type CallESAsCurrentUser = LegacyScopedClusterClient['callAsCurrentUser']; diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts index 3302b0ab84ba8..43ee0c89126e9 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts @@ -5,12 +5,7 @@ */ import { schema } from '@kbn/config-schema'; -import { - AckEventSchema, - NewAgentEventSchema, - AgentTypeSchema, - NewAgentActionSchema, -} from '../models'; +import { NewAgentActionSchema } from '../models'; export const GetAgentsRequestSchema = { query: schema.object({ @@ -27,37 +22,134 @@ export const GetOneAgentRequestSchema = { }), }; -export const PostAgentCheckinRequestSchema = { - params: schema.object({ - agentId: schema.string(), - }), - body: schema.object({ - status: schema.maybe( - schema.oneOf([schema.literal('online'), schema.literal('error'), schema.literal('degraded')]) - ), - local_metadata: schema.maybe(schema.recordOf(schema.string(), schema.any())), - events: schema.maybe(schema.arrayOf(NewAgentEventSchema)), - }), +export const PostAgentCheckinRequestParamsJSONSchema = { + type: 'object', + properties: { + agentId: { type: 'string' }, + }, + required: ['agentId'], }; -export const PostAgentEnrollRequestSchema = { - body: schema.object({ - type: AgentTypeSchema, - shared_id: schema.maybe(schema.string()), - metadata: schema.object({ - local: schema.recordOf(schema.string(), schema.any()), - user_provided: schema.recordOf(schema.string(), schema.any()), - }), - }), +export const PostAgentCheckinRequestBodyJSONSchema = { + type: 'object', + properties: { + status: { type: 'string', enum: ['online', 'error', 'degraded'] }, + local_metadata: { + additionalProperties: { + anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'object' }], + }, + }, + events: { + type: 'array', + items: { + type: 'object', + properties: { + type: { type: 'string', enum: ['STATE', 'ERROR', 'ACTION_RESULT', 'ACTION'] }, + subtype: { + type: 'string', + enum: [ + 'RUNNING', + 'STARTING', + 'IN_PROGRESS', + 'CONFIG', + 'FAILED', + 'STOPPING', + 'STOPPED', + 'DEGRADED', + 'DATA_DUMP', + 'ACKNOWLEDGED', + 'UNKNOWN', + ], + }, + timestamp: { type: 'string' }, + message: { type: 'string' }, + payload: { type: 'object', additionalProperties: true }, + agent_id: { type: 'string' }, + action_id: { type: 'string' }, + policy_id: { type: 'string' }, + stream_id: { type: 'string' }, + }, + required: ['type', 'subtype', 'timestamp', 'message', 'agent_id'], + additionalProperties: false, + }, + }, + }, + additionalProperties: false, }; -export const PostAgentAcksRequestSchema = { - body: schema.object({ - events: schema.arrayOf(AckEventSchema), - }), - params: schema.object({ - agentId: schema.string(), - }), +export const PostAgentEnrollRequestBodyJSONSchema = { + type: 'object', + properties: { + type: { type: 'string', enum: ['EPHEMERAL', 'PERMANENT', 'TEMPORARY'] }, + shared_id: { type: 'string' }, + metadata: { + type: 'object', + properties: { + local: { + type: 'object', + additionalProperties: true, + }, + user_provided: { + type: 'object', + additionalProperties: true, + }, + }, + additionalProperties: false, + required: ['local', 'user_provided'], + }, + }, + additionalProperties: false, + required: ['type', 'metadata'], +}; + +export const PostAgentAcksRequestParamsJSONSchema = { + type: 'object', + properties: { + agentId: { type: 'string' }, + }, + required: ['agentId'], +}; + +export const PostAgentAcksRequestBodyJSONSchema = { + type: 'object', + properties: { + events: { + type: 'array', + item: { + type: 'object', + properties: { + type: { type: 'string', enum: ['STATE', 'ERROR', 'ACTION_RESULT', 'ACTION'] }, + subtype: { + type: 'string', + enum: [ + 'RUNNING', + 'STARTING', + 'IN_PROGRESS', + 'CONFIG', + 'FAILED', + 'STOPPING', + 'STOPPED', + 'DEGRADED', + 'DATA_DUMP', + 'ACKNOWLEDGED', + 'UNKNOWN', + ], + }, + timestamp: { type: 'string' }, + message: { type: 'string' }, + payload: { type: 'object', additionalProperties: true }, + agent_id: { type: 'string' }, + action_id: { type: 'string' }, + policy_id: { type: 'string' }, + stream_id: { type: 'string' }, + }, + required: ['type', 'subtype', 'timestamp', 'message', 'agent_id', 'action_id'], + additionalProperties: false, + }, + }, + }, + additionalProperties: false, + required: ['events'], }; export const PostNewAgentActionRequestSchema = { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/xjson_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/xjson_editor.tsx index 7fb92e89c9f68..e00f9c002e5bc 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/xjson_editor.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/xjson_editor.tsx @@ -14,6 +14,11 @@ interface Props { editorProps: { [key: string]: any }; } +const defaultEditorOptions = { + minimap: { enabled: false }, + lineNumbers: 'off', +}; + export const XJsonEditor: FunctionComponent = ({ field, editorProps }) => { const { value, setValue } = field; const { xJson, setXJson, convertToJson } = Monaco.useXJsonMode(value); @@ -31,7 +36,7 @@ export const XJsonEditor: FunctionComponent = ({ field, editorProps }) => editorProps={{ value: xJson, languageId: XJsonLang.ID, - options: { minimap: { enabled: false } }, + options: defaultEditorOptions, onChange, ...editorProps, }} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx index 9adb3957ea9f4..bda64c0a75612 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx @@ -33,10 +33,22 @@ export const ProcessorSettingsFields: FunctionComponent = ({ processor }) const formDescriptor = getProcessorDescriptor(type as any); if (formDescriptor?.FieldsComponent) { + const renderedFields = ( + + ); return ( <> - - + {renderedFields ? ( + <> + {renderedFields} + + + ) : ( + renderedFields + )} ); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/common_processor_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/common_processor_fields.tsx index 8089b8e7dfad3..e1048d627f66c 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/common_processor_fields.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/common_processor_fields.tsx @@ -16,12 +16,12 @@ import { } from '../../../../../../../shared_imports'; import { TextEditor } from '../../field_components'; -import { to, from } from '../shared'; +import { to, from, EDITOR_PX_HEIGHT } from '../shared'; const ignoreFailureConfig: FieldConfig = { defaultValue: false, deserializer: to.booleanOrUndef, - serializer: from.defaultBoolToUndef(false), + serializer: from.undefinedIfValue(false), label: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.commonFields.ignoreFailureFieldLabel', { @@ -64,8 +64,11 @@ export const CommonProcessorFields: FunctionComponent = () => { componentProps={{ editorProps: { languageId: 'painless', - height: 75, - options: { minimap: { enabled: false } }, + height: EDITOR_PX_HEIGHT.extraSmall, + options: { + lineNumbers: 'off', + minimap: { enabled: false }, + }, }, }} path="fields.if" diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx index 63ebb47dfc573..3d38f9238cdd1 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx @@ -22,7 +22,7 @@ export const fieldsConfig: FieldsConfig = { type: FIELD_TYPES.TOGGLE, defaultValue: false, deserializer: to.booleanOrUndef, - serializer: from.defaultBoolToUndef(false), + serializer: from.undefinedIfValue(false), label: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.commonFields.ignoreMissingFieldLabel', { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/target_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/target_field.tsx index 9bf44425a7c5d..f808c8c11ff0a 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/target_field.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/target_field.tsx @@ -21,8 +21,7 @@ const fieldsConfig: FieldsConfig = { helpText: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.commonFields.targetFieldHelpText', { - defaultMessage: - 'The field to assign the joined value to. If empty, the field is updated in-place.', + defaultMessage: 'Field to assign the value to. If empty, the field is updated in-place.', } ), }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/custom.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/custom.tsx index 82fdc81e0a843..c2aab62cf8933 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/custom.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/custom.tsx @@ -17,6 +17,7 @@ import { const { emptyField, isJsonField } = fieldValidators; import { XJsonEditor } from '../field_components'; +import { EDITOR_PX_HEIGHT } from './shared'; const customConfig: FieldConfig = { type: FIELD_TYPES.TEXT, @@ -78,7 +79,7 @@ export const Custom: FunctionComponent = ({ defaultOptions }) => { componentProps={{ editorProps: { 'data-test-subj': 'processorOptionsEditor', - height: 300, + height: EDITOR_PX_HEIGHT.large, 'aria-label': i18n.translate( 'xpack.ingestPipelines.pipelineEditor.customForm.optionsFieldAriaLabel', { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dissect.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dissect.tsx index 5f9f55ced1a25..344855304e4fd 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dissect.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dissect.tsx @@ -20,6 +20,7 @@ import { import { FieldNameField } from './common_fields/field_name_field'; import { IgnoreMissingField } from './common_fields/ignore_missing_field'; +import { EDITOR_PX_HEIGHT } from './shared'; const { emptyField } = fieldValidators; @@ -80,7 +81,7 @@ export const Dissect: FunctionComponent = () => { component={TextEditor} componentProps={{ editorProps: { - height: 75, + height: EDITOR_PX_HEIGHT.extraSmall, options: { minimap: { enabled: false } }, }, }} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/enrich.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/enrich.tsx index 5986374b338cf..ba1c55b731cc9 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/enrich.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/enrich.tsx @@ -81,7 +81,7 @@ const fieldsConfig: FieldsConfig = { type: FIELD_TYPES.TOGGLE, defaultValue: true, deserializer: to.booleanOrUndef, - serializer: from.defaultBoolToUndef, + serializer: from.undefinedIfValue, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.enrichForm.overrideFieldLabel', { defaultMessage: 'Override', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/foreach.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/foreach.tsx index ce606af086893..c32a85310d21f 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/foreach.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/foreach.tsx @@ -12,7 +12,7 @@ import { FIELD_TYPES, fieldValidators, UseField } from '../../../../../../shared import { XJsonEditor } from '../field_components'; import { FieldNameField } from './common_fields/field_name_field'; -import { FieldsConfig, to } from './shared'; +import { FieldsConfig, to, EDITOR_PX_HEIGHT } from './shared'; const { emptyField, isJsonField } = fieldValidators; @@ -31,15 +31,18 @@ const fieldsConfig: FieldsConfig = { validations: [ { validator: emptyField( - i18n.translate('xpack.ingestPipelines.pipelineEditor.failForm.processorRequiredError', { - defaultMessage: 'A processor is required.', - }) + i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.foreachForm.processorRequiredError', + { + defaultMessage: 'A processor is required.', + } + ) ), }, { validator: isJsonField( i18n.translate( - 'xpack.ingestPipelines.pipelineEditor.failForm.processorInvalidJsonError', + 'xpack.ingestPipelines.pipelineEditor.foreachForm.processorInvalidJsonError', { defaultMessage: 'Invalid JSON', } @@ -64,9 +67,9 @@ export const Foreach: FunctionComponent = () => { component={XJsonEditor} componentProps={{ editorProps: { - height: 200, + height: EDITOR_PX_HEIGHT.medium, 'aria-label': i18n.translate( - 'xpack.ingestPipelines.pipelineEditor.customForm.optionsFieldAriaLabel', + 'xpack.ingestPipelines.pipelineEditor.foreachForm.optionsFieldAriaLabel', { defaultMessage: 'Configuration JSON editor', } diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx index 4575f84683ff0..937fa4d3c4d86 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx @@ -41,7 +41,7 @@ const fieldsConfig: FieldsConfig = { type: FIELD_TYPES.TOGGLE, defaultValue: true, deserializer: to.booleanOrUndef, - serializer: from.defaultBoolToUndef(true), + serializer: from.undefinedIfValue(true), label: i18n.translate('xpack.ingestPipelines.pipelineEditor.geoIPForm.firstOnlyFieldLabel', { defaultMessage: 'First only', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/grok.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/grok.tsx index d021038fda94f..c5c6adbe2a7a8 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/grok.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/grok.tsx @@ -19,7 +19,7 @@ import { XJsonEditor } from '../field_components'; import { FieldNameField } from './common_fields/field_name_field'; import { IgnoreMissingField } from './common_fields/ignore_missing_field'; -import { FieldsConfig, to, from } from './shared'; +import { FieldsConfig, to, from, EDITOR_PX_HEIGHT } from './shared'; const { emptyField, isJsonField } = fieldValidators; @@ -80,7 +80,7 @@ const fieldsConfig: FieldsConfig = { type: FIELD_TYPES.TOGGLE, defaultValue: false, deserializer: to.booleanOrUndef, - serializer: from.defaultBoolToUndef(false), + serializer: from.undefinedIfValue(false), label: i18n.translate('xpack.ingestPipelines.pipelineEditor.grokForm.traceMatchFieldLabel', { defaultMessage: 'Trace match', }), @@ -110,7 +110,7 @@ export const Grok: FunctionComponent = () => { config={fieldsConfig.pattern_definitions} componentProps={{ editorProps: { - height: 200, + height: EDITOR_PX_HEIGHT.medium, 'aria-label': i18n.translate( 'xpack.ingestPipelines.pipelineEditor.grokForm.patternDefinitionsAriaLabel', { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx index a0bda245d667b..a42df6873d57b 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx @@ -13,7 +13,7 @@ import { FIELD_TYPES, fieldValidators, UseField, Field } from '../../../../../.. import { TextEditor } from '../field_components'; -import { FieldsConfig } from './shared'; +import { EDITOR_PX_HEIGHT, FieldsConfig } from './shared'; import { FieldNameField } from './common_fields/field_name_field'; import { IgnoreMissingField } from './common_fields/ignore_missing_field'; import { TargetField } from './common_fields/target_field'; @@ -78,7 +78,7 @@ export const Gsub: FunctionComponent = () => { component={TextEditor} componentProps={{ editorProps: { - height: 75, + height: EDITOR_PX_HEIGHT.extraSmall, options: { minimap: { enabled: false } }, }, }} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/index.ts index c2ded28341de9..e211d682ab0f0 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/index.ts @@ -24,7 +24,19 @@ export { HtmlStrip } from './html_strip'; export { Inference } from './inference'; export { Join } from './join'; export { Json } from './json'; +export { Kv } from './kv'; +export { Lowercase } from './lowercase'; +export { Pipeline } from './pipeline'; +export { Remove } from './remove'; +export { Rename } from './rename'; +export { Script } from './script'; +export { SetProcessor } from './set'; +export { SetSecurityUser } from './set_security_user'; +export { Split } from './split'; +export { Sort } from './sort'; export { Trim } from './trim'; export { Uppercase } from './uppercase'; export { UrlDecode } from './url_decode'; export { UserAgent } from './user_agent'; + +export { FormFieldsComponent } from './shared'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/inference.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/inference.tsx index 68281fc11f340..85f995fa77cea 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/inference.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/inference.tsx @@ -21,7 +21,7 @@ import { XJsonEditor } from '../field_components'; import { TargetField } from './common_fields/target_field'; -import { FieldsConfig, to, from } from './shared'; +import { FieldsConfig, to, from, EDITOR_PX_HEIGHT } from './shared'; const { emptyField, isJsonField } = fieldValidators; @@ -177,7 +177,7 @@ export const Inference: FunctionComponent = () => { component={XJsonEditor} componentProps={{ editorProps: { - height: 200, + height: EDITOR_PX_HEIGHT.medium, options: { minimap: { enabled: false } }, }, }} @@ -192,7 +192,7 @@ export const Inference: FunctionComponent = () => { component={XJsonEditor} componentProps={{ editorProps: { - height: 200, + height: EDITOR_PX_HEIGHT.medium, options: { minimap: { enabled: false } }, }, }} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx index c35a5b463f573..ab077d3337f63 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx @@ -35,7 +35,7 @@ const fieldsConfig: FieldsConfig = { { validator: emptyField( i18n.translate('xpack.ingestPipelines.pipelineEditor.joinForm.separatorRequiredError', { - defaultMessage: 'A separator value is required.', + defaultMessage: 'A value is required.', }) ), }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx index 5c4c53b65b6dc..b68b398325085 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx @@ -29,7 +29,7 @@ const fieldsConfig: FieldsConfig = { defaultMessage: 'Add to root', }), deserializer: to.booleanOrUndef, - serializer: from.defaultBoolToUndef(false), + serializer: from.undefinedIfValue(false), helpText: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.jsonForm.addToRootFieldHelpText', { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/kv.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/kv.tsx new file mode 100644 index 0000000000000..f51bf19ad180a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/kv.tsx @@ -0,0 +1,202 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCode } from '@elastic/eui'; + +import { + FIELD_TYPES, + fieldValidators, + UseField, + Field, + ComboBoxField, + ToggleField, +} from '../../../../../../shared_imports'; + +import { FieldsConfig, from, to } from './shared'; +import { FieldNameField } from './common_fields/field_name_field'; +import { TargetField } from './common_fields/target_field'; +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; + +const { emptyField } = fieldValidators; + +const fieldsConfig: FieldsConfig = { + /* Required fields config */ + field_split: { + type: FIELD_TYPES.TEXT, + deserializer: String, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.fieldSplitFieldLabel', { + defaultMessage: 'Field split', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.fieldSplitHelpText', { + defaultMessage: 'Regex pattern for splitting key-value pairs.', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.fieldSplitRequiredError', { + defaultMessage: 'A value is required.', + }) + ), + }, + ], + }, + value_split: { + type: FIELD_TYPES.TEXT, + deserializer: String, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.valueSplitFieldLabel', { + defaultMessage: 'Value split', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.valueSplitHelpText', { + defaultMessage: 'Regex pattern for splitting the key from the value within a key-value pair.', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.valueSplitRequiredError', { + defaultMessage: 'A value is required.', + }) + ), + }, + ], + }, + + /* Optional fields config */ + include_keys: { + type: FIELD_TYPES.COMBO_BOX, + deserializer: to.arrayOfStrings, + serializer: from.optionalArrayOfStrings, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.includeKeysFieldLabel', { + defaultMessage: 'Include keys', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.includeKeysHelpText', { + defaultMessage: + 'List of keys to filter and insert into document. Defaults to including all keys.', + }), + }, + + exclude_keys: { + type: FIELD_TYPES.COMBO_BOX, + deserializer: to.arrayOfStrings, + serializer: from.optionalArrayOfStrings, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.excludeKeysFieldLabel', { + defaultMessage: 'Exclude keys', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.excludeKeysHelpText', { + defaultMessage: 'List of keys to exclude from document.', + }), + }, + + prefix: { + type: FIELD_TYPES.TEXT, + deserializer: String, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.prefixFieldLabel', { + defaultMessage: 'Prefix', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.prefixHelpText', { + defaultMessage: 'Prefix to be added to extracted keys.', + }), + }, + + trim_key: { + type: FIELD_TYPES.TEXT, + deserializer: String, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.trimKeyFieldLabel', { + defaultMessage: 'Trim key', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.trimKeyHelpText', { + defaultMessage: 'Characters to trim from extracted keys.', + }), + }, + + trim_value: { + type: FIELD_TYPES.TEXT, + deserializer: String, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.trimValueFieldLabel', { + defaultMessage: 'Trim value', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.trimValueHelpText', { + defaultMessage: 'Characters to trim from extracted values.', + }), + }, + + strip_brackets: { + type: FIELD_TYPES.TOGGLE, + defaultValue: false, + deserializer: to.booleanOrUndef, + serializer: from.undefinedIfValue(false), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.stripBracketsFieldLabel', { + defaultMessage: 'Strip brackets', + }), + helpText: ( + {'()'}, + angle: <>, + square: {'[]'}, + singleQuote: {"'"}, + doubleQuote: {'"'}, + }} + /> + ), + }, +}; + +export const Kv: FunctionComponent = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/lowercase.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/lowercase.tsx new file mode 100644 index 0000000000000..9db313a05007f --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/lowercase.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCode } from '@elastic/eui'; + +import { FieldNameField } from './common_fields/field_name_field'; +import { TargetField } from './common_fields/target_field'; +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; + +export const Lowercase: FunctionComponent = () => { + return ( + <> + + + {'field'}, + }} + /> + } + /> + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/pipeline.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/pipeline.tsx new file mode 100644 index 0000000000000..c785cf935833d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/pipeline.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { FIELD_TYPES, fieldValidators, UseField, Field } from '../../../../../../shared_imports'; + +import { FieldsConfig } from './shared'; + +const { emptyField } = fieldValidators; + +const fieldsConfig: FieldsConfig = { + /* Required fields config */ + name: { + type: FIELD_TYPES.TEXT, + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.pipelineForm.pipelineNameFieldLabel', + { + defaultMessage: 'Pipeline name', + } + ), + deserializer: String, + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.pipelineForm.pipelineNameFieldHelpText', + { + defaultMessage: 'Name of the pipeline to execute.', + } + ), + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.pipelineForm.pipelineNameRequiredError', + { + defaultMessage: 'A value is required.', + } + ) + ), + }, + ], + }, +}; + +export const Pipeline: FunctionComponent = () => { + return ; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/remove.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/remove.tsx new file mode 100644 index 0000000000000..3e90ce2b76f7b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/remove.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { + FIELD_TYPES, + UseField, + ComboBoxField, + fieldValidators, +} from '../../../../../../shared_imports'; + +import { FieldsConfig, to } from './shared'; + +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; + +const { emptyField } = fieldValidators; + +const fieldsConfig: FieldsConfig = { + field: { + type: FIELD_TYPES.COMBO_BOX, + deserializer: to.arrayOfStrings, + serializer: (v: string[]) => (v.length === 1 ? v[0] : v), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.removeForm.fieldNameField', { + defaultMessage: 'Fields', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.removeForm.fieldNameHelpText', { + defaultMessage: 'Fields to be removed.', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.removeForm.fieldNameRequiredError', { + defaultMessage: 'A value is required.', + }) + ), + }, + ], + }, +}; + +export const Remove: FunctionComponent = () => { + return ( + <> + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/rename.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/rename.tsx new file mode 100644 index 0000000000000..8b796d9664586 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/rename.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { fieldValidators } from '../../../../../../shared_imports'; + +import { FieldNameField } from './common_fields/field_name_field'; +import { TargetField } from './common_fields/target_field'; +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; + +const { emptyField } = fieldValidators; + +export const Rename: FunctionComponent = () => { + return ( + <> + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/script.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/script.tsx new file mode 100644 index 0000000000000..ae0bbbb490ae9 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/script.tsx @@ -0,0 +1,186 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCode, EuiSwitch, EuiFormRow } from '@elastic/eui'; + +import { FIELD_TYPES, fieldValidators, UseField, Field } from '../../../../../../shared_imports'; + +import { XJsonEditor, TextEditor } from '../field_components'; + +import { FieldsConfig, to, from, FormFieldsComponent, EDITOR_PX_HEIGHT } from './shared'; + +const { isJsonField, emptyField } = fieldValidators; + +const fieldsConfig: FieldsConfig = { + /* Required fields config */ + + id: { + type: FIELD_TYPES.TEXT, + deserializer: String, + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.scriptForm.storedScriptIDFieldLabel', + { + defaultMessage: 'Stored script ID', + } + ), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.scriptForm.storedScriptIDFieldHelpText', + { + defaultMessage: 'Stored script reference.', + } + ), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.scriptForm.idRequiredError', { + defaultMessage: 'A value is required.', + }) + ), + }, + ], + }, + + source: { + type: FIELD_TYPES.TEXT, + deserializer: String, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.scriptForm.sourceFieldLabel', { + defaultMessage: 'Source', + }), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.scriptForm.sourceFieldHelpText', + { + defaultMessage: 'Script to be executed.', + } + ), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.scriptForm.sourceRequiredError', { + defaultMessage: 'A value is required.', + }) + ), + }, + ], + }, + + /* Optional fields config */ + lang: { + type: FIELD_TYPES.TEXT, + deserializer: String, + serializer: from.undefinedIfValue('painless'), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.scriptForm.langFieldLabel', { + defaultMessage: 'Language (optional)', + }), + helpText: ( + {'painless'}, + }} + /> + ), + }, + + params: { + type: FIELD_TYPES.TEXT, + deserializer: to.jsonString, + serializer: from.optionalJson, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.scriptForm.paramsFieldLabel', { + defaultMessage: 'Parameters', + }), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.scriptForm.paramsFieldHelpText', + { + defaultMessage: 'Script parameters.', + } + ), + validations: [ + { + validator: (value) => { + if (value.value) { + return isJsonField( + i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.scriptForm.processorInvalidJsonError', + { + defaultMessage: 'Invalid JSON', + } + ) + )(value); + } + }, + }, + ], + }, +}; + +export const Script: FormFieldsComponent = ({ initialFieldValues }) => { + const [showId, setShowId] = useState(() => !!initialFieldValues?.id); + return ( + <> + + setShowId((v) => !v)} + /> + + + {showId ? ( + + ) : ( + <> + + + + + )} + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx index 88cea620ae804..c282be35e5071 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx @@ -6,9 +6,10 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCode } from '@elastic/eui'; import { - FieldConfig, FIELD_TYPES, fieldValidators, ToggleField, @@ -16,46 +17,68 @@ import { Field, } from '../../../../../../shared_imports'; -const { emptyField } = fieldValidators; +import { FieldsConfig, to, from } from './shared'; -const fieldConfig: FieldConfig = { - type: FIELD_TYPES.TEXT, - label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.fieldFieldLabel', { - defaultMessage: 'Field', - }), - validations: [ - { - validator: emptyField( - i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.fieldRequiredError', { - defaultMessage: 'A field value is required.', - }) - ), - }, - ], -}; +import { FieldNameField } from './common_fields/field_name_field'; -const valueConfig: FieldConfig = { - type: FIELD_TYPES.TEXT, - label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueFieldLabel', { - defaultMessage: 'Value', - }), - validations: [ - { - validator: emptyField( - i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError', { - defaultMessage: 'A value to set is required.', - }) - ), - }, - ], -}; +const { emptyField } = fieldValidators; -const overrideConfig: FieldConfig = { - defaultValue: false, - label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldLabel', { - defaultMessage: 'Override', - }), - type: FIELD_TYPES.TOGGLE, +const fieldsConfig: FieldsConfig = { + /* Required fields config */ + value: { + type: FIELD_TYPES.TEXT, + deserializer: String, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueFieldLabel', { + defaultMessage: 'Value', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueFieldHelpText', { + defaultMessage: 'Value to be set for the field', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError', { + defaultMessage: 'A value is required', + }) + ), + }, + ], + }, + /* Optional fields config */ + override: { + type: FIELD_TYPES.TOGGLE, + defaultValue: true, + deserializer: to.booleanOrUndef, + serializer: from.undefinedIfValue(true), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldLabel', { + defaultMessage: 'Override', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldHelpText', { + defaultMessage: 'If disabled, fields containing non-null values will not be updated.', + }), + }, + ignore_empty_value: { + type: FIELD_TYPES.TOGGLE, + defaultValue: false, + deserializer: to.booleanOrUndef, + serializer: from.undefinedIfValue(false), + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.setForm.ignoreEmptyValueFieldLabel', + { + defaultMessage: 'Ignore empty value', + } + ), + helpText: ( + {'value'}, + nullValue: {'null'}, + }} + /> + ), + }, }; /** @@ -64,11 +87,21 @@ const overrideConfig: FieldConfig = { export const SetProcessor: FunctionComponent = () => { return ( <> - + + + - + - + ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set_security_user.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set_security_user.tsx new file mode 100644 index 0000000000000..78128b3d54c75 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set_security_user.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCode } from '@elastic/eui'; + +import { FIELD_TYPES, UseField, ComboBoxField } from '../../../../../../shared_imports'; + +import { FieldsConfig, to, from } from './shared'; + +import { FieldNameField } from './common_fields/field_name_field'; + +const userProperties: string[] = [ + 'username', + 'roles', + 'email', + 'full_name', + 'metadata', + 'api_key', + 'realm', + 'authentication_type', +]; + +const comboBoxOptions = userProperties.map((prop) => ({ label: prop })); +const helpTextValues = userProperties.join(', '); + +const fieldsConfig: FieldsConfig = { + /* Optional fields config */ + properties: { + type: FIELD_TYPES.COMBO_BOX, + deserializer: to.arrayOfStrings, + serializer: from.optionalArrayOfStrings, + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.setSecurityUserForm.propertiesFieldLabel', + { + defaultMessage: 'Properties (optional)', + } + ), + helpText: ( + [{helpTextValues}], + }} + /> + ), + }, +}; + +export const SetSecurityUser: FunctionComponent = () => { + return ( + <> + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/shared.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/shared.ts index 84b308dd9cd7a..e45469e23e8a0 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/shared.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/shared.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { FunctionComponent } from 'react'; import * as rt from 'io-ts'; import { isRight } from 'fp-ts/lib/Either'; @@ -31,7 +31,8 @@ export function isArrayOfStrings(v: unknown): v is string[] { */ export const to = { booleanOrUndef: (v: unknown): boolean | undefined => (typeof v === 'boolean' ? v : undefined), - arrayOfStrings: (v: unknown): string[] => (isArrayOfStrings(v) ? v : []), + arrayOfStrings: (v: unknown): string[] => + isArrayOfStrings(v) ? v : typeof v === 'string' && v.length ? [v] : [], jsonString: (v: unknown) => (v ? JSON.stringify(v, null, 2) : '{}'), }; @@ -62,7 +63,17 @@ export const from = { } } }, - defaultBoolToUndef: (defaultBool: boolean) => (v: boolean) => (v === defaultBool ? undefined : v), + optionalArrayOfStrings: (v: string[]) => (v.length ? v : undefined), + undefinedIfValue: (value: any) => (v: boolean) => (v === value ? undefined : v), +}; + +export const EDITOR_PX_HEIGHT = { + extraSmall: 75, + small: 100, + medium: 200, + large: 300, }; export type FieldsConfig = Record; + +export type FormFieldsComponent = FunctionComponent<{ initialFieldValues?: Record }>; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/sort.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/sort.tsx new file mode 100644 index 0000000000000..cdd0ff888accf --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/sort.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { FIELD_TYPES, UseField, SelectField } from '../../../../../../shared_imports'; + +import { FieldNameField } from './common_fields/field_name_field'; +import { TargetField } from './common_fields/target_field'; +import { FieldsConfig, from } from './shared'; + +const fieldsConfig: FieldsConfig = { + /* Optional fields config */ + order: { + type: FIELD_TYPES.SELECT, + defaultValue: 'asc', + deserializer: (v) => (v === 'asc' || v === 'desc' ? v : 'asc'), + serializer: from.undefinedIfValue('asc'), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.sortForm.orderFieldLabel', { + defaultMessage: 'Order', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.sortForm.orderFieldHelpText', { + defaultMessage: 'Sort order to use', + }), + }, +}; + +export const Sort: FunctionComponent = () => { + return ( + <> + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/split.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/split.tsx new file mode 100644 index 0000000000000..b48ce74110b39 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/split.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { + FIELD_TYPES, + fieldValidators, + UseField, + Field, + ToggleField, +} from '../../../../../../shared_imports'; + +import { FieldNameField } from './common_fields/field_name_field'; +import { TargetField } from './common_fields/target_field'; +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; +import { FieldsConfig, to, from } from './shared'; + +const { emptyField } = fieldValidators; + +const fieldsConfig: FieldsConfig = { + /* Required fields config */ + separator: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.splitForm.separatorFieldLabel', { + defaultMessage: 'Separator', + }), + deserializer: String, + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.splitForm.separatorFieldHelpText', + { + defaultMessage: 'Regex to match a separator', + } + ), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.splitForm.separatorRequiredError', { + defaultMessage: 'A value is required.', + }) + ), + }, + ], + }, + /* Optional fields config */ + preserve_trailing: { + type: FIELD_TYPES.TOGGLE, + defaultValue: false, + deserializer: to.booleanOrUndef, + serializer: from.undefinedIfValue(false), + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.splitForm.preserveTrailingFieldLabel', + { + defaultMessage: 'Preserve trailing', + } + ), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.splitForm.preserveTrailingFieldHelpText', + { defaultMessage: 'If enabled, preserve any trailing space.' } + ), + }, +}; + +export const Split: FunctionComponent = () => { + return ( + <> + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx index 24f06fc6368aa..8bdeab3666ee2 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx @@ -5,7 +5,6 @@ */ import { i18n } from '@kbn/i18n'; -import { FunctionComponent } from 'react'; import { Append, @@ -28,17 +27,25 @@ import { Inference, Join, Json, + Kv, + Lowercase, + Pipeline, + Remove, + Rename, + Script, + SetProcessor, + SetSecurityUser, + Split, + Sort, Trim, Uppercase, UrlDecode, UserAgent, + FormFieldsComponent, } from '../manage_processor_form/processors'; -// import { SetProcessor } from './processors/set'; -// import { Gsub } from './processors/gsub'; - interface FieldDescriptor { - FieldsComponent?: FunctionComponent; + FieldsComponent?: FormFieldsComponent; docLinkPath: string; /** * A sentence case label that can be displayed to users @@ -190,63 +197,70 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { }), }, kv: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Kv, docLinkPath: '/kv-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.kv', { defaultMessage: 'KV', }), }, lowercase: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Lowercase, docLinkPath: '/lowercase-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.lowercase', { defaultMessage: 'Lowercase', }), }, pipeline: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Pipeline, docLinkPath: '/pipeline-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.pipeline', { defaultMessage: 'Pipeline', }), }, remove: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Remove, docLinkPath: '/remove-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.remove', { defaultMessage: 'Remove', }), }, rename: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Rename, docLinkPath: '/rename-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.rename', { defaultMessage: 'Rename', }), }, script: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Script, docLinkPath: '/script-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.script', { defaultMessage: 'Script', }), }, + set: { + FieldsComponent: SetProcessor, + docLinkPath: '/set-processor.html', + label: i18n.translate('xpack.ingestPipelines.processors.label.set', { + defaultMessage: 'Set', + }), + }, set_security_user: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: SetSecurityUser, docLinkPath: '/ingest-node-set-security-user-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.setSecurityUser', { defaultMessage: 'Set security user', }), }, split: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Split, docLinkPath: '/split-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.split', { defaultMessage: 'Split', }), }, sort: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Sort, docLinkPath: '/sort-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.sort', { defaultMessage: 'Sort', @@ -280,15 +294,6 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { defaultMessage: 'User agent', }), }, - - // --- The below processor descriptors have components implemented --- - set: { - FieldsComponent: undefined, - docLinkPath: '/set-processor.html', - label: i18n.translate('xpack.ingestPipelines.processors.label.set', { - defaultMessage: 'Set', - }), - }, }; export type ProcessorType = keyof typeof mapProcessorTypeToDescriptor; diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts index 936db37f0c629..abdbdf2140400 100644 --- a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -62,6 +62,7 @@ export { RadioGroupField, NumericField, SelectField, + CheckBoxField, } from '../../../../src/plugins/es_ui_shared/static/forms/components'; export { diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx index 194f12cf9291b..0db456e0760ec 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -205,10 +205,10 @@ describe('Datatable Visualization', () => { }, frame, }).groups - ).toHaveLength(1); + ).toHaveLength(2); }); - it('allows all kinds of operations', () => { + it('allows only bucket operations one category', () => { const datasource = createMockDatasource('test'); const frame = mockFrame(); frame.datasourceLayers = { first: datasource.publicAPIMock }; @@ -232,6 +232,40 @@ describe('Datatable Visualization', () => { expect(filterOperations({ ...baseOperation, dataType: 'boolean' })).toEqual(true); expect(filterOperations({ ...baseOperation, dataType: 'other' as DataType })).toEqual(true); expect(filterOperations({ ...baseOperation, dataType: 'date', isBucketed: false })).toEqual( + false + ); + expect(filterOperations({ ...baseOperation, dataType: 'number', isBucketed: false })).toEqual( + false + ); + }); + + it('allows only metric operations in one category', () => { + const datasource = createMockDatasource('test'); + const frame = mockFrame(); + frame.datasourceLayers = { first: datasource.publicAPIMock }; + + const filterOperations = datatableVisualization.getConfiguration({ + layerId: 'first', + state: { + layers: [{ layerId: 'first', columns: [] }], + }, + frame, + }).groups[1].filterOperations; + + const baseOperation: Operation = { + dataType: 'string', + isBucketed: true, + label: '', + }; + expect(filterOperations({ ...baseOperation })).toEqual(false); + expect(filterOperations({ ...baseOperation, dataType: 'number' })).toEqual(false); + expect(filterOperations({ ...baseOperation, dataType: 'date' })).toEqual(false); + expect(filterOperations({ ...baseOperation, dataType: 'boolean' })).toEqual(false); + expect(filterOperations({ ...baseOperation, dataType: 'other' as DataType })).toEqual(false); + expect(filterOperations({ ...baseOperation, dataType: 'date', isBucketed: false })).toEqual( + true + ); + expect(filterOperations({ ...baseOperation, dataType: 'number', isBucketed: false })).toEqual( true ); }); @@ -248,7 +282,7 @@ describe('Datatable Visualization', () => { layerId: 'a', state: { layers: [layer] }, frame, - }).groups[0].accessors + }).groups[1].accessors ).toEqual(['c', 'b']); }); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index 5aff4e14b17f2..836ffcb15cfa1 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -143,15 +143,29 @@ export const datatableVisualization: Visualization groups: [ { groupId: 'columns', - groupLabel: i18n.translate('xpack.lens.datatable.columns', { - defaultMessage: 'Columns', + groupLabel: i18n.translate('xpack.lens.datatable.breakdown', { + defaultMessage: 'Break down by', }), layerId: state.layers[0].layerId, - accessors: sortedColumns, + accessors: sortedColumns.filter((c) => datasource.getOperationForColumnId(c)?.isBucketed), supportsMoreColumns: true, - filterOperations: () => true, + filterOperations: (op) => op.isBucketed, dataTestSubj: 'lnsDatatable_column', }, + { + groupId: 'metrics', + groupLabel: i18n.translate('xpack.lens.datatable.metrics', { + defaultMessage: 'Metrics', + }), + layerId: state.layers[0].layerId, + accessors: sortedColumns.filter( + (c) => !datasource.getOperationForColumnId(c)?.isBucketed + ), + supportsMoreColumns: true, + filterOperations: (op) => !op.isBucketed, + required: true, + dataTestSubj: 'lnsDatatable_metrics', + }, ], }; }, diff --git a/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap index d18a2db614f55..3581151dd5f76 100644 --- a/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap +++ b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap @@ -9,6 +9,20 @@ exports[`DragDrop droppable is reflected in the className 1`] = ` `; +exports[`DragDrop items that have droppable=false get special styling when another item is dragged 1`] = ` +
+ Hello! +
+`; + exports[`DragDrop renders if nothing is being dragged 1`] = `
{ const value = {}; const component = mount( - + Hello! @@ -127,4 +127,63 @@ describe('DragDrop', () => { expect(component).toMatchSnapshot(); }); + + test('items that have droppable=false get special styling when another item is dragged', () => { + const component = mount( + {}}> + + Ignored + + {}} droppable={false}> + Hello! + + + ); + + expect(component.find('[data-test-subj="lnsDragDrop"]').at(1)).toMatchSnapshot(); + }); + + test('additional styles are reflected in the className until drop', () => { + let dragging: string | undefined; + const getAdditionalClasses = jest.fn().mockReturnValue('additional'); + const component = mount( + { + dragging = 'hello'; + }} + > + + Ignored + + {}} + droppable + getAdditionalClassesOnEnter={getAdditionalClasses} + > + Hello! + + + ); + + const dataTransfer = { + setData: jest.fn(), + getData: jest.fn(), + }; + component + .find('[data-test-subj="lnsDragDrop"]') + .first() + .simulate('dragstart', { dataTransfer }); + jest.runAllTimers(); + + component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragover'); + expect(component.find('.additional')).toHaveLength(1); + + component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragleave'); + expect(component.find('.additional')).toHaveLength(0); + + component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragover'); + component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('drop'); + expect(component.find('.additional')).toHaveLength(0); + }); }); diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx index 5a0fc3b3839f7..85bdd24bd4f80 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx @@ -49,6 +49,11 @@ interface BaseProps { */ droppable?: boolean; + /** + * Additional class names to apply when another element is over the drop target + */ + getAdditionalClassesOnEnter?: () => string; + /** * The optional test subject associated with this DOM element. */ @@ -97,6 +102,12 @@ export const DragDrop = (props: Props) => { {...props} dragging={droppable ? dragging : undefined} isDragging={!!(draggable && value === dragging)} + isNotDroppable={ + // If the configuration has provided a droppable flag, but this particular item is not + // droppable, then it should be less prominent. Ignores items that are both + // draggable and drop targets + droppable === false && Boolean(dragging) && value !== dragging + } setDragging={setDragging} /> ); @@ -107,9 +118,13 @@ const DragDropInner = React.memo(function DragDropInner( dragging: unknown; setDragging: (dragging: unknown) => void; isDragging: boolean; + isNotDroppable: boolean; } ) { - const [state, setState] = useState({ isActive: false }); + const [state, setState] = useState({ + isActive: false, + dragEnterClassNames: '', + }); const { className, onDrop, @@ -120,13 +135,20 @@ const DragDropInner = React.memo(function DragDropInner( dragging, setDragging, isDragging, + isNotDroppable, } = props; - const classes = classNames('lnsDragDrop', className, { - 'lnsDragDrop-isDropTarget': droppable, - 'lnsDragDrop-isActiveDropTarget': droppable && state.isActive, - 'lnsDragDrop-isDragging': isDragging, - }); + const classes = classNames( + 'lnsDragDrop', + className, + { + 'lnsDragDrop-isDropTarget': droppable, + 'lnsDragDrop-isActiveDropTarget': droppable && state.isActive, + 'lnsDragDrop-isDragging': isDragging, + 'lnsDragDrop-isNotDroppable': isNotDroppable, + }, + state.dragEnterClassNames + ); const dragStart = (e: DroppableEvent) => { // Setting stopPropgagation causes Chrome failures, so @@ -159,19 +181,25 @@ const DragDropInner = React.memo(function DragDropInner( // An optimization to prevent a bunch of React churn. if (!state.isActive) { - setState({ ...state, isActive: true }); + setState({ + ...state, + isActive: true, + dragEnterClassNames: props.getAdditionalClassesOnEnter + ? props.getAdditionalClassesOnEnter() + : '', + }); } }; const dragLeave = () => { - setState({ ...state, isActive: false }); + setState({ ...state, isActive: false, dragEnterClassNames: '' }); }; const drop = (e: DroppableEvent) => { e.preventDefault(); e.stopPropagation(); - setState({ ...state, isActive: false }); + setState({ ...state, isActive: false, dragEnterClassNames: '' }); setDragging(undefined); if (onDrop && droppable) { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss index 4e13fd95d1961..62bc6d7ed7cc8 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss @@ -27,6 +27,14 @@ overflow: hidden; } +.lnsLayerPanel__dimension-isHidden { + opacity: 0; +} + +.lnsLayerPanel__dimension-isReplacing { + text-decoration: line-through; +} + .lnsLayerPanel__triggerLink { padding: $euiSizeS; width: 100%; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index b3ad03b71770c..85dbee6de524f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -12,6 +12,7 @@ import { createMockDatasource, DatasourceMock, } from '../../mocks'; +import { ChildDragDropProvider } from '../../../drag_drop'; import { EuiFormRow, EuiPopover } from '@elastic/eui'; import { mount } from 'enzyme'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; @@ -272,6 +273,7 @@ describe('LayerPanel', () => { expect(component.find(EuiPopover).prop('isOpen')).toBe(true); }); + it('should close the popover when the active visualization changes', () => { /** * The ID generation system for new dimensions has been messy before, so @@ -324,4 +326,151 @@ describe('LayerPanel', () => { expect(component.find(EuiPopover).prop('isOpen')).toBe(false); }); }); + + // This test is more like an integration test, since the layer panel owns all + // the coordination between drag and drop + describe('drag and drop behavior', () => { + it('should determine if the datasource supports dropping of a field onto empty dimension', () => { + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: [], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + }, + ], + }); + + mockDatasource.canHandleDrop.mockReturnValue(true); + + const draggingField = { field: { name: 'dragged' }, indexPatternId: 'a' }; + + const component = mountWithIntl( + + + + ); + + expect(mockDatasource.canHandleDrop).toHaveBeenCalledWith( + expect.objectContaining({ + dragDropContext: expect.objectContaining({ + dragging: draggingField, + }), + }) + ); + + component.find('DragDrop[data-test-subj="lnsGroup"]').first().simulate('drop'); + + expect(mockDatasource.onDrop).toHaveBeenCalledWith( + expect.objectContaining({ + dragDropContext: expect.objectContaining({ + dragging: draggingField, + }), + }) + ); + }); + + it('should allow drag to move between groups', () => { + (generateId as jest.Mock).mockReturnValue(`newid`); + + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: ['a'], + filterOperations: () => true, + supportsMoreColumns: false, + dataTestSubj: 'lnsGroupA', + }, + { + groupLabel: 'B', + groupId: 'b', + accessors: ['b'], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroupB', + }, + ], + }); + + mockDatasource.canHandleDrop.mockReturnValue(true); + + const draggingOperation = { layerId: 'first', columnId: 'a', groupId: 'a' }; + + const component = mountWithIntl( + + + + ); + + expect(mockDatasource.canHandleDrop).toHaveBeenCalledTimes(2); + expect(mockDatasource.canHandleDrop).toHaveBeenCalledWith( + expect.objectContaining({ + dragDropContext: expect.objectContaining({ + dragging: draggingOperation, + }), + }) + ); + + // Simulate drop on the pre-populated dimension + component.find('DragDrop[data-test-subj="lnsGroupB"]').at(0).simulate('drop'); + expect(mockDatasource.onDrop).toHaveBeenCalledWith( + expect.objectContaining({ + columnId: 'b', + dragDropContext: expect.objectContaining({ + dragging: draggingOperation, + }), + }) + ); + + // Simulate drop on the empty dimension + component.find('DragDrop[data-test-subj="lnsGroupB"]').at(1).simulate('drop'); + expect(mockDatasource.onDrop).toHaveBeenCalledWith( + expect.objectContaining({ + columnId: 'newid', + dragDropContext: expect.objectContaining({ + dragging: draggingOperation, + }), + }) + ); + }); + + it('should prevent dropping in the same group', () => { + mockVisualization.getConfiguration.mockReturnValue({ + groups: [ + { + groupLabel: 'A', + groupId: 'a', + accessors: ['a', 'b'], + filterOperations: () => true, + supportsMoreColumns: true, + dataTestSubj: 'lnsGroup', + }, + ], + }); + + const draggingOperation = { layerId: 'first', columnId: 'a', groupId: 'a' }; + + const component = mountWithIntl( + + + + ); + + expect(mockDatasource.canHandleDrop).not.toHaveBeenCalled(); + + component.find('DragDrop[data-test-subj="lnsGroup"]').at(0).simulate('drop'); + expect(mockDatasource.onDrop).not.toHaveBeenCalled(); + + component.find('DragDrop[data-test-subj="lnsGroup"]').at(1).simulate('drop'); + expect(mockDatasource.onDrop).not.toHaveBeenCalled(); + + component.find('DragDrop[data-test-subj="lnsGroup"]').at(2).simulate('drop'); + expect(mockDatasource.onDrop).not.toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index b2804cfddba58..b45dd13bfa4fd 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -17,8 +17,9 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import classNames from 'classnames'; import { NativeRenderer } from '../../../native_renderer'; -import { StateSetter } from '../../../types'; +import { StateSetter, isDraggedOperation } from '../../../types'; import { DragContext, DragDrop, ChildDragDropProvider } from '../../../drag_drop'; import { LayerSettings } from './layer_settings'; import { trackUiEvent } from '../../../lens_ui_telemetry'; @@ -154,6 +155,7 @@ export function LayerPanel( {groups.map((group, index) => { const newId = generateId(); const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; + return ( { + // If we are dragging another column, add an indication that the behavior will be a replacement' + if ( + isDraggedOperation(dragDropContext.dragging) && + group.groupId !== dragDropContext.dragging.groupId + ) { + return 'lnsLayerPanel__dimension-isReplacing'; + } + return ''; + }} data-test-subj={group.dataTestSubj} + draggable={true} + value={{ columnId: accessor, groupId: group.groupId, layerId }} + label={group.groupLabel} droppable={ - dragDropContext.dragging && + Boolean(dragDropContext.dragging) && + // Verify that the dragged item is not coming from the same group + // since this would be a reorder + (!isDraggedOperation(dragDropContext.dragging) || + dragDropContext.dragging.groupId !== group.groupId) && layerDatasource.canHandleDrop({ ...layerDatasourceDropProps, columnId: accessor, @@ -226,12 +250,22 @@ export function LayerPanel( }) } onDrop={(droppedItem) => { - layerDatasource.onDrop({ + const dropResult = layerDatasource.onDrop({ ...layerDatasourceDropProps, droppedItem, columnId: accessor, filterOperations: group.filterOperations, }); + if (typeof dropResult === 'object') { + // When a column is moved, we delete the reference to the old + props.updateVisualization( + activeVisualization.removeDimension({ + layerId, + columnId: dropResult.deleted, + prevState: props.visualizationState, + }) + ); + } }} > { - const dropSuccess = layerDatasource.onDrop({ + const dropResult = layerDatasource.onDrop({ ...layerDatasourceDropProps, droppedItem, columnId: newId, filterOperations: group.filterOperations, }); - if (dropSuccess) { + if (dropResult) { props.updateVisualization( activeVisualization.setDimension({ layerId, @@ -338,6 +376,17 @@ export function LayerPanel( prevState: props.visualizationState, }) ); + + if (typeof dropResult === 'object') { + // When a column is moved, we delete the reference to the old + props.updateVisualization( + activeVisualization.removeDimension({ + layerId, + columnId: dropResult.deleted, + prevState: props.visualizationState, + }) + ); + } } }} > diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 3ee109376d975..f184d5628ab1c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -1378,6 +1378,66 @@ describe('IndexPatternDimensionEditorPanel', () => { ).toBe(false); }); + it('is droppable if the dragged column is compatible', () => { + expect( + canHandleDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging: { + columnId: 'col1', + groupId: 'a', + layerId: 'myLayer', + }, + }, + state: dragDropState(), + columnId: 'col2', + filterOperations: (op: OperationMetadata) => true, + layerId: 'myLayer', + }) + ).toBe(true); + }); + + it('is not droppable if the dragged column is the same as the current column', () => { + expect( + canHandleDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging: { + columnId: 'col1', + groupId: 'a', + layerId: 'myLayer', + }, + }, + state: dragDropState(), + columnId: 'col1', + filterOperations: (op: OperationMetadata) => true, + layerId: 'myLayer', + }) + ).toBe(false); + }); + + it('is not droppable if the dragged column is incompatible', () => { + expect( + canHandleDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging: { + columnId: 'col1', + groupId: 'a', + layerId: 'myLayer', + }, + }, + state: dragDropState(), + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + layerId: 'myLayer', + }) + ).toBe(false); + }); + it('appends the dropped column when a field is dropped', () => { const dragging = { field: { type: 'number', name: 'bar', aggregatable: true }, @@ -1526,5 +1586,109 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }); }); + + it('updates the column id when moving an operation to an empty dimension', () => { + const dragging = { + columnId: 'col1', + groupId: 'a', + layerId: 'myLayer', + }; + const testState = dragDropState(); + + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => true, + layerId: 'myLayer', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + myLayer: { + ...testState.layers.myLayer, + columnOrder: ['col2'], + columns: { + col2: testState.layers.myLayer.columns.col1, + }, + }, + }, + }); + }); + + it('replaces an operation when moving to a populated dimension', () => { + const dragging = { + columnId: 'col2', + groupId: 'a', + layerId: 'myLayer', + }; + const testState = dragDropState(); + testState.layers.myLayer = { + indexPatternId: 'foo', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: testState.layers.myLayer.columns.col1, + + col2: { + label: 'Top values of src', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + params: { + orderBy: { type: 'column', columnId: 'col3' }, + orderDirection: 'desc', + size: 10, + }, + sourceField: 'src', + }, + col3: { + label: 'Count', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'count', + sourceField: 'Records', + }, + }, + }; + + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + columnId: 'col1', + filterOperations: (op: OperationMetadata) => true, + layerId: 'myLayer', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + myLayer: { + ...testState.layers.myLayer, + columnOrder: ['col1', 'col3'], + columns: { + col1: testState.layers.myLayer.columns.col2, + col3: testState.layers.myLayer.columns.col3, + }, + }, + }, + }); + }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index 1e8f73b19a3b0..1fbbefd8f1117 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -15,6 +15,7 @@ import { DatasourceDimensionEditorProps, DatasourceDimensionDropProps, DatasourceDimensionDropHandlerProps, + isDraggedOperation, } from '../../types'; import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; import { IndexPatternColumn, OperationType } from '../indexpattern'; @@ -99,16 +100,25 @@ export function canHandleDrop(props: DatasourceDimensionDropProps -): boolean { +export function onDrop(props: DatasourceDimensionDropHandlerProps) { const operationFieldSupportMatrix = getOperationFieldSupportMatrix(props); const droppedItem = props.droppedItem; @@ -116,6 +126,42 @@ export function onDrop( return Boolean(operationFieldSupportMatrix.operationByField[field.name]); } + if (isDraggedOperation(droppedItem) && droppedItem.layerId === props.layerId) { + const layer = props.state.layers[props.layerId]; + const op = { ...layer.columns[droppedItem.columnId] }; + if (!props.filterOperations(op)) { + return false; + } + + const newColumns = { ...layer.columns }; + delete newColumns[droppedItem.columnId]; + newColumns[props.columnId] = op; + + const newColumnOrder = [...layer.columnOrder]; + const oldIndex = newColumnOrder.findIndex((c) => c === droppedItem.columnId); + const newIndex = newColumnOrder.findIndex((c) => c === props.columnId); + + if (newIndex === -1) { + newColumnOrder[oldIndex] = props.columnId; + } else { + newColumnOrder.splice(oldIndex, 1); + } + + // Time to replace + props.setState({ + ...props.state, + layers: { + ...props.state.layers, + [props.layerId]: { + ...layer, + columnOrder: newColumnOrder, + columns: newColumns, + }, + }, + }); + return { deleted: droppedItem.columnId }; + } + if (!isDraggedField(droppedItem) || !hasOperationForField(droppedItem.field)) { // TODO: What do we do if we couldn't find a column? return false; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts index 0cd92fd96c952..374dbe77b4ca3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { DataType } from '../types'; import { DraggedField } from './indexpattern'; import { BaseIndexPatternColumn, FieldBasedIndexPatternColumn, } from './operations/definitions/column_types'; -import { DataType } from '../types'; /** * Normalizes the specified operation type. (e.g. document operations diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 729daed7223fe..d8b77afdfe004 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -157,7 +157,7 @@ export interface Datasource { renderDimensionEditor: (domElement: Element, props: DatasourceDimensionEditorProps) => void; renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => void; canHandleDrop: (props: DatasourceDimensionDropProps) => boolean; - onDrop: (props: DatasourceDimensionDropHandlerProps) => boolean; + onDrop: (props: DatasourceDimensionDropHandlerProps) => false | true | { deleted: string }; toExpression: (state: T, layerId: string) => Ast | string | null; @@ -230,6 +230,22 @@ export interface DatasourceLayerPanelProps { setState: StateSetter; } +export interface DraggedOperation { + layerId: string; + groupId: string; + columnId: string; +} + +export function isDraggedOperation( + operationCandidate: unknown +): operationCandidate is DraggedOperation { + return ( + typeof operationCandidate === 'object' && + operationCandidate !== null && + 'columnId' in operationCandidate + ); +} + export type DatasourceDimensionDropProps = SharedDimensionProps & { layerId: string; columnId: string; diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts index b02a82f98af91..0c31015fc9f5e 100644 --- a/x-pack/plugins/lists/public/exceptions/api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/api.test.ts @@ -375,7 +375,7 @@ describe('Exceptions Lists API', () => { namespace_type: 'single,single', page: '1', per_page: '20', - sort_field: 'created_at', + sort_field: 'exception-list.created_at', sort_order: 'desc', }, signal: abortCtrl.signal, @@ -408,7 +408,7 @@ describe('Exceptions Lists API', () => { namespace_type: 'single', page: '1', per_page: '20', - sort_field: 'created_at', + sort_field: 'exception-list.created_at', sort_order: 'desc', }, signal: abortCtrl.signal, @@ -441,7 +441,7 @@ describe('Exceptions Lists API', () => { namespace_type: 'agnostic', page: '1', per_page: '20', - sort_field: 'created_at', + sort_field: 'exception-list.created_at', sort_order: 'desc', }, signal: abortCtrl.signal, @@ -474,7 +474,7 @@ describe('Exceptions Lists API', () => { namespace_type: 'agnostic', page: '1', per_page: '20', - sort_field: 'created_at', + sort_field: 'exception-list.created_at', sort_order: 'desc', }, signal: abortCtrl.signal, @@ -508,7 +508,7 @@ describe('Exceptions Lists API', () => { namespace_type: 'agnostic', page: '1', per_page: '20', - sort_field: 'created_at', + sort_field: 'exception-list.created_at', sort_order: 'desc', }, signal: abortCtrl.signal, diff --git a/x-pack/plugins/lists/public/exceptions/api.ts b/x-pack/plugins/lists/public/exceptions/api.ts index 3f5ec80320503..824a25296260f 100644 --- a/x-pack/plugins/lists/public/exceptions/api.ts +++ b/x-pack/plugins/lists/public/exceptions/api.ts @@ -288,7 +288,7 @@ export const fetchExceptionListsItemsByListIds = async ({ namespace_type: namespaceTypes.join(','), page: pagination.page ? `${pagination.page}` : '1', per_page: pagination.perPage ? `${pagination.perPage}` : '20', - sort_field: 'created_at', + sort_field: 'exception-list.created_at', sort_order: 'desc', ...(filters.trim() !== '' ? { filter: filters } : {}), }; diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js index 5a7d2a9c3ddaa..fdeab0c49e32b 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js @@ -390,6 +390,7 @@ class LinksMenuUI extends Component { defaultMessage: 'Select action for anomaly at {time}', values: { time: formatHumanReadableDateTimeSeconds(anomaly.time) }, })} + data-test-subj="mlAnomaliesListRowActionsButton" /> ); @@ -404,6 +405,7 @@ class LinksMenuUI extends Component { this.closePopover(); this.openCustomUrl(customUrl); }} + data-test-subj={`mlAnomaliesListRowActionCustomUrlButton_${index}`} > {customUrl.url_name} @@ -420,6 +422,7 @@ class LinksMenuUI extends Component { this.closePopover(); this.viewSeries(); }} + data-test-subj="mlAnomaliesListRowActionViewSeriesButton" > - + ); } diff --git a/x-pack/plugins/ml/public/application/components/data_recognizer/recognized_result.js b/x-pack/plugins/ml/public/application/components/data_recognizer/recognized_result.js index a710ce18856f0..1f03dbe134756 100644 --- a/x-pack/plugins/ml/public/application/components/data_recognizer/recognized_result.js +++ b/x-pack/plugins/ml/public/application/components/data_recognizer/recognized_result.js @@ -29,7 +29,7 @@ export const RecognizedResult = ({ config, indexPattern, savedSearch }) => { return ( { return ( - + { setItemSelected(item, e.target.checked); }} + data-test-subj={`mlGridItemCheckbox`} /> ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 88287b963a028..6d73340cc396a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -218,7 +218,7 @@ export const DataFrameAnalyticsList: FC = ({ if (analytics.length === 0) { return ( - <> +
= ({ {isSourceIndexModalVisible === true && ( setIsSourceIndexModalVisible(false)} /> )} - +
); } @@ -251,7 +251,7 @@ export const DataFrameAnalyticsList: FC = ({ ); return ( - <> +
{modals} {!isManagementTable && } @@ -301,6 +301,6 @@ export const DataFrameAnalyticsList: FC = ({ {isSourceIndexModalVisible === true && ( setIsSourceIndexModalVisible(false)} /> )} - +
); }; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js index 74072aa7e9638..6b8d1d80aeda5 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js @@ -55,7 +55,7 @@ export function ResultLinks({ jobs }) { aria-label={openJobsInSingleMetricViewerText} className="results-button" isDisabled={singleMetricEnabled === false || jobActionsDisabled === true} - data-test-subj={`openJobsInSingleMetricViewer openJobsInSingleMetricViewer-${jobs[0].id}`} + data-test-subj="mlOpenJobsInSingleMetricViewerButton" /> )} @@ -66,7 +66,7 @@ export function ResultLinks({ jobs }) { aria-label={openJobsInAnomalyExplorerText} className="results-button" isDisabled={jobActionsDisabled === true} - data-test-subj={`openJobsInAnomalyExplorer openJobsInSingleAnomalyExplorer-${jobs[0].id}`} + data-test-subj="mlOpenJobsInAnomalyExplorerButton" />
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js index a011f21fddfef..44460c0fb8139 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js @@ -57,6 +57,7 @@ class MultiJobActionsMenuUI extends Component { disabled={ anyJobsDeleting || (this.canDeleteJob === false && this.canStartStopDatafeed === false) } + data-test-subj="mlADJobListMultiSelectManagementActionsButton" /> ); @@ -69,6 +70,7 @@ class MultiJobActionsMenuUI extends Component { this.props.showDeleteJobModal(this.props.jobs); this.closePopover(); }} + data-test-subj="mlADJobListMultiSelectDeleteJobActionButton" > this.togglePopover()} disabled={this.canUpdateJob === false} + data-test-subj="mlADJobListMultiSelectEditJobGroupsButton" /> ); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/multi_job_actions.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/multi_job_actions.js index 82563b083b8cc..b9ea18b5d2ed8 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/multi_job_actions.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/multi_job_actions.js @@ -24,7 +24,10 @@ export class MultiJobActions extends Component { render() { const jobsSelected = this.props.selectedJobs.length > 0; return ( -
+
{jobsSelected && ( diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/access_denied_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/access_denied_page.tsx index 1a30b1637fb8d..6a9c66eec83bc 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/access_denied_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/access_denied_page.tsx @@ -23,7 +23,7 @@ import { export const AccessDeniedPage = () => ( - + diff --git a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx index c4508a8c19c5b..395a570083c0d 100644 --- a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx @@ -48,7 +48,7 @@ export const ViewLink: FC = ({ item }) => { iconType="visTable" aria-label={viewJobResultsButtonText} className="results-button" - data-test-subj="mlAnalyticsJobViewButton" + data-test-subj="mlOverviewAnalyticsJobViewButton" isDisabled={disabled} > {i18n.translate('xpack.ml.overview.analytics.viewActionName', { diff --git a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx index 65e7ba9e8ab52..be8038cc5049d 100644 --- a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx @@ -100,6 +100,7 @@ export const AnalyticsPanel: FC = ({ jobCreationDisabled }) => { fill iconType="plusInCircle" isDisabled={jobCreationDisabled} + data-test-subj="mlOverviewCreateDFAJobButton" > {i18n.translate('xpack.ml.overview.analyticsList.createJobButtonText', { defaultMessage: 'Create job', diff --git a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx index 03b66f5c369c1..0bfd2c2e49232 100644 --- a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx @@ -184,6 +184,7 @@ export const AnomalyDetectionPanel: FC = ({ jobCreationDisabled }) => { fill iconType="plusInCircle" isDisabled={jobCreationDisabled} + data-test-subj="mlOverviewCreateADJobButton" > {i18n.translate('xpack.ml.overview.anomalyDetection.createJobButtonText', { defaultMessage: 'Create job', diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap b/x-pack/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap index 21f505cff9aec..34ee304b8bd41 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/__snapshots__/new_calendar.test.js.snap @@ -7,6 +7,7 @@ exports[`NewCalendar Renders new calendar form 1`] = ` /> {isGlobalCalendar === false && ( @@ -166,6 +167,7 @@ export const CalendarForm = ({ selectedOptions={selectedJobOptions} onChange={onJobSelection} isDisabled={saving === true || canCreateCalendar === false} + data-test-subj="mlCalendarJobSelection" /> @@ -183,6 +185,7 @@ export const CalendarForm = ({ selectedOptions={selectedGroupOptions} onChange={onGroupSelection} isDisabled={saving === true || canCreateCalendar === false} + data-test-subj="mlCalendarJobGroupSelection" /> diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap b/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap index df22f1a08ff42..e3edc563e7c3e 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/events_table/__snapshots__/events_table.test.js.snap @@ -34,6 +34,7 @@ exports[`EventsTable Renders events table with no search bar 1`] = ` }, ] } + data-test-subj="mlCalendarEventsTable" itemId="event_id" items={ Array [ @@ -56,6 +57,7 @@ exports[`EventsTable Renders events table with no search bar 1`] = ` } } responsive={true} + rowProps={[Function]} sorting={ Object { "sort": Object { @@ -103,6 +105,7 @@ exports[`EventsTable Renders events table with search bar 1`] = ` }, ] } + data-test-subj="mlCalendarEventsTable" itemId="event_id" items={ Array [ @@ -125,6 +128,7 @@ exports[`EventsTable Renders events table with search bar 1`] = ` } } responsive={true} + rowProps={[Function]} search={ Object { "box": Object { @@ -133,7 +137,7 @@ exports[`EventsTable Renders events table with search bar 1`] = ` "filters": Array [], "toolsRight": Array [ , ( { onDeleteClick(event.event_id); @@ -105,7 +106,7 @@ export const EventsTable = ({ ({ + 'data-test-subj': `mlCalendarEventListRow row-${item.description}`, + })} /> ); diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.test.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.test.js index 0c8e1f07eb355..2faac7d850fa9 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.test.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/import_modal/import_modal.test.js @@ -51,7 +51,7 @@ describe('ImportModal', () => { instance.setState(testState); wrapper.update(); expect(wrapper.state('selectedEvents').length).toBe(2); - const deleteButton = wrapper.find('[data-test-subj="mlEventDelete"]'); + const deleteButton = wrapper.find('[data-test-subj="mlCalendarEventDeleteButton"]'); const button = deleteButton.find('EuiButtonEmpty').first(); button.simulate('click'); diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js index 7efc37d4bf8ce..1fe16e4588bd7 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js @@ -339,7 +339,7 @@ class NewCalendarUI extends Component { return ( - + { test('Import modal shown on Import Events button click', () => { const wrapper = mountWithIntl(); - const importButton = wrapper.find('[data-test-subj="mlImportEvents"]'); + const importButton = wrapper.find('[data-test-subj="mlCalendarImportEventsButton"]'); const button = importButton.find('EuiButton'); button.simulate('click'); @@ -127,7 +127,7 @@ describe('NewCalendar', () => { test('New event modal shown on New event button click', () => { const wrapper = mountWithIntl(); - const importButton = wrapper.find('[data-test-subj="mlNewEvent"]'); + const importButton = wrapper.find('[data-test-subj="mlCalendarNewEventButton"]'); const button = importButton.find('EuiButton'); button.simulate('click'); diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/__snapshots__/calendars_list.test.js.snap b/x-pack/plugins/ml/public/application/settings/calendars/list/__snapshots__/calendars_list.test.js.snap index aeeeeef63a71e..0f7585e3a9fa6 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/__snapshots__/calendars_list.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/__snapshots__/calendars_list.test.js.snap @@ -7,6 +7,7 @@ exports[`CalendarsList Renders calendar list with calendars 1`] = ` /> - + - + +
, - +
`; diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js b/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js index b81cc6fbb4c30..77331c4a987dc 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js @@ -46,8 +46,14 @@ export const CalendarsListTable = ({ truncateText: true, scope: 'row', render: (id) => ( - {id} + + {id} + ), + 'data-test-subj': 'mlCalendarListColumnId', }, { field: 'job_ids_string', @@ -68,6 +74,7 @@ export const CalendarsListTable = ({ jobList ); }, + 'data-test-subj': 'mlCalendarListColumnJobs', }, { field: 'events_length', @@ -80,6 +87,7 @@ export const CalendarsListTable = ({ defaultMessage: '{eventsLength, plural, one {# event} other {# events}}', values: { eventsLength }, }), + 'data-test-subj': 'mlCalendarListColumnEvents', }, ]; @@ -106,6 +114,7 @@ export const CalendarsListTable = ({ isDisabled={ canDeleteCalendar === false || mlNodesAvailable === false || itemsSelected === false } + data-test-subj="mlCalendarButtonDelete" > +
({ + 'data-test-subj': `mlCalendarListRow row-${item.calendar_id}`, + })} /> - +
); }; diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap b/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap index 0ce19b8fa3d57..6e9cd17deabee 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap @@ -7,6 +7,7 @@ exports[`AddItemPopover calls addItems with multiple items on clicking Add butto button={ ); diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap index 447d79ffff32a..c2fab64473228 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap @@ -7,6 +7,7 @@ exports[`EditFilterList adds new items to filter list 1`] = ` /> , , - + - + , - , + , - ], + ] + } + />, + ], + } } - } - selection={ - Object { - "onSelectionChange": [Function], - "selectable": [Function], - "selectableMessage": [Function], + selection={ + Object { + "onSelectionChange": [Function], + "selectable": [Function], + "selectableMessage": [Function], + } } - } - sorting={ - Object { - "sort": Object { - "direction": "asc", - "field": "filter_id", - }, + sorting={ + Object { + "sort": Object { + "direction": "asc", + "field": "filter_id", + }, + } } - } - tableLayout="fixed" - /> + tableLayout="fixed" + /> +
`; exports[`Filter Lists Table renders with filter lists supplied 1`] = ` - + , - , - ], + "box": Object { + "incremental": true, + }, + "filters": Array [], + "toolsRight": Array [ + , + , + ], + } } - } - selection={ - Object { - "onSelectionChange": [Function], - "selectable": [Function], - "selectableMessage": [Function], + selection={ + Object { + "onSelectionChange": [Function], + "selectable": [Function], + "selectableMessage": [Function], + } } - } - sorting={ - Object { - "sort": Object { - "direction": "asc", - "field": "filter_id", - }, + sorting={ + Object { + "sort": Object { + "direction": "asc", + "field": "filter_id", + }, + } } - } - tableLayout="fixed" - /> + tableLayout="fixed" + /> +
`; diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.js b/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.js index 270d5fa350cae..75c72fdab7307 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.js @@ -89,7 +89,7 @@ export class FilterListsUI extends Component { return ( - + - refreshFilterLists()}> + refreshFilterLists()} + data-test-subj="mlFilterListRefreshButton" + > ); } else { @@ -47,6 +48,7 @@ function UsedByIcon({ usedBy }) { aria-label={i18n.translate('xpack.ml.settings.filterLists.table.notInUseAriaLabel', { defaultMessage: 'Not in use', })} + data-test-subj="mlFilterListUsedByIcon notInUse" /> ); } @@ -82,10 +84,16 @@ function getColumns() { defaultMessage: 'ID', }), render: (id) => ( - {id} + + {id} + ), sortable: true, scope: 'row', + 'data-test-subj': 'mlFilterListColumnId', }, { field: 'description', @@ -93,6 +101,7 @@ function getColumns() { defaultMessage: 'Description', }), sortable: true, + 'data-test-subj': 'mlFilterListColumnDescription', }, { field: 'item_count', @@ -100,6 +109,7 @@ function getColumns() { defaultMessage: 'Item count', }), sortable: true, + 'data-test-subj': 'mlFilterListColumnItemCount', }, { field: 'used_by', @@ -108,6 +118,7 @@ function getColumns() { }), render: (usedBy) => , sortable: true, + 'data-test-subj': 'mlFilterListColumnInUse', }, ]; @@ -189,7 +200,7 @@ export function FilterListsTable({ ) : ( - +
({ + 'data-test-subj': `mlCalendarListRow row-${item.filter_id}`, + })} /> - +
)}
); diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx index 93bb62fa1fc58..9d2c49a95fec4 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/entity_control/entity_control.tsx @@ -141,6 +141,7 @@ export class EntityControl extends Component ); diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/modal.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/modal.js index 6527aa3801da9..4fec8b01530f3 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/modal.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/modal.js @@ -31,7 +31,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; export function Modal(props) { return ( - + ({ encryptionKey, job, logger, }: { encryptionKey?: string; - job: ScheduledTaskParamsType; + job: TaskPayloadType; logger: LevelLogger; }): Promise> => { try { diff --git a/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts index 0372d515c21a8..754bc7bc75cb5 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts @@ -8,8 +8,8 @@ import sinon from 'sinon'; import { ReportingConfig } from '../../'; import { ReportingCore } from '../../core'; import { createMockReportingCore } from '../../test_helpers'; -import { ScheduledTaskParams } from '../../types'; -import { ScheduledTaskParamsPDF } from '../printable_pdf/types'; +import { BasePayload } from '../../types'; +import { TaskPayloadPDF } from '../printable_pdf/types'; import { getConditionalHeaders, getCustomLogo } from './'; let mockConfig: ReportingConfig; @@ -37,7 +37,7 @@ describe('conditions', () => { }; const conditionalHeaders = await getConditionalHeaders({ - job: {} as ScheduledTaskParams, + job: {} as BasePayload, filteredHeaders: permittedHeaders, config: mockConfig, }); @@ -64,14 +64,14 @@ test('uses basePath from job when creating saved object service', async () => { baz: 'quix', }; const conditionalHeaders = await getConditionalHeaders({ - job: {} as ScheduledTaskParams, + job: {} as BasePayload, filteredHeaders: permittedHeaders, config: mockConfig, }); const jobBasePath = '/sbp/s/marketing'; await getCustomLogo({ reporting: mockReportingPlugin, - job: { basePath: jobBasePath } as ScheduledTaskParamsPDF, + job: { basePath: jobBasePath } as TaskPayloadPDF, conditionalHeaders, config: mockConfig, }); @@ -94,14 +94,14 @@ test(`uses basePath from server if job doesn't have a basePath when creating sav baz: 'quix', }; const conditionalHeaders = await getConditionalHeaders({ - job: {} as ScheduledTaskParams, + job: {} as BasePayload, filteredHeaders: permittedHeaders, config: mockConfig, }); await getCustomLogo({ reporting: mockReportingPlugin, - job: {} as ScheduledTaskParamsPDF, + job: {} as TaskPayloadPDF, conditionalHeaders, config: mockConfig, }); @@ -139,7 +139,7 @@ describe('config formatting', () => { mockConfig = getMockConfig(mockConfigGet); const conditionalHeaders = await getConditionalHeaders({ - job: {} as ScheduledTaskParams, + job: {} as BasePayload, filteredHeaders: {}, config: mockConfig, }); diff --git a/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.ts b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.ts index 799d023486832..ce83323914eb8 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.ts @@ -7,13 +7,13 @@ import { ReportingConfig } from '../../'; import { ConditionalHeaders } from '../../types'; -export const getConditionalHeaders = ({ +export const getConditionalHeaders = ({ config, job, filteredHeaders, }: { config: ReportingConfig; - job: ScheduledTaskParamsType; + job: TaskPayloadType; filteredHeaders: Record; }) => { const { kbnConfig } = config; diff --git a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts index a3d65a1398a20..8c02fdd69de8b 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts @@ -6,7 +6,7 @@ import { ReportingCore } from '../../core'; import { createMockReportingCore } from '../../test_helpers'; -import { ScheduledTaskParamsPDF } from '../printable_pdf/types'; +import { TaskPayloadPDF } from '../printable_pdf/types'; import { getConditionalHeaders, getCustomLogo } from './'; const mockConfigGet = jest.fn().mockImplementation((key: string) => { @@ -37,7 +37,7 @@ test(`gets logo from uiSettings`, async () => { }); const conditionalHeaders = await getConditionalHeaders({ - job: {} as ScheduledTaskParamsPDF, + job: {} as TaskPayloadPDF, filteredHeaders: permittedHeaders, config: mockConfig, }); @@ -45,7 +45,7 @@ test(`gets logo from uiSettings`, async () => { const { logo } = await getCustomLogo({ reporting: mockReportingPlugin, config: mockConfig, - job: {} as ScheduledTaskParamsPDF, + job: {} as TaskPayloadPDF, conditionalHeaders, }); diff --git a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts index 547cc45258dae..ee61d76c8a933 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts @@ -7,7 +7,7 @@ import { ReportingConfig, ReportingCore } from '../../'; import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../common/constants'; import { ConditionalHeaders } from '../../types'; -import { ScheduledTaskParamsPDF } from '../printable_pdf/types'; // Logo is PDF only +import { TaskPayloadPDF } from '../printable_pdf/types'; // Logo is PDF only export const getCustomLogo = async ({ reporting, @@ -17,7 +17,7 @@ export const getCustomLogo = async ({ }: { reporting: ReportingCore; config: ReportingConfig; - job: ScheduledTaskParamsPDF; + job: TaskPayloadPDF; conditionalHeaders: ConditionalHeaders; }) => { const serverBasePath: string = config.kbnConfig.get('server', 'basePath'); diff --git a/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts index 73d7c7b03c128..355536000326e 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts @@ -5,12 +5,12 @@ */ import { ReportingConfig } from '../../'; -import { ScheduledTaskParamsPNG } from '../png/types'; -import { ScheduledTaskParamsPDF } from '../printable_pdf/types'; +import { TaskPayloadPNG } from '../png/types'; +import { TaskPayloadPDF } from '../printable_pdf/types'; import { getFullUrls } from './get_full_urls'; interface FullUrlsOpts { - job: ScheduledTaskParamsPNG & ScheduledTaskParamsPDF; + job: TaskPayloadPNG & TaskPayloadPDF; config: ReportingConfig; } @@ -35,7 +35,7 @@ beforeEach(() => { mockConfig = getMockConfig(mockConfigGet); }); -const getMockJob = (base: object) => base as ScheduledTaskParamsPNG & ScheduledTaskParamsPDF; +const getMockJob = (base: object) => base as TaskPayloadPNG & TaskPayloadPDF; test(`fails if no URL is passed`, async () => { const fn = () => getFullUrls({ job: getMockJob({}), config: mockConfig } as FullUrlsOpts); diff --git a/x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts index d3362fd190680..d6f472e18bc7b 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts @@ -11,28 +11,24 @@ import { UrlWithStringQuery, } from 'url'; import { ReportingConfig } from '../../'; -import { ScheduledTaskParamsPNG } from '../png/types'; -import { ScheduledTaskParamsPDF } from '../printable_pdf/types'; +import { TaskPayloadPNG } from '../png/types'; +import { TaskPayloadPDF } from '../printable_pdf/types'; import { getAbsoluteUrlFactory } from './get_absolute_url'; import { validateUrls } from './validate_urls'; -function isPngJob( - job: ScheduledTaskParamsPNG | ScheduledTaskParamsPDF -): job is ScheduledTaskParamsPNG { - return (job as ScheduledTaskParamsPNG).relativeUrl !== undefined; +function isPngJob(job: TaskPayloadPNG | TaskPayloadPDF): job is TaskPayloadPNG { + return (job as TaskPayloadPNG).relativeUrl !== undefined; } -function isPdfJob( - job: ScheduledTaskParamsPNG | ScheduledTaskParamsPDF -): job is ScheduledTaskParamsPDF { - return (job as ScheduledTaskParamsPDF).relativeUrls !== undefined; +function isPdfJob(job: TaskPayloadPNG | TaskPayloadPDF): job is TaskPayloadPDF { + return (job as TaskPayloadPDF).relativeUrls !== undefined; } -export function getFullUrls({ +export function getFullUrls({ config, job, }: { config: ReportingConfig; - job: ScheduledTaskParamsPDF | ScheduledTaskParamsPNG; + job: TaskPayloadPDF | TaskPayloadPNG; }) { const [basePath, protocol, hostname, port] = [ config.kbnConfig.get('server', 'basePath'), diff --git a/x-pack/plugins/reporting/server/export_types/common/omit_blacklisted_headers.ts b/x-pack/plugins/reporting/server/export_types/common/omit_blacklisted_headers.ts index e56ffc737764c..b2e0ce23aa3a5 100644 --- a/x-pack/plugins/reporting/server/export_types/common/omit_blacklisted_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/omit_blacklisted_headers.ts @@ -9,11 +9,11 @@ import { KBN_SCREENSHOT_HEADER_BLACKLIST_STARTS_WITH_PATTERN, } from '../../../common/constants'; -export const omitBlacklistedHeaders = ({ +export const omitBlacklistedHeaders = ({ job, decryptedHeaders, }: { - job: ScheduledTaskParamsType; + job: TaskPayloadType; decryptedHeaders: Record; }) => { const filteredHeaders: Record = omitBy( diff --git a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts index 252968e386b53..be18bd7fff361 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts @@ -5,16 +5,16 @@ */ import { cryptoFactory } from '../../lib'; -import { CreateJobFn, ScheduleTaskFnFactory } from '../../types'; +import { CreateJobFn, CreateJobFnFactory } from '../../types'; import { JobParamsDiscoverCsv } from './types'; -export const scheduleTaskFnFactory: ScheduleTaskFnFactory> = function createJobFactoryFn(reporting) { const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); - return async function scheduleTask(jobParams, context, request) { + return async function createJob(jobParams, context, request) { const serializedEncryptedHeaders = await crypto.encrypt(request.headers); const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts index 5eeef0f9906dd..15432d0cbd147 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts @@ -21,7 +21,7 @@ import { LevelLogger } from '../../lib'; import { setFieldFormats } from '../../services'; import { createMockReportingCore } from '../../test_helpers'; import { runTaskFnFactory } from './execute_job'; -import { ScheduledTaskParamsCSV } from './types'; +import { TaskPayloadCSV } from './types'; const delay = (ms: number) => new Promise((resolve) => setTimeout(() => resolve(), ms)); @@ -30,7 +30,7 @@ const getRandomScrollId = () => { return puid.generate(); }; -const getScheduledTaskParams = (baseObj: any) => baseObj as ScheduledTaskParamsCSV; +const getBasePayload = (baseObj: any) => baseObj as TaskPayloadCSV; describe('CSV Execute Job', function () { const encryptionKey = 'testEncryptionKey'; @@ -128,7 +128,7 @@ describe('CSV Execute Job', function () { const runTask = runTaskFnFactory(mockReportingCore, mockLogger); await runTask( 'job456', - getScheduledTaskParams({ + getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -146,7 +146,7 @@ describe('CSV Execute Job', function () { }; const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const job = getScheduledTaskParams({ + const job = getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { @@ -175,7 +175,7 @@ describe('CSV Execute Job', function () { const runTask = runTaskFnFactory(mockReportingCore, mockLogger); await runTask( 'job456', - getScheduledTaskParams({ + getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -193,7 +193,7 @@ describe('CSV Execute Job', function () { const runTask = runTaskFnFactory(mockReportingCore, mockLogger); await runTask( 'job456', - getScheduledTaskParams({ + getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -227,7 +227,7 @@ describe('CSV Execute Job', function () { const runTask = runTaskFnFactory(mockReportingCore, mockLogger); await runTask( 'job456', - getScheduledTaskParams({ + getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -266,7 +266,7 @@ describe('CSV Execute Job', function () { const runTask = runTaskFnFactory(mockReportingCore, mockLogger); await runTask( 'job456', - getScheduledTaskParams({ + getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -296,7 +296,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: undefined, @@ -323,7 +323,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -348,7 +348,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['=SUM(A1:A2)', 'two'], conflictedTypesFields: [], @@ -374,7 +374,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -400,7 +400,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['=SUM(A1:A2)', 'two'], conflictedTypesFields: [], @@ -426,7 +426,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -453,7 +453,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -474,7 +474,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -497,7 +497,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -518,7 +518,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -534,7 +534,7 @@ describe('CSV Execute Job', function () { it('should reject Promise if search call errors out', async function () { callAsCurrentUserStub.rejects(new Error()); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -553,7 +553,7 @@ describe('CSV Execute Job', function () { }); callAsCurrentUserStub.onSecondCall().rejects(new Error()); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -574,7 +574,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -593,7 +593,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -619,7 +619,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -645,7 +645,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -681,7 +681,7 @@ describe('CSV Execute Job', function () { const runTask = runTaskFnFactory(mockReportingCore, mockLogger); runTask( 'job345', - getScheduledTaskParams({ + getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -700,7 +700,7 @@ describe('CSV Execute Job', function () { const runTask = runTaskFnFactory(mockReportingCore, mockLogger); runTask( 'job345', - getScheduledTaskParams({ + getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -718,7 +718,7 @@ describe('CSV Execute Job', function () { const runTask = runTaskFnFactory(mockReportingCore, mockLogger); runTask( 'job345', - getScheduledTaskParams({ + getBasePayload({ headers: encryptedHeaders, fields: [], searchRequest: { index: null, body: null }, @@ -738,7 +738,7 @@ describe('CSV Execute Job', function () { describe('csv content', function () { it('should write column headers to output, even if there are no results', async function () { const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], searchRequest: { index: null, body: null }, @@ -750,7 +750,7 @@ describe('CSV Execute Job', function () { it('should use custom uiSettings csv:separator for header', async function () { mockUiSettingsClient.get.withArgs(CSV_SEPARATOR_SETTING).returns(';'); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], searchRequest: { index: null, body: null }, @@ -762,7 +762,7 @@ describe('CSV Execute Job', function () { it('should escape column headers if uiSettings csv:quoteValues is true', async function () { mockUiSettingsClient.get.withArgs(CSV_QUOTE_VALUES_SETTING).returns(true); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one and a half', 'two', 'three-and-four', 'five & six'], searchRequest: { index: null, body: null }, @@ -774,7 +774,7 @@ describe('CSV Execute Job', function () { it(`shouldn't escape column headers if uiSettings csv:quoteValues is false`, async function () { mockUiSettingsClient.get.withArgs(CSV_QUOTE_VALUES_SETTING).returns(false); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one and a half', 'two', 'three-and-four', 'five & six'], searchRequest: { index: null, body: null }, @@ -792,7 +792,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], searchRequest: { index: null, body: null }, @@ -813,7 +813,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -841,7 +841,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -864,7 +864,7 @@ describe('CSV Execute Job', function () { _scroll_id: 'scrollId', }); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -900,7 +900,7 @@ describe('CSV Execute Job', function () { configGetStub.withArgs('csv', 'maxSizeBytes').returns(1); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], searchRequest: { index: null, body: null }, @@ -930,7 +930,7 @@ describe('CSV Execute Job', function () { configGetStub.withArgs('csv', 'maxSizeBytes').returns(9); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], searchRequest: { index: null, body: null }, @@ -967,7 +967,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -1007,7 +1007,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -1044,7 +1044,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -1070,7 +1070,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], @@ -1096,7 +1096,7 @@ describe('CSV Execute Job', function () { }); const runTask = runTaskFnFactory(mockReportingCore, mockLogger); - const jobParams = getScheduledTaskParams({ + const jobParams = getBasePayload({ headers: encryptedHeaders, fields: ['one', 'two'], conflictedTypesFields: [], diff --git a/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts index b56a08b86b0cd..b308935a29e92 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/execute_job.ts @@ -10,9 +10,9 @@ import Hapi from 'hapi'; import { KibanaRequest } from '../../../../../../src/core/server'; import { CONTENT_TYPE_CSV, CSV_JOB_TYPE } from '../../../common/constants'; import { cryptoFactory, LevelLogger } from '../../lib'; -import { WorkerExecuteFn, RunTaskFnFactory } from '../../types'; -import { ScheduledTaskParamsCSV } from './types'; +import { RunTaskFn, RunTaskFnFactory } from '../../types'; import { createGenerateCsv } from './generate_csv'; +import { TaskPayloadCSV } from './types'; const getRequest = async (headers: string | undefined, crypto: Crypto, logger: LevelLogger) => { const decryptHeaders = async () => { @@ -55,8 +55,8 @@ const getRequest = async (headers: string | undefined, crypto: Crypto, logger: L } as Hapi.Request); }; -export const runTaskFnFactory: RunTaskFnFactory> = function executeJobFactoryFn(reporting, parentLogger) { const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); diff --git a/x-pack/plugins/reporting/server/export_types/csv/index.ts b/x-pack/plugins/reporting/server/export_types/csv/index.ts index 4bca42e0661e5..b54844cdf1742 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/index.ts @@ -13,22 +13,22 @@ import { LICENSE_TYPE_TRIAL, } from '../../../common/constants'; import { CSV_JOB_TYPE as jobType } from '../../../constants'; -import { CreateJobFn, WorkerExecuteFn, ExportTypeDefinition } from '../../types'; -import { scheduleTaskFnFactory } from './create_job'; +import { CreateJobFn, ExportTypeDefinition, RunTaskFn } from '../../types'; +import { createJobFnFactory } from './create_job'; import { runTaskFnFactory } from './execute_job'; import { metadata } from './metadata'; -import { JobParamsDiscoverCsv, ScheduledTaskParamsCSV } from './types'; +import { JobParamsDiscoverCsv, TaskPayloadCSV } from './types'; export const getExportType = (): ExportTypeDefinition< JobParamsDiscoverCsv, CreateJobFn, - ScheduledTaskParamsCSV, - WorkerExecuteFn + TaskPayloadCSV, + RunTaskFn > => ({ ...metadata, jobType, jobContentExtension: 'csv', - scheduleTaskFnFactory, + createJobFnFactory, runTaskFnFactory, validLicenses: [ LICENSE_TYPE_TRIAL, diff --git a/x-pack/plugins/reporting/server/export_types/csv/types.d.ts b/x-pack/plugins/reporting/server/export_types/csv/types.d.ts index e0d09d04a3d3a..f420d8b033170 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/types.d.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CreateJobBaseParams, ScheduledTaskParams } from '../../types'; +import { BaseParams, BasePayload } from '../../types'; export type RawValue = string | object | null | undefined; @@ -28,7 +28,7 @@ export interface IndexPatternSavedObject { }; } -export interface JobParamsDiscoverCsv extends CreateJobBaseParams { +export interface JobParamsDiscoverCsv extends BaseParams { indexPatternId: string; title: string; searchRequest: SearchRequest; @@ -37,7 +37,7 @@ export interface JobParamsDiscoverCsv extends CreateJobBaseParams { conflictedTypesFields: string[]; } -export interface ScheduledTaskParamsCSV extends ScheduledTaskParams { +export interface TaskPayloadCSV extends BasePayload { basePath: string; searchRequest: any; fields: any; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts index 9acfc6d8c608d..1746792981a21 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts @@ -9,7 +9,7 @@ import { get } from 'lodash'; import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; import { cryptoFactory } from '../../lib'; -import { ScheduleTaskFnFactory, TimeRangeParams } from '../../types'; +import { CreateJobFnFactory, TimeRangeParams } from '../../types'; import { JobParamsPanelCsv, SavedObject, @@ -37,7 +37,7 @@ interface VisData { panel: SearchPanel; } -export const scheduleTaskFnFactory: ScheduleTaskFnFactory = function createJobFactoryFn( +export const createJobFnFactory: CreateJobFnFactory = function createJobFactoryFn( reporting, parentLogger ) { @@ -45,7 +45,7 @@ export const scheduleTaskFnFactory: ScheduleTaskFnFactory const crypto = cryptoFactory(config.get('encryptionKey')); const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'create-job']); - return async function scheduleTask(jobParams, headers, context, req) { + return async function createJob(jobParams, headers, context, req) { const { savedObjectType, savedObjectId } = jobParams; const serializedEncryptedHeaders = await crypto.encrypt(headers); diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts index ec7e0a21f0498..3a5deda176b8c 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts @@ -7,16 +7,16 @@ import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { CancellationToken } from '../../../common'; import { CONTENT_TYPE_CSV, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; -import { RunTaskFnFactory, ScheduledTaskParams, TaskRunResult } from '../../types'; +import { BasePayload, RunTaskFnFactory, TaskRunResult } from '../../types'; import { createGenerateCsv } from '../csv/generate_csv'; -import { JobParamsPanelCsv, SearchPanel } from './types'; import { getGenerateCsvParams } from './lib/get_csv_job'; +import { JobParamsPanelCsv, SearchPanel } from './types'; /* * The run function receives the full request which provides the un-encrypted * headers, so encrypted headers are not part of these kind of job params */ -type ImmediateJobParams = Omit, 'headers'>; +type ImmediateJobParams = Omit, 'headers'>; /* * ImmediateExecuteFn receives the job doc payload because the payload was diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts index 7467f415299fa..4b4cfb3f062bf 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts @@ -14,16 +14,16 @@ import { } from '../../../common/constants'; import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../constants'; import { ExportTypeDefinition } from '../../types'; -import { metadata } from './metadata'; -import { ImmediateCreateJobFn, scheduleTaskFnFactory } from './create_job'; +import { createJobFnFactory, ImmediateCreateJobFn } from './create_job'; import { ImmediateExecuteFn, runTaskFnFactory } from './execute_job'; +import { metadata } from './metadata'; import { JobParamsPanelCsv } from './types'; /* * These functions are exported to share with the API route handler that * generates csv from saved object immediately on request. */ -export { scheduleTaskFnFactory } from './create_job'; +export { createJobFnFactory } from './create_job'; export { runTaskFnFactory } from './execute_job'; export const getExportType = (): ExportTypeDefinition< @@ -35,7 +35,7 @@ export const getExportType = (): ExportTypeDefinition< ...metadata, jobType: CSV_FROM_SAVEDOBJECT_JOB_TYPE, jobContentExtension: 'csv', - scheduleTaskFnFactory, + createJobFnFactory, runTaskFnFactory, validLicenses: [ LICENSE_TYPE_TRIAL, diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts index 0d19a24114f06..9c45d23b13a37 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { JobParamPostPayload, ScheduledTaskParams, TimeRangeParams } from '../../types'; +import { JobParamPostPayload, TimeRangeParams } from '../../types'; export interface FakeRequest { headers: Record; @@ -14,11 +14,20 @@ export interface JobParamsPostPayloadPanelCsv extends JobParamPostPayload { state?: any; } +export interface SearchPanel { + indexPatternSavedObjectId: string; + attributes: SavedSearchObjectAttributes; + timerange: TimeRangeParams; +} + +export interface JobPayloadPanelCsv extends JobParamsPanelCsv { + panel: SearchPanel; +} + export interface JobParamsPanelCsv { savedObjectType: string; savedObjectId: string; isImmediate: boolean; - panel?: SearchPanel; post?: JobParamsPostPayloadPanelCsv; visType?: string; } @@ -102,12 +111,6 @@ export interface VisPanel { timerange: TimeRangeParams; } -export interface SearchPanel { - indexPatternSavedObjectId: string; - attributes: SavedSearchObjectAttributes; - timerange: TimeRangeParams; -} - export interface DocValueFields { field: string; format: string; diff --git a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts index 2252177e98085..173a67ad18edf 100644 --- a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts @@ -5,17 +5,17 @@ */ import { cryptoFactory } from '../../../lib'; -import { CreateJobFn, ScheduleTaskFnFactory } from '../../../types'; +import { CreateJobFn, CreateJobFnFactory } from '../../../types'; import { validateUrls } from '../../common'; import { JobParamsPNG } from '../types'; -export const scheduleTaskFnFactory: ScheduleTaskFnFactory> = function createJobFactoryFn(reporting) { const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); - return async function scheduleTask( + return async function createJob( { objectType, title, relativeUrl, browserTimezone, layout }, context, req diff --git a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts index a0bd2d392187a..8a3f514ec7aea 100644 --- a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts +++ b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts @@ -9,8 +9,8 @@ import { ReportingCore } from '../../../'; import { CancellationToken } from '../../../../common'; import { cryptoFactory, LevelLogger } from '../../../lib'; import { createMockReportingCore } from '../../../test_helpers'; -import { ScheduledTaskParamsPNG } from '../types'; import { generatePngObservableFactory } from '../lib/generate_png'; +import { TaskPayloadPNG } from '../types'; import { runTaskFnFactory } from './'; jest.mock('../lib/generate_png', () => ({ generatePngObservableFactory: jest.fn() })); @@ -36,7 +36,7 @@ const encryptHeaders = async (headers: Record) => { return await crypto.encrypt(headers); }; -const getScheduledTaskParams = (baseObj: any) => baseObj as ScheduledTaskParamsPNG; +const getBasePayload = (baseObj: any) => baseObj as TaskPayloadPNG; beforeEach(async () => { const kbnConfig = { @@ -85,7 +85,7 @@ test(`passes browserTimezone to generatePng`, async () => { const browserTimezone = 'UTC'; await runTask( 'pngJobId', - getScheduledTaskParams({ + getBasePayload({ relativeUrl: '/app/kibana#/something', browserTimezone, headers: encryptedHeaders, @@ -133,7 +133,7 @@ test(`returns content_type of application/png`, async () => { const { content_type: contentType } = await runTask( 'pngJobId', - getScheduledTaskParams({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }), + getBasePayload({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }), cancellationToken ); expect(contentType).toBe('image/png'); @@ -148,7 +148,7 @@ test(`returns content of generatePng getBuffer base64 encoded`, async () => { const encryptedHeaders = await encryptHeaders({}); const { content } = await runTask( 'pngJobId', - getScheduledTaskParams({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }), + getBasePayload({ relativeUrl: '/app/kibana#/something', headers: encryptedHeaders }), cancellationToken ); diff --git a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts index 35cd4139df413..96b896b938962 100644 --- a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts @@ -8,7 +8,7 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; import { PNG_JOB_TYPE } from '../../../../common/constants'; -import { WorkerExecuteFn, RunTaskFnFactory, TaskRunResult } from '../../..//types'; +import { RunTaskFn, RunTaskFnFactory, TaskRunResult } from '../../..//types'; import { decryptJobHeaders, getConditionalHeaders, @@ -16,9 +16,9 @@ import { omitBlacklistedHeaders, } from '../../common'; import { generatePngObservableFactory } from '../lib/generate_png'; -import { ScheduledTaskParamsPNG } from '../types'; +import { TaskPayloadPNG } from '../types'; -type QueuedPngExecutorFactory = RunTaskFnFactory>; +type QueuedPngExecutorFactory = RunTaskFnFactory>; export const runTaskFnFactory: QueuedPngExecutorFactory = function executeJobFactoryFn( reporting, diff --git a/x-pack/plugins/reporting/server/export_types/png/index.ts b/x-pack/plugins/reporting/server/export_types/png/index.ts index c966dedb6b076..1cc6836572b7b 100644 --- a/x-pack/plugins/reporting/server/export_types/png/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/index.ts @@ -12,23 +12,23 @@ import { LICENSE_TYPE_TRIAL, PNG_JOB_TYPE as jobType, } from '../../../common/constants'; -import { CreateJobFn, WorkerExecuteFn, ExportTypeDefinition } from '../../types'; -import { scheduleTaskFnFactory } from './create_job'; +import { CreateJobFn, ExportTypeDefinition, RunTaskFn } from '../../types'; +import { createJobFnFactory } from './create_job'; import { runTaskFnFactory } from './execute_job'; import { metadata } from './metadata'; -import { JobParamsPNG, ScheduledTaskParamsPNG } from './types'; +import { JobParamsPNG, TaskPayloadPNG } from './types'; export const getExportType = (): ExportTypeDefinition< JobParamsPNG, CreateJobFn, - ScheduledTaskParamsPNG, - WorkerExecuteFn + TaskPayloadPNG, + RunTaskFn > => ({ ...metadata, jobType, jobContentEncoding: 'base64', jobContentExtension: 'PNG', - scheduleTaskFnFactory, + createJobFnFactory, runTaskFnFactory, validLicenses: [ LICENSE_TYPE_TRIAL, diff --git a/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts b/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts index 5969b5b8abc00..c3d5b2cc60051 100644 --- a/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts +++ b/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts @@ -10,7 +10,8 @@ import { map } from 'rxjs/operators'; import { ReportingCore } from '../../../'; import { LevelLogger } from '../../../lib'; import { LayoutParams, PreserveLayout } from '../../../lib/layouts'; -import { ConditionalHeaders, ScreenshotResults } from '../../../types'; +import { ScreenshotResults } from '../../../lib/screenshots'; +import { ConditionalHeaders } from '../../../types'; export async function generatePngObservableFactory(reporting: ReportingCore) { const getScreenshots = await reporting.getScreenshotsObservable(); diff --git a/x-pack/plugins/reporting/server/export_types/png/types.d.ts b/x-pack/plugins/reporting/server/export_types/png/types.d.ts index 1ddee8419df30..a747f53861a99 100644 --- a/x-pack/plugins/reporting/server/export_types/png/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/png/types.d.ts @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CreateJobBaseParams, ScheduledTaskParams } from '../../../server/types'; -import { LayoutInstance, LayoutParams } from '../../lib/layouts'; +import { BaseParams, BasePayload } from '../../../server/types'; +import { LayoutParams } from '../../lib/layouts'; // Job params: structure of incoming user request data -export interface JobParamsPNG extends CreateJobBaseParams { +export interface JobParamsPNG extends BaseParams { title: string; relativeUrl: string; } // Job payload: structure of stored job data provided by create_job -export interface ScheduledTaskParamsPNG extends ScheduledTaskParams { +export interface TaskPayloadPNG extends BasePayload { basePath?: string; browserTimezone: string; forceNow?: string; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts index 5de089a13bfa4..96e634337e6a9 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts @@ -5,17 +5,17 @@ */ import { cryptoFactory } from '../../../lib'; -import { CreateJobFn, ScheduleTaskFnFactory } from '../../../types'; +import { CreateJobFn, CreateJobFnFactory } from '../../../types'; import { validateUrls } from '../../common'; import { JobParamsPDF } from '../types'; -export const scheduleTaskFnFactory: ScheduleTaskFnFactory> = function createJobFactoryFn(reporting) { const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); - return async function scheduleTaskFn( + return async function createJob( { title, relativeUrls, browserTimezone, layout, objectType }, context, req diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts index 443db1b57fe48..fdc51dc1c9c87 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts @@ -12,7 +12,7 @@ import { CancellationToken } from '../../../../common'; import { cryptoFactory, LevelLogger } from '../../../lib'; import { createMockReportingCore } from '../../../test_helpers'; import { generatePdfObservableFactory } from '../lib/generate_pdf'; -import { ScheduledTaskParamsPDF } from '../types'; +import { TaskPayloadPDF } from '../types'; import { runTaskFnFactory } from './'; let mockReporting: ReportingCore; @@ -36,7 +36,7 @@ const encryptHeaders = async (headers: Record) => { return await crypto.encrypt(headers); }; -const getScheduledTaskParams = (baseObj: any) => baseObj as ScheduledTaskParamsPDF; +const getBasePayload = (baseObj: any) => baseObj as TaskPayloadPDF; beforeEach(async () => { const kbnConfig = { @@ -83,7 +83,7 @@ test(`passes browserTimezone to generatePdf`, async () => { const browserTimezone = 'UTC'; await runTask( 'pdfJobId', - getScheduledTaskParams({ + getBasePayload({ title: 'PDF Params Timezone Test', relativeUrl: '/app/kibana#/something', browserTimezone, @@ -106,7 +106,7 @@ test(`returns content_type of application/pdf`, async () => { const { content_type: contentType } = await runTask( 'pdfJobId', - getScheduledTaskParams({ relativeUrls: [], headers: encryptedHeaders }), + getBasePayload({ relativeUrls: [], headers: encryptedHeaders }), cancellationToken ); expect(contentType).toBe('application/pdf'); @@ -121,7 +121,7 @@ test(`returns content of generatePdf getBuffer base64 encoded`, async () => { const encryptedHeaders = await encryptHeaders({}); const { content } = await runTask( 'pdfJobId', - getScheduledTaskParams({ relativeUrls: [], headers: encryptedHeaders }), + getBasePayload({ relativeUrls: [], headers: encryptedHeaders }), cancellationToken ); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts index 5ace1c987adb5..78808400c9c9a 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts @@ -8,7 +8,7 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; import { PDF_JOB_TYPE } from '../../../../common/constants'; -import { WorkerExecuteFn, RunTaskFnFactory, TaskRunResult } from '../../../types'; +import { RunTaskFn, RunTaskFnFactory, TaskRunResult } from '../../../types'; import { decryptJobHeaders, getConditionalHeaders, @@ -17,9 +17,9 @@ import { omitBlacklistedHeaders, } from '../../common'; import { generatePdfObservableFactory } from '../lib/generate_pdf'; -import { ScheduledTaskParamsPDF } from '../types'; +import { TaskPayloadPDF } from '../types'; -type QueuedPdfExecutorFactory = RunTaskFnFactory>; +type QueuedPdfExecutorFactory = RunTaskFnFactory>; export const runTaskFnFactory: QueuedPdfExecutorFactory = function executeJobFactoryFn( reporting, diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts index 7f21d36c4b72c..cf3ec9cdc8c2d 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/index.ts @@ -12,23 +12,23 @@ import { LICENSE_TYPE_TRIAL, PDF_JOB_TYPE as jobType, } from '../../../common/constants'; -import { CreateJobFn, WorkerExecuteFn, ExportTypeDefinition } from '../../types'; -import { scheduleTaskFnFactory } from './create_job'; +import { CreateJobFn, ExportTypeDefinition, RunTaskFn } from '../../types'; +import { createJobFnFactory } from './create_job'; import { runTaskFnFactory } from './execute_job'; import { metadata } from './metadata'; -import { JobParamsPDF, ScheduledTaskParamsPDF } from './types'; +import { JobParamsPDF, TaskPayloadPDF } from './types'; export const getExportType = (): ExportTypeDefinition< JobParamsPDF, CreateJobFn, - ScheduledTaskParamsPDF, - WorkerExecuteFn + TaskPayloadPDF, + RunTaskFn > => ({ ...metadata, jobType, jobContentEncoding: 'base64', jobContentExtension: 'pdf', - scheduleTaskFnFactory, + createJobFnFactory, runTaskFnFactory, validLicenses: [ LICENSE_TYPE_TRIAL, diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts index 2e0292e1f9ab5..17624c1bedb57 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts @@ -10,7 +10,8 @@ import { mergeMap } from 'rxjs/operators'; import { ReportingCore } from '../../../'; import { LevelLogger } from '../../../lib'; import { createLayout, LayoutInstance, LayoutParams } from '../../../lib/layouts'; -import { ConditionalHeaders, ScreenshotResults } from '../../../types'; +import { ScreenshotResults } from '../../../lib/screenshots'; +import { ConditionalHeaders } from '../../../types'; // @ts-ignore untyped module import { pdf } from './pdf'; import { getTracker } from './tracker'; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts index 7830f87780c2e..3020cbb5f28b0 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts @@ -4,18 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CreateJobBaseParams, ScheduledTaskParams } from '../../../server/types'; +import { BaseParams, BasePayload } from '../../../server/types'; import { LayoutInstance, LayoutParams } from '../../lib/layouts'; // Job params: structure of incoming user request data, after being parsed from RISON -export interface JobParamsPDF extends CreateJobBaseParams { +export interface JobParamsPDF extends BaseParams { title: string; relativeUrls: string[]; layout: LayoutInstance; } // Job payload: structure of stored job data provided by create_job -export interface ScheduledTaskParamsPDF extends ScheduledTaskParams { +export interface TaskPayloadPDF extends BasePayload { basePath?: string; browserTimezone: string; forceNow?: string; diff --git a/x-pack/plugins/reporting/server/lib/create_worker.ts b/x-pack/plugins/reporting/server/lib/create_worker.ts index 5b0f1ddb2f157..dd5c560455274 100644 --- a/x-pack/plugins/reporting/server/lib/create_worker.ts +++ b/x-pack/plugins/reporting/server/lib/create_worker.ts @@ -8,7 +8,7 @@ import { CancellationToken } from '../../common'; import { PLUGIN_ID } from '../../common/constants'; import { ReportingCore } from '../../server'; import { LevelLogger } from '../../server/lib'; -import { ExportTypeDefinition, JobSource, WorkerExecuteFn } from '../../server/types'; +import { ExportTypeDefinition, JobSource, RunTaskFn } from '../../server/types'; import { ESQueueInstance } from './create_queue'; // @ts-ignore untyped dependency import { events as esqueueEvents } from './esqueue'; @@ -22,18 +22,18 @@ export function createWorkerFactory(reporting: ReportingCore, log // Once more document types are added, this will need to be passed in return async function createWorker(queue: ESQueueInstance) { // export type / execute job map - const jobExecutors: Map> = new Map(); + const jobExecutors: Map> = new Map(); for (const exportType of reporting.getExportTypesRegistry().getAll() as Array< - ExportTypeDefinition> + ExportTypeDefinition> >) { const jobExecutor = exportType.runTaskFnFactory(reporting, logger); jobExecutors.set(exportType.jobType, jobExecutor); } - const workerFn = ( - jobSource: JobSource, - jobParams: ScheduledTaskParamsType, + const workerFn = ( + jobSource: JobSource, + jobParams: TaskPayloadType, cancellationToken: CancellationToken ) => { const { diff --git a/x-pack/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/plugins/reporting/server/lib/enqueue_job.ts index 3e3aef92c55a3..5acc6e38dddf9 100644 --- a/x-pack/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/plugins/reporting/server/lib/enqueue_job.ts @@ -6,13 +6,13 @@ import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { ReportingCore } from '../'; -import { CreateJobBaseParams, CreateJobFn, ReportingUser } from '../types'; +import { BaseParams, CreateJobFn, ReportingUser } from '../types'; import { LevelLogger } from './'; import { Report } from './store'; export type EnqueueJobFn = ( exportTypeId: string, - jobParams: CreateJobBaseParams, + jobParams: BaseParams, user: ReportingUser, context: RequestHandlerContext, request: KibanaRequest @@ -26,12 +26,12 @@ export function enqueueJobFactory( return async function enqueueJob( exportTypeId: string, - jobParams: CreateJobBaseParams, + jobParams: BaseParams, user: ReportingUser, context: RequestHandlerContext, request: KibanaRequest ) { - type ScheduleTaskFnType = CreateJobFn; + type CreateJobFnType = CreateJobFn; const exportType = reporting.getExportTypesRegistry().getById(exportTypeId); @@ -39,13 +39,13 @@ export function enqueueJobFactory( throw new Error(`Export type ${exportTypeId} does not exist in the registry!`); } - const [scheduleTask, { store }] = await Promise.all([ - exportType.scheduleTaskFnFactory(reporting, logger) as ScheduleTaskFnType, + const [createJob, { store }] = await Promise.all([ + exportType.createJobFnFactory(reporting, logger) as CreateJobFnType, reporting.getPluginStartDeps(), ]); // add encrytped headers - const payload = await scheduleTask(jobParams, context, request); + const payload = await createJob(jobParams, context, request); // store the pending report, puts it in the Reporting Management UI table const report = await store.addReport(exportType.jobType, user, payload); diff --git a/x-pack/plugins/reporting/server/lib/export_types_registry.ts b/x-pack/plugins/reporting/server/lib/export_types_registry.ts index 501989f21103e..1159221a9224e 100644 --- a/x-pack/plugins/reporting/server/lib/export_types_registry.ts +++ b/x-pack/plugins/reporting/server/lib/export_types_registry.ts @@ -11,8 +11,8 @@ import { getExportType as getTypePng } from '../export_types/png'; import { getExportType as getTypePrintablePdf } from '../export_types/printable_pdf'; import { ExportTypeDefinition } from '../types'; -type GetCallbackFn = ( - item: ExportTypeDefinition +type GetCallbackFn = ( + item: ExportTypeDefinition ) => boolean; // => ExportTypeDefinition @@ -21,8 +21,8 @@ export class ExportTypesRegistry { constructor() {} - register( - item: ExportTypeDefinition + register( + item: ExportTypeDefinition ): void { if (!isString(item.id)) { throw new Error(`'item' must have a String 'id' property `); @@ -43,24 +43,24 @@ export class ExportTypesRegistry { return this._map.size; } - getById( + getById( id: string - ): ExportTypeDefinition { + ): ExportTypeDefinition { if (!this._map.has(id)) { throw new Error(`Unknown id ${id}`); } return this._map.get(id) as ExportTypeDefinition< JobParamsType, - ScheduleTaskFnType, + CreateJobFnType, JobPayloadType, RunTaskFnType >; } - get( - findType: GetCallbackFn - ): ExportTypeDefinition { + get( + findType: GetCallbackFn + ): ExportTypeDefinition { let result; for (const value of this._map.values()) { if (!findType(value)) { @@ -68,7 +68,7 @@ export class ExportTypesRegistry { } const foundResult: ExportTypeDefinition< JobParamsType, - ScheduleTaskFnType, + CreateJobFnType, JobPayloadType, RunTaskFnType > = value; diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.ts index 4fb9fd96ecfe6..6c619a726f428 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.ts @@ -5,10 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import { HeadlessChromiumDriver } from '../../browsers'; -import { AttributesMap, ElementsPositionAndAttribute } from '../../types'; import { LevelLogger, startTrace } from '../'; +import { HeadlessChromiumDriver } from '../../browsers'; import { LayoutInstance } from '../layouts'; +import { AttributesMap, ElementsPositionAndAttribute } from './'; import { CONTEXT_ELEMENTATTRIBUTES } from './constants'; export const getElementPositionAndAttributes = async ( diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts index bc7b7005674a7..1ed8687bea23e 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts @@ -5,9 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import { HeadlessChromiumDriver } from '../../browsers'; -import { ElementsPositionAndAttribute, Screenshot } from '../../types'; import { LevelLogger, startTrace } from '../'; +import { HeadlessChromiumDriver } from '../../browsers'; +import { ElementsPositionAndAttribute, Screenshot } from './'; export const getScreenshots = async ( browser: HeadlessChromiumDriver, diff --git a/x-pack/plugins/reporting/server/lib/screenshots/index.ts b/x-pack/plugins/reporting/server/lib/screenshots/index.ts index 5a04f1a497abf..c1d33cb519384 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/index.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/index.ts @@ -4,4 +4,61 @@ * you may not use this file except in compliance with the Elastic License. */ +import * as Rx from 'rxjs'; +import { LevelLogger } from '../'; +import { ConditionalHeaders } from '../../types'; +import { LayoutInstance } from '../layouts'; + export { screenshotsObservableFactory } from './observable'; + +export interface ScreenshotObservableOpts { + logger: LevelLogger; + urls: string[]; + conditionalHeaders: ConditionalHeaders; + layout: LayoutInstance; + browserTimezone: string; +} + +export interface AttributesMap { + [key: string]: any; +} + +export interface ElementPosition { + boundingClientRect: { + // modern browsers support x/y, but older ones don't + top: number; + left: number; + width: number; + height: number; + }; + scroll: { + x: number; + y: number; + }; +} + +export interface ElementsPositionAndAttribute { + position: ElementPosition; + attributes: AttributesMap; +} + +export interface Screenshot { + base64EncodedData: string; + title: string; + description: string; +} + +export interface ScreenshotResults { + timeRange: string | null; + screenshots: Screenshot[]; + error?: Error; + elementsPositionAndAttributes?: ElementsPositionAndAttribute[]; // NOTE: for testing +} + +export type ScreenshotsObservableFn = ({ + logger, + urls, + conditionalHeaders, + layout, + browserTimezone, +}: ScreenshotObservableOpts) => Rx.Observable; diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts index b25e8fab3abcf..3749e4372bdab 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts @@ -16,11 +16,12 @@ jest.mock('../../browsers/chromium/puppeteer', () => ({ })); import * as Rx from 'rxjs'; +import { LevelLogger } from '../'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { HeadlessChromiumDriver } from '../../browsers'; -import { LevelLogger } from '../'; import { createMockBrowserDriverFactory, createMockLayoutInstance } from '../../test_helpers'; -import { CaptureConfig, ConditionalHeaders, ElementsPositionAndAttribute } from '../../types'; +import { CaptureConfig, ConditionalHeaders } from '../../types'; +import { ElementsPositionAndAttribute } from './'; import * as contexts from './constants'; import { screenshotsObservableFactory } from './observable'; diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts index c6d3d826c88fb..342293d113d24 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts @@ -17,13 +17,13 @@ import { toArray, } from 'rxjs/operators'; import { HeadlessChromiumDriverFactory } from '../../browsers'; +import { CaptureConfig } from '../../types'; import { - CaptureConfig, ElementsPositionAndAttribute, ScreenshotObservableOpts, ScreenshotResults, ScreenshotsObservableFn, -} from '../../types'; +} from './'; import { checkPageIsOpen } from './check_browser_open'; import { DEFAULT_PAGELOAD_SELECTOR } from './constants'; import { getElementPositionAndAttributes } from './get_element_position_data'; diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts index 88f6fa418a1b3..b1309cbdeb94d 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.ts @@ -7,11 +7,7 @@ import { ElasticsearchServiceSetup } from 'src/core/server'; import { LevelLogger, statuses } from '../'; import { ReportingCore } from '../../'; -import { - CreateJobBaseParams, - CreateJobBaseParamsEncryptedFields, - ReportingUser, -} from '../../types'; +import { BaseParams, BaseParamsEncryptedFields, ReportingUser } from '../../types'; import { indexTimestamp } from './index_timestamp'; import { mapping } from './mapping'; import { Report } from './report'; @@ -145,7 +141,7 @@ export class ReportingStore { public async addReport( type: string, user: ReportingUser, - payload: CreateJobBaseParams & CreateJobBaseParamsEncryptedFields + payload: BaseParams & BaseParamsEncryptedFields ): Promise { const timestamp = indexTimestamp(this.indexInterval); const index = `${this.indexPrefix}-${timestamp}`; diff --git a/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts b/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts index f4959b56dfea1..36f0a7fe67d93 100644 --- a/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts +++ b/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import rison from 'rison-node'; import { schema } from '@kbn/config-schema'; -import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; -import { HandlerErrorFunction, HandlerFunction } from './types'; +import rison from 'rison-node'; import { ReportingCore } from '../'; import { API_BASE_URL } from '../../common/constants'; -import { CreateJobBaseParams } from '../types'; +import { BaseParams } from '../types'; +import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; +import { HandlerErrorFunction, HandlerFunction } from './types'; const BASE_GENERATE = `${API_BASE_URL}/generate`; @@ -70,7 +70,7 @@ export function registerGenerateFromJobParams( let jobParams; try { - jobParams = rison.decode(jobParamsRison) as CreateJobBaseParams | null; + jobParams = rison.decode(jobParamsRison) as BaseParams | null; if (!jobParams) { return res.customError({ statusCode: 400, diff --git a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts index a0a8f25de7fc4..517f1dadc0ac1 100644 --- a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { KibanaRequest } from 'src/core/server'; import { ReportingCore } from '../'; import { API_BASE_GENERATE_V1 } from '../../common/constants'; -import { scheduleTaskFnFactory } from '../export_types/csv_from_savedobject/create_job'; +import { createJobFnFactory } from '../export_types/csv_from_savedobject/create_job'; import { runTaskFnFactory } from '../export_types/csv_from_savedobject/execute_job'; import { JobParamsPostPayloadPanelCsv } from '../export_types/csv_from_savedobject/types'; import { LevelLogger as Logger } from '../lib'; @@ -43,7 +43,7 @@ export function registerGenerateCsvFromSavedObjectImmediate( /* * CSV export with the `immediate` option does not queue a job with Reporting's ESQueue to run the job async. Instead, this does: - * - re-use the scheduleTask function to build up es query config + * - re-use the createJob function to build up es query config * - re-use the runTask function to run the scan and scroll queries and capture the entire CSV in a result object. */ router.post( @@ -67,12 +67,12 @@ export function registerGenerateCsvFromSavedObjectImmediate( userHandler(async (user, context, req: CsvFromSavedObjectRequest, res) => { const logger = parentLogger.clone(['savedobject-csv']); const jobParams = getJobParamsFromRequest(req, { isImmediate: true }); - const scheduleTaskFn = scheduleTaskFnFactory(reporting, logger); + const createJob = createJobFnFactory(reporting, logger); const runTaskFn = runTaskFnFactory(reporting, logger); try { - // FIXME: no scheduleTaskFn for immediate download - const jobDocPayload = await scheduleTaskFn(jobParams, req.headers, context, req); + // FIXME: no create job for immediate download + const jobDocPayload = await createJob(jobParams, req.headers, context, req); const { content_type: jobOutputContentType, content: jobOutputContent, diff --git a/x-pack/plugins/reporting/server/routes/generation.test.ts b/x-pack/plugins/reporting/server/routes/generation.test.ts index cef4da9aabbd4..0db0073149e57 100644 --- a/x-pack/plugins/reporting/server/routes/generation.test.ts +++ b/x-pack/plugins/reporting/server/routes/generation.test.ts @@ -75,7 +75,7 @@ describe('POST /api/reporting/generate', () => { jobContentEncoding: 'base64', jobContentExtension: 'pdf', validLicenses: ['basic', 'gold'], - scheduleTaskFnFactory: () => () => ({ scheduleParamsTest: { test1: 'yes' } }), + createJobFnFactory: () => () => ({ jobParamsTest: { test1: 'yes' } }), runTaskFnFactory: () => () => ({ runParamsTest: { test2: 'yes' } }), }); core.getExportTypesRegistry = () => mockExportTypesRegistry; diff --git a/x-pack/plugins/reporting/server/routes/types.d.ts b/x-pack/plugins/reporting/server/routes/types.d.ts index ab1dd841bbbc2..5c34d466197fe 100644 --- a/x-pack/plugins/reporting/server/routes/types.d.ts +++ b/x-pack/plugins/reporting/server/routes/types.d.ts @@ -5,12 +5,12 @@ */ import { KibanaRequest, KibanaResponseFactory, RequestHandlerContext } from 'src/core/server'; -import { CreateJobBaseParams, ReportingUser, ScheduledTaskParams } from '../types'; +import { BaseParams, BasePayload, ReportingUser } from '../types'; export type HandlerFunction = ( user: ReportingUser, exportType: string, - jobParams: CreateJobBaseParams, + jobParams: BaseParams, context: RequestHandlerContext, req: KibanaRequest, res: KibanaResponseFactory @@ -22,7 +22,7 @@ export interface QueuedJobPayload { error?: boolean; source: { job: { - payload: ScheduledTaskParams; + payload: BasePayload; }; }; } diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts index 08313e6892f3c..f2785bce10964 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts @@ -8,8 +8,9 @@ import { Page } from 'puppeteer'; import * as Rx from 'rxjs'; import { chromium, HeadlessChromiumDriver, HeadlessChromiumDriverFactory } from '../browsers'; import { LevelLogger } from '../lib'; +import { ElementsPositionAndAttribute } from '../lib/screenshots'; import * as contexts from '../lib/screenshots/constants'; -import { CaptureConfig, ElementsPositionAndAttribute } from '../types'; +import { CaptureConfig } from '../types'; interface CreateMockBrowserDriverFactoryOpts { evaluate: jest.Mock, any[]>; diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index 542c9cdb3f677..10519842d9dec 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as Rx from 'rxjs'; import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { DataPluginStart } from 'src/plugins/data/server/plugin'; @@ -48,7 +47,7 @@ export interface JobParamPostPayload { } // the pre-processed, encrypted data ready for storage -export interface ScheduledTaskParams { +export interface BasePayload { headers: string; // serialized encrypted headers jobParams: JobParamsType; title: string; @@ -61,7 +60,7 @@ export interface JobSource { _source: { jobtype: string; output: TaskRunResult; - payload: ScheduledTaskParams; + payload: BasePayload; status: JobStatus; }; } @@ -87,62 +86,6 @@ export interface ConditionalHeaders { conditions: ConditionalHeadersConditions; } -/* - * Screenshots - */ - -export interface ScreenshotObservableOpts { - logger: LevelLogger; - urls: string[]; - conditionalHeaders: ConditionalHeaders; - layout: LayoutInstance; - browserTimezone: string; -} - -export interface AttributesMap { - [key: string]: any; -} - -export interface ElementPosition { - boundingClientRect: { - // modern browsers support x/y, but older ones don't - top: number; - left: number; - width: number; - height: number; - }; - scroll: { - x: number; - y: number; - }; -} - -export interface ElementsPositionAndAttribute { - position: ElementPosition; - attributes: AttributesMap; -} - -export interface Screenshot { - base64EncodedData: string; - title: string; - description: string; -} - -export interface ScreenshotResults { - timeRange: string | null; - screenshots: Screenshot[]; - error?: Error; - elementsPositionAndAttributes?: ElementsPositionAndAttribute[]; // NOTE: for testing -} - -export type ScreenshotsObservableFn = ({ - logger, - urls, - conditionalHeaders, - layout, - browserTimezone, -}: ScreenshotObservableOpts) => Rx.Observable; - /* * Plugin Contract */ @@ -169,34 +112,33 @@ export type ReportingUser = { username: AuthenticatedUser['username'] } | false; export type CaptureConfig = ReportingConfigType['capture']; export type ScrollConfig = ReportingConfigType['csv']['scroll']; -export interface CreateJobBaseParams { +export interface BaseParams { browserTimezone: string; layout?: LayoutInstance; // for screenshot type reports objectType: string; } -export interface CreateJobBaseParamsEncryptedFields extends CreateJobBaseParams { +export interface BaseParamsEncryptedFields extends BaseParams { basePath?: string; // for screenshot type reports headers: string; // encrypted headers } -export type CreateJobFn = ( +export type CreateJobFn = ( jobParams: JobParamsType, context: RequestHandlerContext, request: KibanaRequest -) => Promise; +) => Promise; -// rename me -export type WorkerExecuteFn = ( +export type RunTaskFn = ( jobId: string, - job: ScheduledTaskParamsType, + job: TaskPayloadType, cancellationToken: CancellationToken ) => Promise; -export type ScheduleTaskFnFactory = ( +export type CreateJobFnFactory = ( reporting: ReportingCore, logger: LevelLogger -) => ScheduleTaskFnType; +) => CreateJobFnType; export type RunTaskFnFactory = ( reporting: ReportingCore, @@ -205,7 +147,7 @@ export type RunTaskFnFactory = ( export interface ExportTypeDefinition< JobParamsType, - ScheduleTaskFnType, + CreateJobFnType, JobPayloadType, RunTaskFnType > { @@ -214,7 +156,7 @@ export interface ExportTypeDefinition< jobType: string; jobContentEncoding?: string; jobContentExtension: string; - scheduleTaskFnFactory: ScheduleTaskFnFactory; + createJobFnFactory: CreateJobFnFactory; runTaskFnFactory: RunTaskFnFactory; validLicenses: string[]; } diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index a9f39d2db6080..e13aa3b8980e9 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -338,7 +338,7 @@ export const sortFieldOrUndefined = t.union([sort_field, t.undefined]); export type SortFieldOrUndefined = t.TypeOf; export const sort_order = t.keyof({ asc: null, desc: null }); -export type sortOrder = t.TypeOf; +export type SortOrder = t.TypeOf; export const sortOrderOrUndefined = t.union([sort_order, t.undefined]); export type SortOrderOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts new file mode 100644 index 0000000000000..abfbc39189643 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.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; + * you may not use this file except in compliance with the Elastic License. + */ +export * from './add_prepackaged_rules_schema'; +export * from './create_rules_bulk_schema'; +export * from './create_rules_schema'; +export * from './export_rules_schema'; +export * from './find_rules_schema'; +export * from './import_rules_schema'; +export * from './patch_rules_bulk_schema'; +export * from './patch_rules_schema'; +export * from './query_rules_schema'; +export * from './query_signals_index_schema'; +export * from './set_signal_status_schema'; +export * from './update_rules_bulk_schema'; +export * from './update_rules_schema'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts new file mode 100644 index 0000000000000..6c22b8140e738 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/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; + * you may not use this file except in compliance with the Elastic License. + */ +export * from './error_schema'; +export * from './find_rules_schema'; +export * from './import_rules_schema'; +export * from './prepackaged_rules_schema'; +export * from './prepackaged_rules_status_schema'; +export * from './rules_bulk_schema'; +export * from './rules_schema'; +export * from './type_timeline_only_schema'; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/authentications/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/authentications/index.ts index 0fb0609b60ba5..efdc96b33562a 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/authentications/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/authentications/index.ts @@ -19,14 +19,14 @@ import { } from '../../../common'; import { RequestOptionsPaginated } from '../../'; -export interface AuthenticationsStrategyResponse extends IEsSearchResponse { +export interface HostAuthenticationsStrategyResponse extends IEsSearchResponse { edges: AuthenticationsEdges[]; totalCount: number; pageInfo: PageInfoPaginated; inspect?: Maybe; } -export interface AuthenticationsRequestOptions extends RequestOptionsPaginated { +export interface HostAuthenticationsRequestOptions extends RequestOptionsPaginated { defaultIndex: string[]; } diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts index 8ae41a101cee2..902e9909cf728 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts @@ -6,7 +6,7 @@ import { CloudEcs } from '../../../../ecs/cloud'; import { HostEcs, OsEcs } from '../../../../ecs/host'; -import { Maybe, SearchHit, TotalValue } from '../../../common'; +import { Hit, Hits, Maybe, SearchHit, StringOrNumber, TotalValue } from '../../../common'; export enum HostPolicyResponseActionStatus { success = 'success', @@ -98,3 +98,15 @@ export interface HostAggEsData extends SearchHit { sort: string[]; aggregations: HostAggEsItem; } + +export interface HostHit extends Hit { + _source: { + '@timestamp'?: string; + host: HostEcs; + }; + cursor?: string; + firstSeen?: string; + sort?: StringOrNumber[]; +} + +export type HostHits = Hits; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts index 9cb43c91adfd9..f5d46078fcea4 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts @@ -9,10 +9,12 @@ export * from './all'; export * from './common'; export * from './overview'; export * from './first_last_seen'; +export * from './uncommon_processes'; export enum HostsQueries { authentications = 'authentications', firstLastSeen = 'firstLastSeen', hosts = 'hosts', hostOverview = 'hostOverview', + uncommonProcesses = 'uncommonProcesses', } diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/uncommon_processes/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/uncommon_processes/index.ts new file mode 100644 index 0000000000000..28c0ccb7f6f4f --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/uncommon_processes/index.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; + +import { HostEcs } from '../../../../ecs/host'; +import { UserEcs } from '../../../../ecs/user'; +import { + RequestOptionsPaginated, + SortField, + CursorType, + Inspect, + Maybe, + PageInfoPaginated, + Hit, + TotalHit, + StringOrNumber, + Hits, +} from '../../..'; + +export interface HostUncommonProcessesRequestOptions extends RequestOptionsPaginated { + sort: SortField; + defaultIndex: string[]; +} + +export interface HostUncommonProcessesStrategyResponse extends IEsSearchResponse { + edges: UncommonProcessesEdges[]; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export interface UncommonProcessesEdges { + node: UncommonProcessItem; + cursor: CursorType; +} + +export interface UncommonProcessItem { + _id: string; + instances: number; + process: ProcessEcsFields; + hosts: HostEcs[]; + user?: Maybe; +} + +export interface ProcessEcsFields { + hash?: Maybe; + pid?: Maybe; + name?: Maybe; + ppid?: Maybe; + args?: Maybe; + entity_id?: Maybe; + executable?: Maybe; + title?: Maybe; + thread?: Maybe; + working_directory?: Maybe; +} + +export interface ProcessHashData { + md5?: Maybe; + sha1?: Maybe; + sha256?: Maybe; +} + +export interface Thread { + id?: Maybe; + start?: Maybe; +} + +export interface UncommonProcessHit extends Hit { + total: TotalHit; + host: Array<{ + id: string[] | undefined; + name: string[] | undefined; + }>; + _source: { + '@timestamp': string; + process: ProcessEcsFields; + }; + cursor: string; + sort: StringOrNumber[]; +} + +export type ProcessHits = Hits; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index 85ffc6aa4c734..17adf38559a9e 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -8,17 +8,17 @@ import { IEsSearchRequest } from '../../../../../../src/plugins/data/common'; import { ESQuery } from '../../typed_json'; import { HostOverviewStrategyResponse, + HostAuthenticationsRequestOptions, + HostAuthenticationsStrategyResponse, HostOverviewRequestOptions, HostFirstLastSeenStrategyResponse, HostFirstLastSeenRequestOptions, HostsQueries, HostsRequestOptions, HostsStrategyResponse, + HostUncommonProcessesStrategyResponse, + HostUncommonProcessesRequestOptions, } from './hosts'; -import { - AuthenticationsRequestOptions, - AuthenticationsStrategyResponse, -} from './hosts/authentications'; import { NetworkQueries, NetworkTlsStrategyResponse, @@ -66,9 +66,11 @@ export type StrategyResponseType = T extends HostsQ : T extends HostsQueries.hostOverview ? HostOverviewStrategyResponse : T extends HostsQueries.authentications - ? AuthenticationsStrategyResponse + ? HostAuthenticationsStrategyResponse : T extends HostsQueries.firstLastSeen ? HostFirstLastSeenStrategyResponse + : T extends HostsQueries.uncommonProcesses + ? HostUncommonProcessesStrategyResponse : T extends NetworkQueries.tls ? NetworkTlsStrategyResponse : T extends NetworkQueries.http @@ -82,9 +84,11 @@ export type StrategyRequestType = T extends HostsQu : T extends HostsQueries.hostOverview ? HostOverviewRequestOptions : T extends HostsQueries.authentications - ? AuthenticationsRequestOptions + ? HostAuthenticationsRequestOptions : T extends HostsQueries.firstLastSeen ? HostFirstLastSeenRequestOptions + : T extends HostsQueries.uncommonProcesses + ? HostUncommonProcessesRequestOptions : T extends NetworkQueries.tls ? NetworkTlsRequestOptions : T extends NetworkQueries.http diff --git a/x-pack/plugins/security_solution/cypress/support/commands.js b/x-pack/plugins/security_solution/cypress/support/commands.js index 4f382f13bcd5d..f0dd797601176 100644 --- a/x-pack/plugins/security_solution/cypress/support/commands.js +++ b/x-pack/plugins/security_solution/cypress/support/commands.js @@ -45,15 +45,14 @@ Cypress.Commands.add( prevSubject: 'element', }, (input, fileName, fileType = 'text/plain') => { - cy.fixture(fileName) - .then((content) => Cypress.Blob.base64StringToBlob(content, fileType)) - .then((blob) => { - const testFile = new File([blob], fileName, { type: fileType }); - const dataTransfer = new DataTransfer(); + cy.fixture(fileName).then((content) => { + const blob = Cypress.Blob.base64StringToBlob(content, fileType); + const testFile = new File([blob], fileName, { type: fileType }); + const dataTransfer = new DataTransfer(); - dataTransfer.items.add(testFile); - input[0].files = dataTransfer.files; - return input; - }); + dataTransfer.items.add(testFile); + input[0].files = dataTransfer.files; + return input; + }); } ); diff --git a/x-pack/plugins/security_solution/cypress/support/index.js b/x-pack/plugins/security_solution/cypress/support/index.js index 42abecd4b0bad..244781e0ccd01 100644 --- a/x-pack/plugins/security_solution/cypress/support/index.js +++ b/x-pack/plugins/security_solution/cypress/support/index.js @@ -23,7 +23,7 @@ import './commands'; Cypress.Cookies.defaults({ - whitelist: 'sid', + preserve: 'sid', }); Cypress.on('uncaught:exception', (err) => { diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap index 60088a77878ad..a40ccfa7cd1b5 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap @@ -100,7 +100,11 @@ exports[`JobsTableComponent renders correctly against snapshot 1`] = ` ] } loading={true} - noItemsMessage={} + noItemsMessage={ + + } onChange={[Function]} pagination={ Object { diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx index be911a1cd8537..1e9e689dcd6ff 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx @@ -125,7 +125,7 @@ export const JobsTableComponent = ({ isLoading, jobs, onJobStateChange }: JobTab columns={getJobsTableColumns(isLoading, onJobStateChange, basePath)} items={getPaginatedItems(jobs, pageIndex, pageSize)} loading={isLoading} - noItemsMessage={} + noItemsMessage={} pagination={pagination} responsive={false} onChange={({ page }: { page: { index: number } }) => { @@ -141,13 +141,13 @@ export const JobsTable = React.memo(JobsTableComponent); JobsTable.displayName = 'JobsTable'; -export const NoItemsMessage = React.memo(() => ( +export const NoItemsMessage = React.memo(({ basePath }: { basePath: string }) => ( {i18n.NO_ITEMS_TEXT}

} titleSize="xs" actions={ => - Promise.resolve(ruleMock); +export const updateRule = async ({ rule, signal }: UpdateRulesProps): Promise => + Promise.resolve(getRulesSchemaMock()); -export const patchRule = async ({ ruleProperties, signal }: PatchRuleProps): Promise => - Promise.resolve(ruleMock); +export const createRule = async ({ rule, signal }: CreateRulesProps): Promise => + Promise.resolve(getRulesSchemaMock()); + +export const patchRule = async ({ ruleProperties, signal }: PatchRuleProps): Promise => + Promise.resolve(getRulesSchemaMock()); export const getPrePackagedRulesStatus = async ({ signal, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts index f58c95ed71e29..cd1ded544cfe5 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts @@ -6,7 +6,9 @@ import { KibanaServices } from '../../../../common/lib/kibana'; import { - addRule, + createRule, + updateRule, + patchRule, fetchRules, fetchRuleById, enableRules, @@ -19,9 +21,12 @@ import { fetchTags, getPrePackagedRulesStatus, } from './api'; -import { ruleMock, rulesMock } from './mock'; +import { getRulesSchemaMock } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; +import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; +import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/update_rules_schema.mock'; +import { getPatchRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/patch_rules_schema.mock'; +import { rulesMock } from './mock'; import { buildEsQuery } from 'src/plugins/data/common'; - const abortCtrl = new AbortController(); const mockKibanaServices = KibanaServices.get as jest.Mock; jest.mock('../../../../common/lib/kibana'); @@ -30,25 +35,56 @@ const fetchMock = jest.fn(); mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); describe('Detections Rules API', () => { - describe('addRule', () => { + describe('createRule', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue(ruleMock); + fetchMock.mockResolvedValue(getRulesSchemaMock()); }); - test('check parameter url, body', async () => { - await addRule({ rule: ruleMock, signal: abortCtrl.signal }); + test('POSTs rule', async () => { + const payload = getCreateRulesSchemaMock(); + await createRule({ rule: payload, signal: abortCtrl.signal }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', { body: - '{"description":"some desc","enabled":true,"false_positives":[],"filters":[],"from":"now-360s","index":["apm-*-transaction*","auditbeat-*","endgame-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf","language":"kuery","risk_score":75,"name":"Test rule","query":"user.email: \'root@elastic.co\'","references":[],"severity":"high","tags":["APM"],"to":"now","type":"query","threat":[],"throttle":null}', + '{"description":"Detecting root and admin users","name":"Query with a rule id","query":"user.name: root or user.name: admin","severity":"high","type":"query","risk_score":55,"language":"kuery","rule_id":"rule-1"}', method: 'POST', signal: abortCtrl.signal, }); }); + }); - test('happy path', async () => { - const ruleResp = await addRule({ rule: ruleMock, signal: abortCtrl.signal }); - expect(ruleResp).toEqual(ruleMock); + describe('updateRule', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(getRulesSchemaMock()); + }); + + test('PUTs rule', async () => { + const payload = getUpdateRulesSchemaMock(); + await updateRule({ rule: payload, signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', { + body: + '{"description":"some description","name":"Query with a rule id","query":"user.name: root or user.name: admin","severity":"high","type":"query","risk_score":55,"language":"kuery","rule_id":"rule-1"}', + method: 'PUT', + signal: abortCtrl.signal, + }); + }); + }); + + describe('patchRule', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(getRulesSchemaMock()); + }); + + test('PATCHs rule', async () => { + const payload = getPatchRulesSchemaMock(); + await patchRule({ ruleProperties: payload, signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', { + body: JSON.stringify(payload), + method: 'PATCH', + signal: abortCtrl.signal, + }); }); }); @@ -280,7 +316,7 @@ describe('Detections Rules API', () => { describe('fetchRuleById', () => { beforeEach(() => { fetchMock.mockClear(); - fetchMock.mockResolvedValue(ruleMock); + fetchMock.mockResolvedValue(getRulesSchemaMock()); }); test('check parameter url, query', async () => { @@ -296,7 +332,7 @@ describe('Detections Rules API', () => { test('happy path', async () => { const ruleResp = await fetchRuleById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); - expect(ruleResp).toEqual(ruleMock); + expect(ruleResp).toEqual(getRulesSchemaMock()); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index 3538d8ec8c9b9..e254516d11076 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - import { HttpStart } from '../../../../../../../../src/core/public'; import { DETECTION_ENGINE_RULES_URL, @@ -13,13 +12,13 @@ import { DETECTION_ENGINE_TAGS_URL, } from '../../../../../common/constants'; import { - AddRulesProps, + UpdateRulesProps, + CreateRulesProps, DeleteRulesProps, DuplicateRulesProps, EnableRulesProps, FetchRulesProps, FetchRulesResponse, - NewRule, Rule, FetchRuleProps, BasicFetchProps, @@ -33,32 +32,51 @@ import { } from './types'; import { KibanaServices } from '../../../../common/lib/kibana'; import * as i18n from '../../../pages/detection_engine/rules/translations'; +import { RulesSchema } from '../../../../../common/detection_engine/schemas/response'; /** - * Add provided Rule + * Create provided Rule * - * @param rule to add + * @param rule CreateRulesSchema to add * @param signal to cancel request * * @throws An error if response is not OK */ -export const addRule = async ({ rule, signal }: AddRulesProps): Promise => - KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { - method: rule.id != null ? 'PUT' : 'POST', +export const createRule = async ({ rule, signal }: CreateRulesProps): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { + method: 'POST', + body: JSON.stringify(rule), + signal, + }); + +/** + * Update provided Rule using PUT + * + * @param rule UpdateRulesSchema to be updated + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const updateRule = async ({ rule, signal }: UpdateRulesProps): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { + method: 'PUT', body: JSON.stringify(rule), signal, }); /** - * Patch provided Rule + * Patch provided rule + * NOTE: The rule edit flow does NOT use patch as it relies on the + * functionality of PUT to delete field values when not provided, if + * just expecting changes, use this `patchRule` * * @param ruleProperties to patch * @param signal to cancel request * * @throws An error if response is not OK */ -export const patchRule = async ({ ruleProperties, signal }: PatchRuleProps): Promise => - KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { +export const patchRule = async ({ ruleProperties, signal }: PatchRuleProps): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { method: 'PATCH', body: JSON.stringify(ruleProperties), signal, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts index c7ecfb33cd905..a40ab2e487851 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts @@ -6,7 +6,8 @@ export * from './api'; export * from './fetch_index_patterns'; -export * from './persist_rule'; +export * from './use_update_rule'; +export * from './use_create_rule'; export * from './types'; export * from './use_rule'; export * from './use_rules'; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts index fa11cfabcdf8b..c0397b0af6db9 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts @@ -4,36 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { NewRule, FetchRulesResponse, Rule } from './types'; - -export const ruleMock: NewRule = { - description: 'some desc', - enabled: true, - false_positives: [], - filters: [], - from: 'now-360s', - index: [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'packetbeat-*', - 'winlogbeat-*', - ], - interval: '5m', - rule_id: 'bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf', - language: 'kuery', - risk_score: 75, - name: 'Test rule', - query: "user.email: 'root@elastic.co'", - references: [], - severity: 'high', - tags: ['APM'], - to: 'now', - type: 'query', - threat: [], - throttle: null, -}; +import { FetchRulesResponse, Rule } from './types'; export const savedRuleMock: Rule = { author: [], diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index 166bb90113aef..e94e57ad82bcf 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -7,6 +7,7 @@ import * as t from 'io-ts'; import { + SortOrder, author, building_block_type, license, @@ -17,11 +18,12 @@ import { threshold, type, } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { listArray } from '../../../../../common/detection_engine/schemas/types'; import { - listArray, - listArrayOrUndefined, -} from '../../../../../common/detection_engine/schemas/types'; -import { PatchRulesSchema } from '../../../../../common/detection_engine/schemas/request/patch_rules_schema'; + CreateRulesSchema, + PatchRulesSchema, + UpdateRulesSchema, +} from '../../../../../common/detection_engine/schemas/request'; /** * Params is an "record", since it is a type of AlertActionParams which is action templates. @@ -36,48 +38,13 @@ export const action = t.exact( }) ); -export const NewRuleSchema = t.intersection([ - t.type({ - description: t.string, - enabled: t.boolean, - interval: t.string, - name: t.string, - risk_score: t.number, - severity: t.string, - type, - }), - t.partial({ - actions: t.array(action), - anomaly_threshold: t.number, - created_by: t.string, - false_positives: t.array(t.string), - filters: t.array(t.unknown), - from: t.string, - id: t.string, - index: t.array(t.string), - language: t.string, - machine_learning_job_id: t.string, - max_signals: t.number, - query: t.string, - references: t.array(t.string), - rule_id: t.string, - saved_id: t.string, - tags: t.array(t.string), - threat: t.array(t.unknown), - threshold, - throttle: t.union([t.string, t.null]), - to: t.string, - updated_by: t.string, - note: t.string, - exceptions_list: listArrayOrUndefined, - }), -]); - -export const NewRulesSchema = t.array(NewRuleSchema); -export type NewRule = t.TypeOf; +export interface CreateRulesProps { + rule: CreateRulesSchema; + signal: AbortSignal; +} -export interface AddRulesProps { - rule: NewRule; +export interface UpdateRulesProps { + rule: UpdateRulesSchema; signal: AbortSignal; } @@ -185,7 +152,7 @@ export interface FetchRulesProps { export interface FilterOptions { filter: string; sortField: string; - sortOrder: 'asc' | 'desc'; + sortOrder: SortOrder; showCustomRules?: boolean; showElasticRules?: boolean; tags?: string[]; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx similarity index 68% rename from x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.test.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx index 1bf21623992e6..42d6a2a92a4c2 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.test.tsx @@ -6,25 +6,25 @@ import { renderHook, act } from '@testing-library/react-hooks'; -import { usePersistRule, ReturnPersistRule } from './persist_rule'; -import { ruleMock } from './mock'; +import { useCreateRule, ReturnCreateRule } from './use_create_rule'; +import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/update_rules_schema.mock'; jest.mock('./api'); -describe('usePersistRule', () => { +describe('useCreateRule', () => { test('init', async () => { - const { result } = renderHook(() => usePersistRule()); + const { result } = renderHook(() => useCreateRule()); expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]); }); test('saving rule with isLoading === true', async () => { await act(async () => { - const { result, rerender, waitForNextUpdate } = renderHook(() => - usePersistRule() + const { result, rerender, waitForNextUpdate } = renderHook(() => + useCreateRule() ); await waitForNextUpdate(); - result.current[1](ruleMock); + result.current[1](getUpdateRulesSchemaMock()); rerender(); expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]); }); @@ -32,11 +32,11 @@ describe('usePersistRule', () => { test('saved rule with isSaved === true', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - usePersistRule() + const { result, waitForNextUpdate } = renderHook(() => + useCreateRule() ); await waitForNextUpdate(); - result.current[1](ruleMock); + result.current[1](getUpdateRulesSchemaMock()); await waitForNextUpdate(); expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]); }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx similarity index 76% rename from x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.tsx rename to x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx index fd139d59c0a27..2bbd27994fc77 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/persist_rule.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_create_rule.tsx @@ -7,20 +7,20 @@ import { useEffect, useState, Dispatch } from 'react'; import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; +import { CreateRulesSchema } from '../../../../../common/detection_engine/schemas/request'; -import { addRule as persistRule } from './api'; +import { createRule } from './api'; import * as i18n from './translations'; -import { NewRule } from './types'; -interface PersistRuleReturn { +interface CreateRuleReturn { isLoading: boolean; isSaved: boolean; } -export type ReturnPersistRule = [PersistRuleReturn, Dispatch]; +export type ReturnCreateRule = [CreateRuleReturn, Dispatch]; -export const usePersistRule = (): ReturnPersistRule => { - const [rule, setRule] = useState(null); +export const useCreateRule = (): ReturnCreateRule => { + const [rule, setRule] = useState(null); const [isSaved, setIsSaved] = useState(false); const [isLoading, setIsLoading] = useState(false); const [, dispatchToaster] = useStateToaster(); @@ -33,7 +33,7 @@ export const usePersistRule = (): ReturnPersistRule => { if (rule != null) { try { setIsLoading(true); - await persistRule({ rule, signal: abortCtrl.signal }); + await createRule({ rule, signal: abortCtrl.signal }); if (isSubscribed) { setIsSaved(true); } diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx index 6721d89f2799b..2ba78cd90cf9b 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_dissasociate_exception_list.test.tsx @@ -9,7 +9,7 @@ import { act, renderHook } from '@testing-library/react-hooks'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; import * as api from './api'; -import { ruleMock } from './mock'; +import { getRulesSchemaMock } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; import { ReturnUseDissasociateExceptionList, UseDissasociateExceptionListProps, @@ -23,7 +23,7 @@ describe('useDissasociateExceptionList', () => { const onSuccess = jest.fn(); beforeEach(() => { - jest.spyOn(api, 'patchRule').mockResolvedValue(ruleMock); + jest.spyOn(api, 'patchRule').mockResolvedValue(getRulesSchemaMock()); }); afterEach(() => { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx index 9a6ea4f60fdcc..92d46a785b034 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx @@ -10,7 +10,7 @@ import * as api from './api'; jest.mock('./api'); -describe('usePersistRule', () => { +describe('usePrePackagedRules', () => { beforeEach(() => { jest.clearAllMocks(); jest.restoreAllMocks(); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.test.tsx new file mode 100644 index 0000000000000..9603a4151933a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; + +import { useUpdateRule, ReturnUpdateRule } from './use_update_rule'; +import { getUpdateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/update_rules_schema.mock'; + +jest.mock('./api'); + +describe('useUpdateRule', () => { + test('init', async () => { + const { result } = renderHook(() => useUpdateRule()); + + expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]); + }); + + test('saving rule with isLoading === true', async () => { + await act(async () => { + const { result, rerender, waitForNextUpdate } = renderHook(() => + useUpdateRule() + ); + await waitForNextUpdate(); + result.current[1](getUpdateRulesSchemaMock()); + rerender(); + expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]); + }); + }); + + test('saved rule with isSaved === true', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useUpdateRule() + ); + await waitForNextUpdate(); + result.current[1](getUpdateRulesSchemaMock()); + await waitForNextUpdate(); + expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx new file mode 100644 index 0000000000000..a437974e93ba3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_update_rule.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState, Dispatch } from 'react'; + +import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; +import { UpdateRulesSchema } from '../../../../../common/detection_engine/schemas/request'; + +import { updateRule } from './api'; +import * as i18n from './translations'; + +interface UpdateRuleReturn { + isLoading: boolean; + isSaved: boolean; +} + +export type ReturnUpdateRule = [UpdateRuleReturn, Dispatch]; + +export const useUpdateRule = (): ReturnUpdateRule => { + const [rule, setRule] = useState(null); + const [isSaved, setIsSaved] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + setIsSaved(false); + async function saveRule() { + if (rule != null) { + try { + setIsLoading(true); + await updateRule({ rule, signal: abortCtrl.signal }); + if (isSubscribed) { + setIsSaved(true); + } + } catch (error) { + if (isSubscribed) { + errorToToaster({ title: i18n.RULE_ADD_FAILURE, error, dispatchToaster }); + } + } + if (isSubscribed) { + setIsLoading(false); + } + } + } + + saveRule(); + return () => { + isSubscribed = false; + abortCtrl.abort(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [rule]); + + return [{ isLoading, isSaved }, setRule]; +}; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts index d6dc97fbae158..79488231b29ee 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.test.ts @@ -5,7 +5,8 @@ */ import { List } from '../../../../../../common/detection_engine/schemas/types'; -import { NewRule } from '../../../../containers/detection_engine/rules'; +import { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request/create_rules_schema'; +import { Rule } from '../../../../containers/detection_engine/rules'; import { getListMock, getEndpointListMock, @@ -721,13 +722,13 @@ describe('helpers', () => { mockActions = mockActionsStepRule(); }); - test('returns NewRule with type of saved_query when saved_id exists', () => { - const result: NewRule = formatRule(mockDefine, mockAbout, mockSchedule, mockActions); + test('returns rule with type of saved_query when saved_id exists', () => { + const result: Rule = formatRule(mockDefine, mockAbout, mockSchedule, mockActions); expect(result.type).toEqual('saved_query'); }); - test('returns NewRule with type of query when saved_id does not exist', () => { + test('returns rule with type of query when saved_id does not exist', () => { const mockDefineStepRuleWithoutSavedId = { ...mockDefine, queryBar: { @@ -735,7 +736,7 @@ describe('helpers', () => { saved_id: '', }, }; - const result: NewRule = formatRule( + const result: CreateRulesSchema = formatRule( mockDefineStepRuleWithoutSavedId, mockAbout, mockSchedule, @@ -745,10 +746,15 @@ describe('helpers', () => { expect(result.type).toEqual('query'); }); - test('returns NewRule without id if ruleId does not exist', () => { - const result: NewRule = formatRule(mockDefine, mockAbout, mockSchedule, mockActions); + test('returns rule without id if ruleId does not exist', () => { + const result: CreateRulesSchema = formatRule( + mockDefine, + mockAbout, + mockSchedule, + mockActions + ); - expect(result.id).toBeUndefined(); + expect(result).not.toHaveProperty('id'); }); }); 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 f4a40b771c9fa..874ef032b7c5e 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 @@ -14,7 +14,7 @@ import { isMlRule } from '../../../../../../common/machine_learning/helpers'; import { isThresholdRule } from '../../../../../../common/detection_engine/utils'; import { List } from '../../../../../../common/detection_engine/schemas/types'; import { ENDPOINT_LIST_ID } from '../../../../../shared_imports'; -import { NewRule, Rule } from '../../../../containers/detection_engine/rules'; +import { Rule } from '../../../../containers/detection_engine/rules'; import { Type } from '../../../../../../common/detection_engine/schemas/common/schemas'; import { @@ -237,16 +237,19 @@ export const formatActionsStepData = (actionsStepData: ActionsStepRule): Actions }; }; -export const formatRule = ( +// Used to format form data in rule edit and +// create flows so "T" here would likely +// either be CreateRulesSchema or Rule +export const formatRule = ( defineStepData: DefineStepRule, aboutStepData: AboutStepRule, scheduleData: ScheduleStepRule, actionsData: ActionsStepRule, rule?: Rule | null -): NewRule => - deepmerge.all([ +): T => + (deepmerge.all([ formatDefineStepData(defineStepData), formatAboutStepData(aboutStepData, rule?.exceptions_list), formatScheduleStepData(scheduleData), formatActionsStepData(actionsData), - ]) as NewRule; + ]) as unknown) as T; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx index d2eb3228cbbf3..22d84c593b415 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx @@ -9,7 +9,8 @@ import React, { useCallback, useRef, useState, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; import styled, { StyledComponent } from 'styled-components'; -import { usePersistRule } from '../../../../containers/detection_engine/rules'; +import { useCreateRule } from '../../../../containers/detection_engine/rules'; +import { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request'; import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config'; import { @@ -122,7 +123,7 @@ const CreateRulePageComponent: React.FC = () => { [RuleStep.scheduleRule]: false, [RuleStep.ruleActions]: false, }); - const [{ isLoading, isSaved }, setRule] = usePersistRule(); + const [{ isLoading, isSaved }, setRule] = useCreateRule(); const actionMessageParams = useMemo( () => getActionMessageParams((stepsData.current['define-rule'].data as DefineStepRule)?.ruleType), @@ -159,7 +160,7 @@ const CreateRulePageComponent: React.FC = () => { stepsData.current[RuleStep.scheduleRule].isValid ) { setRule( - formatRule( + formatRule( stepsData.current[RuleStep.defineRule].data as DefineStepRule, stepsData.current[RuleStep.aboutRule].data as AboutStepRule, stepsData.current[RuleStep.scheduleRule].data as ScheduleStepRule, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx index 530222ee19624..b251b2eba10ae 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx @@ -17,7 +17,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { FC, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useParams, useHistory } from 'react-router-dom'; -import { useRule, usePersistRule } from '../../../../containers/detection_engine/rules'; +import { useRule, useUpdateRule } from '../../../../containers/detection_engine/rules'; import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config'; import { WrapperPage } from '../../../../../common/components/wrapper_page'; import { @@ -51,6 +51,7 @@ import { } from '../types'; import * as i18n from './translations'; import { SecurityPageName } from '../../../../../app/types'; +import { UpdateRulesSchema } from '../../../../../../common/detection_engine/schemas/request'; interface StepRuleForm { isValid: boolean; @@ -113,7 +114,7 @@ const EditRulePageComponent: FC = () => { [RuleStep.scheduleRule]: null, [RuleStep.ruleActions]: null, }); - const [{ isLoading, isSaved }, setRule] = usePersistRule(); + const [{ isLoading, isSaved }, setRule] = useUpdateRule(); const [tabHasError, setTabHasError] = useState([]); // eslint-disable-next-line react-hooks/exhaustive-deps const actionMessageParams = useMemo(() => getActionMessageParams(rule?.type), [rule]); @@ -261,7 +262,7 @@ const EditRulePageComponent: FC = () => { if (invalidForms.length === 0 && activeForm != null) { setTabHasError([]); setRule({ - ...formatRule( + ...formatRule( (activeFormId === RuleStep.defineRule ? activeForm.data : myDefineRuleForm.data) as DefineStepRule, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts index 891af4b8ca80e..a7603051add49 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts @@ -18,6 +18,7 @@ import { RiskScoreMapping, RuleNameOverride, SeverityMapping, + SortOrder, TimestampOverride, Type, } from '../../../../../common/detection_engine/schemas/common/schemas'; @@ -25,7 +26,7 @@ import { List } from '../../../../../common/detection_engine/schemas/types'; export interface EuiBasicTableSortTypes { field: string; - direction: 'asc' | 'desc'; + direction: SortOrder; } export interface EuiBasicTableOnChange { diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/mock.ts b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/mock.ts index 759b34cd258d5..9e60c35b746da 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/mock.ts +++ b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/mock.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import { SearchResponse } from 'elasticsearch'; -import { AuthenticationsStrategyResponse } from '../../../../common/search_strategy/security_solution/hosts/authentications'; +import { HostAuthenticationsStrategyResponse } from '../../../../common/search_strategy/security_solution/hosts/authentications'; -export const mockData: { Authentications: AuthenticationsStrategyResponse } = { +export const mockData: { Authentications: HostAuthenticationsStrategyResponse } = { Authentications: { rawResponse: { aggregations: { diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx index 79d83404f8c4a..5436469409194 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx @@ -12,15 +12,14 @@ import deepEqual from 'fast-deep-equal'; import { AbortError } from '../../../../../../../src/plugins/data/common'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { HostsQueries } from '../../../../common/search_strategy/security_solution'; import { - Direction, - DocValueFields, - HostPolicyResponseActionStatus, - HostsQueries, - PageInfoPaginated, - AuthenticationsRequestOptions, - AuthenticationsStrategyResponse, + HostAuthenticationsRequestOptions, + HostAuthenticationsStrategyResponse, AuthenticationsEdges, + PageInfoPaginated, + DocValueFields, + SortField, } from '../../../../common/search_strategy'; import { ESTermQuery } from '../../../../common/typed_json'; @@ -75,7 +74,7 @@ export const useAuthentications = ({ const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); const [loading, setLoading] = useState(false); const [authenticationsRequest, setAuthenticationsRequest] = useState< - AuthenticationsRequestOptions + HostAuthenticationsRequestOptions >({ defaultIndex, docValueFields: docValueFields ?? [], @@ -87,10 +86,7 @@ export const useAuthentications = ({ from: startDate, to: endDate, }, - sort: { - direction: Direction.desc, - field: HostPolicyResponseActionStatus.success, - }, + sort: {} as SortField, }); const wrappedLoadMore = useCallback( @@ -125,14 +121,14 @@ export const useAuthentications = ({ }); const authenticationsSearch = useCallback( - (request: AuthenticationsRequestOptions) => { + (request: HostAuthenticationsRequestOptions) => { let didCancel = false; const asyncSearch = async () => { abortCtrl.current = new AbortController(); setLoading(true); const searchSubscription$ = data.search - .search(request, { + .search(request, { strategy: 'securitySolutionSearchStrategy', abortSignal: abortCtrl.current.signal, }) diff --git a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx index f8e5b1bed73cd..82f5a97e9e413 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx @@ -4,36 +4,39 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; -import { compose } from 'redux'; +import deepEqual from 'fast-deep-equal'; +import { noop } from 'lodash/fp'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useSelector } from 'react-redux'; + +import { AbortError } from '../../../../../../../src/plugins/data/common'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; -import { - GetUncommonProcessesQuery, - PageInfoPaginated, - UncommonProcessesEdges, -} from '../../../graphql/types'; -import { inputsModel, State, inputsSelectors } from '../../../common/store'; -import { withKibana, WithKibanaProps } from '../../../common/lib/kibana'; +import { PageInfoPaginated, UncommonProcessesEdges } from '../../../graphql/types'; +import { inputsModel, State } from '../../../common/store'; +import { useKibana } from '../../../common/lib/kibana'; import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; -import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; -import { - QueryTemplatePaginated, - QueryTemplatePaginatedProps, -} from '../../../common/containers/query_template_paginated'; +import { createFilter } from '../../../common/containers/helpers'; + import { hostsModel, hostsSelectors } from '../../store'; -import { uncommonProcessesQuery } from './index.gql_query'; +import { + HostUncommonProcessesRequestOptions, + HostUncommonProcessesStrategyResponse, +} from '../../../../common/search_strategy/security_solution/hosts/uncommon_processes'; +import { HostsQueries } from '../../../../common/search_strategy/security_solution/hosts'; +import { DocValueFields, SortField } from '../../../../common/search_strategy'; + +import * as i18n from './translations'; +import { ESTermQuery } from '../../../../common/typed_json'; +import { getInspectResponse } from '../../../helpers'; +import { InspectResponse } from '../../../types'; const ID = 'uncommonProcessesQuery'; export interface UncommonProcessesArgs { id: string; - inspect: inputsModel.InspectQuery; + inspect: InspectResponse; isInspected: boolean; - loading: boolean; loadPage: (newActivePage: number) => void; pageInfo: PageInfoPaginated; refetch: inputsModel.Refetch; @@ -41,111 +44,164 @@ export interface UncommonProcessesArgs { uncommonProcesses: UncommonProcessesEdges[]; } -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: UncommonProcessesArgs) => React.ReactNode; +interface UseUncommonProcesses { + docValueFields?: DocValueFields[]; + filterQuery?: ESTermQuery | string; + endDate: string; + skip?: boolean; + startDate: string; type: hostsModel.HostsType; } -type UncommonProcessesProps = OwnProps & PropsFromRedux & WithKibanaProps; - -class UncommonProcessesComponentQuery extends QueryTemplatePaginated< - UncommonProcessesProps, - GetUncommonProcessesQuery.Query, - GetUncommonProcessesQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - filterQuery, - id = ID, - isInspected, - kibana, - limit, - skip, - sourceId, - startDate, - } = this.props; - const variables: GetUncommonProcessesQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - inspect: isInspected, - pagination: generateTablePaginationOptions(activePage, limit), - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, +export const useUncommonProcesses = ({ + docValueFields, + filterQuery, + endDate, + skip = false, + startDate, + type, +}: UseUncommonProcesses): [boolean, UncommonProcessesArgs] => { + const getUncommonProcessesSelector = hostsSelectors.uncommonProcessesSelector(); + const { activePage, limit } = useSelector((state: State) => + getUncommonProcessesSelector(state, type) + ); + const { data, notifications, uiSettings } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); + const [loading, setLoading] = useState(false); + const [uncommonProcessesRequest, setUncommonProcessesRequest] = useState< + HostUncommonProcessesRequestOptions + >({ + defaultIndex, + docValueFields: docValueFields ?? [], + factoryQueryType: HostsQueries.uncommonProcesses, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + sort: {} as SortField, + }); + + const wrappedLoadMore = useCallback( + (newActivePage: number) => { + setUncommonProcessesRequest((prevRequest) => { + return { + ...prevRequest, + pagination: generateTablePaginationOptions(newActivePage, limit), + }; + }); + }, + [limit] + ); + + const [uncommonProcessesResponse, setUncommonProcessesResponse] = useState( + { + uncommonProcesses: [], + id: ID, + inspect: { + dsl: [], + response: [], }, - }; - return ( - - query={uncommonProcessesQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const uncommonProcesses = getOr([], 'source.UncommonProcesses.edges', data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), + isInspected: false, + loadPage: wrappedLoadMore, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + refetch: refetch.current, + totalCount: -1, + } + ); + + const uncommonProcessesSearch = useCallback( + (request: HostUncommonProcessesRequestOptions) => { + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search( + request, + { + strategy: 'securitySolutionSearchStrategy', + abortSignal: abortCtrl.current.signal, + } + ) + .subscribe({ + next: (response) => { + if (!response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + setUncommonProcessesResponse((prevResponse) => ({ + ...prevResponse, + uncommonProcesses: response.edges, + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + refetch: refetch.current, + totalCount: response.totalCount, + })); + } + searchSubscription$.unsubscribe(); + } else if (response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + } + notifications.toasts.addWarning(i18n.ERROR_UNCOMMON_PROCESSES); + searchSubscription$.unsubscribe(); + } }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ + title: i18n.FAIL_UNCOMMON_PROCESSES, + text: msg.message, + }); } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - UncommonProcesses: { - ...fetchMoreResult.source.UncommonProcesses, - edges: [...fetchMoreResult.source.UncommonProcesses.edges], - }, - }, - }; }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.UncommonProcesses.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - pageInfo: getOr({}, 'source.UncommonProcesses.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.UncommonProcesses.totalCount', data), - uncommonProcesses, }); - }} - - ); - } -} + }; + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts] + ); -const makeMapStateToProps = () => { - const getUncommonProcessesSelector = hostsSelectors.uncommonProcessesSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getUncommonProcessesSelector(state, type), - isInspected, - }; - }; - return mapStateToProps; -}; + useEffect(() => { + setUncommonProcessesRequest((prevRequest) => { + const myRequest = { + ...prevRequest, + defaultIndex, + docValueFields: docValueFields ?? [], + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + sort: {} as SortField, + }; + if (!skip && !deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [activePage, defaultIndex, docValueFields, endDate, filterQuery, limit, skip, startDate]); -const connector = connect(makeMapStateToProps); + useEffect(() => { + uncommonProcessesSearch(uncommonProcessesRequest); + }, [uncommonProcessesRequest, uncommonProcessesSearch]); -type PropsFromRedux = ConnectedProps; - -export const UncommonProcessesQuery = compose>( - connector, - withKibana -)(UncommonProcessesComponentQuery); + return [loading, uncommonProcessesResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/translations.ts b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/translations.ts new file mode 100644 index 0000000000000..d563d90dfb262 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/translations.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_UNCOMMON_PROCESSES = i18n.translate( + 'xpack.securitySolution.uncommonProcesses.errorSearchDescription', + { + defaultMessage: `An error has occurred on uncommon processes search`, + } +); + +export const FAIL_UNCOMMON_PROCESSES = i18n.translate( + 'xpack.securitySolution.uncommonProcesses.failSearchDescription', + { + defaultMessage: `Failed to run search on uncommon processes`, + } +); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx index f1691dbaa04b4..713958f05a3da 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx @@ -6,7 +6,7 @@ import { getOr } from 'lodash/fp'; import React from 'react'; -import { UncommonProcessesQuery } from '../../containers/uncommon_processes'; +import { useUncommonProcesses } from '../../containers/uncommon_processes'; import { HostsComponentsQueryProps } from './types'; import { UncommonProcessTable } from '../../components/uncommon_process_table'; import { manageQuery } from '../../../common/components/page/manage_query'; @@ -15,49 +15,35 @@ const UncommonProcessTableManage = manageQuery(UncommonProcessTable); export const UncommonProcessQueryTabBody = ({ deleteQuery, + docValueFields, endDate, filterQuery, skip, setQuery, startDate, type, -}: HostsComponentsQueryProps) => ( - - {({ - uncommonProcesses, - totalCount, - loading, - pageInfo, - loadPage, - id, - inspect, - isInspected, - refetch, - }) => ( - - )} - -); +}: HostsComponentsQueryProps) => { + const [ + loading, + { uncommonProcesses, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, + ] = useUncommonProcesses({ docValueFields, endDate, filterQuery, skip, startDate, type }); + return ( + + ); +}; UncommonProcessQueryTabBody.dispalyName = 'UncommonProcessQueryTabBody'; diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts index dee53a624baff..55d52d4ba3252 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts @@ -12,7 +12,6 @@ import { ResolverTree, ResolverEntityIndex, } from '../../../common/endpoint/types'; -import { DEFAULT_INDEX_KEY as defaultIndexKey } from '../../../common/constants'; /** * The data access layer for resolver. All communication with the Kibana server is done through this object. This object is provided to Resolver. In tests, a mock data access layer can be used instead. @@ -38,13 +37,6 @@ export function dataAccessLayerFactory( }); }, - /** - * Used to get the default index pattern from the SIEM application. - */ - indexPatterns(): string[] { - return context.services.uiSettings.get(defaultIndexKey); - }, - /** * Used to get the entity_id for an _id. */ diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts index 43282848dcf9a..631eab18fc014 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts @@ -12,7 +12,7 @@ import { import { mockTreeWithNoProcessEvents } from '../../mocks/resolver_tree'; import { DataAccessLayer } from '../../types'; -type EmptiableRequests = 'relatedEvents' | 'resolverTree' | 'entities' | 'indexPatterns'; +type EmptiableRequests = 'relatedEvents' | 'resolverTree' | 'entities'; interface Metadata { /** @@ -66,15 +66,6 @@ export function emptifyMock( : dataAccessLayer.resolverTree(...args); }, - /** - * Get an array of index patterns that contain events. - */ - indexPatterns(...args): string[] { - return dataShouldBeEmpty.includes('indexPatterns') - ? [] - : dataAccessLayer.indexPatterns(...args); - }, - /** * Get entities matching a document. */ diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts index b0407fa5d7c1d..0883a3787fcce 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts @@ -78,13 +78,6 @@ export function noAncestorsTwoChildren(): { dataAccessLayer: DataAccessLayer; me ); }, - /** - * Get an array of index patterns that contain events. - */ - indexPatterns(): string[] { - return ['index pattern']; - }, - /** * Get entities matching a document. */ diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts new file mode 100644 index 0000000000000..ec0fa93485783 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ResolverRelatedEvents, + ResolverTree, + ResolverEntityIndex, +} from '../../../../common/endpoint/types'; +import { mockEndpointEvent } from '../../mocks/endpoint_event'; +import { mockTreeWithNoAncestorsAnd2Children } from '../../mocks/resolver_tree'; +import { DataAccessLayer } from '../../types'; + +interface Metadata { + /** + * The `_id` of the document being analyzed. + */ + databaseDocumentID: string; + /** + * A record of entityIDs to be used in tests assertions. + */ + entityIDs: { + /** + * The entityID of the node related to the document being analyzed. + */ + origin: 'origin'; + /** + * The entityID of the first child of the origin. + */ + firstChild: 'firstChild'; + /** + * The entityID of the second child of the origin. + */ + secondChild: 'secondChild'; + }; +} + +/** + * A mock DataAccessLayer that will return an origin in two children. The `entity` response will be empty unless + * `awesome_index` is passed in the indices array. + */ +export function noAncestorsTwoChildenInIndexCalledAwesomeIndex(): { + dataAccessLayer: DataAccessLayer; + metadata: Metadata; +} { + const metadata: Metadata = { + databaseDocumentID: '_id', + entityIDs: { origin: 'origin', firstChild: 'firstChild', secondChild: 'secondChild' }, + }; + return { + metadata, + dataAccessLayer: { + /** + * Fetch related events for an entity ID + */ + relatedEvents(entityID: string): Promise { + return Promise.resolve({ + entityID, + events: [ + mockEndpointEvent({ + entityID, + name: 'event', + timestamp: 0, + }), + ], + nextEvent: null, + }); + }, + + /** + * Fetch a ResolverTree for a entityID + */ + resolverTree(): Promise { + return Promise.resolve( + mockTreeWithNoAncestorsAnd2Children({ + originID: metadata.entityIDs.origin, + firstChildID: metadata.entityIDs.firstChild, + secondChildID: metadata.entityIDs.secondChild, + }) + ); + }, + + /** + * Get entities matching a document. + */ + entities({ indices }): Promise { + // Only return values if the `indices` array contains exactly `'awesome_index'` + if (indices.length === 1 && indices[0] === 'awesome_index') { + return Promise.resolve([{ entity_id: metadata.entityIDs.origin }]); + } + return Promise.resolve([]); + }, + }, + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts index 01e75e3eefdbf..95ec0cd1a5f77 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts @@ -76,13 +76,6 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOrigin(): { return Promise.resolve(tree); }, - /** - * Get an array of index patterns that contain events. - */ - indexPatterns(): string[] { - return ['index pattern']; - }, - /** * Get entities matching a document. */ diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts index baddcdfd0cd84..6a4955b104b8f 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts @@ -105,13 +105,6 @@ export function pausifyMock({ return dataAccessLayer.resolverTree(...args); }, - /** - * Get an array of index patterns that contain events. - */ - indexPatterns(...args): string[] { - return dataAccessLayer.indexPatterns(...args); - }, - /** * Get entities matching a document. */ diff --git a/x-pack/plugins/security_solution/public/resolver/mocks/tree_fetcher_parameters.ts b/x-pack/plugins/security_solution/public/resolver/mocks/tree_fetcher_parameters.ts new file mode 100644 index 0000000000000..98efb459a0691 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/mocks/tree_fetcher_parameters.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TreeFetcherParameters } from '../types'; + +/** + * A factory for the most basic `TreeFetcherParameters`. Many tests need to provide this even when the values aren't relevant to the test. + */ +export function mockTreeFetcherParameters(): TreeFetcherParameters { + return { + databaseDocumentID: '', + indices: [], + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.test.ts b/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.test.ts new file mode 100644 index 0000000000000..faa4edfccdc35 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TreeFetcherParameters } from '../types'; + +import { equal } from './tree_fetcher_parameters'; +describe('TreeFetcherParameters#equal:', () => { + const cases: Array<[TreeFetcherParameters, TreeFetcherParameters, boolean]> = [ + // different databaseDocumentID + [{ databaseDocumentID: 'a', indices: [] }, { databaseDocumentID: 'b', indices: [] }, false], + // different indices length + [{ databaseDocumentID: 'a', indices: [''] }, { databaseDocumentID: 'a', indices: [] }, false], + // same indices length, different databaseDocumentID + [{ databaseDocumentID: 'a', indices: [''] }, { databaseDocumentID: 'b', indices: [''] }, false], + // 1 item in `indices` + [{ databaseDocumentID: 'b', indices: [''] }, { databaseDocumentID: 'b', indices: [''] }, true], + // 2 item in `indices` + [ + { databaseDocumentID: 'b', indices: ['1', '2'] }, + { databaseDocumentID: 'b', indices: ['1', '2'] }, + true, + ], + // 2 item in `indices`, but order inversed + [ + { databaseDocumentID: 'b', indices: ['2', '1'] }, + { databaseDocumentID: 'b', indices: ['1', '2'] }, + true, + ], + ]; + describe.each(cases)('%p when compared to %p', (first, second, expected) => { + it(`should ${expected ? '' : 'not'}be equal`, () => { + expect(equal(first, second)).toBe(expected); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.ts b/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.ts new file mode 100644 index 0000000000000..d8280c7490901 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/models/tree_fetcher_parameters.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TreeFetcherParameters } from '../types'; + +/** + * Determine if two instances of `TreeFetcherParameters` are equivalent. Use this to determine if + * a change to a `TreeFetcherParameters` warrants invaliding a request or response. + */ +export function equal(param1: TreeFetcherParameters, param2?: TreeFetcherParameters): boolean { + if (!param2) { + return false; + } + if (param1 === param2) { + return true; + } + if (param1.databaseDocumentID !== param2.databaseDocumentID) { + return false; + } + return arraysContainTheSameElements(param1.indices, param2.indices); +} + +function arraysContainTheSameElements(first: unknown[], second: unknown[]): boolean { + if (first === second) { + return true; + } + if (first.length !== second.length) { + return false; + } + const firstSet = new Set(first); + for (let index = 0; index < second.length; index++) { + if (!firstSet.has(second[index])) { + return false; + } + } + return true; +} diff --git a/x-pack/plugins/security_solution/public/resolver/store/actions.ts b/x-pack/plugins/security_solution/public/resolver/store/actions.ts index e03f24d78e2a2..7d71cbd97b9ee 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/actions.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/actions.ts @@ -115,7 +115,7 @@ interface AppReceivedNewExternalProperties { /** * the `_id` of an ES document. This defines the origin of the Resolver graph. */ - databaseDocumentID?: string; + databaseDocumentID: string; /** * An ID that uniquely identifies this Resolver instance from other concurrent Resolvers. */ @@ -125,6 +125,11 @@ interface AppReceivedNewExternalProperties { * The `search` part of the URL of this page. */ locationSearch: string; + + /** + * Indices that the backend will use to find the document. + */ + indices: string[]; }; } diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts index 466c37d4ad5f1..59d1494ae8c27 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts @@ -5,6 +5,7 @@ */ import { ResolverRelatedEvents, ResolverTree } from '../../../../common/endpoint/types'; +import { TreeFetcherParameters } from '../../types'; interface ServerReturnedResolverData { readonly type: 'serverReturnedResolverData'; @@ -14,9 +15,9 @@ interface ServerReturnedResolverData { */ result: ResolverTree; /** - * The database document ID that was used to fetch the resolver tree + * The database parameters that was used to fetch the resolver tree */ - databaseDocumentID: string; + parameters: TreeFetcherParameters; }; } @@ -25,7 +26,7 @@ interface AppRequestedResolverData { /** * entity ID used to make the request. */ - readonly payload: string; + readonly payload: TreeFetcherParameters; } interface ServerFailedToReturnResolverData { @@ -33,7 +34,7 @@ interface ServerFailedToReturnResolverData { /** * entity ID used to make the failed request */ - readonly payload: string; + readonly payload: TreeFetcherParameters; } interface AppAbortedResolverDataRequest { @@ -41,7 +42,7 @@ interface AppAbortedResolverDataRequest { /** * entity ID used to make the aborted request */ - readonly payload: string; + readonly payload: TreeFetcherParameters; } /** diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts index 21c4f92f8e502..e6e525334e818 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts @@ -12,6 +12,7 @@ import { DataState } from '../../types'; import { DataAction } from './action'; import { ResolverChildNode, ResolverTree } from '../../../../common/endpoint/types'; import * as eventModel from '../../../../common/endpoint/models/event'; +import { mockTreeFetcherParameters } from '../../mocks/tree_fetcher_parameters'; /** * Test the data reducer and selector. @@ -27,7 +28,7 @@ describe('Resolver Data Middleware', () => { type: 'serverReturnedResolverData', payload: { result: tree, - databaseDocumentID: '', + parameters: mockTreeFetcherParameters(), }, }; store.dispatch(action); diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts index c43182ddbf835..c8df95aaee6f4 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts @@ -7,34 +7,53 @@ import { Reducer } from 'redux'; import { DataState } from '../../types'; import { ResolverAction } from '../actions'; +import * as treeFetcherParameters from '../../models/tree_fetcher_parameters'; const initialState: DataState = { relatedEvents: new Map(), relatedEventsReady: new Map(), resolverComponentInstanceID: undefined, + tree: {}, }; export const dataReducer: Reducer = (state = initialState, action) => { if (action.type === 'appReceivedNewExternalProperties') { const nextState: DataState = { ...state, - databaseDocumentID: action.payload.databaseDocumentID, + tree: { + ...state.tree, + currentParameters: { + databaseDocumentID: action.payload.databaseDocumentID, + indices: action.payload.indices, + }, + }, resolverComponentInstanceID: action.payload.resolverComponentInstanceID, }; return nextState; } else if (action.type === 'appRequestedResolverData') { // keep track of what we're requesting, this way we know when to request and when not to. - return { + const nextState: DataState = { ...state, - pendingRequestDatabaseDocumentID: action.payload, + tree: { + ...state.tree, + pendingRequestParameters: { + databaseDocumentID: action.payload.databaseDocumentID, + indices: action.payload.indices, + }, + }, }; + return nextState; } else if (action.type === 'appAbortedResolverDataRequest') { - if (action.payload === state.pendingRequestDatabaseDocumentID) { + if (treeFetcherParameters.equal(action.payload, state.tree.pendingRequestParameters)) { // the request we were awaiting was aborted - return { + const nextState: DataState = { ...state, - pendingRequestDatabaseDocumentID: undefined, + tree: { + ...state.tree, + pendingRequestParameters: undefined, + }, }; + return nextState; } else { return state; } @@ -43,29 +62,35 @@ export const dataReducer: Reducer = (state = initialS const nextState: DataState = { ...state, - /** - * Store the last received data, as well as the databaseDocumentID it relates to. - */ - lastResponse: { - result: action.payload.result, - databaseDocumentID: action.payload.databaseDocumentID, - successful: true, - }, + tree: { + ...state.tree, + /** + * Store the last received data, as well as the databaseDocumentID it relates to. + */ + lastResponse: { + result: action.payload.result, + parameters: action.payload.parameters, + successful: true, + }, - // This assumes that if we just received something, there is no longer a pending request. - // This cannot model multiple in-flight requests - pendingRequestDatabaseDocumentID: undefined, + // This assumes that if we just received something, there is no longer a pending request. + // This cannot model multiple in-flight requests + pendingRequestParameters: undefined, + }, }; return nextState; } else if (action.type === 'serverFailedToReturnResolverData') { /** Only handle this if we are expecting a response */ - if (state.pendingRequestDatabaseDocumentID !== undefined) { + if (state.tree.pendingRequestParameters !== undefined) { const nextState: DataState = { ...state, - pendingRequestDatabaseDocumentID: undefined, - lastResponse: { - databaseDocumentID: state.pendingRequestDatabaseDocumentID, - successful: false, + tree: { + ...state.tree, + pendingRequestParameters: undefined, + lastResponse: { + parameters: state.tree.pendingRequestParameters, + successful: false, + }, }, }; return nextState; @@ -76,16 +101,18 @@ export const dataReducer: Reducer = (state = initialS action.type === 'userRequestedRelatedEventData' || action.type === 'appDetectedMissingEventData' ) { - return { + const nextState: DataState = { ...state, relatedEventsReady: new Map([...state.relatedEventsReady, [action.payload, false]]), }; + return nextState; } else if (action.type === 'serverReturnedRelatedEventData') { - return { + const nextState: DataState = { ...state, relatedEventsReady: new Map([...state.relatedEventsReady, [action.payload.entityID, true]]), relatedEvents: new Map([...state.relatedEvents, [action.payload.entityID, action.payload]]), }; + return nextState; } else { return state; } diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts index dc478ede72790..539325faffdf0 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts @@ -18,6 +18,7 @@ import { } from '../../mocks/resolver_tree'; import { uniquePidForProcess } from '../../models/process_event'; import { EndpointEvent } from '../../../../common/endpoint/types'; +import { mockTreeFetcherParameters } from '../../mocks/tree_fetcher_parameters'; describe('data state', () => { let actions: ResolverAction[] = []; @@ -39,29 +40,32 @@ describe('data state', () => { */ const viewAsAString = (dataState: DataState) => { return [ - ['is loading', selectors.isLoading(dataState)], - ['has an error', selectors.hasError(dataState)], + ['is loading', selectors.isTreeLoading(dataState)], + ['has an error', selectors.hadErrorLoadingTree(dataState)], ['has more children', selectors.hasMoreChildren(dataState)], ['has more ancestors', selectors.hasMoreAncestors(dataState)], - ['document to fetch', selectors.databaseDocumentIDToFetch(dataState)], - ['requires a pending request to be aborted', selectors.databaseDocumentIDToAbort(dataState)], + ['parameters to fetch', selectors.treeParametersToFetch(dataState)], + [ + 'requires a pending request to be aborted', + selectors.treeRequestParametersToAbort(dataState), + ], ] .map(([message, value]) => `${message}: ${JSON.stringify(value)}`) .join('\n'); }; - it(`shouldn't initially be loading, or have an error, or have more children or ancestors, or have a document to fetch, or have a pending request that needs to be aborted.`, () => { + it(`shouldn't initially be loading, or have an error, or have more children or ancestors, or have a request to make, or have a pending request that needs to be aborted.`, () => { expect(viewAsAString(state())).toMatchInlineSnapshot(` "is loading: false has an error: false has more children: false has more ancestors: false - document to fetch: null + parameters to fetch: null requires a pending request to be aborted: null" `); }); - describe('when there is a databaseDocumentID but no pending request', () => { + describe('when there are parameters to fetch but no pending request', () => { const databaseDocumentID = 'databaseDocumentID'; const resolverComponentInstanceID = 'resolverComponentInstanceID'; beforeEach(() => { @@ -74,12 +78,13 @@ describe('data state', () => { // `locationSearch` doesn't matter for this test locationSearch: '', + indices: [], }, }, ]; }); - it('should need to fetch the databaseDocumentID', () => { - expect(selectors.databaseDocumentIDToFetch(state())).toBe(databaseDocumentID); + it('should need to request the tree', () => { + expect(selectors.treeParametersToFetch(state())?.databaseDocumentID).toBe(databaseDocumentID); }); it('should not be loading, have an error, have more children or ancestors, or have a pending request that needs to be aborted.', () => { expect(viewAsAString(state())).toMatchInlineSnapshot(` @@ -87,39 +92,41 @@ describe('data state', () => { has an error: false has more children: false has more ancestors: false - document to fetch: \\"databaseDocumentID\\" + parameters to fetch: {\\"databaseDocumentID\\":\\"databaseDocumentID\\",\\"indices\\":[]} requires a pending request to be aborted: null" `); }); }); - describe('when there is a pending request but no databaseDocumentID', () => { + describe('when there is a pending request but no current tree fetching parameters', () => { const databaseDocumentID = 'databaseDocumentID'; beforeEach(() => { actions = [ { type: 'appRequestedResolverData', - payload: databaseDocumentID, + payload: { databaseDocumentID, indices: [] }, }, ]; }); it('should be loading', () => { - expect(selectors.isLoading(state())).toBe(true); + expect(selectors.isTreeLoading(state())).toBe(true); }); it('should have a request to abort', () => { - expect(selectors.databaseDocumentIDToAbort(state())).toBe(databaseDocumentID); + expect(selectors.treeRequestParametersToAbort(state())?.databaseDocumentID).toBe( + databaseDocumentID + ); }); - it('should not have an error, more children, more ancestors, or a document to fetch.', () => { + it('should not have an error, more children, more ancestors, or request to make.', () => { expect(viewAsAString(state())).toMatchInlineSnapshot(` "is loading: true has an error: false has more children: false has more ancestors: false - document to fetch: null - requires a pending request to be aborted: \\"databaseDocumentID\\"" + parameters to fetch: null + requires a pending request to be aborted: {\\"databaseDocumentID\\":\\"databaseDocumentID\\",\\"indices\\":[]}" `); }); }); - describe('when there is a pending request for the current databaseDocumentID', () => { + describe('when there is a pending request that was made using the current parameters', () => { const databaseDocumentID = 'databaseDocumentID'; const resolverComponentInstanceID = 'resolverComponentInstanceID'; beforeEach(() => { @@ -132,27 +139,28 @@ describe('data state', () => { // `locationSearch` doesn't matter for this test locationSearch: '', + indices: [], }, }, { type: 'appRequestedResolverData', - payload: databaseDocumentID, + payload: { databaseDocumentID, indices: [] }, }, ]; }); it('should be loading', () => { - expect(selectors.isLoading(state())).toBe(true); + expect(selectors.isTreeLoading(state())).toBe(true); }); it('should not have a request to abort', () => { - expect(selectors.databaseDocumentIDToAbort(state())).toBe(null); + expect(selectors.treeRequestParametersToAbort(state())).toBe(null); }); - it('should not have an error, more children, more ancestors, a document to begin fetching, or a pending request that should be aborted.', () => { + it('should not have an error, more children, more ancestors, a request to make, or a pending request that should be aborted.', () => { expect(viewAsAString(state())).toMatchInlineSnapshot(` "is loading: true has an error: false has more children: false has more ancestors: false - document to fetch: null + parameters to fetch: null requires a pending request to be aborted: null" `); }); @@ -160,28 +168,28 @@ describe('data state', () => { beforeEach(() => { actions.push({ type: 'serverFailedToReturnResolverData', - payload: databaseDocumentID, + payload: { databaseDocumentID, indices: [] }, }); }); it('should not be loading', () => { - expect(selectors.isLoading(state())).toBe(false); + expect(selectors.isTreeLoading(state())).toBe(false); }); it('should have an error', () => { - expect(selectors.hasError(state())).toBe(true); + expect(selectors.hadErrorLoadingTree(state())).toBe(true); }); - it('should not be loading, have more children, have more ancestors, have a document to fetch, or have a pending request that needs to be aborted.', () => { + it('should not be loading, have more children, have more ancestors, have a request to make, or have a pending request that needs to be aborted.', () => { expect(viewAsAString(state())).toMatchInlineSnapshot(` "is loading: false has an error: true has more children: false has more ancestors: false - document to fetch: null + parameters to fetch: null requires a pending request to be aborted: null" `); }); }); }); - describe('when there is a pending request for a different databaseDocumentID than the current one', () => { + describe('when there is a pending request that was made with parameters that are different than the current tree fetching parameters', () => { const firstDatabaseDocumentID = 'first databaseDocumentID'; const secondDatabaseDocumentID = 'second databaseDocumentID'; const resolverComponentInstanceID1 = 'resolverComponentInstanceID1'; @@ -196,12 +204,13 @@ describe('data state', () => { resolverComponentInstanceID: resolverComponentInstanceID1, // `locationSearch` doesn't matter for this test locationSearch: '', + indices: [], }, }, // this happens when the middleware starts the request { type: 'appRequestedResolverData', - payload: firstDatabaseDocumentID, + payload: { databaseDocumentID: firstDatabaseDocumentID, indices: [] }, }, // receive a different databaseDocumentID. this should cause the middleware to abort the existing request and start a new one { @@ -211,18 +220,23 @@ describe('data state', () => { resolverComponentInstanceID: resolverComponentInstanceID2, // `locationSearch` doesn't matter for this test locationSearch: '', + indices: [], }, }, ]; }); it('should be loading', () => { - expect(selectors.isLoading(state())).toBe(true); + expect(selectors.isTreeLoading(state())).toBe(true); }); - it('should need to fetch the second databaseDocumentID', () => { - expect(selectors.databaseDocumentIDToFetch(state())).toBe(secondDatabaseDocumentID); + it('should need to request the tree using the second set of parameters', () => { + expect(selectors.treeParametersToFetch(state())?.databaseDocumentID).toBe( + secondDatabaseDocumentID + ); }); it('should need to abort the request for the databaseDocumentID', () => { - expect(selectors.databaseDocumentIDToFetch(state())).toBe(secondDatabaseDocumentID); + expect(selectors.treeParametersToFetch(state())?.databaseDocumentID).toBe( + secondDatabaseDocumentID + ); }); it('should use the correct location for the second resolver', () => { expect(selectors.resolverComponentInstanceID(state())).toBe(resolverComponentInstanceID2); @@ -233,25 +247,27 @@ describe('data state', () => { has an error: false has more children: false has more ancestors: false - document to fetch: \\"second databaseDocumentID\\" - requires a pending request to be aborted: \\"first databaseDocumentID\\"" + parameters to fetch: {\\"databaseDocumentID\\":\\"second databaseDocumentID\\",\\"indices\\":[]} + requires a pending request to be aborted: {\\"databaseDocumentID\\":\\"first databaseDocumentID\\",\\"indices\\":[]}" `); }); describe('and when the old request was aborted', () => { beforeEach(() => { actions.push({ type: 'appAbortedResolverDataRequest', - payload: firstDatabaseDocumentID, + payload: { databaseDocumentID: firstDatabaseDocumentID, indices: [] }, }); }); it('should not require a pending request to be aborted', () => { - expect(selectors.databaseDocumentIDToAbort(state())).toBe(null); + expect(selectors.treeRequestParametersToAbort(state())).toBe(null); }); it('should have a document to fetch', () => { - expect(selectors.databaseDocumentIDToFetch(state())).toBe(secondDatabaseDocumentID); + expect(selectors.treeParametersToFetch(state())?.databaseDocumentID).toBe( + secondDatabaseDocumentID + ); }); it('should not be loading', () => { - expect(selectors.isLoading(state())).toBe(false); + expect(selectors.isTreeLoading(state())).toBe(false); }); it('should not have an error, more children, or more ancestors.', () => { expect(viewAsAString(state())).toMatchInlineSnapshot(` @@ -259,7 +275,7 @@ describe('data state', () => { has an error: false has more children: false has more ancestors: false - document to fetch: \\"second databaseDocumentID\\" + parameters to fetch: {\\"databaseDocumentID\\":\\"second databaseDocumentID\\",\\"indices\\":[]} requires a pending request to be aborted: null" `); }); @@ -267,14 +283,14 @@ describe('data state', () => { beforeEach(() => { actions.push({ type: 'appRequestedResolverData', - payload: secondDatabaseDocumentID, + payload: { databaseDocumentID: secondDatabaseDocumentID, indices: [] }, }); }); it('should not have a document ID to fetch', () => { - expect(selectors.databaseDocumentIDToFetch(state())).toBe(null); + expect(selectors.treeParametersToFetch(state())).toBe(null); }); it('should be loading', () => { - expect(selectors.isLoading(state())).toBe(true); + expect(selectors.isTreeLoading(state())).toBe(true); }); it('should not have an error, more children, more ancestors, or a pending request that needs to be aborted.', () => { expect(viewAsAString(state())).toMatchInlineSnapshot(` @@ -282,7 +298,7 @@ describe('data state', () => { has an error: false has more children: false has more ancestors: false - document to fetch: null + parameters to fetch: null requires a pending request to be aborted: null" `); }); @@ -303,7 +319,7 @@ describe('data state', () => { secondAncestorID, }), // this value doesn't matter - databaseDocumentID: '', + parameters: mockTreeFetcherParameters(), }, }); }); @@ -331,7 +347,7 @@ describe('data state', () => { secondAncestorID, }), // this value doesn't matter - databaseDocumentID: '', + parameters: mockTreeFetcherParameters(), }, }); }); @@ -355,7 +371,7 @@ describe('data state', () => { payload: { result: mockTreeWithNoAncestorsAnd2Children({ originID, firstChildID, secondChildID }), // this value doesn't matter - databaseDocumentID: '', + parameters: mockTreeFetcherParameters(), }, }); }); @@ -386,7 +402,7 @@ describe('data state', () => { payload: { result: tree, // this value doesn't matter - databaseDocumentID: '', + parameters: mockTreeFetcherParameters(), }, }); }); @@ -417,7 +433,7 @@ describe('data state', () => { payload: { result: tree, // this value doesn't matter - databaseDocumentID: '', + parameters: mockTreeFetcherParameters(), }, }); }); @@ -433,7 +449,7 @@ describe('data state', () => { payload: { result: tree, // this value doesn't matter - databaseDocumentID: '', + parameters: mockTreeFetcherParameters(), }, }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index eaa80b46471fa..e647828ddb606 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -15,6 +15,7 @@ import { AABB, VisibleEntites, SectionData, + TreeFetcherParameters, } from '../../types'; import { isGraphableProcess, @@ -34,6 +35,7 @@ import { LegacyEndpointEvent, } from '../../../../common/endpoint/types'; import * as resolverTreeModel from '../../models/resolver_tree'; +import * as treeFetcherParametersModel from '../../models/tree_fetcher_parameters'; import * as isometricTaxiLayoutModel from '../../models/indexed_process_tree/isometric_taxi_layout'; import * as eventModel from '../../../../common/endpoint/models/event'; import * as vector2 from '../../models/vector2'; @@ -42,26 +44,25 @@ import { formatDate } from '../../view/panels/panel_content_utilities'; /** * If there is currently a request. */ -export function isLoading(state: DataState): boolean { - return state.pendingRequestDatabaseDocumentID !== undefined; +export function isTreeLoading(state: DataState): boolean { + return state.tree.pendingRequestParameters !== undefined; } /** - * A string for uniquely identifying the instance of resolver within the app. + * If a request was made and it threw an error or returned a failure response code. */ -export function resolverComponentInstanceID(state: DataState): string { - return state.resolverComponentInstanceID ? state.resolverComponentInstanceID : ''; +export function hadErrorLoadingTree(state: DataState): boolean { + if (state.tree.lastResponse) { + return !state.tree.lastResponse.successful; + } + return false; } /** - * If a request was made and it threw an error or returned a failure response code. + * A string for uniquely identifying the instance of resolver within the app. */ -export function hasError(state: DataState): boolean { - if (state.lastResponse && state.lastResponse.successful === false) { - return true; - } else { - return false; - } +export function resolverComponentInstanceID(state: DataState): string { + return state.resolverComponentInstanceID ? state.resolverComponentInstanceID : ''; } /** @@ -69,11 +70,7 @@ export function hasError(state: DataState): boolean { * we're currently interested in. */ const resolverTreeResponse = (state: DataState): ResolverTree | undefined => { - if (state.lastResponse && state.lastResponse.successful) { - return state.lastResponse.result; - } else { - return undefined; - } + return state.tree.lastResponse?.successful ? state.tree.lastResponse.result : undefined; }; /** @@ -458,18 +455,24 @@ export const relatedEventInfoByEntityId: ( ); /** - * If we need to fetch, this is the ID to fetch. + * If the tree resource needs to be fetched then these are the parameters that should be used. */ -export function databaseDocumentIDToFetch(state: DataState): string | null { - // If there is an ID, it must match either the last received version, or the pending version. - // Otherwise, we need to fetch it - // NB: this technique will not allow for refreshing of data. +export function treeParametersToFetch(state: DataState): TreeFetcherParameters | null { + /** + * If there are current tree parameters that don't match the parameters used in the pending request (if there is a pending request) and that don't match the parameters used in the last completed request (if there was a last completed request) then we need to fetch the tree resource using the current parameters. + */ if ( - state.databaseDocumentID !== undefined && - state.databaseDocumentID !== state.pendingRequestDatabaseDocumentID && - state.databaseDocumentID !== state.lastResponse?.databaseDocumentID + state.tree.currentParameters !== undefined && + !treeFetcherParametersModel.equal( + state.tree.currentParameters, + state.tree.lastResponse?.parameters + ) && + !treeFetcherParametersModel.equal( + state.tree.currentParameters, + state.tree.pendingRequestParameters + ) ) { - return state.databaseDocumentID; + return state.tree.currentParameters; } else { return null; } @@ -692,15 +695,18 @@ export const nodesAndEdgelines: ( /** * If there is a pending request that's for a entity ID that doesn't matche the `entityID`, then we should cancel it. */ -export function databaseDocumentIDToAbort(state: DataState): string | null { +export function treeRequestParametersToAbort(state: DataState): TreeFetcherParameters | null { /** - * If there is a pending request, and its not for the current databaseDocumentID (even, if the current databaseDocumentID is undefined) then we should abort the request. + * If there is a pending request, and its not for the current parameters (even, if the current parameters are undefined) then we should abort the request. */ if ( - state.pendingRequestDatabaseDocumentID !== undefined && - state.pendingRequestDatabaseDocumentID !== state.databaseDocumentID + state.tree.pendingRequestParameters !== undefined && + !treeFetcherParametersModel.equal( + state.tree.pendingRequestParameters, + state.tree.currentParameters + ) ) { - return state.pendingRequestDatabaseDocumentID; + return state.tree.pendingRequestParameters; } else { return null; } diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts index e91c455c9445f..28948debae891 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts @@ -12,6 +12,7 @@ import { LegacyEndpointEvent, ResolverEvent } from '../../../../common/endpoint/ import { visibleNodesAndEdgeLines } from '../selectors'; import { mockProcessEvent } from '../../models/process_event_test_helpers'; import { mock as mockResolverTree } from '../../models/resolver_tree'; +import { mockTreeFetcherParameters } from '../../mocks/tree_fetcher_parameters'; describe('resolver visible entities', () => { let processA: LegacyEndpointEvent; @@ -112,7 +113,7 @@ describe('resolver visible entities', () => { ]; const action: ResolverAction = { type: 'serverReturnedResolverData', - payload: { result: mockResolverTree({ events })!, databaseDocumentID: '' }, + payload: { result: mockResolverTree({ events })!, parameters: mockTreeFetcherParameters() }, }; const cameraAction: ResolverAction = { type: 'userSetRasterSize', payload: [300, 200] }; store.dispatch(action); @@ -140,7 +141,7 @@ describe('resolver visible entities', () => { ]; const action: ResolverAction = { type: 'serverReturnedResolverData', - payload: { result: mockResolverTree({ events })!, databaseDocumentID: '' }, + payload: { result: mockResolverTree({ events })!, parameters: mockTreeFetcherParameters() }, }; const cameraAction: ResolverAction = { type: 'userSetRasterSize', payload: [2000, 2000] }; store.dispatch(action); diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts index 0ec340efbdac9..ef4ca2380ebf4 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/resolver_tree_fetcher.ts @@ -28,32 +28,31 @@ export function ResolverTreeFetcher( // if the entityID changes while return async () => { const state = api.getState(); - const databaseDocumentIDToFetch = selectors.databaseDocumentIDToFetch(state); + const databaseParameters = selectors.treeParametersToFetch(state); - if (selectors.databaseDocumentIDToAbort(state) && lastRequestAbortController) { + if (selectors.treeRequestParametersToAbort(state) && lastRequestAbortController) { lastRequestAbortController.abort(); // calling abort will cause an action to be fired - } else if (databaseDocumentIDToFetch !== null) { + } else if (databaseParameters !== null) { lastRequestAbortController = new AbortController(); let result: ResolverTree | undefined; // Inform the state that we've made the request. Without this, the middleware will try to make the request again // immediately. api.dispatch({ type: 'appRequestedResolverData', - payload: databaseDocumentIDToFetch, + payload: databaseParameters, }); try { - const indices: string[] = dataAccessLayer.indexPatterns(); const matchingEntities: ResolverEntityIndex = await dataAccessLayer.entities({ - _id: databaseDocumentIDToFetch, - indices, + _id: databaseParameters.databaseDocumentID, + indices: databaseParameters.indices ?? [], signal: lastRequestAbortController.signal, }); if (matchingEntities.length < 1) { // If no entity_id could be found for the _id, bail out with a failure. api.dispatch({ type: 'serverFailedToReturnResolverData', - payload: databaseDocumentIDToFetch, + payload: databaseParameters, }); return; } @@ -67,12 +66,12 @@ export function ResolverTreeFetcher( if (error instanceof DOMException && error.name === 'AbortError') { api.dispatch({ type: 'appAbortedResolverDataRequest', - payload: databaseDocumentIDToFetch, + payload: databaseParameters, }); } else { api.dispatch({ type: 'serverFailedToReturnResolverData', - payload: databaseDocumentIDToFetch, + payload: databaseParameters, }); } } @@ -81,7 +80,7 @@ export function ResolverTreeFetcher( type: 'serverReturnedResolverData', payload: { result, - databaseDocumentID: databaseDocumentIDToFetch, + parameters: databaseParameters, }, }); } diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts index f113e861d3ce9..d15274f0363ac 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.test.ts @@ -14,6 +14,7 @@ import { mockTreeWithNoAncestorsAnd2Children, } from '../mocks/resolver_tree'; import { SafeResolverEvent } from '../../../common/endpoint/types'; +import { mockTreeFetcherParameters } from '../mocks/tree_fetcher_parameters'; describe('resolver selectors', () => { const actions: ResolverAction[] = []; @@ -43,7 +44,7 @@ describe('resolver selectors', () => { secondAncestorID, }), // this value doesn't matter - databaseDocumentID: '', + parameters: mockTreeFetcherParameters(), }, }); }); @@ -77,7 +78,7 @@ describe('resolver selectors', () => { payload: { result: mockTreeWithNoAncestorsAnd2Children({ originID, firstChildID, secondChildID }), // this value doesn't matter - databaseDocumentID: '', + parameters: mockTreeFetcherParameters(), }, }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index bdea08df3d7f5..8ea0bc9199cb6 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -84,14 +84,14 @@ export const layout: (state: ResolverState) => IsometricTaxiLayout = composeSele /** * If we need to fetch, this is the entity ID to fetch. */ -export const databaseDocumentIDToFetch = composeSelectors( +export const treeParametersToFetch = composeSelectors( dataStateSelector, - dataSelectors.databaseDocumentIDToFetch + dataSelectors.treeParametersToFetch ); -export const databaseDocumentIDToAbort = composeSelectors( +export const treeRequestParametersToAbort = composeSelectors( dataStateSelector, - dataSelectors.databaseDocumentIDToAbort + dataSelectors.treeRequestParametersToAbort ); export const resolverComponentInstanceID = composeSelectors( @@ -207,12 +207,15 @@ function uiStateSelector(state: ResolverState) { /** * Whether or not the resolver is pending fetching data */ -export const isLoading = composeSelectors(dataStateSelector, dataSelectors.isLoading); +export const isTreeLoading = composeSelectors(dataStateSelector, dataSelectors.isTreeLoading); /** * Whether or not the resolver encountered an error while fetching data */ -export const hasError = composeSelectors(dataStateSelector, dataSelectors.hasError); +export const hadErrorLoadingTree = composeSelectors( + dataStateSelector, + dataSelectors.hadErrorLoadingTree +); /** * True if the children cursor is not null diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index a6520c8f0e06f..9d10d1c2b64a7 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -49,6 +49,7 @@ export class Simulator { dataAccessLayer, resolverComponentInstanceID, databaseDocumentID, + indices, history, }: { /** @@ -59,10 +60,14 @@ export class Simulator { * A string that uniquely identifies this Resolver instance among others mounted in the DOM. */ resolverComponentInstanceID: string; + /** + * Indices that the backend would use to find the document ID. + */ + indices: string[]; /** * a databaseDocumentID to pass to Resolver. Resolver will use this in requests to the mock data layer. */ - databaseDocumentID?: string; + databaseDocumentID: string; history?: HistoryPackageHistoryInterface; }) { // create the spy middleware (for debugging tests) @@ -99,6 +104,7 @@ export class Simulator { store={this.store} coreStart={coreStart} databaseDocumentID={databaseDocumentID} + indices={indices} /> ); } @@ -124,6 +130,20 @@ export class Simulator { this.wrapper.setProps({ resolverComponentInstanceID: value }); } + /** + * Change the indices (updates the React component props.) + */ + public set indices(value: string[]) { + this.wrapper.setProps({ indices: value }); + } + + /** + * Get the indices (updates the React component props.) + */ + public get indices(): string[] { + return this.wrapper.prop('indices'); + } + /** * Call this to console.log actions (and state). Use this to debug your tests. * State and actions aren't exposed otherwise because the tests using this simulator should diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx index 5d5a414761dbf..89218e9fca8ce 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/mock_resolver.tsx @@ -99,6 +99,7 @@ export const MockResolver = React.memo((props: MockResolverProps) => { ref={resolverRef} databaseDocumentID={props.databaseDocumentID} resolverComponentInstanceID={props.resolverComponentInstanceID} + indices={props.indices} /> diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index e8304bf838e2d..1e7e2a8eba8a8 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -194,53 +194,68 @@ export interface VisibleEntites { connectingEdgeLineSegments: EdgeLineSegment[]; } +export interface TreeFetcherParameters { + /** + * The `_id` for an ES document. Used to select a process that we'll show the graph for. + */ + databaseDocumentID: string; + + /** + * The indices that the backend will use to search for the document ID. + */ + indices: string[]; +} + /** * State for `data` reducer which handles receiving Resolver data from the back-end. */ export interface DataState { readonly relatedEvents: Map; readonly relatedEventsReady: Map; - /** - * The `_id` for an ES document. Used to select a process that we'll show the graph for. - */ - readonly databaseDocumentID?: string; - /** - * The id used for the pending request, if there is one. - */ - readonly pendingRequestDatabaseDocumentID?: string; + + readonly tree: { + /** + * The parameters passed from the resolver properties + */ + readonly currentParameters?: TreeFetcherParameters; + + /** + * The id used for the pending request, if there is one. + */ + readonly pendingRequestParameters?: TreeFetcherParameters; + /** + * The parameters and response from the last successful request. + */ + readonly lastResponse?: { + /** + * The id used in the request. + */ + readonly parameters: TreeFetcherParameters; + } & ( + | { + /** + * If a response with a success code was received, this is `true`. + */ + readonly successful: true; + /** + * The ResolverTree parsed from the response. + */ + readonly result: ResolverTree; + } + | { + /** + * If the request threw an exception or the response had a failure code, this will be false. + */ + readonly successful: false; + } + ); + }; /** * An ID that is used to differentiate this Resolver instance from others concurrently running on the same page. * Used to prevent collisions in things like query parameters. */ readonly resolverComponentInstanceID?: string; - - /** - * The parameters and response from the last successful request. - */ - readonly lastResponse?: { - /** - * The id used in the request. - */ - readonly databaseDocumentID: string; - } & ( - | { - /** - * If a response with a success code was received, this is `true`. - */ - readonly successful: true; - /** - * The ResolverTree parsed from the response. - */ - readonly result: ResolverTree; - } - | { - /** - * If the request threw an exception or the response had a failure code, this will be false. - */ - readonly successful: false; - } - ); } /** @@ -494,11 +509,6 @@ export interface DataAccessLayer { */ resolverTree: (entityID: string, signal: AbortSignal) => Promise; - /** - * Get an array of index patterns that contain events. - */ - indexPatterns: () => string[]; - /** * Get entities matching a document. */ @@ -524,13 +534,18 @@ export interface ResolverProps { * The `_id` value of an event in ES. * Used as the origin of the Resolver graph. */ - databaseDocumentID?: string; + databaseDocumentID: string; /** * An ID that is used to differentiate this Resolver instance from others concurrently running on the same page. * Used to prevent collisions in things like query parameters. */ resolverComponentInstanceID: string; + + /** + * Indices that the backend should use to find the originating document. + */ + indices: string[]; } /** diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx index 1e5ac093cac77..223ce728f4267 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { noAncestorsTwoChildenInIndexCalledAwesomeIndex } from '../data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index'; import { noAncestorsTwoChildren } from '../data_access_layer/mocks/no_ancestors_two_children'; import { Simulator } from '../test_utilities/simulator'; // Extend jest with a custom matcher @@ -19,6 +20,62 @@ let entityIDs: { origin: string; firstChild: string; secondChild: string }; // the resolver component instance ID, used by the react code to distinguish piece of global state from those used by other resolver instances const resolverComponentInstanceID = 'resolverComponentInstanceID'; +describe("Resolver, when rendered with the `indices` prop set to `[]` and the `databaseDocumentID` prop set to `_id`, and when the document is found in an index called 'awesome_index'", () => { + beforeEach(async () => { + // create a mock data access layer + const { + metadata: dataAccessLayerMetadata, + dataAccessLayer, + } = noAncestorsTwoChildenInIndexCalledAwesomeIndex(); + + // save a reference to the entity IDs exposed by the mock data layer + entityIDs = dataAccessLayerMetadata.entityIDs; + + // save a reference to the `_id` supported by the mock data layer + databaseDocumentID = dataAccessLayerMetadata.databaseDocumentID; + + // create a resolver simulator, using the data access layer and an arbitrary component instance ID + simulator = new Simulator({ + databaseDocumentID, + dataAccessLayer, + resolverComponentInstanceID, + indices: [], + }); + }); + + it('should render no processes', async () => { + await expect( + simulator.map(() => ({ + processes: simulator.processNodeElements().length, + })) + ).toYieldEqualTo({ + processes: 0, + }); + }); + + describe("when rerendered with the `indices` prop set to `['awesome_index'`]", () => { + beforeEach(async () => { + simulator.indices = ['awesome_index']; + }); + // Combining assertions here for performance. Unfortunately, Enzyme + jsdom + React is slow. + it(`should have 3 nodes, with the entityID's 'origin', 'firstChild', and 'secondChild'. 'origin' should be selected when the simulator has the right indices`, async () => { + await expect( + simulator.map(() => ({ + selectedOriginCount: simulator.selectedProcessNode(entityIDs.origin).length, + unselectedFirstChildCount: simulator.unselectedProcessNode(entityIDs.firstChild).length, + unselectedSecondChildCount: simulator.unselectedProcessNode(entityIDs.secondChild).length, + nodePrimaryButtonCount: simulator.testSubject('resolver:node:primary-button').length, + })) + ).toYieldEqualTo({ + selectedOriginCount: 1, + unselectedFirstChildCount: 1, + unselectedSecondChildCount: 1, + nodePrimaryButtonCount: 3, + }); + }); + }); +}); + describe('Resolver, when analyzing a tree that has no ancestors and 2 children', () => { beforeEach(async () => { // create a mock data access layer @@ -31,7 +88,12 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children', databaseDocumentID = dataAccessLayerMetadata.databaseDocumentID; // create a resolver simulator, using the data access layer and an arbitrary component instance ID - simulator = new Simulator({ databaseDocumentID, dataAccessLayer, resolverComponentInstanceID }); + simulator = new Simulator({ + databaseDocumentID, + dataAccessLayer, + resolverComponentInstanceID, + indices: [], + }); }); describe('when it has loaded', () => { @@ -159,7 +221,12 @@ describe('Resolver, when analyzing a tree that has two related events for the or databaseDocumentID = dataAccessLayerMetadata.databaseDocumentID; // create a resolver simulator, using the data access layer and an arbitrary component instance ID - simulator = new Simulator({ databaseDocumentID, dataAccessLayer, resolverComponentInstanceID }); + simulator = new Simulator({ + databaseDocumentID, + dataAccessLayer, + resolverComponentInstanceID, + indices: [], + }); }); describe('when it has loaded', () => { diff --git a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx index 6497cc2971980..95fe68d95d702 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.test.tsx @@ -34,6 +34,7 @@ describe('graph controls: when relsover is loaded with an origin node', () => { dataAccessLayer, databaseDocumentID, resolverComponentInstanceID, + indices: [], }); originEntityID = entityIDs.origin; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx index 1add907ae933d..7021e476e6439 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx @@ -49,6 +49,7 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children, an dataAccessLayer, resolverComponentInstanceID, history: memoryHistory, + indices: [], }); return simulatorInstance; } diff --git a/x-pack/plugins/security_solution/public/resolver/view/query_params.test.ts b/x-pack/plugins/security_solution/public/resolver/view/query_params.test.ts index a86237e0e2b45..e42de5009a0f1 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/query_params.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/query_params.test.ts @@ -29,7 +29,12 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children', databaseDocumentID = dataAccessLayerMetadata.databaseDocumentID; // create a resolver simulator, using the data access layer and an arbitrary component instance ID - simulator = new Simulator({ databaseDocumentID, dataAccessLayer, resolverComponentInstanceID }); + simulator = new Simulator({ + databaseDocumentID, + dataAccessLayer, + resolverComponentInstanceID, + indices: [], + }); }); describe("when the second child node's first button has been clicked", () => { diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx index c357ee18acfeb..d8d8de640d786 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_loading_state.test.tsx @@ -26,6 +26,7 @@ describe('Resolver: data loading and resolution states', () => { dataAccessLayer, databaseDocumentID, resolverComponentInstanceID, + indices: [], }); }); @@ -56,6 +57,7 @@ describe('Resolver: data loading and resolution states', () => { dataAccessLayer, databaseDocumentID, resolverComponentInstanceID, + indices: [], }); }); @@ -85,6 +87,7 @@ describe('Resolver: data loading and resolution states', () => { dataAccessLayer, databaseDocumentID, resolverComponentInstanceID, + indices: [], }); }); @@ -114,6 +117,7 @@ describe('Resolver: data loading and resolution states', () => { dataAccessLayer, databaseDocumentID, resolverComponentInstanceID, + indices: [], }); }); @@ -145,6 +149,7 @@ describe('Resolver: data loading and resolution states', () => { dataAccessLayer, databaseDocumentID, resolverComponentInstanceID, + indices: [], }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx index aa845e7283ebe..f4d471b384b35 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx @@ -31,7 +31,7 @@ export const ResolverWithoutProviders = React.memo( * Use `forwardRef` so that the `Simulator` used in testing can access the top level DOM element. */ React.forwardRef(function ( - { className, databaseDocumentID, resolverComponentInstanceID }: ResolverProps, + { className, databaseDocumentID, resolverComponentInstanceID, indices }: ResolverProps, refToForward ) { useResolverQueryParamCleaner(); @@ -39,7 +39,7 @@ export const ResolverWithoutProviders = React.memo( * This is responsible for dispatching actions that include any external data. * `databaseDocumentID` */ - useStateSyncingActions({ databaseDocumentID, resolverComponentInstanceID }); + useStateSyncingActions({ databaseDocumentID, resolverComponentInstanceID, indices }); const { timestamp } = useContext(SideEffectContext); @@ -69,8 +69,8 @@ export const ResolverWithoutProviders = React.memo( }, [cameraRef, refToForward] ); - const isLoading = useSelector(selectors.isLoading); - const hasError = useSelector(selectors.hasError); + const isLoading = useSelector(selectors.isTreeLoading); + const hasError = useSelector(selectors.hadErrorLoadingTree); const activeDescendantId = useSelector(selectors.ariaActiveDescendant); const { colorMap } = useResolverTheme(); diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx index 630ee2f7ff7f0..495cd238d22fc 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx @@ -20,6 +20,7 @@ import { mock as mockResolverTree } from '../models/resolver_tree'; import { ResolverAction } from '../store/actions'; import { createStore } from 'redux'; import { resolverReducer } from '../store/reducer'; +import { mockTreeFetcherParameters } from '../mocks/tree_fetcher_parameters'; describe('useCamera on an unpainted element', () => { let element: HTMLElement; @@ -181,7 +182,7 @@ describe('useCamera on an unpainted element', () => { if (tree !== null) { const serverResponseAction: ResolverAction = { type: 'serverReturnedResolverData', - payload: { result: tree, databaseDocumentID: '' }, + payload: { result: tree, parameters: mockTreeFetcherParameters() }, }; act(() => { store.dispatch(serverResponseAction); diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_state_syncing_actions.ts b/x-pack/plugins/security_solution/public/resolver/view/use_state_syncing_actions.ts index eaba4438bb1fe..7f3cdcbec76a2 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_state_syncing_actions.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_state_syncing_actions.ts @@ -15,19 +15,21 @@ import { useResolverDispatch } from './use_resolver_dispatch'; export function useStateSyncingActions({ databaseDocumentID, resolverComponentInstanceID, + indices, }: { /** * The `_id` of an event in ES. Used to determine the origin of the Resolver graph. */ - databaseDocumentID?: string; + databaseDocumentID: string; resolverComponentInstanceID: string; + indices: string[]; }) { const dispatch = useResolverDispatch(); const locationSearch = useLocation().search; useLayoutEffect(() => { dispatch({ type: 'appReceivedNewExternalProperties', - payload: { databaseDocumentID, resolverComponentInstanceID, locationSearch }, + payload: { databaseDocumentID, resolverComponentInstanceID, locationSearch, indices }, }); - }, [dispatch, databaseDocumentID, resolverComponentInstanceID, locationSearch]); + }, [dispatch, databaseDocumentID, resolverComponentInstanceID, locationSearch, indices]); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx index ededf70152968..dc9557da70f91 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx @@ -19,7 +19,7 @@ import styled from 'styled-components'; import { FULL_SCREEN } from '../timeline/body/column_headers/translations'; import { EXIT_FULL_SCREEN } from '../../../common/components/exit_full_screen/translations'; -import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants'; +import { DEFAULT_INDEX_KEY, FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants'; import { useFullScreen } from '../../../common/containers/use_full_screen'; import { State } from '../../../common/store'; import { TimelineId, TimelineType } from '../../../../common/types/timeline'; @@ -33,6 +33,8 @@ import { Resolver } from '../../../resolver/view'; import { useAllCasesModal } from '../../../cases/components/use_all_cases_modal'; import * as i18n from './translations'; +import { useUiSetting$ } from '../../../common/lib/kibana'; +import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index'; const OverlayContainer = styled.div` height: 100%; @@ -137,6 +139,16 @@ const GraphOverlayComponent = ({ globalFullScreen, ]); + const { signalIndexName } = useSignalIndex(); + const [siemDefaultIndices] = useUiSetting$(DEFAULT_INDEX_KEY); + const indices: string[] | null = useMemo(() => { + if (signalIndexName === null) { + return null; + } else { + return [...siemDefaultIndices, signalIndexName]; + } + }, [signalIndexName, siemDefaultIndices]); + return ( @@ -178,10 +190,13 @@ const GraphOverlayComponent = ({ - + {graphEventId !== undefined && indices !== null && ( + + )} ); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts index bcac559d61f79..510bb6c545558 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/entity.ts @@ -18,11 +18,6 @@ export function handleEntities(): RequestHandler> = { latest: '@timestamp', @@ -31,7 +31,7 @@ export const buildQuery = ({ pagination: { querySize }, defaultIndex, docValueFields, -}: AuthenticationsRequestOptions) => { +}: HostAuthenticationsRequestOptions) => { const esFields = reduceFields(authenticationFields, { ...hostFieldsMap, ...sourceFieldsMap }); const filter = [ diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx index 200818c40dec5..ded9a7917d921 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx @@ -12,8 +12,8 @@ import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants import { HostsQueries, AuthenticationsEdges, - AuthenticationsRequestOptions, - AuthenticationsStrategyResponse, + HostAuthenticationsRequestOptions, + HostAuthenticationsStrategyResponse, AuthenticationHit, } from '../../../../../../common/search_strategy/security_solution/hosts'; @@ -23,7 +23,7 @@ import { auditdFieldsMap, buildQuery as buildAuthenticationQuery } from './dsl/q import { formatAuthenticationData, getHits } from './helpers'; export const authentications: SecuritySolutionFactory = { - buildDsl: (options: AuthenticationsRequestOptions) => { + buildDsl: (options: HostAuthenticationsRequestOptions) => { if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); } @@ -31,9 +31,9 @@ export const authentications: SecuritySolutionFactory - ): Promise => { + ): Promise => { const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; const totalCount = getOr(0, 'aggregations.user_count.value', response.rawResponse); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/helpers.ts index 48e210d822918..56f7aec2327a5 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/helpers.ts @@ -5,11 +5,11 @@ */ import { set } from '@elastic/safer-lodash-set/fp'; import { get, has, head } from 'lodash/fp'; +import { hostFieldsMap } from '../../../../../common/ecs/ecs_fields'; import { HostsEdges, HostItem, } from '../../../../../common/search_strategy/security_solution/hosts'; -import { hostFieldsMap } from '../../../../lib/ecs_fields'; import { HostAggEsItem, HostBuckets, HostValue } from '../../../../lib/hosts/types'; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts index 6585abde60281..38d81c229ac5f 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts @@ -13,11 +13,13 @@ import { SecuritySolutionFactory } from '../types'; import { allHosts } from './all'; import { overviewHost } from './overview'; import { firstLastSeenHost } from './last_first_seen'; +import { uncommonProcesses } from './uncommon_processes'; import { authentications } from './authentications'; export const hostsFactory: Record> = { [HostsQueries.hosts]: allHosts, [HostsQueries.hostOverview]: overviewHost, [HostsQueries.firstLastSeen]: firstLastSeenHost, + [HostsQueries.uncommonProcesses]: uncommonProcesses, [HostsQueries.authentications]: authentications, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/helpers.ts index c7b0d8acc8782..ed705e7f6ad56 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/helpers.ts @@ -5,8 +5,8 @@ */ import { set } from '@elastic/safer-lodash-set/fp'; import { get, has, head } from 'lodash/fp'; +import { hostFieldsMap } from '../../../../../../common/ecs/ecs_fields'; import { HostItem } from '../../../../../../common/search_strategy/security_solution/hosts'; -import { hostFieldsMap } from '../../../../../lib/ecs_fields'; import { HostAggEsItem, HostBuckets, HostValue } from '../../../../../lib/hosts/types'; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/query.host_overview.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/query.host_overview.dsl.ts index 913bc90df04be..85cc87414c38e 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/query.host_overview.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/query.host_overview.dsl.ts @@ -5,8 +5,8 @@ */ import { ISearchRequestParams } from '../../../../../../../../../src/plugins/data/common'; +import { cloudFieldsMap, hostFieldsMap } from '../../../../../../common/ecs/ecs_fields'; import { HostOverviewRequestOptions } from '../../../../../../common/search_strategy/security_solution'; -import { cloudFieldsMap, hostFieldsMap } from '../../../../../lib/ecs_fields'; import { buildFieldsTermAggregation } from '../../../../../lib/hosts/helpers'; import { reduceFields } from '../../../../../utils/build_query/reduce_fields'; import { HOST_FIELDS } from './helpers'; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/dsl/query.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/dsl/query.dsl.ts new file mode 100644 index 0000000000000..2e2d889dda116 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/dsl/query.dsl.ts @@ -0,0 +1,226 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createQueryFilterClauses } from '../../../../../../utils/build_query'; +import { reduceFields } from '../../../../../../utils/build_query/reduce_fields'; +import { + hostFieldsMap, + processFieldsMap, + userFieldsMap, +} from '../../../../../../../common/ecs/ecs_fields'; +import { RequestOptionsPaginated } from '../../../../../../../common/search_strategy/security_solution'; +import { uncommonProcessesFields } from '../helpers'; + +export const buildQuery = ({ + defaultIndex, + filterQuery, + pagination: { querySize }, + timerange: { from, to }, +}: RequestOptionsPaginated) => { + const processUserFields = reduceFields(uncommonProcessesFields, { + ...processFieldsMap, + ...userFieldsMap, + }); + const hostFields = reduceFields(uncommonProcessesFields, hostFieldsMap); + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const agg = { + process_count: { + cardinality: { + field: 'process.name', + }, + }, + }; + + const dslQuery = { + allowNoIndices: true, + index: defaultIndex, + ignoreUnavailable: true, + body: { + aggregations: { + ...agg, + group_by_process: { + terms: { + size: querySize, + field: 'process.name', + order: [ + { + host_count: 'asc', + }, + { + _count: 'asc', + }, + { + _key: 'asc', + }, + ], + }, + aggregations: { + process: { + top_hits: { + size: 1, + sort: [{ '@timestamp': { order: 'desc' } }], + _source: processUserFields, + }, + }, + host_count: { + cardinality: { + field: 'host.name', + }, + }, + hosts: { + terms: { + field: 'host.name', + }, + aggregations: { + host: { + top_hits: { + size: 1, + _source: hostFields, + }, + }, + }, + }, + }, + }, + }, + query: { + bool: { + should: [ + { + bool: { + filter: [ + { + term: { + 'agent.type': 'auditbeat', + }, + }, + { + term: { + 'event.module': 'auditd', + }, + }, + { + term: { + 'event.action': 'executed', + }, + }, + ], + }, + }, + { + bool: { + filter: [ + { + term: { + 'agent.type': 'auditbeat', + }, + }, + { + term: { + 'event.module': 'system', + }, + }, + { + term: { + 'event.dataset': 'process', + }, + }, + { + term: { + 'event.action': 'process_started', + }, + }, + ], + }, + }, + { + bool: { + filter: [ + { + term: { + 'agent.type': 'winlogbeat', + }, + }, + { + term: { + 'event.code': '4688', + }, + }, + ], + }, + }, + { + bool: { + filter: [ + { + term: { + 'winlog.event_id': 1, + }, + }, + { + term: { + 'winlog.channel': 'Microsoft-Windows-Sysmon/Operational', + }, + }, + ], + }, + }, + { + bool: { + filter: [ + { + term: { + 'event.type': 'process_start', + }, + }, + { + term: { + 'event.category': 'process', + }, + }, + ], + }, + }, + { + bool: { + filter: [ + { + term: { + 'event.category': 'process', + }, + }, + { + term: { + 'event.type': 'start', + }, + }, + ], + }, + }, + ], + minimum_should_match: 1, + filter, + }, + }, + }, + size: 0, + track_total_hits: false, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts new file mode 100644 index 0000000000000..5c3d76175b7e4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { get, getOr } from 'lodash/fp'; +import { set } from '@elastic/safer-lodash-set/fp'; + +import { mergeFieldsWithHit } from '../../../../../utils/build_query'; +import { + ProcessHits, + UncommonProcessesEdges, + UncommonProcessHit, +} from '../../../../../../common/search_strategy/security_solution/hosts/uncommon_processes'; +import { toArray } from '../../../../helpers/to_array'; +import { HostHits } from '../../../../../../common/search_strategy'; + +export const uncommonProcessesFields = [ + '_id', + 'instances', + 'process.args', + 'process.name', + 'user.id', + 'user.name', + 'hosts.name', +]; + +export const getHits = (buckets: readonly UncommonProcessBucket[]): readonly UncommonProcessHit[] => + buckets.map((bucket: Readonly) => ({ + _id: bucket.process.hits.hits[0]._id, + _index: bucket.process.hits.hits[0]._index, + _type: bucket.process.hits.hits[0]._type, + _score: bucket.process.hits.hits[0]._score, + _source: bucket.process.hits.hits[0]._source, + sort: bucket.process.hits.hits[0].sort, + cursor: bucket.process.hits.hits[0].cursor, + total: bucket.process.hits.total, + host: getHosts(bucket.hosts.buckets), + })); + +export interface UncommonProcessBucket { + key: string; + hosts: { + buckets: Array<{ key: string; host: HostHits }>; + }; + process: ProcessHits; +} + +export const getHosts = (buckets: ReadonlyArray<{ key: string; host: HostHits }>) => + buckets.map((bucket) => { + const source = get('host.hits.hits[0]._source', bucket); + return { + id: [bucket.key], + name: get('host.name', source), + }; + }); + +export const formatUncommonProcessesData = ( + fields: readonly string[], + hit: UncommonProcessHit, + fieldMap: Readonly> +): UncommonProcessesEdges => + fields.reduce( + (flattenedFields, fieldName) => { + flattenedFields.node._id = hit._id; + flattenedFields.node.instances = getOr(0, 'total.value', hit); + flattenedFields.node.hosts = hit.host; + + if (hit.cursor) { + flattenedFields.cursor.value = hit.cursor; + } + + const mergedResult = mergeFieldsWithHit(fieldName, flattenedFields, fieldMap, hit); + let fieldPath = `node.${fieldName}`; + let fieldValue = get(fieldPath, mergedResult); + if (fieldPath === 'node.hosts.name') { + fieldPath = `node.hosts.0.name`; + fieldValue = get(fieldPath, mergedResult); + } + return set(fieldPath, toArray(fieldValue), mergedResult); + }, + { + node: { + _id: '', + instances: 0, + process: {}, + hosts: [], + }, + cursor: { + value: '', + tiebreaker: null, + }, + } + ); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.ts new file mode 100644 index 0000000000000..fcc76eebe4cf5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; + +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; +import { HostsQueries } from '../../../../../../common/search_strategy/security_solution'; +import { processFieldsMap, userFieldsMap } from '../../../../../../common/ecs/ecs_fields'; +import { + HostUncommonProcessesRequestOptions, + HostUncommonProcessesStrategyResponse, +} from '../../../../../../common/search_strategy/security_solution/hosts/uncommon_processes'; + +import { inspectStringifyObject } from '../../../../../utils/build_query'; + +import { SecuritySolutionFactory } from '../../types'; +import { buildQuery } from './dsl/query.dsl'; +import { formatUncommonProcessesData, getHits, uncommonProcessesFields } from './helpers'; + +export const uncommonProcesses: SecuritySolutionFactory = { + buildDsl: (options: HostUncommonProcessesRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + return buildQuery(options); + }, + parse: async ( + options: HostUncommonProcessesRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + const totalCount = getOr(0, 'aggregations.process_count.value', response.rawResponse); + const buckets = getOr([], 'aggregations.group_by_process.buckets', response.rawResponse); + const hits = getHits(buckets); + + const uncommonProcessesEdges = hits.map((hit) => + formatUncommonProcessesData(uncommonProcessesFields, hit, { + ...processFieldsMap, + ...userFieldsMap, + }) + ); + + const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; + const edges = uncommonProcessesEdges.splice(cursorStart, querySize - cursorStart); + const inspect = { + dsl: [inspectStringifyObject(buildQuery(options))], + response: [inspectStringifyObject(response)], + }; + + const showMorePagesIndicator = totalCount > fakeTotalCount; + return { + ...response, + edges, + inspect, + pageInfo: { + activePage: activePage ? activePage : 0, + fakeTotalCount, + showMorePagesIndicator, + }, + totalCount, + }; + }, +}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d1cc4e79e6c07..eacb1febd20ff 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1603,7 +1603,6 @@ "home.loadTutorials.unableToLoadErrorMessage": "チュートリアルが読み込めません。", "home.pageTitle": "ホーム", "home.recentlyAccessed.recentlyViewedTitle": "最近閲覧", - "home.sampleData.ecommerceSpec.averageSalesPerRegionTitle": "[e コマース] 地域ごとの平均売上", "home.sampleData.ecommerceSpec.averageSalesPriceTitle": "[e コマース] 平均販売価格", "home.sampleData.ecommerceSpec.averageSoldQuantityTitle": "[e コマース] 平均販売数", "home.sampleData.ecommerceSpec.controlsTitle": "[e コマース] コントロール", @@ -1634,7 +1633,6 @@ "home.sampleData.flightsSpec.globalFlightDashboardDescription": "ES-Air、Logstash Airways、Kibana Airlines、JetBeats のサンプル飛行データを分析します", "home.sampleData.flightsSpec.globalFlightDashboardTitle": "[フライト] グローバルフライトダッシュボード", "home.sampleData.flightsSpec.markdownInstructionsTitle": "[フライト] マークダウンの指示", - "home.sampleData.flightsSpec.originCountryTicketPricesTitle": "[フライト] 出発国の運賃", "home.sampleData.flightsSpec.originCountryTitle": "[Flights] 出発国と到着国の比較", "home.sampleData.flightsSpec.totalFlightCancellationsTitle": "[フライト] フライト欠航合計", "home.sampleData.flightsSpec.totalFlightDelaysTitle": "[フライト] フライト遅延合計", @@ -1649,7 +1647,6 @@ "home.sampleData.logsSpec.markdownInstructionsTitle": "[ログ] マークダウンの指示", "home.sampleData.logsSpec.responseCodesOverTimeTitle": "[ログ] 一定期間の応答コードと注釈", "home.sampleData.logsSpec.sourceAndDestinationSankeyChartTitle": "[ログ] ソースと行先のサンキーダイアグラム", - "home.sampleData.logsSpec.uniqueVisitorsByCountryTitle": "[ログ] 国ごとのユニークビジター", "home.sampleData.logsSpec.uniqueVisitorsTitle": "[ログ] ユニークビジターと平均バイトの比較", "home.sampleData.logsSpec.visitorOSTitle": "[ログ] OS 別のビジター", "home.sampleData.logsSpec.webTrafficDescription": "Elastic Web サイトのサンプル Webトラフィックログデータを分析します", @@ -9471,8 +9468,6 @@ "xpack.ingestPipelines.pipelineEditor.removeProcessorModal.cancelButtonLabel": "キャンセル", "xpack.ingestPipelines.pipelineEditor.removeProcessorModal.confirmationButtonLabel": "プロセッサーの削除", "xpack.ingestPipelines.pipelineEditor.removeProcessorModal.titleText": "{type}プロセッサーの削除", - "xpack.ingestPipelines.pipelineEditor.setForm.fieldFieldLabel": "フィールド", - "xpack.ingestPipelines.pipelineEditor.setForm.fieldRequiredError": "フィールド値が必要です。", "xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldLabel": "無効化", "xpack.ingestPipelines.pipelineEditor.setForm.valueFieldLabel": "値", "xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError": "設定する値が必要です。", @@ -9560,7 +9555,6 @@ "xpack.lens.configure.editConfig": "構成の編集", "xpack.lens.configure.emptyConfig": "ここにフィールドをドロップ", "xpack.lens.dataPanelWrapper.switchDatasource": "データソースに切り替える", - "xpack.lens.datatable.columns": "フィールド", "xpack.lens.datatable.conjunctionSign": " と ", "xpack.lens.datatable.expressionHelpLabel": "データベースレンダー", "xpack.lens.datatable.label": "データテーブル", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 60019cae0c65d..bd30703dd5bd6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1604,7 +1604,6 @@ "home.loadTutorials.unableToLoadErrorMessage": "无法加载教程", "home.pageTitle": "主页", "home.recentlyAccessed.recentlyViewedTitle": "最近查看", - "home.sampleData.ecommerceSpec.averageSalesPerRegionTitle": "[电子商务] 每地区平均销售额", "home.sampleData.ecommerceSpec.averageSalesPriceTitle": "[电子商务] 平均销售价格", "home.sampleData.ecommerceSpec.averageSoldQuantityTitle": "[电子商务] 平均销售数量", "home.sampleData.ecommerceSpec.controlsTitle": "[电子商务] 控制", @@ -1635,7 +1634,6 @@ "home.sampleData.flightsSpec.globalFlightDashboardDescription": "分析 ES-Air、Logstash Airways、Kibana Airlines 和 JetBeats 的模拟航班数据", "home.sampleData.flightsSpec.globalFlightDashboardTitle": "[航班] 全球航班仪表板", "home.sampleData.flightsSpec.markdownInstructionsTitle": "[航班] Markdown 说明", - "home.sampleData.flightsSpec.originCountryTicketPricesTitle": "[航班] 始发国/地区票价", "home.sampleData.flightsSpec.originCountryTitle": "[航班] 始发国/地区与到达国/地区", "home.sampleData.flightsSpec.totalFlightCancellationsTitle": "[航班] 航班取消总数", "home.sampleData.flightsSpec.totalFlightDelaysTitle": "[航班] 航班延误总数", @@ -1650,7 +1648,6 @@ "home.sampleData.logsSpec.markdownInstructionsTitle": "[日志] Markdown 说明", "home.sampleData.logsSpec.responseCodesOverTimeTitle": "[日志] 时移响应代码 + 注释", "home.sampleData.logsSpec.sourceAndDestinationSankeyChartTitle": "[日志] 始发地和到达地 Sankey 图", - "home.sampleData.logsSpec.uniqueVisitorsByCountryTitle": "[日志] 按国家/地区划分的独立访客", "home.sampleData.logsSpec.uniqueVisitorsTitle": "[日志] 独立访客与平均字节数", "home.sampleData.logsSpec.visitorOSTitle": "[日志] 按 OS 划分的访客", "home.sampleData.logsSpec.webTrafficDescription": "分析 Elastic 网站的模拟 Web 流量日志数据", @@ -9477,8 +9474,6 @@ "xpack.ingestPipelines.pipelineEditor.removeProcessorModal.cancelButtonLabel": "取消", "xpack.ingestPipelines.pipelineEditor.removeProcessorModal.confirmationButtonLabel": "删除处理器", "xpack.ingestPipelines.pipelineEditor.removeProcessorModal.titleText": "删除 {type} 处理器", - "xpack.ingestPipelines.pipelineEditor.setForm.fieldFieldLabel": "字段", - "xpack.ingestPipelines.pipelineEditor.setForm.fieldRequiredError": "字段值必填。", "xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldLabel": "覆盖", "xpack.ingestPipelines.pipelineEditor.setForm.valueFieldLabel": "值", "xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError": "需要设置值。", @@ -9566,7 +9561,6 @@ "xpack.lens.configure.editConfig": "编辑配置", "xpack.lens.configure.emptyConfig": "将字段拖放到此处", "xpack.lens.dataPanelWrapper.switchDatasource": "切换到数据源", - "xpack.lens.datatable.columns": "字段", "xpack.lens.datatable.conjunctionSign": " & ", "xpack.lens.datatable.expressionHelpLabel": "数据表呈现器", "xpack.lens.datatable.label": "数据表", diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx index a49251811239f..16d0250c5721e 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx @@ -21,7 +21,12 @@ import { EuiLink, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { txtChangeButton, txtTriggerPickerHelpText, txtTriggerPickerLabel } from './i18n'; +import { + txtChangeButton, + txtTriggerPickerHelpText, + txtTriggerPickerLabel, + txtTriggerPickerHelpTooltip, +} from './i18n'; import './action_wizard.scss'; import { ActionFactory, BaseActionFactoryContext } from '../../dynamic_actions'; import { Trigger, TriggerId } from '../../../../../../src/plugins/ui_actions/public'; @@ -157,14 +162,17 @@ const TriggerPicker: React.FC = ({ const selectedTrigger = selectedTriggers ? selectedTriggers[0] : undefined; return (
{txtTriggerPickerLabel}{' '} - - {txtTriggerPickerHelpText} - + + + {txtTriggerPickerHelpText} + +
), @@ -271,7 +279,7 @@ const SelectedActionFactory: React.FC = ({ /> )} - +
; + placeContext?: ActionFactoryPlaceContext; } /** @@ -81,8 +81,8 @@ export function createFlyoutManageDrilldowns({ const isCreateOnly = props.viewMode === 'create'; const factoryContext: BaseActionFactoryContext = useMemo( - () => ({ ...props.extraContext, triggers: props.supportedTriggers }), - [props.extraContext, props.supportedTriggers] + () => ({ ...props.placeContext, triggers: props.supportedTriggers }), + [props.placeContext, props.supportedTriggers] ); const actionFactories = useCompatibleActionFactoriesForCurrentContext( allActionFactories, @@ -137,7 +137,7 @@ export function createFlyoutManageDrilldowns({ function mapToDrilldownToDrilldownListItem(drilldown: SerializedEvent): DrilldownListItem { const actionFactory = allActionFactoriesById[drilldown.action.factoryId]; const drilldownFactoryContext: BaseActionFactoryContext = { - ...props.extraContext, + ...props.placeContext, triggers: drilldown.triggers as TriggerId[], }; return { @@ -204,7 +204,7 @@ export function createFlyoutManageDrilldowns({ setRoute(Routes.Manage); setCurrentEditId(null); }} - extraActionFactoryContext={props.extraContext} + actionFactoryPlaceContext={props.placeContext} initialDrilldownWizardConfig={resolveInitialDrilldownWizardConfig()} supportedTriggers={props.supportedTriggers} getTrigger={getTrigger} diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx index a908d53bf6ae7..c8e3f454bd53d 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx @@ -18,7 +18,7 @@ import { import { DrilldownHelloBar } from '../drilldown_hello_bar'; import { ActionFactory, BaseActionFactoryContext } from '../../../dynamic_actions'; import { Trigger, TriggerId } from '../../../../../../../src/plugins/ui_actions/public'; -import { ExtraActionFactoryContext } from '../types'; +import { ActionFactoryPlaceContext } from '../types'; export interface DrilldownWizardConfig { name: string; @@ -44,7 +44,7 @@ export interface FlyoutDrilldownWizardProps< showWelcomeMessage?: boolean; onWelcomeHideClick?: () => void; - extraActionFactoryContext?: ExtraActionFactoryContext; + actionFactoryPlaceContext?: ActionFactoryPlaceContext; docsLink?: string; @@ -143,7 +143,7 @@ export function FlyoutDrilldownWizard ({ - ...extraActionFactoryContext, + ...actionFactoryPlaceContext, triggers: wizardConfig.selectedTriggers ?? [], }), - [extraActionFactoryContext, wizardConfig.selectedTriggers] + [actionFactoryPlaceContext, wizardConfig.selectedTriggers] ); const isActionValid = ( diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/types.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/types.ts index 870b55c24fb58..811680bf380f7 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/types.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/types.ts @@ -10,6 +10,6 @@ import { BaseActionFactoryContext } from '../../dynamic_actions'; * Interface used as piece of ActionFactoryContext that is passed in from drilldown wizard component to action factories * Omitted values are added inside the wizard and then full {@link BaseActionFactoryContext} passed into action factory methods */ -export type ExtraActionFactoryContext< +export type ActionFactoryPlaceContext< ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext > = Omit; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/README.md b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/README.md new file mode 100644 index 0000000000000..acad968fa46c2 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/README.md @@ -0,0 +1 @@ +This directory contains reusable building blocks for creating custom URL drilldowns diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/index.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/index.ts new file mode 100644 index 0000000000000..70399617136bd --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { UrlDrilldownCollectConfig } from './url_drilldown_collect_config/url_drilldown_collect_config'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts new file mode 100644 index 0000000000000..78f7218dce22e --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/i18n.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const txtUrlTemplatePlaceholder = i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplatePlaceholderText', + { + defaultMessage: 'Example: {exampleUrl}', + values: { + exampleUrl: 'https://www.my-url.com/?{{event.key}}={{event.value}}', + }, + } +); + +export const txtUrlPreviewHelpText = i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewHelpText', + { + defaultMessage: 'Please note that \\{\\{event.*\\}\\} variables replaced by dummy values.', + } +); + +export const txtAddVariableButtonTitle = i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.addVariableButtonTitle', + { + defaultMessage: 'Add variable', + } +); + +export const txtUrlTemplateLabel = i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateLabel', + { + defaultMessage: 'Enter URL template:', + } +); + +export const txtUrlTemplateSyntaxHelpLinkText = i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateSyntaxHelpLinkText', + { + defaultMessage: 'Syntax help', + } +); + +export const txtUrlTemplateVariablesHelpLinkText = i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesHelpLinkText', + { + defaultMessage: 'Help', + } +); + +export const txtUrlTemplateVariablesFilterPlaceholderText = i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlTemplateVariablesFilterPlaceholderText', + { + defaultMessage: 'Filter variables', + } +); + +export const txtUrlTemplatePreviewLabel = i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLabel', + { + defaultMessage: 'URL preview:', + } +); + +export const txtUrlTemplatePreviewLinkText = i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.urlPreviewLinkText', + { + defaultMessage: 'Preview', + } +); + +export const txtUrlTemplateOpenInNewTab = i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownCollectConfig.openInNewTabLabel', + { + defaultMessage: 'Open in new tab', + } +); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/index.scss b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/index.scss new file mode 100644 index 0000000000000..475c3f2a915ea --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/index.scss @@ -0,0 +1,5 @@ +.uaeUrlDrilldownCollectConfig__urlTemplateFormRow { + .euiFormRow__label { + align-self: flex-end; + } +} diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx new file mode 100644 index 0000000000000..e6c9797623e9f --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { UrlDrilldownConfig, UrlDrilldownScope } from '../../../types'; +import { UrlDrilldownCollectConfig } from '../url_drilldown_collect_config'; + +export const Demo = () => { + const [config, onConfig] = React.useState({ + openInNewTab: false, + url: { template: '' }, + }); + + const fakeScope: UrlDrilldownScope = { + kibanaUrl: 'http://localhost:5601/', + context: { + filters: [ + { + query: { match: { extension: { query: 'jpg', type: 'phrase' } } }, + meta: { index: 'logstash-*', negate: false, disabled: false, alias: null }, + }, + { + query: { match: { '@tags': { query: 'info', type: 'phrase' } } }, + meta: { index: 'logstash-*', negate: false, disabled: false, alias: null }, + }, + { + query: { match: { _type: { query: 'nginx', type: 'phrase' } } }, + meta: { index: 'logstash-*', negate: false, disabled: false, alias: null }, + }, + ], + }, + event: { + key: 'fakeKey', + value: 'fakeValue', + }, + }; + + return ( + <> + + {JSON.stringify(config)} + + ); +}; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.story.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.story.tsx new file mode 100644 index 0000000000000..244ea9bd2a97e --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.story.tsx @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { Demo } from './test_samples/demo'; + +storiesOf('UrlDrilldownCollectConfig', module).add('default', () => ); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.test.tsx new file mode 100644 index 0000000000000..f55818379ef3f --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Demo } from './test_samples/demo'; +import { cleanup, fireEvent, render } from '@testing-library/react/pure'; +import React from 'react'; + +afterEach(cleanup); + +test('configure valid URL template', () => { + const screen = render(); + + const urlTemplate = 'https://elastic.co/?{{event.key}}={{event.value}}'; + fireEvent.change(screen.getByLabelText(/Enter URL template/i), { + target: { value: urlTemplate }, + }); + + const preview = screen.getByLabelText(/URL preview/i) as HTMLTextAreaElement; + expect(preview.value).toMatchInlineSnapshot(`"https://elastic.co/?fakeKey=fakeValue"`); + expect(preview.disabled).toEqual(true); + const previewLink = screen.getByText('Preview') as HTMLAnchorElement; + expect(previewLink.href).toMatchInlineSnapshot(`"https://elastic.co/?fakeKey=fakeValue"`); + expect(previewLink.target).toMatchInlineSnapshot(`"_blank"`); +}); + +test('configure invalid URL template', () => { + const screen = render(); + + const urlTemplate = 'https://elastic.co/?{{event.wrongKey}}={{event.wrongValue}}'; + fireEvent.change(screen.getByLabelText(/Enter URL template/i), { + target: { value: urlTemplate }, + }); + + const previewTextArea = screen.getByLabelText(/URL preview/i) as HTMLTextAreaElement; + expect(previewTextArea.disabled).toEqual(true); + expect(previewTextArea.value).toEqual(urlTemplate); + expect(screen.getByText(/invalid format/i)).toBeInTheDocument(); // check that error is shown + + const previewLink = screen.getByText('Preview') as HTMLAnchorElement; + expect(previewLink.href).toEqual(urlTemplate); + expect(previewLink.target).toMatchInlineSnapshot(`"_blank"`); +}); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx new file mode 100644 index 0000000000000..dabf09e4b6e9f --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx @@ -0,0 +1,226 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useRef, useState } from 'react'; +import { + EuiCheckbox, + EuiFormRow, + EuiIcon, + EuiLink, + EuiPopover, + EuiPopoverFooter, + EuiPopoverTitle, + EuiSelectable, + EuiText, + EuiTextArea, + EuiSelectableOption, +} from '@elastic/eui'; +import { UrlDrilldownConfig, UrlDrilldownScope } from '../../types'; +import { compile } from '../../url_template'; +import { validateUrlTemplate } from '../../url_validation'; +import { buildScopeSuggestions } from '../../url_drilldown_scope'; +import './index.scss'; +import { + txtAddVariableButtonTitle, + txtUrlPreviewHelpText, + txtUrlTemplateSyntaxHelpLinkText, + txtUrlTemplateVariablesHelpLinkText, + txtUrlTemplateVariablesFilterPlaceholderText, + txtUrlTemplateLabel, + txtUrlTemplateOpenInNewTab, + txtUrlTemplatePlaceholder, + txtUrlTemplatePreviewLabel, + txtUrlTemplatePreviewLinkText, +} from './i18n'; + +export interface UrlDrilldownCollectConfig { + config: UrlDrilldownConfig; + onConfig: (newConfig: UrlDrilldownConfig) => void; + scope: UrlDrilldownScope; + syntaxHelpDocsLink?: string; +} + +export const UrlDrilldownCollectConfig: React.FC = ({ + config, + onConfig, + scope, + syntaxHelpDocsLink, +}) => { + const textAreaRef = useRef(null); + const urlTemplate = config.url.template ?? ''; + const compiledUrl = React.useMemo(() => { + try { + return compile(urlTemplate, scope); + } catch { + return urlTemplate; + } + }, [urlTemplate, scope]); + const scopeVariables = React.useMemo(() => buildScopeSuggestions(scope), [scope]); + + function updateUrlTemplate(newUrlTemplate: string) { + if (config.url.template !== newUrlTemplate) { + onConfig({ + ...config, + url: { + ...config.url, + template: newUrlTemplate, + }, + }); + } + } + const { error, isValid } = React.useMemo( + () => validateUrlTemplate({ template: urlTemplate }, scope), + [urlTemplate, scope] + ); + const isEmpty = !urlTemplate; + const isInvalid = !isValid && !isEmpty; + return ( + <> + + {txtUrlTemplateSyntaxHelpLinkText} + + ) + } + labelAppend={ + { + if (textAreaRef.current) { + updateUrlTemplate( + urlTemplate.substr(0, textAreaRef.current!.selectionStart) + + `{{${variable}}}` + + urlTemplate.substr(textAreaRef.current!.selectionEnd) + ); + } else { + updateUrlTemplate(urlTemplate + `{{${variable}}}`); + } + }} + /> + } + > + updateUrlTemplate(event.target.value)} + rows={3} + inputRef={textAreaRef} + /> + + + + {txtUrlTemplatePreviewLinkText} + + + } + helpText={txtUrlPreviewHelpText} + > + + + + onConfig({ ...config, openInNewTab: !config.openInNewTab })} + /> + + + ); +}; + +function AddVariableButton({ + variables, + onSelect, + variablesHelpLink, +}: { + variables: string[]; + onSelect: (variable: string) => void; + variablesHelpLink?: string; +}) { + const [isVariablesPopoverOpen, setIsVariablesPopoverOpen] = useState(false); + const closePopover = () => setIsVariablesPopoverOpen(false); + + const options: EuiSelectableOption[] = variables.map((variable: string) => ({ + key: variable, + label: variable, + })); + + return ( + + setIsVariablesPopoverOpen(true)}> + {txtAddVariableButtonTitle} + + + } + isOpen={isVariablesPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downLeft" + withTitle + > + { + const selected = newOptions.find((o) => o.checked === 'on'); + if (!selected) return; + onSelect(selected.key!); + closePopover(); + }} + listProps={{ + showIcons: false, + }} + > + {(list, search) => ( +
+ {search} + {list} + {variablesHelpLink && ( + + + {txtUrlTemplateVariablesHelpLinkText} + + + )} +
+ )} +
+
+ ); +} diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/index.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/index.ts new file mode 100644 index 0000000000000..7b7a850050c41 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +export { UrlDrilldownConfig, UrlDrilldownGlobalScope, UrlDrilldownScope } from './types'; +export { UrlDrilldownCollectConfig } from './components'; +export { + validateUrlTemplate as urlDrilldownValidateUrlTemplate, + validateUrl as urlDrilldownValidateUrl, +} from './url_validation'; +export { compile as urlDrilldownCompileUrl } from './url_template'; +export { globalScopeProvider as urlDrilldownGlobalScopeProvider } from './url_drilldown_global_scope'; +export { + buildScope as urlDrilldownBuildScope, + buildScopeSuggestions as urlDrilldownBuildScopeSuggestions, +} from './url_drilldown_scope'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts new file mode 100644 index 0000000000000..31c7481c9d63e --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/types.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface UrlDrilldownConfig { + url: { format?: 'handlebars_v1'; template: string }; + openInNewTab: boolean; +} + +/** + * URL drilldown has 3 sources for variables: global, context and event variables + */ +export interface UrlDrilldownScope< + ContextScope extends object = object, + EventScope extends object = object +> extends UrlDrilldownGlobalScope { + /** + * Dynamic variables that are differ depending on where drilldown is created and used, + * For example: variables extracted from embeddable panel + */ + context?: ContextScope; + + /** + * Variables extracted from trigger context + */ + event?: EventScope; +} + +/** + * Global static variables like, for example, `kibanaUrl` + * Such variables won’t change depending on a place where url drilldown is used. + */ +export interface UrlDrilldownGlobalScope { + kibanaUrl: string; +} diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_global_scope.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_global_scope.ts new file mode 100644 index 0000000000000..afc7fa590a2f0 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_global_scope.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup } from 'kibana/public'; +import { UrlDrilldownGlobalScope } from './types'; + +interface UrlDrilldownGlobalScopeDeps { + core: CoreSetup; +} + +export function globalScopeProvider({ + core, +}: UrlDrilldownGlobalScopeDeps): () => UrlDrilldownGlobalScope { + return () => ({ + kibanaUrl: window.location.origin + core.http.basePath.get(), + }); +} diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.test.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.test.ts new file mode 100644 index 0000000000000..f95fc5e70ae00 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { buildScope, buildScopeSuggestions } from './url_drilldown_scope'; + +test('buildScopeSuggestions', () => { + expect( + buildScopeSuggestions( + buildScope({ + globalScope: { + kibanaUrl: 'http://localhost:5061/', + }, + eventScope: { + key: '__testKey__', + value: '__testValue__', + }, + contextScope: { + filters: [ + { + query: { match: { extension: { query: 'jpg', type: 'phrase' } } }, + meta: { index: 'logstash-*', negate: false, disabled: false, alias: null }, + }, + { + query: { match: { '@tags': { query: 'info', type: 'phrase' } } }, + meta: { index: 'logstash-*', negate: false, disabled: false, alias: null }, + }, + { + query: { match: { _type: { query: 'nginx', type: 'phrase' } } }, + meta: { index: 'logstash-*', negate: false, disabled: false, alias: null }, + }, + ], + query: { + query: '', + language: 'kquery', + }, + }, + }) + ) + ).toMatchInlineSnapshot(` + Array [ + "event.key", + "event.value", + "context.filters", + "context.query.language", + "context.query.query", + "kibanaUrl", + ] + `); +}); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts new file mode 100644 index 0000000000000..d499812a9d5ae --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import partition from 'lodash/partition'; +import { UrlDrilldownGlobalScope, UrlDrilldownScope } from './types'; +import { getFlattenedObject } from '../../../../../../src/core/public'; + +export function buildScope< + ContextScope extends object = object, + EventScope extends object = object +>({ + globalScope, + contextScope, + eventScope, +}: { + globalScope: UrlDrilldownGlobalScope; + contextScope?: ContextScope; + eventScope?: EventScope; +}): UrlDrilldownScope { + return { + ...globalScope, + context: contextScope, + event: eventScope, + }; +} + +/** + * Builds list of variables for suggestion from scope + * keys sorted alphabetically, except {{event.$}} variables are pulled to the top + * @param scope + */ +export function buildScopeSuggestions(scope: UrlDrilldownGlobalScope): string[] { + const allKeys = Object.keys(getFlattenedObject(scope)).sort(); + const [eventKeys, otherKeys] = partition(allKeys, (key) => key.startsWith('event')); + return [...eventKeys, ...otherKeys]; +} diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts new file mode 100644 index 0000000000000..64b8cc49292b3 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.test.ts @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { compile } from './url_template'; +import moment from 'moment-timezone'; + +test('should compile url without variables', () => { + const url = 'https://elastic.co'; + expect(compile(url, {})).toBe(url); +}); + +test('should fail on unknown syntax', () => { + const url = 'https://elastic.co/{{}'; + expect(() => compile(url, {})).toThrowError(); +}); + +test('should fail on not existing variable', () => { + const url = 'https://elastic.co/{{fake}}'; + expect(() => compile(url, {})).toThrowError(); +}); + +test('should fail on not existing nested variable', () => { + const url = 'https://elastic.co/{{fake.fake}}'; + expect(() => compile(url, { fake: {} })).toThrowError(); +}); + +test('should replace existing variable', () => { + const url = 'https://elastic.co/{{foo}}'; + expect(compile(url, { foo: 'bar' })).toMatchInlineSnapshot(`"https://elastic.co/bar"`); +}); + +test('should fail on unknown helper', () => { + const url = 'https://elastic.co/{{fake foo}}'; + expect(() => compile(url, { foo: 'bar' })).toThrowError(); +}); + +describe('json helper', () => { + test('should replace with json', () => { + const url = 'https://elastic.co/{{json foo bar}}'; + expect(compile(url, { foo: { foo: 'bar' }, bar: { bar: 'foo' } })).toMatchInlineSnapshot( + `"https://elastic.co/%5B%7B%22foo%22:%22bar%22%7D,%7B%22bar%22:%22foo%22%7D%5D"` + ); + }); + test('should replace with json and skip encoding', () => { + const url = 'https://elastic.co/{{{json foo bar}}}'; + expect(compile(url, { foo: { foo: 'bar' }, bar: { bar: 'foo' } })).toMatchInlineSnapshot( + `"https://elastic.co/%5B%7B%22foo%22:%22bar%22%7D,%7B%22bar%22:%22foo%22%7D%5D"` + ); + }); + test('should throw on unknown key', () => { + const url = 'https://elastic.co/{{{json fake}}}'; + expect(() => compile(url, { foo: { foo: 'bar' }, bar: { bar: 'foo' } })).toThrowError(); + }); +}); + +describe('rison helper', () => { + test('should replace with rison', () => { + const url = 'https://elastic.co/{{rison foo bar}}'; + expect(compile(url, { foo: { foo: 'bar' }, bar: { bar: 'foo' } })).toMatchInlineSnapshot( + `"https://elastic.co/!((foo:bar),(bar:foo))"` + ); + }); + test('should replace with rison and skip encoding', () => { + const url = 'https://elastic.co/{{{rison foo bar}}}'; + expect(compile(url, { foo: { foo: 'bar' }, bar: { bar: 'foo' } })).toMatchInlineSnapshot( + `"https://elastic.co/!((foo:bar),(bar:foo))"` + ); + }); + test('should throw on unknown key', () => { + const url = 'https://elastic.co/{{{rison fake}}}'; + expect(() => compile(url, { foo: { foo: 'bar' }, bar: { bar: 'foo' } })).toThrowError(); + }); +}); + +describe('date helper', () => { + let spy: jest.SpyInstance; + const date = new Date('2020-08-18T14:45:00.000Z'); + beforeAll(() => { + spy = jest.spyOn(global.Date, 'now').mockImplementation(() => date.valueOf()); + moment.tz.setDefault('UTC'); + }); + afterAll(() => { + spy.mockRestore(); + moment.tz.setDefault('Browser'); + }); + + test('uses datemath', () => { + const url = 'https://elastic.co/{{date time}}'; + expect(compile(url, { time: 'now' })).toMatchInlineSnapshot( + `"https://elastic.co/2020-08-18T14:45:00.000Z"` + ); + }); + + test('can use format', () => { + const url = 'https://elastic.co/{{date time "dddd, MMMM Do YYYY, h:mm:ss a"}}'; + expect(compile(url, { time: 'now' })).toMatchInlineSnapshot( + `"https://elastic.co/Tuesday,%20August%2018th%202020,%202:45:00%20pm"` + ); + }); + + test('throws if missing variable', () => { + const url = 'https://elastic.co/{{date time}}'; + expect(() => compile(url, {})).toThrowError(); + }); + + test("doesn't throw if non valid date", () => { + const url = 'https://elastic.co/{{date time}}'; + expect(compile(url, { time: 'fake' })).toMatchInlineSnapshot(`"https://elastic.co/fake"`); + }); + + test("doesn't throw on boolean or number", () => { + const url = 'https://elastic.co/{{date time}}'; + expect(compile(url, { time: false })).toMatchInlineSnapshot(`"https://elastic.co/false"`); + expect(compile(url, { time: 24 })).toMatchInlineSnapshot( + `"https://elastic.co/1970-01-01T00:00:00.024Z"` + ); + }); + + test('works with ISO string', () => { + const url = 'https://elastic.co/{{date time}}'; + expect(compile(url, { time: date.toISOString() })).toMatchInlineSnapshot( + `"https://elastic.co/2020-08-18T14:45:00.000Z"` + ); + }); + + test('works with ts', () => { + const url = 'https://elastic.co/{{date time}}'; + expect(compile(url, { time: date.valueOf() })).toMatchInlineSnapshot( + `"https://elastic.co/2020-08-18T14:45:00.000Z"` + ); + }); + test('works with ts string', () => { + const url = 'https://elastic.co/{{date time}}'; + expect(compile(url, { time: String(date.valueOf()) })).toMatchInlineSnapshot( + `"https://elastic.co/2020-08-18T14:45:00.000Z"` + ); + }); +}); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts new file mode 100644 index 0000000000000..2c3537636b9da --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_template.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { create as createHandlebars, HelperDelegate, HelperOptions } from 'handlebars'; +import { encode, RisonValue } from 'rison-node'; +import dateMath from '@elastic/datemath'; +import moment, { Moment } from 'moment'; + +const handlebars = createHandlebars(); + +function createSerializationHelper( + fnName: string, + serializeFn: (value: unknown) => string +): HelperDelegate { + return (...args) => { + const { hash } = args.slice(-1)[0] as HelperOptions; + const hasHash = Object.keys(hash).length > 0; + const hasValues = args.length > 1; + if (hasHash && hasValues) { + throw new Error(`[${fnName}]: both value list and hash are not supported`); + } + if (hasHash) { + if (Object.values(hash).some((v) => typeof v === 'undefined')) + throw new Error(`[${fnName}]: unknown variable`); + return serializeFn(hash); + } else { + const values = args.slice(0, -1) as unknown[]; + if (values.some((value) => typeof value === 'undefined')) + throw new Error(`[${fnName}]: unknown variable`); + if (values.length === 0) throw new Error(`[${fnName}]: unknown variable`); + if (values.length === 1) return serializeFn(values[0]); + return serializeFn(values); + } + }; +} + +handlebars.registerHelper('json', createSerializationHelper('json', JSON.stringify)); +handlebars.registerHelper( + 'rison', + createSerializationHelper('rison', (v) => encode(v as RisonValue)) +); + +handlebars.registerHelper('date', (...args) => { + const values = args.slice(0, -1) as [string | Date, string | undefined]; + // eslint-disable-next-line prefer-const + let [date, format] = values; + if (typeof date === 'undefined') throw new Error(`[date]: unknown variable`); + let momentDate: Moment | undefined; + if (typeof date === 'string') { + momentDate = dateMath.parse(date); + if (!momentDate || !momentDate.isValid()) { + const ts = Number(date); + if (!Number.isNaN(ts)) { + momentDate = moment(ts); + } + } + } else { + momentDate = moment(date); + } + + if (!momentDate || !momentDate.isValid()) { + // do not throw error here, because it could be that in preview `__testValue__` is not parsable, + // but in runtime it is + return date; + } + return format ? momentDate.format(format) : momentDate.toISOString(); +}); + +export function compile(url: string, context: object): string { + const template = handlebars.compile(url, { strict: true, noEscape: true }); + return encodeURI(template(context)); +} diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.test.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.test.ts new file mode 100644 index 0000000000000..cb6f4a28402d1 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.test.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { validateUrl, validateUrlTemplate } from './url_validation'; + +describe('validateUrl', () => { + describe('unsafe urls', () => { + const unsafeUrls = [ + // eslint-disable-next-line no-script-url + 'javascript:evil()', + // eslint-disable-next-line no-script-url + 'JavaScript:abc', + 'evilNewProtocol:abc', + ' \n Java\n Script:abc', + 'javascript:', + 'javascript:', + 'j avascript:', + 'javascript:', + 'javascript:', + 'jav ascript:alert();', + // 'jav\u0000ascript:alert();', CI fails on this one + 'data:;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', + 'data:,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', + 'data:iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', + 'data:text/javascript;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', + 'data:application/x-msdownload;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', + ]; + + for (const url of unsafeUrls) { + test(`unsafe ${url}`, () => { + expect(validateUrl(url).isValid).toBe(false); + }); + } + }); + + describe('invalid urls', () => { + const invalidUrls = ['elastic.co', 'www.elastic.co', 'test', '', ' ', 'https://']; + for (const url of invalidUrls) { + test(`invalid ${url}`, () => { + expect(validateUrl(url).isValid).toBe(false); + }); + } + }); + + describe('valid urls', () => { + const validUrls = [ + 'https://elastic.co', + 'https://www.elastic.co', + 'http://elastic', + 'mailto:someone', + ]; + for (const url of validUrls) { + test(`valid ${url}`, () => { + expect(validateUrl(url).isValid).toBe(true); + }); + } + }); +}); + +describe('validateUrlTemplate', () => { + test('domain in variable is allowed', () => { + expect( + validateUrlTemplate( + { template: '{{kibanaUrl}}/test' }, + { kibanaUrl: 'http://localhost:5601/app' } + ).isValid + ).toBe(true); + }); + + test('unsafe domain in variable is not allowed', () => { + expect( + // eslint-disable-next-line no-script-url + validateUrlTemplate({ template: '{{kibanaUrl}}/test' }, { kibanaUrl: 'javascript:evil()' }) + .isValid + ).toBe(false); + }); + + test('if missing variable then invalid', () => { + expect( + validateUrlTemplate({ template: '{{url}}/test' }, { kibanaUrl: 'http://localhost:5601/app' }) + .isValid + ).toBe(false); + }); +}); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.ts new file mode 100644 index 0000000000000..b32f5d84c6775 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_validation.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { UrlDrilldownConfig, UrlDrilldownScope } from './types'; +import { compile } from './url_template'; + +const generalFormatError = i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatGeneralErrorMessage', + { + defaultMessage: 'Invalid format. Example: {exampleUrl}', + values: { + exampleUrl: 'https://www.my-url.com/?{{event.key}}={{event.value}}', + }, + } +); + +const formatError = (message: string) => + i18n.translate( + 'xpack.uiActionsEnhanced.drilldowns.urlDrilldownValidation.urlFormatErrorMessage', + { + defaultMessage: 'Invalid format: {message}', + values: { + message, + }, + } + ); + +const SAFE_URL_PATTERN = /^(?:(?:https?|mailto):|[^&:/?#]*(?:[/?#]|$))/gi; +export function validateUrl(url: string): { isValid: boolean; error?: string } { + if (!url) + return { + isValid: false, + error: generalFormatError, + }; + + try { + new URL(url); + if (!url.match(SAFE_URL_PATTERN)) throw new Error(); + return { isValid: true }; + } catch (e) { + return { + isValid: false, + error: generalFormatError, + }; + } +} + +export function validateUrlTemplate( + urlTemplate: UrlDrilldownConfig['url'], + scope: UrlDrilldownScope +): { isValid: boolean; error?: string } { + if (!urlTemplate.template) + return { + isValid: false, + error: generalFormatError, + }; + + try { + const compiledUrl = compile(urlTemplate.template, scope); + return validateUrl(compiledUrl); + } catch (e) { + return { + isValid: false, + error: formatError(e.message), + }; + } +} diff --git a/x-pack/plugins/ui_actions_enhanced/public/index.ts b/x-pack/plugins/ui_actions_enhanced/public/index.ts index a255bc28f5c68..4a899b24852a9 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/index.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/index.ts @@ -32,3 +32,4 @@ export { } from './dynamic_actions'; export { DrilldownDefinition as UiActionsEnhancedDrilldownDefinition } from './drilldowns'; +export * from './drilldowns/url_drilldown'; diff --git a/x-pack/plugins/ui_actions_enhanced/scripts/storybook.js b/x-pack/plugins/ui_actions_enhanced/scripts/storybook.js index 1e3ab0d96b81c..bf43167a3ae51 100644 --- a/x-pack/plugins/ui_actions_enhanced/scripts/storybook.js +++ b/x-pack/plugins/ui_actions_enhanced/scripts/storybook.js @@ -9,8 +9,5 @@ import { join } from 'path'; // eslint-disable-next-line require('@kbn/storybook').runStorybookCli({ name: 'ui_actions_enhanced', - storyGlobs: [ - join(__dirname, '..', 'public', 'components', '**', '*.story.tsx'), - join(__dirname, '..', 'public', 'drilldowns', 'components', '**', '*.story.tsx'), - ], + storyGlobs: [join(__dirname, '..', 'public', '**', '*.story.tsx')], }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts index b927b563eb54a..78ca2af12ec3f 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts @@ -28,5 +28,8 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./alerts_space1')); loadTestFile(require.resolve('./alerts_default_space')); loadTestFile(require.resolve('./builtin_alert_types')); + + // note that this test will destroy existing spaces + loadTestFile(require.resolve('./migrations')); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts new file mode 100644 index 0000000000000..81f7c8c97ba8c --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { getUrlPrefix } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function createGetTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('migrations', () => { + before(async () => { + await esArchiver.load('alerts'); + }); + + after(async () => { + await esArchiver.unload('alerts'); + }); + + it('7.10.0 migrates the `alerting` consumer to be the `alerts`', async () => { + const response = await supertest.get( + `${getUrlPrefix(``)}/api/alerts/alert/74f3e6d7-b7bb-477d-ac28-92ee22728e6e` + ); + + expect(response.status).to.eql(200); + expect(response.body.consumer).to.equal('alerts'); + }); + + it('7.10.0 migrates the `metrics` consumer to be the `infrastructure`', async () => { + const response = await supertest.get( + `${getUrlPrefix(``)}/api/alerts/alert/74f3e6d7-b7bb-477d-ac28-fdf248d5f2a4` + ); + + expect(response.status).to.eql(200); + expect(response.body.consumer).to.equal('infrastructure'); + }); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts similarity index 99% rename from x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts rename to x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts index 29ead0db1c634..c300412c393bc 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts @@ -22,7 +22,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const filterBar = getService('filterBar'); - describe('Dashboard Drilldowns', function () { + describe('Dashboard to dashboard drilldown', function () { before(async () => { log.debug('Dashboard Drilldowns:initTests'); await PageObjects.common.navigateToApp('dashboard'); diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts new file mode 100644 index 0000000000000..12de29c4fde10 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_url_drilldown.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +const DRILLDOWN_TO_DISCOVER_URL = 'Go to discover'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const dashboardPanelActions = getService('dashboardPanelActions'); + const dashboardDrilldownPanelActions = getService('dashboardDrilldownPanelActions'); + const dashboardDrilldownsManage = getService('dashboardDrilldownsManage'); + const PageObjects = getPageObjects(['dashboard', 'common', 'header', 'timePicker', 'discover']); + const log = getService('log'); + const browser = getService('browser'); + const testSubjects = getService('testSubjects'); + + describe('Dashboard to URL drilldown', function () { + before(async () => { + log.debug('Dashboard to URL:initTests'); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + }); + + it('should create dashboard to URL drilldown and use it to navigate to discover', async () => { + await PageObjects.dashboard.gotoDashboardEditMode( + dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME + ); + + // create drilldown + await dashboardPanelActions.openContextMenu(); + await dashboardDrilldownPanelActions.expectExistsCreateDrilldownAction(); + await dashboardDrilldownPanelActions.clickCreateDrilldown(); + await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutOpen(); + + const urlTemplate = `{{kibanaUrl}}/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:'{{event.from}}',to:'{{event.to}}'))&_a=(columns:!(_source),filters:{{rison context.panel.filters}},index:'{{context.panel.indexPatternId}}',interval:auto,query:(language:{{context.panel.query.language}},query:'{{context.panel.query.query}}'),sort:!())`; + + await dashboardDrilldownsManage.fillInDashboardToURLDrilldownWizard({ + drilldownName: DRILLDOWN_TO_DISCOVER_URL, + destinationURLTemplate: urlTemplate, + trigger: 'SELECT_RANGE_TRIGGER', + }); + await dashboardDrilldownsManage.saveChanges(); + await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutClose(); + + // check that drilldown notification badge is shown + expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(2); + + // save dashboard, navigate to view mode + await PageObjects.dashboard.saveDashboard( + dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME, + { + saveAsNew: false, + waitDialogIsClosed: true, + } + ); + + const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + + await brushAreaChart(); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_DISCOVER_URL); + + await PageObjects.discover.waitForDiscoverAppOnScreen(); + + // check that new time range duration was applied + const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); + }); + }); + + // utils which shouldn't be a part of test flow, but also too specific to be moved to pageobject or service + async function brushAreaChart() { + const areaChart = await testSubjects.find('visualizationLoader'); + expect(await areaChart.getAttribute('data-title')).to.be('Visualization漢字 AreaChart'); + await browser.dragAndDrop( + { + location: areaChart, + offset: { + x: -100, + y: 0, + }, + }, + { + location: areaChart, + offset: { + x: 100, + y: 0, + }, + } + ); + } +} diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts index ff604b18e1d51..57454f50266da 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts @@ -22,7 +22,8 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { await esArchiver.unload('dashboard/drilldowns'); }); - loadTestFile(require.resolve('./dashboard_drilldowns')); + loadTestFile(require.resolve('./dashboard_to_dashboard_drilldown')); + loadTestFile(require.resolve('./dashboard_to_url_drilldown')); loadTestFile(require.resolve('./explore_data_panel_action')); // Disabled for now as it requires xpack.discoverEnhanced.actions.exploreDataInChart.enabled diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts index 18a7a4b26a752..3fbe101d4ff19 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; @@ -433,9 +432,7 @@ export default function ({ getService }: FtrProviderContext) { 'job creation displays the created job in the job list' ); await ml.jobTable.refreshJobList(); - await ml.jobTable.filterWithSearchString(testData.jobId); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === testData.jobId)).to.have.length(1); + await ml.jobTable.filterWithSearchString(testData.jobId, 1); await ml.testExecution.logTestStep( 'job creation displays details for the created job in the job list' @@ -651,9 +648,7 @@ export default function ({ getService }: FtrProviderContext) { 'job cloning displays the created job in the job list' ); await ml.jobTable.refreshJobList(); - await ml.jobTable.filterWithSearchString(testData.jobIdClone); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === testData.jobIdClone)).to.have.length(1); + await ml.jobTable.filterWithSearchString(testData.jobIdClone, 1); await ml.testExecution.logTestStep( 'job cloning displays details for the created job in the job list' diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts b/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts index 9e48c71ab0eba..5353f53e74d0b 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { Job, Datafeed } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs'; @@ -62,9 +61,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(JOB_CONFIG.job_id); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === JOB_CONFIG.job_id)).to.have.length(1); + await ml.jobTable.filterWithSearchString(JOB_CONFIG.job_id, 1); await ml.jobTable.clickOpenJobInSingleMetricViewerButton(JOB_CONFIG.job_id); await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts index cfbebd478fcb8..4fdbda6e54893 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { Job, Datafeed } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs'; @@ -86,9 +85,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('open job in anomaly explorer'); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(testData.jobConfig.job_id); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === testData.jobConfig.job_id)).to.have.length(1); + await ml.jobTable.filterWithSearchString(testData.jobConfig.job_id, 1); await ml.jobTable.clickOpenJobInAnomalyExplorerButton(testData.jobConfig.job_id); await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts index c410aff292ffa..2524d0486171b 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../../plugins/ml/common/constants/categorization_job'; @@ -202,9 +201,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(jobId); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === jobId)).to.have.length(1); + await ml.jobTable.filterWithSearchString(jobId, 1); await ml.testExecution.logTestStep( 'job creation displays details for the created job in the job list' @@ -320,9 +317,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(jobIdClone); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === jobIdClone)).to.have.length(1); + await ml.jobTable.filterWithSearchString(jobIdClone, 1); await ml.testExecution.logTestStep( 'job cloning displays details for the created job in the job list' @@ -353,9 +348,7 @@ export default function ({ getService }: FtrProviderContext) { 'job deletion does not display the deleted job in the job list any more' ); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(jobIdClone); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === jobIdClone)).to.have.length(0); + await ml.jobTable.filterWithSearchString(jobIdClone, 0); await ml.testExecution.logTestStep( 'job deletion does not have results for the deleted job any more' diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts index 22b4c4a1fdfe3..af30946ee08ce 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; @@ -315,9 +314,7 @@ export default function ({ getService }: FtrProviderContext) { 'job creation displays the created job in the job list' ); await ml.jobTable.refreshJobList(); - await ml.jobTable.filterWithSearchString(testData.jobId); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === testData.jobId)).to.have.length(1); + await ml.jobTable.filterWithSearchString(testData.jobId, 1); await ml.testExecution.logTestStep( 'job creation displays details for the created job in the job list' diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts index 8702cfd734454..5324890b269bc 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; @@ -205,9 +204,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(jobId); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === jobId)).to.have.length(1); + await ml.jobTable.filterWithSearchString(jobId, 1); await ml.testExecution.logTestStep( 'job creation displays details for the created job in the job list' @@ -340,9 +337,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(jobIdClone); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === jobIdClone)).to.have.length(1); + await ml.jobTable.filterWithSearchString(jobIdClone, 1); await ml.testExecution.logTestStep( 'job cloning displays details for the created job in the job list' diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts index 3ec78eccf3de4..4797334ee57af 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; @@ -231,9 +230,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(jobId); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === jobId)).to.have.length(1); + await ml.jobTable.filterWithSearchString(jobId, 1); await ml.testExecution.logTestStep( 'job creation displays details for the created job in the job list' @@ -377,9 +374,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(jobIdClone); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === jobIdClone)).to.have.length(1); + await ml.jobTable.filterWithSearchString(jobIdClone, 1); await ml.testExecution.logTestStep( 'job cloning displays details for the created job in the job list' diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts index 170b88efd70f5..ea3a42e2f27c8 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; @@ -410,9 +409,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(testData.jobId); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === testData.jobId)).to.have.length(1); + await ml.jobTable.filterWithSearchString(testData.jobId, 1); await ml.testExecution.logTestStep( 'job creation displays details for the created job in the job list' diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts index ba5628661bfc2..89612e51eee13 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; @@ -184,9 +183,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(jobId); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === jobId)).to.have.length(1); + await ml.jobTable.filterWithSearchString(jobId, 1); await ml.testExecution.logTestStep( 'job creation displays details for the created job in the job list' @@ -303,9 +300,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(jobIdClone); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === jobIdClone)).to.have.length(1); + await ml.jobTable.filterWithSearchString(jobIdClone, 1); await ml.testExecution.logTestStep( 'job cloning displays details for the created job in the job list' @@ -336,9 +331,7 @@ export default function ({ getService }: FtrProviderContext) { 'job deletion does not display the deleted job in the job list any more' ); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(jobIdClone); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === jobIdClone)).to.have.length(0); + await ml.jobTable.filterWithSearchString(jobIdClone, 0); await ml.testExecution.logTestStep( 'job deletion does not have results for the deleted job any more' diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts index e1ab3f8e092c3..1dc4708c57dbc 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { Job, Datafeed } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs'; @@ -60,9 +59,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('open job in single metric viewer'); await ml.jobTable.waitForJobsToLoad(); - await ml.jobTable.filterWithSearchString(JOB_CONFIG.job_id); - const rows = await ml.jobTable.parseJobTable(); - expect(rows.filter((row) => row.id === JOB_CONFIG.job_id)).to.have.length(1); + await ml.jobTable.filterWithSearchString(JOB_CONFIG.job_id, 1); await ml.jobTable.clickOpenJobInSingleMetricViewerButton(JOB_CONFIG.job_id); await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts index 6beefaafa3792..e12c853a0be83 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; @@ -147,13 +146,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('displays the created job in the analytics table'); await ml.dataFrameAnalyticsTable.refreshAnalyticsTable(); - await ml.dataFrameAnalyticsTable.filterWithSearchString(testData.jobId); - const rows = await ml.dataFrameAnalyticsTable.parseAnalyticsTable(); - const filteredRows = rows.filter((row) => row.id === testData.jobId); - expect(filteredRows).to.have.length( - 1, - `Filtered analytics table should have 1 row for job id '${testData.jobId}' (got matching items '${filteredRows}')` - ); + await ml.dataFrameAnalyticsTable.filterWithSearchString(testData.jobId, 1); await ml.testExecution.logTestStep( 'displays details for the created job in the analytics table' @@ -208,9 +201,11 @@ export default function ({ getService }: FtrProviderContext) { await ml.api.assertIndicesNotEmpty(testData.destinationIndex); await ml.testExecution.logTestStep('displays the results view for created job'); - await ml.dataFrameAnalyticsTable.openResultsView(); - await ml.dataFrameAnalytics.assertClassificationEvaluatePanelElementsExists(); - await ml.dataFrameAnalytics.assertClassificationTablePanelExists(); + await ml.dataFrameAnalyticsTable.openResultsView(testData.jobId); + await ml.dataFrameAnalyticsResults.assertClassificationEvaluatePanelElementsExists(); + await ml.dataFrameAnalyticsResults.assertClassificationTablePanelExists(); + await ml.dataFrameAnalyticsResults.assertResultsTableExists(); + await ml.dataFrameAnalyticsResults.assertResultsTableNotEmpty(); }); }); } diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts index 5494f2f963d37..532de930bc1a1 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts @@ -148,7 +148,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToMl(); await ml.navigation.navigateToDataFrameAnalytics(); await ml.dataFrameAnalyticsTable.waitForAnalyticsToLoad(); - await ml.dataFrameAnalyticsTable.filterWithSearchString(testData.job.id as string); + await ml.dataFrameAnalyticsTable.filterWithSearchString(testData.job.id as string, 1); await ml.dataFrameAnalyticsTable.cloneJob(testData.job.id as string); }); @@ -217,13 +217,7 @@ export default function ({ getService }: FtrProviderContext) { ); await ml.dataFrameAnalyticsCreation.navigateToJobManagementPage(); await ml.dataFrameAnalyticsTable.refreshAnalyticsTable(); - await ml.dataFrameAnalyticsTable.filterWithSearchString(cloneJobId); - const rows = await ml.dataFrameAnalyticsTable.parseAnalyticsTable(); - const filteredRows = rows.filter((row) => row.id === cloneJobId); - expect(filteredRows).to.have.length( - 1, - `Filtered analytics table should have 1 row for job id '${cloneJobId}' (got matching items '${filteredRows}')` - ); + await ml.dataFrameAnalyticsTable.filterWithSearchString(cloneJobId, 1); }); }); } diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts index e4bc7b940aaea..b5b0f4c94f262 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; @@ -163,13 +162,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('displays the created job in the analytics table'); await ml.dataFrameAnalyticsTable.refreshAnalyticsTable(); - await ml.dataFrameAnalyticsTable.filterWithSearchString(testData.jobId); - const rows = await ml.dataFrameAnalyticsTable.parseAnalyticsTable(); - const filteredRows = rows.filter((row) => row.id === testData.jobId); - expect(filteredRows).to.have.length( - 1, - `Filtered analytics table should have 1 row for job id '${testData.jobId}' (got matching items '${filteredRows}')` - ); + await ml.dataFrameAnalyticsTable.filterWithSearchString(testData.jobId, 1); await ml.testExecution.logTestStep( 'displays details for the created job in the analytics table' @@ -224,8 +217,10 @@ export default function ({ getService }: FtrProviderContext) { await ml.api.assertIndicesNotEmpty(testData.destinationIndex); await ml.testExecution.logTestStep('displays the results view for created job'); - await ml.dataFrameAnalyticsTable.openResultsView(); - await ml.dataFrameAnalytics.assertOutlierTablePanelExists(); + await ml.dataFrameAnalyticsTable.openResultsView(testData.jobId); + await ml.dataFrameAnalyticsResults.assertOutlierTablePanelExists(); + await ml.dataFrameAnalyticsResults.assertResultsTableExists(); + await ml.dataFrameAnalyticsResults.assertResultsTableNotEmpty(); }); }); } diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts index af9c5417e4826..c58220f2d1ad2 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; @@ -147,13 +146,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('displays the created job in the analytics table'); await ml.dataFrameAnalyticsTable.refreshAnalyticsTable(); - await ml.dataFrameAnalyticsTable.filterWithSearchString(testData.jobId); - const rows = await ml.dataFrameAnalyticsTable.parseAnalyticsTable(); - const filteredRows = rows.filter((row) => row.id === testData.jobId); - expect(filteredRows).to.have.length( - 1, - `Filtered analytics table should have 1 row for job id '${testData.jobId}' (got matching items '${filteredRows}')` - ); + await ml.dataFrameAnalyticsTable.filterWithSearchString(testData.jobId, 1); await ml.testExecution.logTestStep( 'displays details for the created job in the analytics table' @@ -208,9 +201,11 @@ export default function ({ getService }: FtrProviderContext) { await ml.api.assertIndicesNotEmpty(testData.destinationIndex); await ml.testExecution.logTestStep('displays the results view for created job'); - await ml.dataFrameAnalyticsTable.openResultsView(); - await ml.dataFrameAnalytics.assertRegressionEvaluatePanelElementsExists(); - await ml.dataFrameAnalytics.assertRegressionTablePanelExists(); + await ml.dataFrameAnalyticsTable.openResultsView(testData.jobId); + await ml.dataFrameAnalyticsResults.assertRegressionEvaluatePanelElementsExists(); + await ml.dataFrameAnalyticsResults.assertRegressionTablePanelExists(); + await ml.dataFrameAnalyticsResults.assertResultsTableExists(); + await ml.dataFrameAnalyticsResults.assertResultsTableNotEmpty(); }); }); } diff --git a/x-pack/test/functional/apps/ml/index.ts b/x-pack/test/functional/apps/ml/index.ts index 2d8aac3b8dddf..e224f5c8bb128 100644 --- a/x-pack/test/functional/apps/ml/index.ts +++ b/x-pack/test/functional/apps/ml/index.ts @@ -20,10 +20,8 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { after(async () => { await ml.securityCommon.cleanMlUsers(); await ml.securityCommon.cleanMlRoles(); - await ml.testResources.deleteSavedSearches(); await ml.testResources.deleteDashboards(); - await ml.testResources.deleteIndexPatternByTitle('ft_farequote'); await ml.testResources.deleteIndexPatternByTitle('ft_ecommerce'); await ml.testResources.deleteIndexPatternByTitle('ft_categorization'); @@ -31,7 +29,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await ml.testResources.deleteIndexPatternByTitle('ft_bank_marketing'); await ml.testResources.deleteIndexPatternByTitle('ft_ihp_outlier'); await ml.testResources.deleteIndexPatternByTitle('ft_egs_regression'); - await esArchiver.unload('ml/farequote'); await esArchiver.unload('ml/ecommerce'); await esArchiver.unload('ml/categorization'); @@ -39,11 +36,12 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await esArchiver.unload('ml/bm_classification'); await esArchiver.unload('ml/ihp_outlier'); await esArchiver.unload('ml/egs_regression'); - await ml.testResources.resetKibanaTimeZone(); + await ml.securityUI.logout(); }); loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./permissions')); loadTestFile(require.resolve('./pages')); loadTestFile(require.resolve('./anomaly_detection')); loadTestFile(require.resolve('./data_visualizer')); diff --git a/x-pack/test/functional/apps/ml/pages.ts b/x-pack/test/functional/apps/ml/pages.ts index 5d084d5abe11e..27b61a7dbc0f8 100644 --- a/x-pack/test/functional/apps/ml/pages.ts +++ b/x-pack/test/functional/apps/ml/pages.ts @@ -17,7 +17,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('loads the ML pages', async () => { - await ml.testExecution.logTestStep('home'); + await ml.testExecution.logTestStep('loads the ML home page'); await ml.navigation.navigateToMl(); await ml.testExecution.logTestStep('loads the overview page'); @@ -34,10 +34,10 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('loads the settings page'); await ml.navigation.navigateToSettings(); - await ml.settings.assertSettingsManageCalendarsLinkExists(); - await ml.settings.assertSettingsCreateCalendarLinkExists(); - await ml.settings.assertSettingsManageFilterListsLinkExists(); - await ml.settings.assertSettingsCreateFilterListLinkExists(); + await ml.settings.assertManageCalendarsLinkExists(); + await ml.settings.assertCreateCalendarLinkExists(); + await ml.settings.assertManageFilterListsLinkExists(); + await ml.settings.assertCreateFilterListLinkExists(); await ml.testExecution.logTestStep('loads the data frame analytics page'); await ml.navigation.navigateToDataFrameAnalytics(); diff --git a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts new file mode 100644 index 0000000000000..eed7489b09fe6 --- /dev/null +++ b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts @@ -0,0 +1,480 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import path from 'path'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +import { USER } from '../../../services/ml/security_common'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + const testUsers = [USER.ML_POWERUSER, USER.ML_POWERUSER_SPACES]; + + describe('for user with full ML access', function () { + this.tags(['skipFirefox', 'mlqa']); + + describe('with no data loaded', function () { + for (const user of testUsers) { + describe(`(${user})`, function () { + before(async () => { + await ml.securityUI.loginAs(user); + await ml.api.cleanMlIndices(); + }); + + after(async () => { + await ml.securityUI.logout(); + }); + + it('should display the ML file data vis link on the Kibana home page', async () => { + await ml.testExecution.logTestStep('should load the Kibana home page'); + await ml.navigation.navigateToKibanaHome(); + + await ml.testExecution.logTestStep('should display the ML file data vis link'); + await ml.commonUI.assertKibanaHomeFileDataVisLinkExists(); + }); + + it('should display the ML entry in Kibana app menu', async () => { + await ml.testExecution.logTestStep('should open the Kibana app menu'); + await ml.navigation.openKibanaNav(); + + await ml.testExecution.logTestStep('should display the ML nav link'); + await ml.navigation.assertKibanaNavMLEntryExists(); + }); + + it('should display elements on ML Overview page correctly', async () => { + await ml.testExecution.logTestStep('should load the ML overview page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToOverview(); + + await ml.testExecution.logTestStep('should display enabled AD create job button'); + await ml.overviewPage.assertADCreateJobButtonExists(); + await ml.overviewPage.assertADCreateJobButtonEnabled(true); + + await ml.testExecution.logTestStep('should display enabled DFA create job button'); + await ml.overviewPage.assertDFACreateJobButtonExists(); + await ml.overviewPage.assertDFACreateJobButtonEnabled(true); + }); + }); + } + }); + + describe('with data loaded', function () { + const adJobId = 'fq_single_permission'; + const dfaJobId = 'iph_outlier_permission'; + const calendarId = 'calendar_permission'; + const eventDescription = 'calendar_event_permission'; + const filterId = 'filter_permission'; + const filterItems = ['filter_item_permission']; + + const ecIndexPattern = 'ft_module_sample_ecommerce'; + const ecExpectedTotalCount = 287; + const ecExpectedFieldPanelCount = 2; + const ecExpectedModuleId = 'sample_data_ecommerce'; + + const uploadFilePath = path.join( + __dirname, + '..', + 'data_visualizer', + 'files_to_import', + 'artificial_server_log' + ); + const expectedUploadFileTitle = 'artificial_server_log'; + + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await esArchiver.loadIfNeeded('ml/ihp_outlier'); + await esArchiver.loadIfNeeded('ml/module_sample_ecommerce'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + await ml.testResources.createIndexPatternIfNeeded('ft_ihp_outlier', '@timestamp'); + await ml.testResources.createIndexPatternIfNeeded(ecIndexPattern, 'order_date'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + await ml.api.createAndRunAnomalyDetectionLookbackJob( + ml.commonConfig.getADFqMultiMetricJobConfig(adJobId), + ml.commonConfig.getADFqDatafeedConfig(adJobId) + ); + + await ml.api.createAndRunDFAJob( + ml.commonConfig.getDFAIhpOutlierDetectionJobConfig(dfaJobId) + ); + + await ml.api.createCalendar(calendarId, { + calendar_id: calendarId, + job_ids: [], + description: 'Test calendar', + }); + await ml.api.createCalendarEvents(calendarId, [ + { + description: eventDescription, + start_time: 1513641600000, + end_time: 1513728000000, + }, + ]); + + await ml.api.createFilter(filterId, { + description: 'Test filter list', + items: filterItems, + }); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + await ml.api.deleteIndices(`user-${dfaJobId}`); + await ml.api.deleteCalendar(calendarId); + await ml.api.deleteFilter(filterId); + }); + + for (const user of testUsers) { + describe(`(${user})`, function () { + before(async () => { + await ml.securityUI.loginAs(user); + }); + + after(async () => { + await ml.securityUI.logout(); + }); + + it('should display elements on Anomaly Detection page correctly', async () => { + await ml.testExecution.logTestStep('should load the AD job management page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToAnomalyDetection(); + + await ml.testExecution.logTestStep('should display the stats bar and the AD job table'); + await ml.jobManagement.assertJobStatsBarExists(); + await ml.jobManagement.assertJobTableExists(); + + await ml.testExecution.logTestStep('should display an enabled "Create job" button'); + await ml.jobManagement.assertCreateNewJobButtonExists(); + await ml.jobManagement.assertCreateNewJobButtonEnabled(true); + + await ml.testExecution.logTestStep('should display the AD job in the list'); + await ml.jobTable.filterWithSearchString(adJobId, 1); + + await ml.testExecution.logTestStep('should display enabled AD job result links'); + await ml.jobTable.assertJobActionSingleMetricViewerButtonEnabled(adJobId, true); + await ml.jobTable.assertJobActionAnomalyExplorerButtonEnabled(adJobId, true); + + await ml.testExecution.logTestStep('should display enabled AD job row action buttons'); + await ml.jobTable.assertJobActionsMenuButtonEnabled(adJobId, true); + await ml.jobTable.assertJobActionStartDatafeedButtonEnabled(adJobId, true); + await ml.jobTable.assertJobActionCloneJobButtonEnabled(adJobId, true); + await ml.jobTable.assertJobActionEditJobButtonEnabled(adJobId, true); + await ml.jobTable.assertJobActionDeleteJobButtonEnabled(adJobId, true); + + await ml.testExecution.logTestStep('should select the job'); + await ml.jobTable.selectJobRow(adJobId); + + await ml.testExecution.logTestStep('should display enabled multi select result links'); + await ml.jobTable.assertMultiSelectActionSingleMetricViewerButtonEnabled(true); + await ml.jobTable.assertMultiSelectActionAnomalyExplorerButtonEnabled(true); + + await ml.testExecution.logTestStep( + 'should display enabled multi select action buttons' + ); + await ml.jobTable.assertMultiSelectManagementActionsButtonEnabled(true); + await ml.jobTable.assertMultiSelectStartDatafeedActionButtonEnabled(true); + await ml.jobTable.assertMultiSelectDeleteJobActionButtonEnabled(true); + await ml.jobTable.deselectJobRow(adJobId); + }); + + it('should display elements on Single Metric Viewer page correctly', async () => { + await ml.testExecution.logTestStep('should open AD job in the single metric viewer'); + await ml.jobTable.clickOpenJobInSingleMetricViewerButton(adJobId); + await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); + + await ml.testExecution.logTestStep('should pre-fill the AD job selection'); + await ml.jobSelection.assertJobSelection([adJobId]); + + await ml.testExecution.logTestStep('should pre-fill the detector input'); + await ml.singleMetricViewer.assertDetectorInputExsist(); + await ml.singleMetricViewer.assertDetectorInputValue('0'); + + await ml.testExecution.logTestStep('should input the airline entity value'); + await ml.singleMetricViewer.assertEntityInputExsist('airline'); + await ml.singleMetricViewer.assertEntityInputSelection('airline', []); + await ml.singleMetricViewer.selectEntityValue('airline', 'AAL'); + + await ml.testExecution.logTestStep('should display the chart'); + await ml.singleMetricViewer.assertChartExsist(); + + await ml.testExecution.logTestStep('should display the annotations section'); + await ml.singleMetricViewer.assertAnnotationsExists('loaded'); + + await ml.testExecution.logTestStep('should display the anomalies table with entries'); + await ml.anomaliesTable.assertTableExists(); + await ml.anomaliesTable.assertTableNotEmpty(); + + await ml.testExecution.logTestStep('should display enabled anomaly row action buttons'); + await ml.anomaliesTable.assertAnomalyActionsMenuButtonExists(0); + await ml.anomaliesTable.assertAnomalyActionsMenuButtonEnabled(0, true); + await ml.anomaliesTable.assertAnomalyActionConfigureRulesButtonEnabled(0, true); + + await ml.testExecution.logTestStep( + 'should display the forecast modal with enabled run button' + ); + await ml.singleMetricViewer.assertForecastButtonExists(); + await ml.singleMetricViewer.assertForecastButtonEnabled(true); + await ml.singleMetricViewer.openForecastModal(); + await ml.singleMetricViewer.assertForecastModalRunButtonEnabled(true); + await ml.singleMetricViewer.closeForecastModal(); + }); + + it('should display elements on Anomaly Explorer page correctly', async () => { + await ml.testExecution.logTestStep('should open AD job in the anomaly explorer'); + await ml.singleMetricViewer.openAnomalyExplorer(); + await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); + + await ml.testExecution.logTestStep('should pre-fill the AD job selection'); + await ml.jobSelection.assertJobSelection([adJobId]); + + await ml.testExecution.logTestStep('should display the influencers list'); + await ml.anomalyExplorer.assertInfluencerListExists(); + await ml.anomalyExplorer.assertInfluencerFieldListNotEmpty('airline'); + + await ml.testExecution.logTestStep('should display the swim lanes'); + await ml.anomalyExplorer.assertOverallSwimlaneExists(); + await ml.anomalyExplorer.assertSwimlaneViewByExists(); + + await ml.testExecution.logTestStep('should display the annotations panel'); + await ml.anomalyExplorer.assertAnnotationsPanelExists('loaded'); + + await ml.testExecution.logTestStep('should display the anomalies table with entries'); + await ml.anomaliesTable.assertTableExists(); + await ml.anomaliesTable.assertTableNotEmpty(); + + await ml.testExecution.logTestStep('should display enabled anomaly row action button'); + await ml.anomaliesTable.assertAnomalyActionsMenuButtonExists(0); + await ml.anomaliesTable.assertAnomalyActionsMenuButtonEnabled(0, true); + + await ml.testExecution.logTestStep( + 'should display enabled configure rules action button' + ); + await ml.anomaliesTable.assertAnomalyActionConfigureRulesButtonEnabled(0, true); + + await ml.testExecution.logTestStep('should display enabled view series action button'); + await ml.anomaliesTable.assertAnomalyActionViewSeriesButtonEnabled(0, true); + }); + + it('should display elements on Data Frame Analytics page correctly', async () => { + await ml.testExecution.logTestStep('should load the DFA job management page'); + await ml.navigation.navigateToDataFrameAnalytics(); + + await ml.testExecution.logTestStep( + 'should display the stats bar and the analytics table' + ); + await ml.dataFrameAnalytics.assertAnalyticsStatsBarExists(); + await ml.dataFrameAnalytics.assertAnalyticsTableExists(); + + await ml.testExecution.logTestStep('should display an enabled "Create job" button'); + await ml.dataFrameAnalytics.assertCreateNewAnalyticsButtonExists(); + await ml.dataFrameAnalytics.assertCreateNewAnalyticsButtonEnabled(true); + + await ml.testExecution.logTestStep('should display the DFA job in the list'); + await ml.dataFrameAnalyticsTable.filterWithSearchString(dfaJobId, 1); + + await ml.testExecution.logTestStep( + 'should display enabled DFA job view and action menu' + ); + await ml.dataFrameAnalyticsTable.assertJobRowViewButtonEnabled(dfaJobId, true); + await ml.dataFrameAnalyticsTable.assertJowRowActionsMenuButtonEnabled(dfaJobId, true); + await ml.dataFrameAnalyticsTable.assertJobActionViewButtonEnabled(dfaJobId, true); + + await ml.testExecution.logTestStep('should display enabled DFA job row action buttons'); + await ml.dataFrameAnalyticsTable.assertJobActionStartButtonEnabled(dfaJobId, false); // job already completed + await ml.dataFrameAnalyticsTable.assertJobActionEditButtonEnabled(dfaJobId, true); + await ml.dataFrameAnalyticsTable.assertJobActionCloneButtonEnabled(dfaJobId, true); + await ml.dataFrameAnalyticsTable.assertJobActionDeleteButtonEnabled(dfaJobId, true); + await ml.dataFrameAnalyticsTable.ensureJobActionsMenuClosed(dfaJobId); + }); + + it('should display elements on Data Frame Analytics results view page correctly', async () => { + await ml.testExecution.logTestStep('displays the results view for created job'); + await ml.dataFrameAnalyticsTable.openResultsView(dfaJobId); + await ml.dataFrameAnalyticsResults.assertOutlierTablePanelExists(); + await ml.dataFrameAnalyticsResults.assertResultsTableExists(); + await ml.dataFrameAnalyticsResults.assertResultsTableNotEmpty(); + }); + + it('should display elements on Data Visualizer home page correctly', async () => { + await ml.testExecution.logTestStep('should load the data visualizer page'); + await ml.navigation.navigateToDataVisualizer(); + + await ml.testExecution.logTestStep( + 'should display the "import data" card with enabled button' + ); + await ml.dataVisualizer.assertDataVisualizerImportDataCardExists(); + await ml.dataVisualizer.assertUploadFileButtonEnabled(true); + + await ml.testExecution.logTestStep( + 'should display the "select index pattern" card with enabled button' + ); + await ml.dataVisualizer.assertDataVisualizerIndexDataCardExists(); + await ml.dataVisualizer.assertSelectIndexButtonEnabled(true); + }); + + it('should display elements on Index Data Visualizer page correctly', async () => { + await ml.testExecution.logTestStep( + 'should load an index into the data visualizer page' + ); + await ml.dataVisualizer.navigateToIndexPatternSelection(); + await ml.jobSourceSelection.selectSourceForIndexBasedDataVisualizer(ecIndexPattern); + + await ml.testExecution.logTestStep('should display the time range step'); + await ml.dataVisualizerIndexBased.assertTimeRangeSelectorSectionExists(); + + await ml.testExecution.logTestStep('should load data for full time range'); + await ml.dataVisualizerIndexBased.clickUseFullDataButton(ecExpectedTotalCount); + + await ml.testExecution.logTestStep('should display the panels of fields'); + await ml.dataVisualizerIndexBased.assertFieldsPanelsExist(ecExpectedFieldPanelCount); + + await ml.testExecution.logTestStep('should display the actions panel with cards'); + await ml.dataVisualizerIndexBased.assertActionsPanelExists(); + await ml.dataVisualizerIndexBased.assertCreateAdvancedJobCardExists(); + await ml.dataVisualizerIndexBased.assertRecognizerCardExists(ecExpectedModuleId); + }); + + it('should display elements on File Data Visualizer page correctly', async () => { + await ml.testExecution.logTestStep( + 'should load the file data visualizer file selection' + ); + await ml.navigation.navigateToDataVisualizer(); + await ml.dataVisualizer.navigateToFileUpload(); + + await ml.testExecution.logTestStep( + 'should select a file and load visualizer result page' + ); + await ml.dataVisualizerFileBased.selectFile(uploadFilePath); + + await ml.testExecution.logTestStep( + 'should displays components of the file details page' + ); + await ml.dataVisualizerFileBased.assertFileTitle(expectedUploadFileTitle); + await ml.dataVisualizerFileBased.assertFileContentPanelExists(); + await ml.dataVisualizerFileBased.assertSummaryPanelExists(); + await ml.dataVisualizerFileBased.assertFileStatsPanelExists(); + await ml.dataVisualizerFileBased.assertImportButtonEnabled(true); + }); + + it('should display elements on Settings home page correctly', async () => { + await ml.testExecution.logTestStep('should load the settings page'); + await ml.navigation.navigateToSettings(); + + await ml.testExecution.logTestStep('should display enabled calendar controls'); + await ml.settings.assertManageCalendarsLinkExists(); + await ml.settings.assertManageCalendarsLinkEnabled(true); + await ml.settings.assertCreateCalendarLinkExists(); + await ml.settings.assertCreateCalendarLinkEnabled(true); + + await ml.testExecution.logTestStep('should display enabled filter list controls'); + await ml.settings.assertManageFilterListsLinkExists(); + await ml.settings.assertManageFilterListsLinkEnabled(true); + await ml.settings.assertCreateFilterListLinkExists(); + await ml.settings.assertCreateFilterListLinkEnabled(true); + }); + + it('should display elements on Calendar management page correctly', async () => { + await ml.testExecution.logTestStep('should load the calendar management page'); + await ml.settings.navigateToCalendarManagement(); + + await ml.testExecution.logTestStep('should display enabled create calendar button'); + await ml.settingsCalendar.assertCreateCalendarButtonEnabled(true); + + await ml.testExecution.logTestStep('should display the calendar in the list'); + await ml.settingsCalendar.filterWithSearchString(calendarId, 1); + + await ml.testExecution.logTestStep('should enable delete calendar button on selection'); + await ml.settingsCalendar.assertDeleteCalendarButtonEnabled(false); + await ml.settingsCalendar.selectCalendarRow(calendarId); + await ml.settingsCalendar.assertDeleteCalendarButtonEnabled(true); + + await ml.testExecution.logTestStep('should load the calendar edit page'); + await ml.settingsCalendar.openCalendarEditForm(calendarId); + + await ml.testExecution.logTestStep( + 'should display enabled elements of the edit calendar page' + ); + await ml.settingsCalendar.assertApplyToAllJobsSwitchEnabled(true); + await ml.settingsCalendar.assertJobSelectionEnabled(true); + await ml.settingsCalendar.assertJobGroupSelectionEnabled(true); + await ml.settingsCalendar.assertNewEventButtonEnabled(true); + await ml.settingsCalendar.assertImportEventsButtonEnabled(true); + + await ml.testExecution.logTestStep('should display the event in the list'); + await ml.settingsCalendar.assertEventRowExists(eventDescription); + + await ml.testExecution.logTestStep('should display enabled delete event button'); + await ml.settingsCalendar.assertDeleteEventButtonEnabled(eventDescription, true); + }); + + it('should display elements on Filter Lists management page correctly', async () => { + await ml.testExecution.logTestStep('should load the filter list management page'); + await ml.navigation.navigateToSettings(); + await ml.settings.navigateToFilterListsManagement(); + + await ml.testExecution.logTestStep('should display enabled create filter list button'); + await ml.settingsFilterList.assertCreateFilterListButtonEnabled(true); + + await ml.testExecution.logTestStep('should display the filter list in the table'); + await ml.settingsFilterList.filterWithSearchString(filterId, 1); + + await ml.testExecution.logTestStep( + 'should enable delete filter list button on selection' + ); + await ml.settingsFilterList.assertDeleteFilterListButtonEnabled(false); + await ml.settingsFilterList.selectFilterListRow(filterId); + await ml.settingsFilterList.assertDeleteFilterListButtonEnabled(true); + + await ml.testExecution.logTestStep('should load the filter list edit page'); + await ml.settingsFilterList.openFilterListEditForm(filterId); + + await ml.testExecution.logTestStep( + 'should display enabled elements of the edit calendar page' + ); + await ml.settingsFilterList.assertEditDescriptionButtonEnabled(true); + await ml.settingsFilterList.assertAddItemButtonEnabled(true); + + await ml.testExecution.logTestStep('should display the filter item in the list'); + await ml.settingsFilterList.assertFilterItemExists(filterItems[0]); + + await ml.testExecution.logTestStep( + 'should enable delete filter item button on selection' + ); + await ml.settingsFilterList.assertDeleteItemButtonEnabled(false); + await ml.settingsFilterList.selectFilterItem(filterItems[0]); + await ml.settingsFilterList.assertDeleteItemButtonEnabled(true); + }); + + it('should display elements on Stack Management ML page correctly', async () => { + await ml.testExecution.logTestStep( + 'should load the stack management with the ML menu item being present' + ); + await ml.navigation.navigateToStackManagement(); + + await ml.testExecution.logTestStep( + 'should load the jobs list page in stack management' + ); + await ml.navigation.navigateToStackManagementJobsListPage(); + + await ml.testExecution.logTestStep('should display the AD job in the list'); + await ml.jobTable.filterWithSearchString(adJobId, 1); + + await ml.testExecution.logTestStep( + 'should load the analytics jobs list page in stack management' + ); + await ml.navigation.navigateToStackManagementJobsListPageAnalyticsTab(); + + await ml.testExecution.logTestStep('should display the DFA job in the list'); + await ml.dataFrameAnalyticsTable.filterWithSearchString(dfaJobId, 1); + }); + }); + } + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/permissions/index.ts b/x-pack/test/functional/apps/ml/permissions/index.ts new file mode 100644 index 0000000000000..2d415b1d094a4 --- /dev/null +++ b/x-pack/test/functional/apps/ml/permissions/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('permissions', function () { + this.tags(['skipFirefox']); + + loadTestFile(require.resolve('./full_ml_access')); + loadTestFile(require.resolve('./read_ml_access')); + loadTestFile(require.resolve('./no_ml_access')); + }); +} diff --git a/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts new file mode 100644 index 0000000000000..6fd78458a6ce5 --- /dev/null +++ b/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +import { USER } from '../../../services/ml/security_common'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'error']); + const ml = getService('ml'); + + const testUsers = [USER.ML_UNAUTHORIZED, USER.ML_UNAUTHORIZED_SPACES]; + + describe('for user with no ML access', function () { + this.tags(['skipFirefox', 'mlqa']); + + for (const user of testUsers) { + describe(`(${user})`, function () { + before(async () => { + await ml.securityUI.loginAs(user); + }); + + after(async () => { + await ml.securityUI.logout(); + }); + + it('should not allow to access the ML app', async () => { + await ml.testExecution.logTestStep('should not load the ML overview page'); + await PageObjects.common.navigateToUrl('ml', '', { + shouldLoginIfPrompted: false, + ensureCurrentUrl: false, + }); + + await PageObjects.error.expectNotFound(); + }); + + it('should not display the ML file data vis link on the Kibana home page', async () => { + await ml.testExecution.logTestStep('should load the Kibana home page'); + await ml.navigation.navigateToKibanaHome(); + + await ml.testExecution.logTestStep('should not display the ML file data vis link'); + await ml.commonUI.assertKibanaHomeFileDataVisLinkNotExists(); + }); + + it('should not display the ML entry in Kibana app menu', async () => { + await ml.testExecution.logTestStep('should open the Kibana app menu'); + await ml.navigation.openKibanaNav(); + + await ml.testExecution.logTestStep('should not display the ML nav link'); + await ml.navigation.assertKibanaNavMLEntryNotExists(); + }); + + it('should not allow to access the Stack Management ML page', async () => { + await ml.testExecution.logTestStep( + 'should load the stack management with the ML menu item being present' + ); + await ml.navigation.navigateToStackManagement(); + + await ml.testExecution.logTestStep( + 'should display the access denied page in stack management' + ); + await ml.navigation.navigateToStackManagementJobsListPage({ + expectAccessDenied: true, + }); + }); + }); + } + }); +} diff --git a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts new file mode 100644 index 0000000000000..a358e57f792c7 --- /dev/null +++ b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts @@ -0,0 +1,426 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import path from 'path'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +import { USER } from '../../../services/ml/security_common'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + const testUsers = [USER.ML_VIEWER, USER.ML_VIEWER_SPACES]; + + describe('for user with read ML access', function () { + this.tags(['skipFirefox', 'mlqa']); + + describe('with no data loaded', function () { + for (const user of testUsers) { + describe(`(${user})`, function () { + before(async () => { + await ml.securityUI.loginAs(user); + await ml.api.cleanMlIndices(); + }); + + after(async () => { + await ml.securityUI.logout(); + }); + + it('should not display the ML file data vis link on the Kibana home page', async () => { + await ml.testExecution.logTestStep('should load the Kibana home page'); + await ml.navigation.navigateToKibanaHome(); + + await ml.testExecution.logTestStep('should not display the ML file data vis link'); + await ml.commonUI.assertKibanaHomeFileDataVisLinkNotExists(); + }); + + it('should display the ML entry in Kibana app menu', async () => { + await ml.testExecution.logTestStep('should open the Kibana app menu'); + await ml.navigation.openKibanaNav(); + + await ml.testExecution.logTestStep('should display the ML nav link'); + await ml.navigation.assertKibanaNavMLEntryExists(); + }); + + it('should display elements on ML Overview page correctly', async () => { + await ml.testExecution.logTestStep('should load the ML overview page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToOverview(); + + await ml.testExecution.logTestStep('should display disabled AD create job button'); + await ml.overviewPage.assertADCreateJobButtonExists(); + await ml.overviewPage.assertADCreateJobButtonEnabled(false); + + await ml.testExecution.logTestStep('should display disabled DFA create job button'); + await ml.overviewPage.assertDFACreateJobButtonExists(); + await ml.overviewPage.assertDFACreateJobButtonEnabled(false); + }); + }); + } + }); + + describe('with no data loaded', function () { + const adJobId = 'fq_single_permission'; + const dfaJobId = 'iph_outlier_permission'; + const calendarId = 'calendar_permission'; + const eventDescription = 'calendar_event_permission'; + const filterId = 'filter_permission'; + const filterItems = ['filter_item_permission']; + + const ecIndexPattern = 'ft_module_sample_ecommerce'; + const ecExpectedTotalCount = 287; + const ecExpectedFieldPanelCount = 2; + + const uploadFilePath = path.join( + __dirname, + '..', + 'data_visualizer', + 'files_to_import', + 'artificial_server_log' + ); + const expectedUploadFileTitle = 'artificial_server_log'; + + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await esArchiver.loadIfNeeded('ml/ihp_outlier'); + await esArchiver.loadIfNeeded('ml/module_sample_ecommerce'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + await ml.testResources.createIndexPatternIfNeeded('ft_ihp_outlier', '@timestamp'); + await ml.testResources.createIndexPatternIfNeeded(ecIndexPattern, 'order_date'); + await ml.testResources.setKibanaTimeZoneToUTC(); + + await ml.api.createAndRunAnomalyDetectionLookbackJob( + ml.commonConfig.getADFqMultiMetricJobConfig(adJobId), + ml.commonConfig.getADFqDatafeedConfig(adJobId) + ); + + await ml.api.createAndRunDFAJob( + ml.commonConfig.getDFAIhpOutlierDetectionJobConfig(dfaJobId) + ); + + await ml.api.createCalendar(calendarId, { + calendar_id: calendarId, + job_ids: [], + description: 'Test calendar', + }); + await ml.api.createCalendarEvents(calendarId, [ + { + description: eventDescription, + start_time: 1513641600000, + end_time: 1513728000000, + }, + ]); + + await ml.api.createFilter(filterId, { + description: 'Test filter list', + items: filterItems, + }); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + await ml.api.deleteIndices(`user-${dfaJobId}`); + await ml.api.deleteCalendar(calendarId); + await ml.api.deleteFilter(filterId); + }); + + for (const user of testUsers) { + describe(`(${user})`, function () { + before(async () => { + await ml.securityUI.loginAs(user); + }); + + after(async () => { + await ml.securityUI.logout(); + }); + + it('should display elements on Anomaly Detection page correctly', async () => { + await ml.testExecution.logTestStep('should load the AD job management page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToAnomalyDetection(); + + await ml.testExecution.logTestStep('should display the stats bar and the AD job table'); + await ml.jobManagement.assertJobStatsBarExists(); + await ml.jobManagement.assertJobTableExists(); + + await ml.testExecution.logTestStep('should display a disabled "Create job" button'); + await ml.jobManagement.assertCreateNewJobButtonExists(); + await ml.jobManagement.assertCreateNewJobButtonEnabled(false); + + await ml.testExecution.logTestStep('should display the AD job in the list'); + await ml.jobTable.filterWithSearchString(adJobId, 1); + + await ml.testExecution.logTestStep('should display enabled AD job result links'); + await ml.jobTable.assertJobActionSingleMetricViewerButtonEnabled(adJobId, true); + await ml.jobTable.assertJobActionAnomalyExplorerButtonEnabled(adJobId, true); + + await ml.testExecution.logTestStep('should display disabled AD job row action button'); + await ml.jobTable.assertJobActionsMenuButtonEnabled(adJobId, false); + + await ml.testExecution.logTestStep('should select the job'); + await ml.jobTable.selectJobRow(adJobId); + + await ml.testExecution.logTestStep('should display enabled multi select result links'); + await ml.jobTable.assertMultiSelectActionSingleMetricViewerButtonEnabled(true); + await ml.jobTable.assertMultiSelectActionAnomalyExplorerButtonEnabled(true); + + await ml.testExecution.logTestStep( + 'should display disabled multi select action button' + ); + await ml.jobTable.assertMultiSelectManagementActionsButtonEnabled(false); + await ml.jobTable.deselectJobRow(adJobId); + }); + + it('should display elements on Single Metric Viewer page correctly', async () => { + await ml.testExecution.logTestStep('should open AD job in the single metric viewer'); + await ml.jobTable.clickOpenJobInSingleMetricViewerButton(adJobId); + await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); + + await ml.testExecution.logTestStep('should pre-fill the AD job selection'); + await ml.jobSelection.assertJobSelection([adJobId]); + + await ml.testExecution.logTestStep('should pre-fill the detector input'); + await ml.singleMetricViewer.assertDetectorInputExsist(); + await ml.singleMetricViewer.assertDetectorInputValue('0'); + + await ml.testExecution.logTestStep('should input the airline entity value'); + await ml.singleMetricViewer.assertEntityInputExsist('airline'); + await ml.singleMetricViewer.assertEntityInputSelection('airline', []); + await ml.singleMetricViewer.selectEntityValue('airline', 'AAL'); + + await ml.testExecution.logTestStep('should display the chart'); + await ml.singleMetricViewer.assertChartExsist(); + + await ml.testExecution.logTestStep('should display the annotations section'); + await ml.singleMetricViewer.assertAnnotationsExists('loaded'); + + await ml.testExecution.logTestStep('should display the anomalies table with entries'); + await ml.anomaliesTable.assertTableExists(); + await ml.anomaliesTable.assertTableNotEmpty(); + + await ml.testExecution.logTestStep('should not display the anomaly row action button'); + await ml.anomaliesTable.assertAnomalyActionsMenuButtonNotExists(0); + + await ml.testExecution.logTestStep( + 'should display the forecast modal with disabled run button' + ); + await ml.singleMetricViewer.assertForecastButtonExists(); + await ml.singleMetricViewer.assertForecastButtonEnabled(true); + await ml.singleMetricViewer.openForecastModal(); + await ml.singleMetricViewer.assertForecastModalRunButtonEnabled(false); + await ml.singleMetricViewer.closeForecastModal(); + }); + + it('should display elements on Anomaly Explorer page correctly', async () => { + await ml.testExecution.logTestStep('should open AD job in the anomaly explorer'); + await ml.singleMetricViewer.openAnomalyExplorer(); + await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); + + await ml.testExecution.logTestStep('should pre-fill the AD job selection'); + await ml.jobSelection.assertJobSelection([adJobId]); + + await ml.testExecution.logTestStep('should display the influencers list'); + await ml.anomalyExplorer.assertInfluencerListExists(); + await ml.anomalyExplorer.assertInfluencerFieldListNotEmpty('airline'); + + await ml.testExecution.logTestStep('should display the swim lanes'); + await ml.anomalyExplorer.assertOverallSwimlaneExists(); + await ml.anomalyExplorer.assertSwimlaneViewByExists(); + + await ml.testExecution.logTestStep('should display the annotations panel'); + await ml.anomalyExplorer.assertAnnotationsPanelExists('loaded'); + + await ml.testExecution.logTestStep('should display the anomalies table with entries'); + await ml.anomaliesTable.assertTableExists(); + await ml.anomaliesTable.assertTableNotEmpty(); + + await ml.testExecution.logTestStep('should display enabled anomaly row action button'); + await ml.anomaliesTable.assertAnomalyActionsMenuButtonExists(0); + await ml.anomaliesTable.assertAnomalyActionsMenuButtonEnabled(0, true); + + await ml.testExecution.logTestStep('should not display configure rules action button'); + await ml.anomaliesTable.assertAnomalyActionConfigureRulesButtonNotExists(0); + + await ml.testExecution.logTestStep('should display enabled view series action button'); + await ml.anomaliesTable.assertAnomalyActionViewSeriesButtonEnabled(0, true); + }); + + it('should display elements on Data Frame Analytics page correctly', async () => { + await ml.testExecution.logTestStep('should load the DFA job management page'); + await ml.navigation.navigateToDataFrameAnalytics(); + + await ml.testExecution.logTestStep( + 'should display the stats bar and the analytics table' + ); + await ml.dataFrameAnalytics.assertAnalyticsStatsBarExists(); + await ml.dataFrameAnalytics.assertAnalyticsTableExists(); + + await ml.testExecution.logTestStep('should display a disabled "Create job" button'); + await ml.dataFrameAnalytics.assertCreateNewAnalyticsButtonExists(); + await ml.dataFrameAnalytics.assertCreateNewAnalyticsButtonEnabled(false); + + await ml.testExecution.logTestStep('should display the DFA job in the list'); + await ml.dataFrameAnalyticsTable.filterWithSearchString(dfaJobId, 1); + + await ml.testExecution.logTestStep( + 'should display enabled DFA job view and action menu' + ); + await ml.dataFrameAnalyticsTable.assertJobRowViewButtonEnabled(dfaJobId, true); + await ml.dataFrameAnalyticsTable.assertJowRowActionsMenuButtonEnabled(dfaJobId, true); + await ml.dataFrameAnalyticsTable.assertJobActionViewButtonEnabled(dfaJobId, true); + + await ml.testExecution.logTestStep( + 'should display disabled DFA job row action buttons' + ); + await ml.dataFrameAnalyticsTable.assertJobActionStartButtonEnabled(dfaJobId, false); // job already completed + await ml.dataFrameAnalyticsTable.assertJobActionEditButtonEnabled(dfaJobId, false); + await ml.dataFrameAnalyticsTable.assertJobActionCloneButtonEnabled(dfaJobId, false); + await ml.dataFrameAnalyticsTable.assertJobActionDeleteButtonEnabled(dfaJobId, false); + await ml.dataFrameAnalyticsTable.ensureJobActionsMenuClosed(dfaJobId); + }); + + it('should display elements on Data Frame Analytics results view page correctly', async () => { + await ml.testExecution.logTestStep('displays the results view for created job'); + await ml.dataFrameAnalyticsTable.openResultsView(dfaJobId); + await ml.dataFrameAnalyticsResults.assertOutlierTablePanelExists(); + await ml.dataFrameAnalyticsResults.assertResultsTableExists(); + await ml.dataFrameAnalyticsResults.assertResultsTableNotEmpty(); + }); + + it('should display elements on Data Visualizer home page correctly', async () => { + await ml.testExecution.logTestStep('should load the data visualizer page'); + await ml.navigation.navigateToDataVisualizer(); + + await ml.testExecution.logTestStep( + 'should display the "import data" card with enabled button' + ); + await ml.dataVisualizer.assertDataVisualizerImportDataCardExists(); + await ml.dataVisualizer.assertUploadFileButtonEnabled(true); + + await ml.testExecution.logTestStep( + 'should display the "select index pattern" card with enabled button' + ); + await ml.dataVisualizer.assertDataVisualizerIndexDataCardExists(); + await ml.dataVisualizer.assertSelectIndexButtonEnabled(true); + }); + + it('should display elements on Index Data Visualizer page correctly', async () => { + await ml.testExecution.logTestStep( + 'should load an index into the data visualizer page' + ); + await ml.dataVisualizer.navigateToIndexPatternSelection(); + await ml.jobSourceSelection.selectSourceForIndexBasedDataVisualizer(ecIndexPattern); + + await ml.testExecution.logTestStep('should display the time range step'); + await ml.dataVisualizerIndexBased.assertTimeRangeSelectorSectionExists(); + + await ml.testExecution.logTestStep('should load data for full time range'); + await ml.dataVisualizerIndexBased.clickUseFullDataButton(ecExpectedTotalCount); + + await ml.testExecution.logTestStep('should display the panels of fields'); + await ml.dataVisualizerIndexBased.assertFieldsPanelsExist(ecExpectedFieldPanelCount); + + await ml.testExecution.logTestStep('should not display the actions panel'); + await ml.dataVisualizerIndexBased.assertActionsPanelNotExists(); + }); + + it('should display elements on File Data Visualizer page correctly', async () => { + await ml.testExecution.logTestStep( + 'should load the file data visualizer file selection' + ); + await ml.navigation.navigateToDataVisualizer(); + await ml.dataVisualizer.navigateToFileUpload(); + + await ml.testExecution.logTestStep( + 'should select a file and load visualizer result page' + ); + await ml.dataVisualizerFileBased.selectFile(uploadFilePath); + + await ml.testExecution.logTestStep( + 'should displays components of the file details page' + ); + await ml.dataVisualizerFileBased.assertFileTitle(expectedUploadFileTitle); + await ml.dataVisualizerFileBased.assertFileContentPanelExists(); + await ml.dataVisualizerFileBased.assertSummaryPanelExists(); + await ml.dataVisualizerFileBased.assertFileStatsPanelExists(); + await ml.dataVisualizerFileBased.assertImportButtonEnabled(false); + }); + + it('should display elements on Settings home page correctly', async () => { + await ml.testExecution.logTestStep('should load the settings page'); + await ml.navigation.navigateToSettings(); + + await ml.testExecution.logTestStep( + 'should display enabled calendar management and disabled calendar create links' + ); + await ml.settings.assertManageCalendarsLinkExists(); + await ml.settings.assertManageCalendarsLinkEnabled(true); + await ml.settings.assertCreateCalendarLinkExists(); + await ml.settings.assertCreateCalendarLinkEnabled(false); + + await ml.testExecution.logTestStep('should display disabled filter list controls'); + await ml.settings.assertManageFilterListsLinkExists(); + await ml.settings.assertManageFilterListsLinkEnabled(false); + await ml.settings.assertCreateFilterListLinkExists(); + await ml.settings.assertCreateFilterListLinkEnabled(false); + }); + + it('should display elements on Calendar management page correctly', async () => { + await ml.testExecution.logTestStep('should load the calendar management page'); + await ml.settings.navigateToCalendarManagement(); + + await ml.testExecution.logTestStep('should display disabled create calendar button'); + await ml.settingsCalendar.assertCreateCalendarButtonEnabled(false); + + await ml.testExecution.logTestStep('should display the calendar in the list'); + await ml.settingsCalendar.filterWithSearchString(calendarId, 1); + + await ml.testExecution.logTestStep( + 'should not enable delete calendar button on selection' + ); + await ml.settingsCalendar.assertDeleteCalendarButtonEnabled(false); + await ml.settingsCalendar.selectCalendarRow(calendarId); + await ml.settingsCalendar.assertDeleteCalendarButtonEnabled(false); + + await ml.testExecution.logTestStep('should load the calendar edit page'); + await ml.settingsCalendar.openCalendarEditForm(calendarId); + + await ml.testExecution.logTestStep( + 'should display disabled elements of the edit calendar page' + ); + await ml.settingsCalendar.assertApplyToAllJobsSwitchEnabled(false); + await ml.settingsCalendar.assertJobSelectionEnabled(false); + await ml.settingsCalendar.assertJobGroupSelectionEnabled(false); + await ml.settingsCalendar.assertNewEventButtonEnabled(false); + await ml.settingsCalendar.assertImportEventsButtonEnabled(false); + + await ml.testExecution.logTestStep('should display the event in the list'); + await ml.settingsCalendar.assertEventRowExists(eventDescription); + + await ml.testExecution.logTestStep('should display enabled delete event button'); + await ml.settingsCalendar.assertDeleteEventButtonEnabled(eventDescription, false); + }); + + it('should display elements on Stack Management ML page correctly', async () => { + await ml.testExecution.logTestStep( + 'should load the stack management with the ML menu item being present' + ); + await ml.navigation.navigateToStackManagement(); + + await ml.testExecution.logTestStep( + 'should display the access denied page in stack management' + ); + await ml.navigation.navigateToStackManagementJobsListPage({ + expectAccessDenied: true, + }); + }); + }); + } + }); + }); +} diff --git a/x-pack/test/functional/apps/transform/cloning.ts b/x-pack/test/functional/apps/transform/cloning.ts index d5c972cb8bd1f..b6ccd68bb2096 100644 --- a/x-pack/test/functional/apps/transform/cloning.ts +++ b/x-pack/test/functional/apps/transform/cloning.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; import { TransformPivotConfig } from '../../../../plugins/transform/public/app/common'; @@ -99,9 +98,7 @@ export default function ({ getService }: FtrProviderContext) { 'should display the original transform in the transform list' ); await transform.table.refreshTransformList(); - await transform.table.filterWithSearchString(transformConfig.id); - const rows = await transform.table.parseTransformTable(); - expect(rows.filter((row) => row.id === transformConfig.id)).to.have.length(1); + await transform.table.filterWithSearchString(transformConfig.id, 1); await transform.testExecution.logTestStep('should show the actions popover'); await transform.table.assertTransformRowActions(false); @@ -212,9 +209,7 @@ export default function ({ getService }: FtrProviderContext) { 'should display the created transform in the transform list' ); await transform.table.refreshTransformList(); - await transform.table.filterWithSearchString(testData.transformId); - const rows = await transform.table.parseTransformTable(); - expect(rows.filter((row) => row.id === testData.transformId)).to.have.length(1); + await transform.table.filterWithSearchString(testData.transformId, 1); }); }); } diff --git a/x-pack/test/functional/apps/transform/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation_index_pattern.ts index daecc26186ac1..4e2b832838b7d 100644 --- a/x-pack/test/functional/apps/transform/creation_index_pattern.ts +++ b/x-pack/test/functional/apps/transform/creation_index_pattern.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -402,9 +401,7 @@ export default function ({ getService }: FtrProviderContext) { 'displays the created transform in the transform list' ); await transform.table.refreshTransformList(); - await transform.table.filterWithSearchString(testData.transformId); - const rows = await transform.table.parseTransformTable(); - expect(rows.filter((row) => row.id === testData.transformId)).to.have.length(1); + await transform.table.filterWithSearchString(testData.transformId, 1); await transform.testExecution.logTestStep( 'transform creation displays details for the created transform in the transform list' diff --git a/x-pack/test/functional/apps/transform/creation_saved_search.ts b/x-pack/test/functional/apps/transform/creation_saved_search.ts index d3cbc1159a9c7..229ff97782362 100644 --- a/x-pack/test/functional/apps/transform/creation_saved_search.ts +++ b/x-pack/test/functional/apps/transform/creation_saved_search.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -208,9 +207,7 @@ export default function ({ getService }: FtrProviderContext) { 'displays the created transform in the transform list' ); await transform.table.refreshTransformList(); - await transform.table.filterWithSearchString(testData.transformId); - const rows = await transform.table.parseTransformTable(); - expect(rows.filter((row) => row.id === testData.transformId)).to.have.length(1); + await transform.table.filterWithSearchString(testData.transformId, 1); await transform.testExecution.logTestStep( 'transform creation displays details for the created transform in the transform list' diff --git a/x-pack/test/functional/apps/transform/editing.ts b/x-pack/test/functional/apps/transform/editing.ts index 5582d279833e7..460e7c5b24a98 100644 --- a/x-pack/test/functional/apps/transform/editing.ts +++ b/x-pack/test/functional/apps/transform/editing.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; import { TransformPivotConfig } from '../../../../plugins/transform/public/app/common'; @@ -73,9 +72,7 @@ export default function ({ getService }: FtrProviderContext) { 'should display the original transform in the transform list' ); await transform.table.refreshTransformList(); - await transform.table.filterWithSearchString(transformConfig.id); - const rows = await transform.table.parseTransformTable(); - expect(rows.filter((row) => row.id === transformConfig.id)).to.have.length(1); + await transform.table.filterWithSearchString(transformConfig.id, 1); await transform.testExecution.logTestStep('should show the actions popover'); await transform.table.assertTransformRowActions(false); @@ -127,9 +124,7 @@ export default function ({ getService }: FtrProviderContext) { 'should display the updated transform in the transform list' ); await transform.table.refreshTransformList(); - await transform.table.filterWithSearchString(transformConfig.id); - const rows = await transform.table.parseTransformTable(); - expect(rows.filter((row) => row.id === transformConfig.id)).to.have.length(1); + await transform.table.filterWithSearchString(transformConfig.id, 1); await transform.testExecution.logTestStep( 'should display the updated transform in the transform list row cells' diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index 6dd22a1f4a204..a01f3fa5d53a5 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -5,10 +5,9 @@ */ import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, loadTestFile, getPageObjects }: FtrProviderContext) { +export default function ({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const transform = getService('transform'); - const PageObjects = getPageObjects(['security']); describe('transform', function () { this.tags(['ciGroup9', 'transform']); @@ -31,7 +30,7 @@ export default function ({ getService, loadTestFile, getPageObjects }: FtrProvid await esArchiver.unload('ml/ecommerce'); await transform.testResources.resetKibanaTimeZone(); - await PageObjects.security.logout(); + await transform.securityUI.logout(); }); loadTestFile(require.resolve('./creation_index_pattern')); diff --git a/x-pack/test/functional/es_archives/endpoint/resolver/signals/data.json.gz b/x-pack/test/functional/es_archives/endpoint/resolver/signals/data.json.gz new file mode 100644 index 0000000000000..e1b9c01101f6e Binary files /dev/null and b/x-pack/test/functional/es_archives/endpoint/resolver/signals/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/endpoint/resolver/signals/mappings.json b/x-pack/test/functional/es_archives/endpoint/resolver/signals/mappings.json new file mode 100644 index 0000000000000..ad77961a41445 --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/resolver/signals/mappings.json @@ -0,0 +1,3239 @@ +{ + "type": "index", + "value": { + "aliases": { + ".siem-signals-default": { + "is_write_index": true + } + }, + "index": ".siem-signals-default-000001", + "mappings": { + "dynamic": "false", + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "client": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "cloud": { + "properties": { + "account": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "availability_zone": { + "ignore_above": 1024, + "type": "keyword" + }, + "instance": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "machine": { + "properties": { + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "region": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "container": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "tag": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "runtime": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dll": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dns": { + "properties": { + "answers": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "ttl": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "header_flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "op_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "question": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "resolved_ip": { + "type": "ip" + }, + "response_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "error": { + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "norms": false, + "type": "text" + }, + "stack_trace": { + "doc_values": false, + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "doc_values": false, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "file": { + "properties": { + "accessed": { + "type": "date" + }, + "attributes": { + "ignore_above": 1024, + "type": "keyword" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "created": { + "type": "date" + }, + "ctime": { + "type": "date" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "directory": { + "ignore_above": 1024, + "type": "keyword" + }, + "drive_letter": { + "ignore_above": 1, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mime_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "size": { + "type": "long" + }, + "target_path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "http": { + "properties": { + "request": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "referrer": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "response": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "status_code": { + "type": "long" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "log": { + "properties": { + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "logger": { + "ignore_above": 1024, + "type": "keyword" + }, + "origin": { + "properties": { + "file": { + "properties": { + "line": { + "type": "integer" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "function": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "original": { + "doc_values": false, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "syslog": { + "properties": { + "facility": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "priority": { + "type": "long" + }, + "severity": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "network": { + "properties": { + "application": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "community_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "direction": { + "ignore_above": 1024, + "type": "keyword" + }, + "forwarded_ip": { + "type": "ip" + }, + "iana_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "inner": { + "properties": { + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "transport": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "observer": { + "properties": { + "egress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "organization": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "package": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "build_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "checksum": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "install_scope": { + "ignore_above": 1024, + "type": "keyword" + }, + "installed": { + "type": "date" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "registry": { + "properties": { + "data": { + "properties": { + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "strings": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hive": { + "ignore_above": 1024, + "type": "keyword" + }, + "key": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "related": { + "properties": { + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "author": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "ruleset": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "server": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "service": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "signal": { + "properties": { + "ancestors": { + "properties": { + "depth": { + "type": "long" + }, + "id": { + "type": "keyword" + }, + "rule": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "original_event": { + "properties": { + "action": { + "type": "keyword" + }, + "category": { + "type": "keyword" + }, + "code": { + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + }, + "module": { + "type": "keyword" + }, + "original": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "outcome": { + "type": "keyword" + }, + "provider": { + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "original_time": { + "type": "date" + }, + "parent": { + "properties": { + "depth": { + "type": "long" + }, + "id": { + "type": "keyword" + }, + "index": { + "type": "keyword" + }, + "rule": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "author": { + "type": "keyword" + }, + "building_block_type": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "enabled": { + "type": "keyword" + }, + "false_positives": { + "type": "keyword" + }, + "filters": { + "type": "object" + }, + "from": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "immutable": { + "type": "keyword" + }, + "index": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "language": { + "type": "keyword" + }, + "license": { + "type": "keyword" + }, + "max_signals": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "output_index": { + "type": "keyword" + }, + "query": { + "type": "keyword" + }, + "references": { + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_mapping": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "value": { + "type": "keyword" + } + } + }, + "rule_id": { + "type": "keyword" + }, + "rule_name_override": { + "type": "keyword" + }, + "saved_id": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + }, + "severity_mapping": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + }, + "value": { + "type": "keyword" + } + } + }, + "size": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "threat": { + "properties": { + "framework": { + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "reference": { + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "reference": { + "type": "keyword" + } + } + } + } + }, + "threshold": { + "properties": { + "field": { + "type": "keyword" + }, + "value": { + "type": "float" + } + } + }, + "timeline_id": { + "type": "keyword" + }, + "timeline_title": { + "type": "keyword" + }, + "timestamp_override": { + "type": "keyword" + }, + "to": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "threshold_count": { + "type": "float" + } + } + }, + "source": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, + "threat": { + "properties": { + "framework": { + "ignore_above": 1024, + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "tls": { + "properties": { + "cipher": { + "ignore_above": 1024, + "type": "keyword" + }, + "client": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "server_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + }, + "supported_ciphers": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "established": { + "type": "boolean" + }, + "next_protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "resumed": { + "type": "boolean" + }, + "server": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3s": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + }, + "version_protocol": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "trace": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "transaction": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "url": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user_agent": { + "properties": { + "device": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vulnerability": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "classification": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "enumeration": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "report_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "scanner": { + "properties": { + "vendor": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "score": { + "properties": { + "base": { + "type": "float" + }, + "environmental": { + "type": "float" + }, + "temporal": { + "type": "float" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "severity": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "lifecycle": { + "name": ".siem-signals-default", + "rollover_alias": ".siem-signals-default" + }, + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "1", + "number_of_shards": "1", + "routing": { + "allocation": { + "include": { + "_tier": "data_hot" + } + } + } + } + } + } +} diff --git a/x-pack/test/functional/services/dashboard/drilldowns_manage.ts b/x-pack/test/functional/services/dashboard/drilldowns_manage.ts index a01fde3a5233f..7b66591fcf764 100644 --- a/x-pack/test/functional/services/dashboard/drilldowns_manage.ts +++ b/x-pack/test/functional/services/dashboard/drilldowns_manage.ts @@ -12,6 +12,8 @@ const DASHBOARD_TO_DASHBOARD_ACTION_LIST_ITEM = 'actionFactoryItem-DASHBOARD_TO_DASHBOARD_DRILLDOWN'; const DASHBOARD_TO_DASHBOARD_ACTION_WIZARD = 'selectedActionFactory-DASHBOARD_TO_DASHBOARD_DRILLDOWN'; +const DASHBOARD_TO_URL_ACTION_LIST_ITEM = 'actionFactoryItem-URL_DRILLDOWN'; +const DASHBOARD_TO_URL_ACTION_WIZARD = 'selectedActionFactory-URL_DRILLDOWN'; const DESTINATION_DASHBOARD_SELECT = 'dashboardDrilldownSelectDashboard'; const DRILLDOWN_WIZARD_SUBMIT = 'drilldownWizardSubmit'; @@ -68,10 +70,32 @@ export function DashboardDrilldownsManageProvider({ getService }: FtrProviderCon await this.selectDestinationDashboard(destinationDashboardTitle); } + async fillInDashboardToURLDrilldownWizard({ + drilldownName, + destinationURLTemplate, + trigger, + }: { + drilldownName: string; + destinationURLTemplate: string; + trigger: 'VALUE_CLICK_TRIGGER' | 'SELECT_RANGE_TRIGGER'; + }) { + await this.fillInDrilldownName(drilldownName); + await this.selectDashboardToURLActionIfNeeded(); + await this.selectTriggerIfNeeded(trigger); + await this.fillInURLTemplate(destinationURLTemplate); + } + async fillInDrilldownName(name: string) { await testSubjects.setValue('drilldownNameInput', name); } + async selectDashboardToURLActionIfNeeded() { + if (await testSubjects.exists(DASHBOARD_TO_URL_ACTION_LIST_ITEM)) { + await testSubjects.click(DASHBOARD_TO_URL_ACTION_LIST_ITEM); + } + await testSubjects.existOrFail(DASHBOARD_TO_URL_ACTION_WIZARD); + } + async selectDashboardToDashboardActionIfNeeded() { if (await testSubjects.exists(DASHBOARD_TO_DASHBOARD_ACTION_LIST_ITEM)) { await testSubjects.click(DASHBOARD_TO_DASHBOARD_ACTION_LIST_ITEM); @@ -83,6 +107,18 @@ export function DashboardDrilldownsManageProvider({ getService }: FtrProviderCon await comboBox.set(DESTINATION_DASHBOARD_SELECT, title); } + async selectTriggerIfNeeded(trigger: 'VALUE_CLICK_TRIGGER' | 'SELECT_RANGE_TRIGGER') { + if (await testSubjects.exists(`triggerPicker`)) { + const container = await testSubjects.find(`triggerPicker-${trigger}`); + const radio = await container.findByCssSelector('input[type=radio]'); + await radio.click(); + } + } + + async fillInURLTemplate(destinationURLTemplate: string) { + await testSubjects.setValue('urlInput', destinationURLTemplate); + } + async saveChanges() { await testSubjects.click(DRILLDOWN_WIZARD_SUBMIT); } diff --git a/x-pack/test/functional/services/ml/anomalies_table.ts b/x-pack/test/functional/services/ml/anomalies_table.ts index 26af97d008feb..0231d941fb85f 100644 --- a/x-pack/test/functional/services/ml/anomalies_table.ts +++ b/x-pack/test/functional/services/ml/anomalies_table.ts @@ -3,11 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export function MachineLearningAnomaliesTableProvider({ getService }: FtrProviderContext) { + const retry = getService('retry'); const testSubjects = getService('testSubjects'); return { @@ -15,12 +17,97 @@ export function MachineLearningAnomaliesTableProvider({ getService }: FtrProvide await testSubjects.existOrFail('mlAnomaliesTable'); }, + async getTableRows() { + return await testSubjects.findAll('mlAnomaliesTable > ~mlAnomaliesListRow'); + }, + + async getRowSubjByRowIndex(rowIndex: number) { + const tableRows = await this.getTableRows(); + expect(tableRows.length).to.be.greaterThan( + rowIndex, + `Expected anomalies table to have at least ${rowIndex + 1} rows (got ${ + tableRows.length + } rows)` + ); + const row = tableRows[rowIndex]; + const rowSubj = await row.getAttribute('data-test-subj'); + + return rowSubj; + }, + async assertTableNotEmpty() { - const tableRows = await testSubjects.findAll('mlAnomaliesTable > ~mlAnomaliesListRow'); + const tableRows = await this.getTableRows(); expect(tableRows.length).to.be.greaterThan( 0, `Anomalies table should have at least one row (got '${tableRows.length}')` ); }, + + async assertAnomalyActionsMenuButtonExists(rowIndex: number) { + const rowSubj = await this.getRowSubjByRowIndex(rowIndex); + await testSubjects.existOrFail(`${rowSubj} > mlAnomaliesListRowActionsButton`); + }, + + async assertAnomalyActionsMenuButtonNotExists(rowIndex: number) { + const rowSubj = await this.getRowSubjByRowIndex(rowIndex); + await testSubjects.missingOrFail(`${rowSubj} > mlAnomaliesListRowActionsButton`); + }, + + async assertAnomalyActionsMenuButtonEnabled(rowIndex: number, expectedValue: boolean) { + const rowSubj = await this.getRowSubjByRowIndex(rowIndex); + const isEnabled = await testSubjects.isEnabled( + `${rowSubj} > mlAnomaliesListRowActionsButton` + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected actions menu button for anomalies list entry #${rowIndex} to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async ensureAnomalyActionsMenuOpen(rowIndex: number) { + await retry.tryForTime(30 * 1000, async () => { + const rowSubj = await this.getRowSubjByRowIndex(rowIndex); + if (!(await testSubjects.exists('mlAnomaliesListRowActionsMenu'))) { + await testSubjects.click(`${rowSubj} > mlAnomaliesListRowActionsButton`); + await testSubjects.existOrFail('mlAnomaliesListRowActionsMenu', { timeout: 5000 }); + } + }); + }, + + async assertAnomalyActionConfigureRulesButtonExists(rowIndex: number) { + await this.ensureAnomalyActionsMenuOpen(rowIndex); + await testSubjects.existOrFail('mlAnomaliesListRowActionConfigureRulesButton'); + }, + + async assertAnomalyActionConfigureRulesButtonNotExists(rowIndex: number) { + await this.ensureAnomalyActionsMenuOpen(rowIndex); + await testSubjects.missingOrFail('mlAnomaliesListRowActionConfigureRulesButton'); + }, + + async assertAnomalyActionConfigureRulesButtonEnabled(rowIndex: number, expectedValue: boolean) { + await this.ensureAnomalyActionsMenuOpen(rowIndex); + const isEnabled = await testSubjects.isEnabled( + 'mlAnomaliesListRowActionConfigureRulesButton' + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected "configure rules" action button for anomalies list entry #${rowIndex} to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async assertAnomalyActionViewSeriesButtonEnabled(rowIndex: number, expectedValue: boolean) { + await this.ensureAnomalyActionsMenuOpen(rowIndex); + const isEnabled = await testSubjects.isEnabled('mlAnomaliesListRowActionViewSeriesButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "view series" action button for anomalies list entry #${rowIndex} to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, }; } diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index 401a96c5c11bd..5c9718539f47b 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -11,7 +11,7 @@ import { Annotation } from '../../../../plugins/ml/common/types/annotations'; import { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/application/data_frame_analytics/common'; import { FtrProviderContext } from '../../ftr_provider_context'; import { DATAFEED_STATE, JOB_STATE } from '../../../../plugins/ml/common/constants/states'; -import { DATA_FRAME_TASK_STATE } from '../../../../plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common'; +import { DATA_FRAME_TASK_STATE } from '../../../../plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/data_frame_task_state'; import { Datafeed, Job } from '../../../../plugins/ml/common/types/anomaly_detection_jobs'; export type MlApi = ProvidedType; import { @@ -722,5 +722,25 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { } }); }, + + async runDFAJob(dfaId: string) { + log.debug(`Starting data frame analytics job '${dfaId}'...`); + const startResponse = await esSupertest + .post(`/_ml/data_frame/analytics/${dfaId}/_start`) + .set({ 'Content-Type': 'application/json' }) + .expect(200) + .then((res: any) => res.body); + + expect(startResponse) + .to.have.property('acknowledged') + .eql(true, 'Response for start data frame analytics job request should be acknowledged'); + }, + + async createAndRunDFAJob(dfaConfig: DataFrameAnalyticsConfig) { + await this.createDataFrameAnalyticsJob(dfaConfig); + await this.runDFAJob(dfaConfig.id); + await this.waitForDFAJobTrainingRecordCountToBePositive(dfaConfig.id); + await this.waitForAnalyticsState(dfaConfig.id, DATA_FRAME_TASK_STATE.STOPPED); + }, }; } diff --git a/x-pack/test/functional/services/ml/common_config.ts b/x-pack/test/functional/services/ml/common_config.ts new file mode 100644 index 0000000000000..74538145135bc --- /dev/null +++ b/x-pack/test/functional/services/ml/common_config.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DeepPartial } from '../../../../plugins/ml/common/types/common'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +import { Job, Datafeed } from '../../../../plugins/ml/common/types/anomaly_detection_jobs'; +import { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/application/data_frame_analytics/common'; + +const FQ_SM_JOB_CONFIG: Job = { + job_id: ``, + description: 'mean(responsetime) on farequote dataset with 15m bucket span', + groups: ['farequote', 'automated', 'single-metric'], + analysis_config: { + bucket_span: '15m', + influencers: [], + detectors: [ + { + function: 'mean', + field_name: 'responsetime', + }, + ], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '10mb' }, + model_plot_config: { enabled: true }, +}; + +const FQ_MM_JOB_CONFIG: Job = { + job_id: `fq_multi_1_ae`, + description: + 'mean/min/max(responsetime) partition=airline on farequote dataset with 1h bucket span', + groups: ['farequote', 'automated', 'multi-metric'], + analysis_config: { + bucket_span: '1h', + influencers: ['airline'], + detectors: [ + { function: 'mean', field_name: 'responsetime', partition_field_name: 'airline' }, + { function: 'min', field_name: 'responsetime', partition_field_name: 'airline' }, + { function: 'max', field_name: 'responsetime', partition_field_name: 'airline' }, + ], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '20mb' }, + model_plot_config: { enabled: true }, +}; + +const FQ_DATAFEED_CONFIG: Datafeed = { + datafeed_id: '', + indices: ['ft_farequote'], + job_id: '', + query: { bool: { must: [{ match_all: {} }] } }, +}; + +const IHP_OUTLIER_DETECTION_CONFIG: DeepPartial = { + id: '', + description: 'Outlier detection job based on the Iowa house prices dataset', + source: { + index: ['ft_ihp_outlier'], + query: { + match_all: {}, + }, + }, + dest: { + index: '', + results_field: 'ml', + }, + analysis: { + outlier_detection: {}, + }, + analyzed_fields: { + includes: [], + excludes: [], + }, + model_memory_limit: '5mb', +}; + +export function MachineLearningCommonConfigsProvider({}: FtrProviderContext) { + return { + getADFqSingleMetricJobConfig(jobId: string): Job { + const jobConfig = { ...FQ_SM_JOB_CONFIG, job_id: jobId }; + return jobConfig; + }, + + getADFqMultiMetricJobConfig(jobId: string): Job { + const jobConfig = { ...FQ_MM_JOB_CONFIG, job_id: jobId }; + return jobConfig; + }, + + getADFqDatafeedConfig(jobId: string): Datafeed { + const datafeedConfig = { + ...FQ_DATAFEED_CONFIG, + datafeed_id: `datafeed-${jobId}`, + job_id: jobId, + }; + return datafeedConfig; + }, + + getDFAIhpOutlierDetectionJobConfig(dfaId: string): DataFrameAnalyticsConfig { + const dfaConfig = { + ...IHP_OUTLIER_DETECTION_CONFIG, + id: dfaId, + dest: { ...IHP_OUTLIER_DETECTION_CONFIG.dest, index: `user-${dfaId}` }, + }; + return dfaConfig as DataFrameAnalyticsConfig; + }, + }; +} diff --git a/x-pack/test/functional/services/ml/common_ui.ts b/x-pack/test/functional/services/ml/common_ui.ts index b66fd7087654d..319dc54fa6421 100644 --- a/x-pack/test/functional/services/ml/common_ui.ts +++ b/x-pack/test/functional/services/ml/common_ui.ts @@ -78,5 +78,13 @@ export function MachineLearningCommonUIProvider({ getService }: FtrProviderConte await testSubjects.missingOrFail('mlLoadingIndicator'); }); }, + + async assertKibanaHomeFileDataVisLinkExists() { + await testSubjects.existOrFail('homeSynopsisLinkml_file_data_visualizer'); + }, + + async assertKibanaHomeFileDataVisLinkNotExists() { + await testSubjects.missingOrFail('homeSynopsisLinkml_file_data_visualizer'); + }, }; } diff --git a/x-pack/test/functional/services/ml/data_frame_analytics.ts b/x-pack/test/functional/services/ml/data_frame_analytics.ts index 634b0d4d41fca..670e16ce4af94 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import expect from '@kbn/expect'; + import { FtrProviderContext } from '../../ftr_provider_context'; import { MlApi } from './api'; @@ -32,29 +34,14 @@ export function MachineLearningDataFrameAnalyticsProvider( await testSubjects.existOrFail('mlAnalyticsButtonCreate'); }, - async assertRegressionEvaluatePanelElementsExists() { - await testSubjects.existOrFail('mlDFAnalyticsRegressionExplorationEvaluatePanel'); - await testSubjects.existOrFail('mlDFAnalyticsRegressionGenMSEstat'); - await testSubjects.existOrFail('mlDFAnalyticsRegressionGenRSquaredStat'); - await testSubjects.existOrFail('mlDFAnalyticsRegressionTrainingMSEstat'); - await testSubjects.existOrFail('mlDFAnalyticsRegressionTrainingRSquaredStat'); - }, - - async assertRegressionTablePanelExists() { - await testSubjects.existOrFail('mlDFAnalyticsExplorationTablePanel'); - }, - - async assertClassificationEvaluatePanelElementsExists() { - await testSubjects.existOrFail('mlDFAnalyticsClassificationExplorationEvaluatePanel'); - await testSubjects.existOrFail('mlDFAnalyticsClassificationExplorationConfusionMatrix'); - }, - - async assertClassificationTablePanelExists() { - await testSubjects.existOrFail('mlDFAnalyticsExplorationTablePanel'); - }, - - async assertOutlierTablePanelExists() { - await testSubjects.existOrFail('mlDFAnalyticsOutlierExplorationTablePanel'); + async assertCreateNewAnalyticsButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlAnalyticsButtonCreate'); + expect(isEnabled).to.eql( + expectedValue, + `Expected data frame analytics "create" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); }, async assertAnalyticsStatsBarExists() { diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_results.ts b/x-pack/test/functional/services/ml/data_frame_analytics_results.ts new file mode 100644 index 0000000000000..b6a6ff8eb6c63 --- /dev/null +++ b/x-pack/test/functional/services/ml/data_frame_analytics_results.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function MachineLearningDataFrameAnalyticsResultsProvider({ + getService, +}: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async assertRegressionEvaluatePanelElementsExists() { + await testSubjects.existOrFail('mlDFAnalyticsRegressionExplorationEvaluatePanel'); + await testSubjects.existOrFail('mlDFAnalyticsRegressionGenMSEstat'); + await testSubjects.existOrFail('mlDFAnalyticsRegressionGenRSquaredStat'); + await testSubjects.existOrFail('mlDFAnalyticsRegressionTrainingMSEstat'); + await testSubjects.existOrFail('mlDFAnalyticsRegressionTrainingRSquaredStat'); + }, + + async assertRegressionTablePanelExists() { + await testSubjects.existOrFail('mlDFAnalyticsExplorationTablePanel'); + }, + + async assertClassificationEvaluatePanelElementsExists() { + await testSubjects.existOrFail('mlDFAnalyticsClassificationExplorationEvaluatePanel'); + await testSubjects.existOrFail('mlDFAnalyticsClassificationExplorationConfusionMatrix'); + }, + + async assertClassificationTablePanelExists() { + await testSubjects.existOrFail('mlDFAnalyticsExplorationTablePanel'); + }, + + async assertOutlierTablePanelExists() { + await testSubjects.existOrFail('mlDFAnalyticsOutlierExplorationTablePanel'); + }, + + async assertResultsTableExists() { + await testSubjects.existOrFail('mlExplorationDataGrid loaded', { timeout: 5000 }); + }, + + async getResultTableRows() { + return await testSubjects.findAll('mlExplorationDataGrid loaded > dataGridRow'); + }, + + async assertResultsTableNotEmpty() { + const resultTableRows = await this.getResultTableRows(); + expect(resultTableRows.length).to.be.greaterThan( + 0, + `DFA results table should have at least one row (got '${resultTableRows.length}')` + ); + }, + }; +} diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_table.ts b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts index 608a1f2bee3e1..cd2f26f3a660d 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_table.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts @@ -9,8 +9,9 @@ import { WebElementWrapper } from 'test/functional/services/lib/web_element_wrap import { FtrProviderContext } from '../../ftr_provider_context'; export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: FtrProviderContext) { - const testSubjects = getService('testSubjects'); const find = getService('find'); + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); return new (class AnalyticsTable { public async parseAnalyticsTable() { @@ -62,6 +63,11 @@ export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: F return rows; } + public rowSelector(analyticsId: string, subSelector?: string) { + const row = `~mlAnalyticsTable > ~row-${analyticsId}`; + return !subSelector ? row : `${row} > ${subSelector}`; + } + public async waitForRefreshButtonLoaded() { await testSubjects.existOrFail('~mlAnalyticsRefreshListButton', { timeout: 10 * 1000 }); await testSubjects.existOrFail('mlAnalyticsRefreshListButton loaded', { timeout: 30 * 1000 }); @@ -84,17 +90,29 @@ export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: F return await tableListContainer.findByClassName('euiFieldSearch'); } - async assertJobViewButtonExists() { - await testSubjects.existOrFail('mlAnalyticsJobViewButton'); + public async assertJobRowViewButtonExists(analyticsId: string) { + await testSubjects.existOrFail(this.rowSelector(analyticsId, 'mlAnalyticsJobViewButton')); } - public async openEditFlyout(analyticsId: string) { - await this.openRowActions(analyticsId); - await testSubjects.click('mlAnalyticsJobEditButton'); - await testSubjects.existOrFail('mlAnalyticsEditFlyout', { timeout: 5000 }); + public async assertJobRowViewButtonEnabled(analyticsId: string, expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled( + this.rowSelector(analyticsId, 'mlAnalyticsJobViewButton') + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected data frame analytics row "view results" button for job '${analyticsId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async openResultsView(analyticsId: string) { + await this.assertJobRowViewButtonExists(analyticsId); + await testSubjects.click(this.rowSelector(analyticsId, 'mlAnalyticsJobViewButton')); + await testSubjects.existOrFail('mlPageDataFrameAnalyticsExploration', { timeout: 20 * 1000 }); } - async assertAnalyticsSearchInputValue(expectedSearchValue: string) { + public async assertAnalyticsSearchInputValue(expectedSearchValue: string) { const searchBarInput = await this.getAnalyticsSearchInput(); const actualSearchValue = await searchBarInput.getAttribute('value'); expect(actualSearchValue).to.eql( @@ -103,18 +121,19 @@ export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: F ); } - public async openResultsView() { - await this.assertJobViewButtonExists(); - await testSubjects.click('mlAnalyticsJobViewButton'); - await testSubjects.existOrFail('mlPageDataFrameAnalyticsExploration', { timeout: 20 * 1000 }); - } - - public async filterWithSearchString(filter: string) { + public async filterWithSearchString(filter: string, expectedRowCount: number = 1) { await this.waitForAnalyticsToLoad(); const searchBarInput = await this.getAnalyticsSearchInput(); await searchBarInput.clearValueWithKeyboard(); await searchBarInput.type(filter); await this.assertAnalyticsSearchInputValue(filter); + + const rows = await this.parseAnalyticsTable(); + const filteredRows = rows.filter((row) => row.id === filter); + expect(filteredRows).to.have.length( + expectedRowCount, + `Filtered DFA job table should have ${expectedRowCount} row(s) for filter '${filter}' (got matching items '${filteredRows}')` + ); } public async assertAnalyticsRowFields(analyticsId: string, expectedRow: object) { @@ -129,15 +148,102 @@ export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: F ); } - public async openRowActions(analyticsId: string) { - await find.clickByCssSelector( - `[data-test-subj="mlAnalyticsTableRow row-${analyticsId}"] [data-test-subj=euiCollapsedItemActionsButton]` + public async assertJowRowActionsMenuButtonEnabled(analyticsId: string, expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled( + this.rowSelector(analyticsId, 'euiCollapsedItemActionsButton') + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected row action menu button for DFA job '${analyticsId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async ensureJobActionsMenuOpen(analyticsId: string) { + await retry.tryForTime(30 * 1000, async () => { + if (!(await testSubjects.exists('mlAnalyticsJobDeleteButton'))) { + await testSubjects.click(this.rowSelector(analyticsId, 'euiCollapsedItemActionsButton')); + await testSubjects.existOrFail('mlAnalyticsJobDeleteButton', { timeout: 5000 }); + } + }); + } + + public async ensureJobActionsMenuClosed(analyticsId: string) { + await retry.tryForTime(30 * 1000, async () => { + if (await testSubjects.exists('mlAnalyticsJobDeleteButton')) { + await testSubjects.click(this.rowSelector(analyticsId, 'euiCollapsedItemActionsButton')); + await testSubjects.missingOrFail('mlAnalyticsJobDeleteButton', { timeout: 5000 }); + } + }); + } + + public async assertJobActionViewButtonEnabled(analyticsId: string, expectedValue: boolean) { + await this.ensureJobActionsMenuOpen(analyticsId); + const actionMenuViewButton = await find.byCssSelector( + '[data-test-subj="mlAnalyticsJobViewButton"][class="euiContextMenuItem"]' + ); + const isEnabled = await actionMenuViewButton.isEnabled(); + expect(isEnabled).to.eql( + expectedValue, + `Expected "view" action menu button for DFA job '${analyticsId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertJobActionStartButtonEnabled(analyticsId: string, expectedValue: boolean) { + await this.ensureJobActionsMenuOpen(analyticsId); + const isEnabled = await testSubjects.isEnabled('mlAnalyticsJobStartButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "start" action menu button for DFA job '${analyticsId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertJobActionEditButtonEnabled(analyticsId: string, expectedValue: boolean) { + await this.ensureJobActionsMenuOpen(analyticsId); + const isEnabled = await testSubjects.isEnabled('mlAnalyticsJobEditButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "edit" action menu button for DFA job '${analyticsId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` ); - await find.existsByCssSelector('.euiPanel', 20 * 1000); + } + + public async assertJobActionCloneButtonEnabled(analyticsId: string, expectedValue: boolean) { + await this.ensureJobActionsMenuOpen(analyticsId); + const isEnabled = await testSubjects.isEnabled('mlAnalyticsJobCloneButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "clone" action menu button for DFA job '${analyticsId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertJobActionDeleteButtonEnabled(analyticsId: string, expectedValue: boolean) { + await this.ensureJobActionsMenuOpen(analyticsId); + const isEnabled = await testSubjects.isEnabled('mlAnalyticsJobDeleteButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "delete" action menu button for DFA job '${analyticsId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async openEditFlyout(analyticsId: string) { + await this.ensureJobActionsMenuOpen(analyticsId); + await testSubjects.click('mlAnalyticsJobEditButton'); + await testSubjects.existOrFail('mlAnalyticsEditFlyout', { timeout: 5000 }); } public async cloneJob(analyticsId: string) { - await this.openRowActions(analyticsId); + await this.ensureJobActionsMenuOpen(analyticsId); await testSubjects.click(`mlAnalyticsJobCloneButton`); await testSubjects.existOrFail('mlAnalyticsCreationContainer'); } diff --git a/x-pack/test/functional/services/ml/data_visualizer.ts b/x-pack/test/functional/services/ml/data_visualizer.ts index c60ae29b6b3f4..976410d43a28f 100644 --- a/x-pack/test/functional/services/ml/data_visualizer.ts +++ b/x-pack/test/functional/services/ml/data_visualizer.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import expect from '@kbn/expect'; + import { FtrProviderContext } from '../../ftr_provider_context'; export function MachineLearningDataVisualizerProvider({ getService }: FtrProviderContext) { @@ -18,6 +20,26 @@ export function MachineLearningDataVisualizerProvider({ getService }: FtrProvide await testSubjects.existOrFail('mlDataVisualizerCardIndexData'); }, + async assertSelectIndexButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlDataVisualizerSelectIndexButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "select index" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + + async assertUploadFileButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlDataVisualizerUploadFileButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "upload file" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + async navigateToIndexPatternSelection() { await testSubjects.click('mlDataVisualizerSelectIndexButton'); await testSubjects.existOrFail('mlPageSourceSelection'); diff --git a/x-pack/test/functional/services/ml/data_visualizer_file_based.ts b/x-pack/test/functional/services/ml/data_visualizer_file_based.ts index 14c6f8de7d329..61496debb97e2 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_file_based.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_file_based.ts @@ -54,6 +54,16 @@ export function MachineLearningDataVisualizerFileBasedProvider( await testSubjects.existOrFail('mlFileDataVisFileStatsPanel'); }, + async assertImportButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlFileDataVisOpenImportPageButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "import" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + async navigateToFileImport() { await testSubjects.click('mlFileDataVisOpenImportPageButton'); await testSubjects.existOrFail('mlPageFileDataVisImport'); diff --git a/x-pack/test/functional/services/ml/data_visualizer_index_based.ts b/x-pack/test/functional/services/ml/data_visualizer_index_based.ts index 7789ca78363df..31cd17e4df826 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_index_based.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_index_based.ts @@ -119,6 +119,30 @@ export function MachineLearningDataVisualizerIndexBasedProvider({ await this.assertFieldsPanelCardCount(panelFieldTypes, expectedCardCount); }, + async assertActionsPanelExists() { + await testSubjects.existOrFail('mlDataVisualizerActionsPanel'); + }, + + async assertActionsPanelNotExists() { + await testSubjects.missingOrFail('mlDataVisualizerActionsPanel'); + }, + + async assertCreateAdvancedJobCardExists() { + await testSubjects.existOrFail('mlDataVisualizerCreateAdvancedJobCard'); + }, + + async assertCreateAdvancedJobCardNotExists() { + await testSubjects.missingOrFail('mlDataVisualizerCreateAdvancedJobCard'); + }, + + async assertRecognizerCardExists(moduleId: string) { + await testSubjects.existOrFail(`mlRecognizerCard ${moduleId}`); + }, + + async assertRecognizerCardNotExists(moduleId: string) { + await testSubjects.missingOrFail(`mlRecognizerCard ${moduleId}`); + }, + async clickCreateAdvancedJobButton() { await testSubjects.clickWhenNotDisabled('mlDataVisualizerCreateAdvancedJobCard'); }, diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index d7ff60440bf31..325ea41ae3977 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -10,11 +10,13 @@ import { MachineLearningAnomaliesTableProvider } from './anomalies_table'; import { MachineLearningAnomalyExplorerProvider } from './anomaly_explorer'; import { MachineLearningAPIProvider } from './api'; import { MachineLearningCommonAPIProvider } from './common_api'; +import { MachineLearningCommonConfigsProvider } from './common_config'; import { MachineLearningCommonUIProvider } from './common_ui'; import { MachineLearningCustomUrlsProvider } from './custom_urls'; import { MachineLearningDataFrameAnalyticsProvider } from './data_frame_analytics'; import { MachineLearningDataFrameAnalyticsCreationProvider } from './data_frame_analytics_creation'; import { MachineLearningDataFrameAnalyticsEditProvider } from './data_frame_analytics_edit'; +import { MachineLearningDataFrameAnalyticsResultsProvider } from './data_frame_analytics_results'; import { MachineLearningDataFrameAnalyticsTableProvider } from './data_frame_analytics_table'; import { MachineLearningDataVisualizerProvider } from './data_visualizer'; import { MachineLearningDataVisualizerFileBasedProvider } from './data_visualizer_file_based'; @@ -30,9 +32,12 @@ import { MachineLearningJobWizardCategorizationProvider } from './job_wizard_cat import { MachineLearningJobWizardMultiMetricProvider } from './job_wizard_multi_metric'; import { MachineLearningJobWizardPopulationProvider } from './job_wizard_population'; import { MachineLearningNavigationProvider } from './navigation'; +import { MachineLearningOverviewPageProvider } from './overview_page'; import { MachineLearningSecurityCommonProvider } from './security_common'; import { MachineLearningSecurityUIProvider } from './security_ui'; import { MachineLearningSettingsProvider } from './settings'; +import { MachineLearningSettingsCalendarProvider } from './settings_calendar'; +import { MachineLearningSettingsFilterListProvider } from './settings_filter_list'; import { MachineLearningSingleMetricViewerProvider } from './single_metric_viewer'; import { MachineLearningTestExecutionProvider } from './test_execution'; import { MachineLearningTestResourcesProvider } from './test_resources'; @@ -44,6 +49,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { const anomaliesTable = MachineLearningAnomaliesTableProvider(context); const anomalyExplorer = MachineLearningAnomalyExplorerProvider(context); const api = MachineLearningAPIProvider(context); + const commonConfig = MachineLearningCommonConfigsProvider(context); const customUrls = MachineLearningCustomUrlsProvider(context); const dataFrameAnalytics = MachineLearningDataFrameAnalyticsProvider(context, api); const dataFrameAnalyticsCreation = MachineLearningDataFrameAnalyticsCreationProvider( @@ -52,6 +58,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { api ); const dataFrameAnalyticsEdit = MachineLearningDataFrameAnalyticsEditProvider(context, commonUI); + const dataFrameAnalyticsResults = MachineLearningDataFrameAnalyticsResultsProvider(context); const dataFrameAnalyticsTable = MachineLearningDataFrameAnalyticsTableProvider(context); const dataVisualizer = MachineLearningDataVisualizerProvider(context); const dataVisualizerFileBased = MachineLearningDataVisualizerFileBasedProvider(context, commonUI); @@ -67,9 +74,12 @@ export function MachineLearningProvider(context: FtrProviderContext) { const jobWizardMultiMetric = MachineLearningJobWizardMultiMetricProvider(context); const jobWizardPopulation = MachineLearningJobWizardPopulationProvider(context); const navigation = MachineLearningNavigationProvider(context); + const overviewPage = MachineLearningOverviewPageProvider(context); const securityCommon = MachineLearningSecurityCommonProvider(context); const securityUI = MachineLearningSecurityUIProvider(context, securityCommon); const settings = MachineLearningSettingsProvider(context); + const settingsCalendar = MachineLearningSettingsCalendarProvider(context); + const settingsFilterList = MachineLearningSettingsFilterListProvider(context); const singleMetricViewer = MachineLearningSingleMetricViewerProvider(context); const testExecution = MachineLearningTestExecutionProvider(context); const testResources = MachineLearningTestResourcesProvider(context); @@ -79,11 +89,13 @@ export function MachineLearningProvider(context: FtrProviderContext) { anomalyExplorer, api, commonAPI, + commonConfig, commonUI, customUrls, dataFrameAnalytics, dataFrameAnalyticsCreation, dataFrameAnalyticsEdit, + dataFrameAnalyticsResults, dataFrameAnalyticsTable, dataVisualizer, dataVisualizerFileBased, @@ -99,9 +111,12 @@ export function MachineLearningProvider(context: FtrProviderContext) { jobWizardMultiMetric, jobWizardPopulation, navigation, + overviewPage, securityCommon, securityUI, settings, + settingsCalendar, + settingsFilterList, singleMetricViewer, testExecution, testResources, diff --git a/x-pack/test/functional/services/ml/job_management.ts b/x-pack/test/functional/services/ml/job_management.ts index 085bb31258012..4c6148ad6fac6 100644 --- a/x-pack/test/functional/services/ml/job_management.ts +++ b/x-pack/test/functional/services/ml/job_management.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import expect from '@kbn/expect'; + import { FtrProviderContext } from '../../ftr_provider_context'; import { MlApi } from './api'; @@ -29,6 +31,16 @@ export function MachineLearningJobManagementProvider( await testSubjects.existOrFail('mlCreateNewJobButton'); }, + async assertCreateNewJobButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlCreateNewJobButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected AD "Create job" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + async assertJobStatsBarExists() { await testSubjects.existOrFail('~mlJobStatsBar'); }, diff --git a/x-pack/test/functional/services/ml/job_table.ts b/x-pack/test/functional/services/ml/job_table.ts index 58a1afad88e11..54c03c876af8a 100644 --- a/x-pack/test/functional/services/ml/job_table.ts +++ b/x-pack/test/functional/services/ml/job_table.ts @@ -158,12 +158,19 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte await testSubjects.existOrFail('mlJobListTable loaded', { timeout: 30 * 1000 }); } - public async filterWithSearchString(filter: string) { + public async filterWithSearchString(filter: string, expectedRowCount: number = 1) { await this.waitForJobsToLoad(); const searchBar = await testSubjects.find('mlJobListSearchBar'); const searchBarInput = await searchBar.findByTagName('input'); await searchBarInput.clearValueWithKeyboard(); await searchBarInput.type(filter); + + const rows = await this.parseJobTable(); + const filteredRows = rows.filter((row) => row.id === filter); + expect(filteredRows).to.have.length( + expectedRowCount, + `Filtered AD job table should have ${expectedRowCount} row(s) for filter '${filter}' (got matching items '${filteredRows}')` + ); } public async assertJobRowFields(jobId: string, expectedRow: object) { @@ -201,7 +208,93 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte } } - public async clickActionsMenu(jobId: string) { + public async assertJobActionSingleMetricViewerButtonEnabled( + jobId: string, + expectedValue: boolean + ) { + const isEnabled = await testSubjects.isEnabled( + this.rowSelector(jobId, 'mlOpenJobsInSingleMetricViewerButton') + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected "open in single metric viewer" job action button for AD job '${jobId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertJobActionAnomalyExplorerButtonEnabled( + jobId: string, + expectedValue: boolean + ) { + const isEnabled = await testSubjects.isEnabled( + this.rowSelector(jobId, 'mlOpenJobsInAnomalyExplorerButton') + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected "open in anomaly explorer" job action button for AD job '${jobId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertJobActionsMenuButtonEnabled(jobId: string, expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled( + this.rowSelector(jobId, 'euiCollapsedItemActionsButton') + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected actions menu button for AD job '${jobId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertJobActionStartDatafeedButtonEnabled(jobId: string, expectedValue: boolean) { + await this.ensureJobActionsMenuOpen(jobId); + const isEnabled = await testSubjects.isEnabled('mlActionButtonStartDatafeed'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "start datafeed" action button for AD job '${jobId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertJobActionCloneJobButtonEnabled(jobId: string, expectedValue: boolean) { + await this.ensureJobActionsMenuOpen(jobId); + const isEnabled = await testSubjects.isEnabled('mlActionButtonCloneJob'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "clone job" action button for AD job '${jobId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertJobActionEditJobButtonEnabled(jobId: string, expectedValue: boolean) { + await this.ensureJobActionsMenuOpen(jobId); + const isEnabled = await testSubjects.isEnabled('mlActionButtonEditJob'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "edit job" action button for AD job '${jobId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertJobActionDeleteJobButtonEnabled(jobId: string, expectedValue: boolean) { + await this.ensureJobActionsMenuOpen(jobId); + const isEnabled = await testSubjects.isEnabled('mlActionButtonDeleteJob'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "delete job" action button for AD job '${jobId}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async ensureJobActionsMenuOpen(jobId: string) { await retry.tryForTime(30 * 1000, async () => { if (!(await testSubjects.exists('mlActionButtonDeleteJob'))) { await testSubjects.click(this.rowSelector(jobId, 'euiCollapsedItemActionsButton')); @@ -211,13 +304,13 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte } public async clickCloneJobAction(jobId: string) { - await this.clickActionsMenu(jobId); + await this.ensureJobActionsMenuOpen(jobId); await testSubjects.click('mlActionButtonCloneJob'); await testSubjects.existOrFail('~mlPageJobWizard'); } public async clickDeleteJobAction(jobId: string) { - await this.clickActionsMenu(jobId); + await this.ensureJobActionsMenuOpen(jobId); await testSubjects.click('mlActionButtonDeleteJob'); await testSubjects.existOrFail('mlDeleteJobConfirmModal'); } @@ -228,13 +321,138 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte } public async clickOpenJobInSingleMetricViewerButton(jobId: string) { - await testSubjects.click(`~openJobsInSingleMetricViewer-${jobId}`); + await testSubjects.click(this.rowSelector(jobId, 'mlOpenJobsInSingleMetricViewerButton')); await testSubjects.existOrFail('~mlPageSingleMetricViewer'); } public async clickOpenJobInAnomalyExplorerButton(jobId: string) { - await testSubjects.click(`~openJobsInSingleAnomalyExplorer-${jobId}`); + await testSubjects.click(this.rowSelector(jobId, 'mlOpenJobsInAnomalyExplorerButton')); await testSubjects.existOrFail('~mlPageAnomalyExplorer'); } + + public async isJobRowSelected(jobId: string): Promise { + return await testSubjects.isChecked(this.rowSelector(jobId, `checkboxSelectRow-${jobId}`)); + } + + public async assertJobRowSelected(jobId: string, expectedValue: boolean) { + const isSelected = await this.isJobRowSelected(jobId); + expect(isSelected).to.eql( + expectedValue, + `Expected job row for AD job '${jobId}' to be '${ + expectedValue ? 'selected' : 'deselected' + }' (got '${isSelected ? 'selected' : 'deselected'}')` + ); + } + + public async selectJobRow(jobId: string) { + if ((await this.isJobRowSelected(jobId)) === false) { + await testSubjects.click(this.rowSelector(jobId, `checkboxSelectRow-${jobId}`)); + } + + await this.assertJobRowSelected(jobId, true); + await this.assertMultiSelectActionsAreaActive(); + } + + public async deselectJobRow(jobId: string) { + if ((await this.isJobRowSelected(jobId)) === true) { + await testSubjects.click(this.rowSelector(jobId, `checkboxSelectRow-${jobId}`)); + } + + await this.assertJobRowSelected(jobId, false); + await this.assertMultiSelectActionsAreaInactive(); + } + + public async assertMultiSelectActionsAreaActive() { + await testSubjects.existOrFail('mlADJobListMultiSelectActionsArea active'); + } + + public async assertMultiSelectActionsAreaInactive() { + await testSubjects.existOrFail('mlADJobListMultiSelectActionsArea inactive', { + allowHidden: true, + }); + } + + public async assertMultiSelectActionSingleMetricViewerButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled( + '~mlADJobListMultiSelectActionsArea > mlOpenJobsInSingleMetricViewerButton' + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected AD jobs multi select "open in single metric viewer" action button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertMultiSelectActionAnomalyExplorerButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled( + '~mlADJobListMultiSelectActionsArea > mlOpenJobsInAnomalyExplorerButton' + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected AD jobs multi select "open in anomaly explorer" action button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertMultiSelectActionEditJobGroupsButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled( + '~mlADJobListMultiSelectActionsArea > mlADJobListMultiSelectEditJobGroupsButton' + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected AD jobs multi select "edit job groups" action button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertMultiSelectManagementActionsButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled( + '~mlADJobListMultiSelectActionsArea > mlADJobListMultiSelectManagementActionsButton' + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected AD jobs multi select "management actions" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertMultiSelectStartDatafeedActionButtonEnabled(expectedValue: boolean) { + await this.ensureMultiSelectManagementActionsMenuOpen(); + const isEnabled = await testSubjects.isEnabled( + 'mlADJobListMultiSelectStartDatafeedActionButton' + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected AD jobs multi select "management actions" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async assertMultiSelectDeleteJobActionButtonEnabled(expectedValue: boolean) { + await this.ensureMultiSelectManagementActionsMenuOpen(); + const isEnabled = await testSubjects.isEnabled('mlADJobListMultiSelectDeleteJobActionButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected AD jobs multi select "management actions" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async ensureMultiSelectManagementActionsMenuOpen() { + await retry.tryForTime(30 * 1000, async () => { + if (!(await testSubjects.exists('mlADJobListMultiSelectDeleteJobActionButton'))) { + await testSubjects.click('mlADJobListMultiSelectManagementActionsButton'); + await testSubjects.existOrFail('mlADJobListMultiSelectDeleteJobActionButton', { + timeout: 5000, + }); + } + }); + } })(); } diff --git a/x-pack/test/functional/services/ml/navigation.ts b/x-pack/test/functional/services/ml/navigation.ts index 116c9deb7c2dc..9b53e5ce2f7e7 100644 --- a/x-pack/test/functional/services/ml/navigation.ts +++ b/x-pack/test/functional/services/ml/navigation.ts @@ -84,14 +84,22 @@ export function MachineLearningNavigationProvider({ await this.navigateToArea('~mlMainTab & ~settings', 'mlPageSettings'); }, - async navigateToStackManagementJobsListPage() { + async navigateToStackManagementJobsListPage({ + expectAccessDenied = false, + }: { + expectAccessDenied?: boolean; + } = {}) { // clicks the jobsListLink and loads the jobs list page await testSubjects.click('jobsListLink'); await retry.tryForTime(60 * 1000, async () => { - // verify that the overall page is present - await testSubjects.existOrFail('mlPageStackManagementJobsList'); - // verify that the default tab with the anomaly detection jobs list got loaded - await testSubjects.existOrFail('ml-jobs-list'); + if (expectAccessDenied === true) { + await testSubjects.existOrFail('mlPageAccessDenied'); + } else { + // verify that the overall page is present + await testSubjects.existOrFail('mlPageStackManagementJobsList'); + // verify that the default tab with the anomaly detection jobs list got loaded + await testSubjects.existOrFail('ml-jobs-list'); + } }); }, @@ -100,7 +108,7 @@ export function MachineLearningNavigationProvider({ await testSubjects.click('mlStackManagementJobsListAnalyticsTab'); await retry.tryForTime(60 * 1000, async () => { // verify that the empty prompt for analytics jobs list got loaded - await testSubjects.existOrFail('mlNoDataFrameAnalyticsFound'); + await testSubjects.existOrFail('mlAnalyticsJobList'); }); }, @@ -121,5 +129,35 @@ export function MachineLearningNavigationProvider({ await testSubjects.existOrFail('mlPageSingleMetricViewer'); }); }, + + async openKibanaNav() { + if (!(await testSubjects.exists('collapsibleNav'))) { + await testSubjects.click('toggleNavButton'); + } + await testSubjects.existOrFail('collapsibleNav'); + }, + + async assertKibanaNavMLEntryExists() { + const navArea = await testSubjects.find('collapsibleNav'); + const mlNavLink = await navArea.findAllByCssSelector('[title="Machine Learning"]'); + if (mlNavLink.length === 0) { + throw new Error(`expected ML link in nav menu to exist`); + } + }, + + async assertKibanaNavMLEntryNotExists() { + const navArea = await testSubjects.find('collapsibleNav'); + const mlNavLink = await navArea.findAllByCssSelector('[title="Machine Learning"]'); + if (mlNavLink.length !== 0) { + throw new Error(`expected ML link in nav menu to not exist`); + } + }, + + async navigateToKibanaHome() { + await retry.tryForTime(60 * 1000, async () => { + await PageObjects.common.navigateToApp('home'); + await testSubjects.existOrFail('homeApp', { timeout: 2000 }); + }); + }, }; } diff --git a/x-pack/test/functional/services/ml/overview_page.ts b/x-pack/test/functional/services/ml/overview_page.ts new file mode 100644 index 0000000000000..93b95a80cae37 --- /dev/null +++ b/x-pack/test/functional/services/ml/overview_page.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function MachineLearningOverviewPageProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async assertADCreateJobButtonExists() { + await testSubjects.existOrFail('mlOverviewCreateADJobButton'); + }, + + async assertADCreateJobButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlOverviewCreateADJobButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected AD "Create job" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + + async assertDFACreateJobButtonExists() { + await testSubjects.existOrFail('mlOverviewCreateDFAJobButton'); + }, + + async assertDFACreateJobButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlOverviewCreateDFAJobButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected AD "Create job" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + }; +} diff --git a/x-pack/test/functional/services/ml/security_common.ts b/x-pack/test/functional/services/ml/security_common.ts index cb331c95accb8..67a28a0ab96cc 100644 --- a/x-pack/test/functional/services/ml/security_common.ts +++ b/x-pack/test/functional/services/ml/security_common.ts @@ -10,9 +10,12 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export type MlSecurityCommon = ProvidedType; export enum USER { - ML_POWERUSER = 'ml_poweruser', - ML_VIEWER = 'ml_viewer', - ML_UNAUTHORIZED = 'ml_unauthorized', + ML_POWERUSER = 'ft_ml_poweruser', + ML_POWERUSER_SPACES = 'ft_ml_poweruser_spaces', + ML_VIEWER = 'ft_ml_viewer', + ML_VIEWER_SPACES = 'ft_ml_viewer_spaces', + ML_UNAUTHORIZED = 'ft_ml_unauthorized', + ML_UNAUTHORIZED_SPACES = 'ft_ml_unauthorized_spaces', } export function MachineLearningSecurityCommonProvider({ getService }: FtrProviderContext) { @@ -20,53 +23,104 @@ export function MachineLearningSecurityCommonProvider({ getService }: FtrProvide const roles = [ { - name: 'ml_source', + name: 'ft_ml_source', elasticsearch: { indices: [{ names: ['*'], privileges: ['read', 'view_index_metadata'] }], }, kibana: [], }, { - name: 'ml_dest', + name: 'ft_ml_source_readonly', + elasticsearch: { + indices: [{ names: ['*'], privileges: ['read'] }], + }, + kibana: [], + }, + { + name: 'ft_ml_dest', elasticsearch: { indices: [{ names: ['user-*'], privileges: ['read', 'index', 'manage'] }], }, kibana: [], }, { - name: 'ml_dest_readonly', + name: 'ft_ml_dest_readonly', elasticsearch: { indices: [{ names: ['user-*'], privileges: ['read'] }], }, kibana: [], }, { - name: 'ml_ui_extras', + name: 'ft_ml_ui_extras', elasticsearch: { cluster: ['manage_ingest_pipelines', 'monitor'], }, kibana: [], }, + { + name: 'ft_default_space_ml_all', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], feature: { ml: ['all'] }, spaces: ['default'] }], + }, + { + name: 'ft_default_space_ml_read', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], feature: { ml: ['read'] }, spaces: ['default'] }], + }, + { + name: 'ft_default_space_ml_none', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], feature: { discover: ['read'] }, spaces: ['default'] }], + }, ]; const users = [ { - name: 'ml_poweruser', + name: 'ft_ml_poweruser', full_name: 'ML Poweruser', password: 'mlp001', - roles: ['kibana_admin', 'machine_learning_admin', 'ml_source', 'ml_dest', 'ml_ui_extras'], + roles: [ + 'kibana_admin', + 'machine_learning_admin', + 'ft_ml_source', + 'ft_ml_dest', + 'ft_ml_ui_extras', + ], }, { - name: 'ml_viewer', + name: 'ft_ml_poweruser_spaces', + full_name: 'ML Poweruser', + password: 'mlps001', + roles: ['ft_default_space_ml_all', 'ft_ml_source', 'ft_ml_dest', 'ft_ml_ui_extras'], + }, + { + name: 'ft_ml_viewer', full_name: 'ML Viewer', password: 'mlv001', - roles: ['kibana_admin', 'machine_learning_user', 'ml_source', 'ml_dest_readonly'], + roles: [ + 'kibana_admin', + 'machine_learning_user', + 'ft_ml_source_readonly', + 'ft_ml_dest_readonly', + ], + }, + { + name: 'ft_ml_viewer_spaces', + full_name: 'ML Viewer', + password: 'mlvs001', + roles: ['ft_default_space_ml_read', 'ft_ml_source_readonly', 'ft_ml_dest_readonly'], }, { - name: 'ml_unauthorized', + name: 'ft_ml_unauthorized', full_name: 'ML Unauthorized', password: 'mlu001', - roles: ['kibana_admin', 'ml_source'], + roles: ['kibana_admin', 'ft_ml_source_readonly'], + }, + { + name: 'ft_ml_unauthorized_spaces', + full_name: 'ML Unauthorized', + password: 'mlus001', + roles: ['ft_default_space_ml_none', 'ft_ml_source_readonly'], }, ]; diff --git a/x-pack/test/functional/services/ml/security_ui.ts b/x-pack/test/functional/services/ml/security_ui.ts index 73516ca58dd5d..e09467ff36a34 100644 --- a/x-pack/test/functional/services/ml/security_ui.ts +++ b/x-pack/test/functional/services/ml/security_ui.ts @@ -31,5 +31,9 @@ export function MachineLearningSecurityUIProvider( async loginAsMlViewer() { await this.loginAs(USER.ML_VIEWER); }, + + async logout() { + await PageObjects.security.forceLogout(); + }, }; } diff --git a/x-pack/test/functional/services/ml/settings.ts b/x-pack/test/functional/services/ml/settings.ts index f7428d06899bf..de77358836fea 100644 --- a/x-pack/test/functional/services/ml/settings.ts +++ b/x-pack/test/functional/services/ml/settings.ts @@ -4,26 +4,78 @@ * you may not use this file except in compliance with the Elastic License. */ +import expect from '@kbn/expect'; + import { FtrProviderContext } from '../../ftr_provider_context'; export function MachineLearningSettingsProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); return { - async assertSettingsManageCalendarsLinkExists() { + async assertManageCalendarsLinkExists() { await testSubjects.existOrFail('mlCalendarsMngButton'); }, - async assertSettingsCreateCalendarLinkExists() { + async assertManageCalendarsLinkEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlCalendarsMngButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "manage calendars" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async assertCreateCalendarLinkExists() { await testSubjects.existOrFail('mlCalendarsCreateButton'); }, - async assertSettingsManageFilterListsLinkExists() { + async assertCreateCalendarLinkEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlCalendarsCreateButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "create calendars" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async assertManageFilterListsLinkExists() { await testSubjects.existOrFail('mlFilterListsMngButton'); }, - async assertSettingsCreateFilterListLinkExists() { + async assertManageFilterListsLinkEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlFilterListsMngButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "manage filter lists" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async assertCreateFilterListLinkExists() { await testSubjects.existOrFail('mlFilterListsCreateButton'); }, + + async assertCreateFilterListLinkEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlFilterListsCreateButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "create filter lists" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async navigateToCalendarManagement() { + await testSubjects.click('mlCalendarsMngButton'); + await testSubjects.existOrFail('mlPageCalendarManagement'); + }, + + async navigateToFilterListsManagement() { + await testSubjects.click('mlFilterListsMngButton'); + await testSubjects.existOrFail('mlPageFilterListManagement'); + }, }; } diff --git a/x-pack/test/functional/services/ml/settings_calendar.ts b/x-pack/test/functional/services/ml/settings_calendar.ts new file mode 100644 index 0000000000000..34d18c6e12c47 --- /dev/null +++ b/x-pack/test/functional/services/ml/settings_calendar.ts @@ -0,0 +1,196 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function MachineLearningSettingsCalendarProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async parseCalendarTable() { + const table = await testSubjects.find('~mlCalendarTable'); + const $ = await table.parseDomContent(); + const rows = []; + + for (const tr of $.findTestSubjects('~mlCalendarListRow').toArray()) { + const $tr = $(tr); + + rows.push({ + id: $tr + .findTestSubject('mlCalendarListColumnId') + .find('.euiTableCellContent') + .text() + .trim(), + jobs: $tr + .findTestSubject('mlCalendarListColumnJobs') + .find('.euiTableCellContent') + .text() + .trim(), + events: $tr + .findTestSubject('mlCalendarListColumnEvents') + .find('.euiTableCellContent') + .text() + .trim(), + }); + } + + return rows; + }, + + rowSelector(calendarId: string, subSelector?: string) { + const row = `~mlCalendarTable > ~row-${calendarId}`; + return !subSelector ? row : `${row} > ${subSelector}`; + }, + + async filterWithSearchString(filter: string, expectedRowCount: number = 1) { + const tableListContainer = await testSubjects.find('mlCalendarTableContainer'); + const searchBarInput = await tableListContainer.findByClassName('euiFieldSearch'); + await searchBarInput.clearValueWithKeyboard(); + await searchBarInput.type(filter); + + const rows = await this.parseCalendarTable(); + const filteredRows = rows.filter((row) => row.id === filter); + expect(filteredRows).to.have.length( + expectedRowCount, + `Filtered calendar table should have ${expectedRowCount} row(s) for filter '${filter}' (got matching items '${filteredRows}')` + ); + }, + + async isCalendarRowSelected(calendarId: string): Promise { + return await testSubjects.isChecked( + this.rowSelector(calendarId, `checkboxSelectRow-${calendarId}`) + ); + }, + + async assertCalendarRowSelected(calendarId: string, expectedValue: boolean) { + const isSelected = await this.isCalendarRowSelected(calendarId); + expect(isSelected).to.eql( + expectedValue, + `Expected calendar row for calendar '${calendarId}' to be '${ + expectedValue ? 'selected' : 'deselected' + }' (got '${isSelected ? 'selected' : 'deselected'}')` + ); + }, + + async selectCalendarRow(calendarId: string) { + if ((await this.isCalendarRowSelected(calendarId)) === false) { + await testSubjects.click(this.rowSelector(calendarId, `checkboxSelectRow-${calendarId}`)); + } + + await this.assertCalendarRowSelected(calendarId, true); + }, + + async deselectCalendarRow(calendarId: string) { + if ((await this.isCalendarRowSelected(calendarId)) === true) { + await testSubjects.click(this.rowSelector(calendarId, `checkboxSelectRow-${calendarId}`)); + } + + await this.assertCalendarRowSelected(calendarId, false); + }, + + async assertCreateCalendarButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlCalendarButtonCreate'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "create calendar" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async assertDeleteCalendarButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlCalendarButtonDelete'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "delete calendar" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async openCalendarEditForm(calendarId: string) { + await testSubjects.click(this.rowSelector(calendarId, 'mlEditCalendarLink')); + await testSubjects.existOrFail('mlPageCalendarEdit'); + }, + + async assertApplyToAllJobsSwitchEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlCalendarApplyToAllJobsSwitch'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "apply calendar to all jobs" switch to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async assertJobSelectionEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled( + 'mlCalendarJobSelection > comboBoxToggleListButton' + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected "job" selection to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + + async assertJobGroupSelectionEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled( + 'mlCalendarJobGroupSelection > comboBoxToggleListButton' + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected "job group" selection to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + + async assertNewEventButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlCalendarNewEventButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "new event" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + + async assertImportEventsButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlCalendarImportEventsButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "imports events" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + + eventRowSelector(eventDescription: string, subSelector?: string) { + const row = `~mlCalendarEventsTable > ~row-${eventDescription}`; + return !subSelector ? row : `${row} > ${subSelector}`; + }, + + async assertEventRowExists(eventDescription: string) { + await testSubjects.existOrFail(this.eventRowSelector(eventDescription)); + }, + + async assertDeleteEventButtonEnabled(eventDescription: string, expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled( + this.eventRowSelector(eventDescription, 'mlCalendarEventDeleteButton') + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected "delete event" button for event '${eventDescription}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + }; +} diff --git a/x-pack/test/functional/services/ml/settings_filter_list.ts b/x-pack/test/functional/services/ml/settings_filter_list.ts new file mode 100644 index 0000000000000..fb1f203b65562 --- /dev/null +++ b/x-pack/test/functional/services/ml/settings_filter_list.ts @@ -0,0 +1,200 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function MachineLearningSettingsFilterListProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async parseFilterListTable() { + const table = await testSubjects.find('~mlFilterListsTable'); + const $ = await table.parseDomContent(); + const rows = []; + + for (const tr of $.findTestSubjects('~mlCalendarListRow').toArray()) { + const $tr = $(tr); + + const inUseSubject = $tr + .findTestSubject('mlFilterListColumnInUse') + .findTestSubject('~mlFilterListUsedByIcon') + .attr('data-test-subj'); + const inUseString = inUseSubject.split(' ')[1]; + const inUse = inUseString === 'inUse' ? true : false; + + rows.push({ + id: $tr + .findTestSubject('mlFilterListColumnId') + .find('.euiTableCellContent') + .text() + .trim(), + description: $tr + .findTestSubject('mlFilterListColumnDescription') + .find('.euiTableCellContent') + .text() + .trim(), + itemCount: $tr + .findTestSubject('mlFilterListColumnItemCount') + .find('.euiTableCellContent') + .text() + .trim(), + inUse, + }); + } + + return rows; + }, + + rowSelector(filterId: string, subSelector?: string) { + const row = `~mlFilterListsTable > ~row-${filterId}`; + return !subSelector ? row : `${row} > ${subSelector}`; + }, + + async filterWithSearchString(filter: string, expectedRowCount: number = 1) { + const tableListContainer = await testSubjects.find('mlFilterListTableContainer'); + const searchBarInput = await tableListContainer.findByClassName('euiFieldSearch'); + await searchBarInput.clearValueWithKeyboard(); + await searchBarInput.type(filter); + + const rows = await this.parseFilterListTable(); + const filteredRows = rows.filter((row) => row.id === filter); + expect(filteredRows).to.have.length( + expectedRowCount, + `Filtered filter list table should have ${expectedRowCount} row(s) for filter '${filter}' (got matching items '${filteredRows}')` + ); + }, + + async isFilterListRowSelected(filterId: string): Promise { + return await testSubjects.isChecked( + this.rowSelector(filterId, `checkboxSelectRow-${filterId}`) + ); + }, + + async assertFilterListRowSelected(filterId: string, expectedValue: boolean) { + const isSelected = await this.isFilterListRowSelected(filterId); + expect(isSelected).to.eql( + expectedValue, + `Expected filter list row for filter list '${filterId}' to be '${ + expectedValue ? 'selected' : 'deselected' + }' (got '${isSelected ? 'selected' : 'deselected'}')` + ); + }, + + async selectFilterListRow(filterId: string) { + if ((await this.isFilterListRowSelected(filterId)) === false) { + await testSubjects.click(this.rowSelector(filterId, `checkboxSelectRow-${filterId}`)); + } + + await this.assertFilterListRowSelected(filterId, true); + }, + + async deselectFilterListRow(filterId: string) { + if ((await this.isFilterListRowSelected(filterId)) === true) { + await testSubjects.click(this.rowSelector(filterId, `checkboxSelectRow-${filterId}`)); + } + + await this.assertFilterListRowSelected(filterId, false); + }, + + async assertCreateFilterListButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlFilterListsButtonCreate'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "create filter list" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async assertDeleteFilterListButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlFilterListsDeleteButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "delete filter list" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async openFilterListEditForm(filterId: string) { + await testSubjects.click(this.rowSelector(filterId, 'mlEditFilterListLink')); + await testSubjects.existOrFail('mlPageFilterListEdit'); + }, + + async assertEditDescriptionButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlFilterListEditDescriptionButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "edit filter list description" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + + async assertAddItemButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlFilterListAddItemButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "add item" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + + async assertDeleteItemButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlFilterListDeleteItemButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "delete item" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + + filterItemSelector(filterItem: string, subSelector?: string) { + const row = `mlGridItem ${filterItem}`; + return !subSelector ? row : `${row} > ${subSelector}`; + }, + + async assertFilterItemExists(filterItem: string) { + await testSubjects.existOrFail(this.filterItemSelector(filterItem)); + }, + + async isFilterItemSelected(filterItem: string): Promise { + return await testSubjects.isChecked( + this.filterItemSelector(filterItem, 'mlGridItemCheckbox') + ); + }, + + async assertFilterItemSelected(filterItem: string, expectedValue: boolean) { + const isSelected = await this.isFilterItemSelected(filterItem); + expect(isSelected).to.eql( + expectedValue, + `Expected filter item '${filterItem}' to be '${ + expectedValue ? 'selected' : 'deselected' + }' (got '${isSelected ? 'selected' : 'deselected'}')` + ); + }, + + async selectFilterItem(filterItem: string) { + if ((await this.isFilterItemSelected(filterItem)) === false) { + await testSubjects.click(this.filterItemSelector(filterItem)); + } + + await this.assertFilterItemSelected(filterItem, true); + }, + + async deselectFilterItem(filterItem: string) { + if ((await this.isFilterItemSelected(filterItem)) === true) { + await testSubjects.click(this.filterItemSelector(filterItem)); + } + + await this.assertFilterItemSelected(filterItem, false); + }, + }; +} diff --git a/x-pack/test/functional/services/ml/single_metric_viewer.ts b/x-pack/test/functional/services/ml/single_metric_viewer.ts index a65ac09a0b056..c6b912d83fae6 100644 --- a/x-pack/test/functional/services/ml/single_metric_viewer.ts +++ b/x-pack/test/functional/services/ml/single_metric_viewer.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export function MachineLearningSingleMetricViewerProvider({ getService }: FtrProviderContext) { + const comboBox = getService('comboBox'); const testSubjects = getService('testSubjects'); return { @@ -15,12 +16,24 @@ export function MachineLearningSingleMetricViewerProvider({ getService }: FtrPro await testSubjects.existOrFail('mlNoSingleMetricJobsFound'); }, - async assertForecastButtonExistsExsist() { + async assertForecastButtonExists() { await testSubjects.existOrFail( 'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerButtonForecast' ); }, + async assertForecastButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled( + 'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerButtonForecast' + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected "forecast" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + async assertDetectorInputExsist() { await testSubjects.existOrFail( 'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerDetectorSelect' @@ -46,6 +59,28 @@ export function MachineLearningSingleMetricViewerProvider({ getService }: FtrPro await this.assertDetectorInputValue(detectorOptionValue); }, + async assertEntityInputExsist(entityFieldName: string) { + await testSubjects.existOrFail(`mlSingleMetricViewerEntitySelection ${entityFieldName}`); + }, + + async assertEntityInputSelection(entityFieldName: string, expectedIdentifier: string[]) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + `mlSingleMetricViewerEntitySelection ${entityFieldName} > comboBoxInput` + ); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected entity field selection for '${entityFieldName}' to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); + }, + + async selectEntityValue(entityFieldName: string, entityFieldValue: string) { + await comboBox.set( + `mlSingleMetricViewerEntitySelection ${entityFieldName} > comboBoxInput`, + entityFieldValue + ); + await this.assertEntityInputSelection(entityFieldName, [entityFieldValue]); + }, + async assertChartExsist() { await testSubjects.existOrFail('mlSingleMetricViewerChart'); }, @@ -55,5 +90,32 @@ export function MachineLearningSingleMetricViewerProvider({ getService }: FtrPro timeout: 30 * 1000, }); }, + + async openForecastModal() { + await testSubjects.click( + 'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerButtonForecast' + ); + await testSubjects.existOrFail('mlModalForecast'); + }, + + async closeForecastModal() { + await testSubjects.click('mlModalForecast > mlModalForecastButtonClose'); + await testSubjects.missingOrFail('mlModalForecast'); + }, + + async assertForecastModalRunButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlModalForecast > mlModalForecastButtonRun'); + expect(isEnabled).to.eql( + expectedValue, + `Expected forecast "run" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + + async openAnomalyExplorer() { + await testSubjects.click('mlAnomalyResultsViewSelectorExplorer'); + await testSubjects.existOrFail('mlPageAnomalyExplorer'); + }, }; } diff --git a/x-pack/test/functional/services/transform/security_ui.ts b/x-pack/test/functional/services/transform/security_ui.ts index 9c167e429941f..ced4625d0eb0e 100644 --- a/x-pack/test/functional/services/transform/security_ui.ts +++ b/x-pack/test/functional/services/transform/security_ui.ts @@ -31,5 +31,9 @@ export function TransformSecurityUIProvider( async loginAsTransformViewer() { await this.loginAs(USER.TRANSFORM_VIEWER); }, + + async logout() { + await PageObjects.security.forceLogout(); + }, }; } diff --git a/x-pack/test/functional/services/transform/transform_table.ts b/x-pack/test/functional/services/transform/transform_table.ts index 37d8b6e51072f..77e52b642261b 100644 --- a/x-pack/test/functional/services/transform/transform_table.ts +++ b/x-pack/test/functional/services/transform/transform_table.ts @@ -116,12 +116,19 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { await testSubjects.existOrFail('transformListTable loaded', { timeout: 30 * 1000 }); } - public async filterWithSearchString(filter: string) { + public async filterWithSearchString(filter: string, expectedRowCount: number = 1) { await this.waitForTransformsToLoad(); const tableListContainer = await testSubjects.find('transformListTableContainer'); const searchBarInput = await tableListContainer.findByClassName('euiFieldSearch'); await searchBarInput.clearValueWithKeyboard(); await searchBarInput.type(filter); + + const rows = await this.parseTransformTable(); + const filteredRows = rows.filter((row) => row.id === filter); + expect(filteredRows).to.have.length( + expectedRowCount, + `Filtered DFA job table should have ${expectedRowCount} row(s) for filter '${filter}' (got matching items '${filteredRows}')` + ); } public async assertTransformRowFields(transformId: string, expectedRow: object) { diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx index 4afd71fd67a69..f3d1eb60bf1c0 100644 --- a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx +++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx @@ -82,6 +82,7 @@ const AppRoot = React.memo( diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity.ts new file mode 100644 index 0000000000000..7fbba4e04798d --- /dev/null +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { eventsIndexPattern } from '../../../../plugins/security_solution/common/endpoint/constants'; +import { ResolverEntityIndex } from '../../../../plugins/security_solution/common/endpoint/types'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('Resolver tests for the entity route', () => { + before(async () => { + await esArchiver.load('endpoint/resolver/signals'); + }); + + after(async () => { + await esArchiver.unload('endpoint/resolver/signals'); + }); + + it('returns an event even if it does not have a mapping for entity_id', async () => { + // this id is from the es archive + const _id = 'fa7eb1546f44fd47d8868be8d74e0082e19f22df493c67a7725457978eb648ab'; + const { body }: { body: ResolverEntityIndex } = await supertest.get( + `/api/endpoint/resolver/entity?_id=${_id}&indices=${eventsIndexPattern}&indices=.siem-signals-default` + ); + expect(body).eql([ + { + // this value is from the es archive + entity_id: + 'MTIwNWY1NWQtODRkYS00MzkxLWIyNWQtYTNkNGJmNDBmY2E1LTc1NTItMTMyNDM1NDY1MTQuNjI0MjgxMDA=', + }, + ]); + }); + + it('does not return an event when it does not have the entity_id field in the document', async () => { + // this id is from the es archive + const _id = 'no-entity-id-field'; + const { body }: { body: ResolverEntityIndex } = await supertest.get( + `/api/endpoint/resolver/entity?_id=${_id}&indices=${eventsIndexPattern}&indices=.siem-signals-default` + ); + expect(body).to.be.empty(); + }); + + it('does not return an event when it does not have the process field in the document', async () => { + // this id is from the es archive + const _id = 'no-process-field'; + const { body }: { body: ResolverEntityIndex } = await supertest.get( + `/api/endpoint/resolver/entity?_id=${_id}&indices=${eventsIndexPattern}&indices=.siem-signals-default` + ); + expect(body).to.be.empty(); + }); + }); +} diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts index fc603af3619a4..ecfc1ef5bb7f5 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/index.ts @@ -10,6 +10,7 @@ export default function (providerContext: FtrProviderContext) { describe('Resolver tests', () => { loadTestFile(require.resolve('./entity_id')); + loadTestFile(require.resolve('./entity')); loadTestFile(require.resolve('./children')); loadTestFile(require.resolve('./tree')); loadTestFile(require.resolve('./alerts')); diff --git a/yarn.lock b/yarn.lock index 57a2a9d8bc506..95066c9fa8cda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1055,7 +1055,7 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@cypress/listr-verbose-renderer@0.4.1": +"@cypress/listr-verbose-renderer@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#a77492f4b11dcc7c446a34b3e28721afd33c642a" integrity sha1-p3SS9LEdzHxEajSz4ochr9M8ZCo= @@ -1065,7 +1065,7 @@ date-fns "^1.27.2" figures "^1.7.0" -"@cypress/request@2.88.5": +"@cypress/request@^2.88.5": version "2.88.5" resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.5.tgz#8d7ecd17b53a849cfd5ab06d5abe7d84976375d7" integrity sha512-TzEC1XMi1hJkywWpRfD2clreTa/Z+lOrXDCxxBTBPEcY5azdPi56A6Xw+O4tWJnaJH3iIE7G5aDXZC6JgRZLcA== @@ -1103,7 +1103,7 @@ "@babel/preset-env" "^7.0.0" babel-loader "^8.0.2" -"@cypress/xvfb@1.2.4": +"@cypress/xvfb@^1.2.4": version "1.2.4" resolved "https://registry.yarnpkg.com/@cypress/xvfb/-/xvfb-1.2.4.tgz#2daf42e8275b39f4aa53c14214e557bd14e7748a" integrity sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q== @@ -4268,10 +4268,10 @@ dependencies: "@types/node" "*" -"@types/node-forge@^0.9.0": - version "0.9.0" - resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-0.9.0.tgz#e9f678ec09283f9f35cb8de6c01f86be9278ac08" - integrity sha512-J00+BIHJOfagO1Qs67Jp5CZO3VkFxY8YKMt44oBhXr+3ZYNnl8wv/vtcJyPjuH0QZ+q7+5nnc6o/YH91ZJy2pQ== +"@types/node-forge@^0.9.5": + version "0.9.5" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-0.9.5.tgz#648231d79da197216290429020698d4e767365a0" + integrity sha512-rrN3xfA/oZIzwOnO3d2wRQz7UdeVkmMMPjWUCfpPTPuKFVb3D6G10LuiVHYYmvrivBBLMx4m0P/FICoDbNZUMA== dependencies: "@types/node" "*" @@ -4663,12 +4663,12 @@ resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.0.13.tgz#ca039c23a9e27ebea53e0901ef928ea2a1a6d313" integrity sha512-d7c/C/+H/knZ3L8/cxhicHUiTDxdgap0b/aNJfsmLwFu/iOP17mdgbQsbHA3SJmrzsjD0l3UEE5SN4xxuz5ung== -"@types/sinonjs__fake-timers@6.0.1": +"@types/sinonjs__fake-timers@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e" integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA== -"@types/sizzle@*", "@types/sizzle@2.3.2": +"@types/sizzle@*", "@types/sizzle@^2.3.2": version "2.3.2" resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== @@ -5590,7 +5590,7 @@ ajv@^4.7.0: co "^4.6.0" json-stable-stringify "^1.0.1" -ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.5.5, ajv@^6.9.1: +ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.5.5, ajv@^6.9.1: version "6.12.4" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.4.tgz#0614facc4522127fa713445c6bfd3ebd376e2234" integrity sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ== @@ -6128,7 +6128,7 @@ aproba@^1.0.3, aproba@^1.1.1: resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== -arch@2.1.2: +arch@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.2.tgz#0c52bbe7344bb4fa260c443d2cbad9c00ff2f0bf" integrity sha512-NTBIIbAfkJeIletyABbVtdPgeKfDafR+1mZV/AyyfC1UkVkp9iUjV+wwmqtUgphHYajbI86jejBJp5e+jkGTiQ== @@ -6590,6 +6590,11 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + atob-lite@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/atob-lite/-/atob-lite-2.0.0.tgz#0fef5ad46f1bd7a8502c65727f0367d5ee43d696" @@ -7387,6 +7392,11 @@ bl@^4.0.1: inherits "^2.0.4" readable-stream "^3.4.0" +blob-util@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" + integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ== + block-stream@*: version "0.0.9" resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" @@ -7409,7 +7419,7 @@ bluebird@3.5.5, bluebird@^3.5.0, bluebird@^3.5.1, bluebird@^3.5.5: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f" integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w== -bluebird@3.7.2: +bluebird@3.7.2, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -7954,7 +7964,7 @@ cacheable-request@^7.0.1: normalize-url "^4.1.0" responselike "^2.0.0" -cachedir@2.3.0: +cachedir@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw== @@ -8337,7 +8347,7 @@ check-disk-space@^2.1.0: resolved "https://registry.yarnpkg.com/check-disk-space/-/check-disk-space-2.1.0.tgz#2e77fe62f30d9676dc37a524ea2008f40c780295" integrity sha512-f0nx9oJF/AVF8nhSYlF1EBvMNnO+CXyLwKhPvN1943iOMI9TWhQigLZm80jAf0wzQhwKkzA8XXjyvuVUeGGcVQ== -check-more-types@2.24.0: +check-more-types@^2.24.0: version "2.24.0" resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA= @@ -8643,6 +8653,16 @@ cli-table3@0.5.1: optionalDependencies: colors "^1.1.2" +cli-table3@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee" + integrity sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ== + dependencies: + object-assign "^4.1.0" + string-width "^4.2.0" + optionalDependencies: + colors "^1.1.2" + cli-table@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" @@ -9042,11 +9062,6 @@ commander@3.0.2: resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== -commander@4.1.1, commander@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" - integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== - commander@^2.13.0, commander@^2.15.1, commander@^2.16.0, commander@^2.19.0: version "2.20.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" @@ -9067,12 +9082,17 @@ commander@^3.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.0.tgz#0641ea00838c7a964627f04cddc336a2deddd60a" integrity sha512-pl3QrGOBa9RZaslQiqnnKX2J068wcQw7j9AIaBQ9/JEp5RY6je4jKTImg0Bd+rpoONSe7GUFSgkxLeo17m3Pow== +commander@^4.0.1, commander@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + commander@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/commander/-/commander-5.0.0.tgz#dbf1909b49e5044f8fdaf0adc809f0c0722bdfd0" integrity sha512-JrDGPAKjMGSP1G0DUoaceEJ3DZgAfr/q6X7FVk4+U5KxUSKviYGM2k6zWkfyyBHy5rAtzgYJFa1ro2O9PtoxwQ== -common-tags@1.8.0: +common-tags@1.8.0, common-tags@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw== @@ -9946,48 +9966,49 @@ cypress-multi-reporters@^1.2.3: debug "^4.1.1" lodash "^4.17.11" -cypress@4.11.0: - version "4.11.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.11.0.tgz#054b0b85fd3aea793f186249ee1216126d5f0a7e" - integrity sha512-6Yd598+KPATM+dU1Ig0g2hbA+R/o1MAKt0xIejw4nZBVLSplCouBzqeKve6XsxGU6n4HMSt/+QYsWfFcoQeSEw== - dependencies: - "@cypress/listr-verbose-renderer" "0.4.1" - "@cypress/request" "2.88.5" - "@cypress/xvfb" "1.2.4" - "@types/sinonjs__fake-timers" "6.0.1" - "@types/sizzle" "2.3.2" - arch "2.1.2" - bluebird "3.7.2" - cachedir "2.3.0" - chalk "2.4.2" - check-more-types "2.24.0" - cli-table3 "0.5.1" - commander "4.1.1" - common-tags "1.8.0" - debug "4.1.1" - eventemitter2 "6.4.2" - execa "1.0.0" - executable "4.1.1" - extract-zip "1.7.0" - fs-extra "8.1.0" - getos "3.2.1" - is-ci "2.0.0" - is-installed-globally "0.3.2" - lazy-ass "1.6.0" - listr "0.14.3" - lodash "4.17.19" - log-symbols "3.0.0" - minimist "1.2.5" - moment "2.26.0" - ospath "1.2.2" - pretty-bytes "5.3.0" - ramda "0.26.1" - request-progress "3.0.0" - supports-color "7.1.0" - tmp "0.1.0" - untildify "4.0.0" - url "0.11.0" - yauzl "2.10.0" +cypress@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-5.0.0.tgz#6957e299b790af8b1cd7bea68261b8935646f72e" + integrity sha512-jhPd0PMO1dPSBNpx6pHVLkmnnaTfMy3wCoacHAKJ9LJG06y16zqUNSFri64N4BjuGe8y6mNMt8TdgKnmy9muCg== + dependencies: + "@cypress/listr-verbose-renderer" "^0.4.1" + "@cypress/request" "^2.88.5" + "@cypress/xvfb" "^1.2.4" + "@types/sinonjs__fake-timers" "^6.0.1" + "@types/sizzle" "^2.3.2" + arch "^2.1.2" + blob-util "2.0.2" + bluebird "^3.7.2" + cachedir "^2.3.0" + chalk "^4.1.0" + check-more-types "^2.24.0" + cli-table3 "~0.6.0" + commander "^4.1.1" + common-tags "^1.8.0" + debug "^4.1.1" + eventemitter2 "^6.4.2" + execa "^4.0.2" + executable "^4.1.1" + extract-zip "^1.7.0" + fs-extra "^9.0.1" + getos "^3.2.1" + is-ci "^2.0.0" + is-installed-globally "^0.3.2" + lazy-ass "^1.6.0" + listr "^0.14.3" + lodash "^4.17.19" + log-symbols "^4.0.0" + minimist "^1.2.5" + moment "^2.27.0" + ospath "^1.2.2" + pretty-bytes "^5.3.0" + ramda "~0.26.1" + request-progress "^3.0.0" + supports-color "^7.1.0" + tmp "~0.2.1" + untildify "^4.0.0" + url "^0.11.0" + yauzl "^2.10.0" cytoscape-dagre@^2.2.2: version "2.2.2" @@ -10310,7 +10331,7 @@ debug@3.2.6, debug@3.X, debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, dependencies: ms "^2.1.1" -debug@4, debug@4.1.1, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: +debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== @@ -12264,10 +12285,10 @@ event-target-shim@^5.0.0: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== -eventemitter2@6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.2.tgz#f31f8b99d45245f0edbc5b00797830ff3b388970" - integrity sha512-r/Pwupa5RIzxIHbEKCkNXqpEQIIT4uQDxmP4G/Lug/NokVUWj0joz/WzWl3OxRpC5kDrH/WdiUJoR+IrwvXJEw== +eventemitter2@^6.4.2: + version "6.4.3" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.3.tgz#35c563619b13f3681e7eb05cbdaf50f56ba58820" + integrity sha512-t0A2msp6BzOf+QAcI6z9XMktLj52OjGQg+8SJH6v5+3uxNpWYRR3wQmfA+6xtMU9kOC59qk9licus5dYcrYkMQ== eventemitter2@~0.4.13: version "0.4.14" @@ -12314,19 +12335,6 @@ exec-sh@^0.3.2: resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b" integrity sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg== -execa@1.0.0, execa@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== - dependencies: - cross-spawn "^6.0.0" - get-stream "^4.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - execa@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-0.1.1.tgz#b09c2a9309bc0ef0501479472db3180f8d4c3edd" @@ -12387,6 +12395,19 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + execa@^3.2.0: version "3.4.0" resolved "https://registry.yarnpkg.com/execa/-/execa-3.4.0.tgz#c08ed4550ef65d858fac269ffc8572446f37eb89" @@ -12425,7 +12446,7 @@ execall@^1.0.0: dependencies: clone-regexp "^1.0.0" -executable@4.1.1: +executable@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c" integrity sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg== @@ -13463,15 +13484,6 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@8.1.0, fs-extra@^8.0.1, fs-extra@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" - integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^4.0.0" - universalify "^0.1.0" - fs-extra@^0.30.0: version "0.30.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0" @@ -13501,6 +13513,25 @@ fs-extra@^7.0.0, fs-extra@^7.0.1, fs-extra@~7.0.1: jsonfile "^4.0.0" universalify "^0.1.0" +fs-extra@^8.0.1, fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-extra@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" + integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^1.0.0" + fs-minipass@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" @@ -13785,7 +13816,7 @@ getopts@^2.2.5: resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.2.5.tgz#67a0fe471cacb9c687d817cab6450b96dde8313b" integrity sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA== -getos@3.2.1, getos@^3.1.0: +getos@^3.1.0, getos@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5" integrity sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q== @@ -16300,13 +16331,6 @@ is-callable@^1.2.0: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== -is-ci@2.0.0, is-ci@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" - integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== - dependencies: - ci-info "^2.0.0" - is-ci@^1.0.10: version "1.1.0" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.1.0.tgz#247e4162e7860cebbdaf30b774d6b0ac7dcfe7a5" @@ -16314,6 +16338,13 @@ is-ci@^1.0.10: dependencies: ci-info "^1.0.0" +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" @@ -16480,14 +16511,6 @@ is-hexadecimal@^1.0.0: resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.1.tgz#6e084bbc92061fbb0971ec58b6ce6d404e24da69" integrity sha1-bghLvJIGH7sJcexYts5tQE4k2mk= -is-installed-globally@0.3.2, is-installed-globally@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" - integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== - dependencies: - global-dirs "^2.0.1" - is-path-inside "^3.0.1" - is-installed-globally@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" @@ -16496,6 +16519,14 @@ is-installed-globally@^0.1.0: global-dirs "^0.1.0" is-path-inside "^1.0.0" +is-installed-globally@^0.3.1, is-installed-globally@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" + integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== + dependencies: + global-dirs "^2.0.1" + is-path-inside "^3.0.1" + is-integer@^1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/is-integer/-/is-integer-1.0.7.tgz#6bde81aacddf78b659b6629d629cadc51a886d5c" @@ -18003,6 +18034,15 @@ jsonfile@^4.0.0: optionalDependencies: graceful-fs "^4.1.6" +jsonfile@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.0.1.tgz#98966cba214378c8c84b82e085907b40bf614179" + integrity sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg== + dependencies: + universalify "^1.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" @@ -18304,7 +18344,7 @@ latest-version@^5.0.0: dependencies: package-json "^6.3.0" -lazy-ass@1.6.0: +lazy-ass@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" integrity sha1-eZllXoZGwX8In90YfRUNMyTVRRM= @@ -18537,7 +18577,7 @@ listr-verbose-renderer@^0.5.0: date-fns "^1.27.2" figures "^2.0.0" -listr@0.14.3: +listr@0.14.3, listr@^0.14.3: version "0.14.3" resolved "https://registry.yarnpkg.com/listr/-/listr-0.14.3.tgz#2fea909604e434be464c50bddba0d496928fa586" integrity sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA== @@ -18986,7 +19026,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.17.11, lodash@4.17.15, lodash@4.17.19, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.11.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.2, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: +lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.11.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.2, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== @@ -19917,7 +19957,7 @@ minimist@1.2.0: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= -minimist@1.2.5, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5, minimist@~1.2.0: +minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5, minimist@~1.2.0: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -20184,16 +20224,16 @@ moment-timezone@^0.5.27: dependencies: moment ">= 2.9.0" -moment@2.26.0: - version "2.26.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a" - integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw== - "moment@>= 2.9.0", moment@>=1.6.0, moment@>=2.14.0, moment@^2.10.6, moment@^2.24.0: version "2.24.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== +moment@^2.27.0: + version "2.27.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" + integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== + monaco-editor@~0.17.0: version "0.17.1" resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.17.1.tgz#8fbe96ca54bfa75262706e044f8f780e904aa45c" @@ -20591,15 +20631,10 @@ node-forge@0.9.0: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ== -node-forge@^0.7.6: - version "0.7.6" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac" - integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw== - -node-forge@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5" - integrity sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ== +node-forge@^0.10.0, node-forge@^0.7.6: + version "0.10.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" + integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== node-gyp@^3.8.0: version "3.8.0" @@ -21586,7 +21621,7 @@ osenv@0, osenv@^0.1.0, osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" -ospath@1.2.2: +ospath@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" integrity sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs= @@ -22645,16 +22680,16 @@ prettier@^2.1.1: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.1.tgz#d9485dd5e499daa6cb547023b87a6cf51bee37d6" integrity sha512-9bY+5ZWCfqj3ghYBLxApy2zf6m+NJo5GzmLTpr9FsApsfjriNnS2dahWReHMi7qNPhhHl9SYHJs2cHZLgexNIw== -pretty-bytes@5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.3.0.tgz#f2849e27db79fb4d6cfe24764fc4134f165989f2" - integrity sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg== - pretty-bytes@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9" integrity sha1-sr+C5zUNZcbDOqlaqlpPYyf2HNk= +pretty-bytes@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.3.0.tgz#f2849e27db79fb4d6cfe24764fc4134f165989f2" + integrity sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg== + pretty-error@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3" @@ -23147,16 +23182,16 @@ railroad-diagrams@^1.0.0: resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234= -ramda@0.26.1, ramda@^0.26, ramda@^0.26.1: - version "0.26.1" - resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" - integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== - ramda@^0.21.0: version "0.21.0" resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.21.0.tgz#a001abedb3ff61077d4ff1d577d44de77e8d0a35" integrity sha1-oAGr7bP/YQd9T/HVd9RN536NCjU= +ramda@^0.26, ramda@^0.26.1, ramda@~0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" + integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== + randexp@0.4.6: version "0.4.6" resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" @@ -24871,7 +24906,7 @@ replace-homedir@^1.0.0: is-absolute "^1.0.0" remove-trailing-separator "^1.1.0" -request-progress@3.0.0: +request-progress@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe" integrity sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4= @@ -27201,13 +27236,6 @@ supports-color@6.1.0, supports-color@^6.1.0: dependencies: has-flag "^3.0.0" -supports-color@7.1.0, supports-color@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" - integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== - dependencies: - has-flag "^4.0.0" - supports-color@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-0.2.0.tgz#d92de2694eb3f67323973d7ae3d8b55b4c22190a" @@ -27246,6 +27274,13 @@ supports-color@^7.0.0: dependencies: has-flag "^4.0.0" +supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + supports-hyperlinks@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-1.0.1.tgz#71daedf36cc1060ac5100c351bb3da48c29c0ef7" @@ -27889,6 +27924,13 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" +tmp@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + tmpl@1.0.x: version "1.0.4" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" @@ -28777,6 +28819,11 @@ universalify@^0.1.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +universalify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" + integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== + unlazy-loader@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/unlazy-loader/-/unlazy-loader-0.1.3.tgz#2efdf05c489da311055586bf3cfca0c541dd8fa5" @@ -28817,11 +28864,6 @@ unstated@^2.1.1: dependencies: create-react-context "^0.1.5" -untildify@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" - integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== - untildify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/untildify/-/untildify-2.1.0.tgz#17eb2807987f76952e9c0485fc311d06a826a2e0" @@ -28834,6 +28876,11 @@ untildify@^3.0.3: resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.3.tgz#1e7b42b140bcfd922b22e70ca1265bfe3634c7c9" integrity sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA== +untildify@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" + integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== + unzip-response@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-1.0.2.tgz#b984f0877fc0a89c2c773cc1ef7b5b232b5b06fe" @@ -28981,7 +29028,7 @@ url-to-options@^1.0.1: resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" integrity sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k= -url@0.11.0, url@^0.11.0: +url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=