diff --git a/.sass-lint.yml b/.sass-lint.yml index d6eaaf391de1a28..9eed50602f5205a 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -1,6 +1,7 @@ files: include: - 'src/legacy/core_plugins/metrics/**/*.s+(a|c)ss' + - 'src/plugins/index_pattern_management/**/*.s+(a|c)ss' - 'src/plugins/timelion/**/*.s+(a|c)ss' - 'src/plugins/vis_type_vislib/**/*.s+(a|c)ss' - 'src/plugins/vis_type_vega/**/*.s+(a|c)ss' diff --git a/docs/canvas/canvas-edit-workpads.asciidoc b/docs/canvas/canvas-edit-workpads.asciidoc new file mode 100644 index 000000000000000..6558def8a747481 --- /dev/null +++ b/docs/canvas/canvas-edit-workpads.asciidoc @@ -0,0 +1,136 @@ +[role="xpack"] +[[edit-workpads]] +== Edit workpads + +To create the look and feel that you want, apply format settings to the entire workpad, or individual elements. + +[float] +[[create-variables]] +=== Create variables + +When you frequently use copy and paste, create variables to easily reuse strings and patterns. For example, when you clone a large workpad and need to connect your elements to a new index, use variables to update +each element instead of updating them manually. + +. Create the variables. +.. Click *Add a variable*. +.. Specify the variable options, then click *Save changes*. + +. Apply the variable. +.. Copy the variable. +.. Select the element you want to change, then open the expression editor. +.. Paste the variable. + +For example, to change the index pattern for a set of charts: + +Specify the variable options. + +[role="screenshot"] +image::images/specify_variable_syntax.png[Specify the variable syntax] + +Copy the variable, then apply it to each element you want to update in the *Expression editor*. + +[role="screenshot"] +image::images/copy_variable_syntax.png[Copy the variable syntax] + +[float] +[[apply-changes-to-the-entire-workpad]] +=== Apply changes to the entire workpad + +With stylesheets, you can change the look of the entire workpad, including fonts, colors, layout, and more. + +To get started, enter the changes you want to make in the *Global CSS overrides* text editor, then click *Apply stylesheet*. + +For example, to change the background for the entire workpad, enter: + +[source,text] +-------------------------------------------------- +.canvasPage { +background-color: #3990e6; +} +-------------------------------------------------- + +[float] +[[change-the-element-settings]] +=== Change the element settings + +Element settings enable you to change the display options at the element level. For example, use the element settings to change the dimensions, style, or location of an element. + +[float] +[[change-the-display-options]] +==== Change the display options + +Choose the display options for your elements. The options available depend on the element you select. + +To change the element display options, click *Display*, then make your changes in the editor. + +To use CSS overrides: + +. Click *+* next to *Element style*, then select *CSS*. +. In the *CSS* text editor, enter the changes you want to make, then click *Apply stylesheet*. + +For example, to center an element, enter: + +[source,text] +-------------------------------------------------- +.canvasRenderEl h1 { +text.align: center; +} +-------------------------------------------------- + +[float] +[[clone-elements]] +==== Clone elements +To use an element with the same functionality and appearance in multiple places, clone the element. + +Select the element, then click *Edit > Clone*. + +[role="screenshot"] +image::images/clone_element.gif[Clone elements] + +[float] +[[move-and-resize-elements]] +==== Move and resize elements + +Canvas provides you with many options to move and resize the elements on your workpad. + +* To move elements, click and hold the element, then drag to the new location. + +* To move elements by 1 pixel, select the element, press and hold Shift, then use your arrow keys. + +* To move elements by 10 pixels, select the element, then use your arrow keys. + +* To resize elements, click and drag the resize handles to the new dimensions. + +[float] +[[edit-elements]] +==== Edit elements + +The element editing options allow you to arrange and organize the elements on your workpad page. + +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] +[[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*. diff --git a/docs/canvas/canvas-elements.asciidoc b/docs/canvas/canvas-elements.asciidoc deleted file mode 100644 index 9c7467bb452fd60..000000000000000 --- a/docs/canvas/canvas-elements.asciidoc +++ /dev/null @@ -1,167 +0,0 @@ -[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/canvas/canvas-present-workpad.asciidoc b/docs/canvas/canvas-present-workpad.asciidoc index e0139ab94310410..a6d801b74fce162 100644 --- a/docs/canvas/canvas-present-workpad.asciidoc +++ b/docs/canvas/canvas-present-workpad.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[canvas-present-workpad]] -=== Present your workpad +== Present your workpad When you are ready to present your workpad, use and enable the presentation options. @@ -21,3 +21,31 @@ image::images/canvas-autoplay-interval.png[Element autoplay interval] image::images/canvas-fullscreen.png[Fullscreen mode] . When you are ready to exit fullscreen mode, press the Esc (Escape) key. + +[float] +[[zoom-in-out]] +=== Use the zoom options + +To get a closer look at a portion of your workpad, use the zoom options. + +. Click *View > Zoom*. + +. Select the zoom option. ++ +[role="screenshot"] +image::images/canvas-zoom-controls.png[Zoom controls] + +[float] +[[configure-auto-refresh-interval]] +=== Change the auto-refresh interval + +Change how often the data refreshes on your workpad. + +. Click *View > Auto refresh settings*. + +. Select the interval you want to use, or *Set a custom interval*. ++ +[role="screenshot"] +image::images/canvas-refresh-interval.png[Element data refresh interval] ++ +To manually refresh the data, click image:canvas/images/canvas-refresh-data.png[]. diff --git a/docs/canvas/canvas-share-workpad.asciidoc b/docs/canvas/canvas-share-workpad.asciidoc index a095253c6cff36f..f6cd2d93a9372c9 100644 --- a/docs/canvas/canvas-share-workpad.asciidoc +++ b/docs/canvas/canvas-share-workpad.asciidoc @@ -1,12 +1,12 @@ [role="xpack"] [[workpad-share-options]] -=== Share your workpad +== Share your workpad When you've finished your workpad, you can share it outside of {kib}. [float] [[export-single-workpad]] -==== Export workpads +=== Export workpads Create a JSON file of your workpad that you can export outside of {kib}. @@ -19,9 +19,9 @@ Want to export multiple workpads? Go to the *Canvas* home page, select the workp [float] [[create-workpad-pdf]] -==== Create a PDF +=== Create a PDF -If you have a license that supports the {report-features}, you can create a PDF copy of your workpad that you can save and share outside {kib}. +If you have a subscription that supports the {report-features}, you can create a PDF copy of your workpad that you can save and share outside {kib}. Click *Share > PDF reports > Generate PDF*. @@ -32,9 +32,9 @@ For more information, refer to <> or a script. Click *Share > PDF reports > Copy POST URL*. @@ -45,7 +45,7 @@ For more information, refer to <>. +* Build presentations of your own live data with <>. * Learn more about <> — the building blocks of your workpad. diff --git a/docs/canvas/canvas-workpad.asciidoc b/docs/canvas/canvas-workpad.asciidoc deleted file mode 100644 index 9d49c5f492bbb73..000000000000000 --- a/docs/canvas/canvas-workpad.asciidoc +++ /dev/null @@ -1,140 +0,0 @@ -[role="xpack"] -[[create-canvas-workpad]] -== Create a workpad - -A Canvas _workpad_ provides you with a workspace where you can build presentations of your live data. - -To create a workpad, choose one of the following options: - -* <> - -* <> - -* <> - -* <> - -[float] -[[blank-canvas-workpad]] -=== Start with a blank workpad - -To use the background colors, images, and data of your choice, start with a blank workpad. - -. Open the menu, then go to *Canvas*. - -. On the *Canvas workpads* view, click *Create workpad*. - -. Add a *Name* to your workpad. - -. In the *Width* and *Height* fields, specify the size. - -. Select the layout. -+ -For example, click *720p* for a traditional presentation layout. - -. Click the *Background color* picker, then select the background color for your workpad. -+ -[role="screenshot"] -image::images/canvas-background-color-picker.png[Canvas color picker] - -[float] -[[canvas-template-workpad]] -=== Create a workpad from a template - -If you're unsure about where to start, you can use one of the preconfigured templates that come with Canvas. - -. Open the menu, then go to *Canvas*. - -. On the *Canvas workpads* view, select *Templates*. - -. Click the preconfigured template that you want to use. - -. Add your own *Name* to the workpad. - -[float] -[[import-canvas-workpad]] -=== Import an existing workpad - -When you want to use a workpad that someone else has already started, import the JSON file into Canvas. - -. Open the menu, then go to *Canvas*. - -. On the *Canvas workpads* view, click and drag the file to the *Import workpad JSON file* field. - -[float] -[[sample-data-workpad]] -=== Use a sample data workpad - -Each of the sample data sets comes with a Canvas workpad that you can use for your own workpad inspiration. - -. Add a {kibana-ref}/add-sample-data.html[sample data set]. - -. On the *Add Data* page, click *View data*, then select *Canvas*. - -[float] -[[apply-workpad-styles]] -=== Apply a set of styles to the entire workpad - -To make your workpad look exactly the way you want, use the editor to apply CSS overrides. - -. Expand *Global CSS overrides*. - -. Enter the CSS. -+ -For example, to change the background on every page, enter: -+ -[source,text] --------------------------------------------------- -.canvasPage { -background-color: #3990e6; -} --------------------------------------------------- - -. Click *Apply stylesheet*. - -[float] -[[configure-auto-refresh-interval]] -=== Change the auto-refresh interval - -Change how often the data refreshes on your workpad. - -. Click *View > Auto refresh settings*. - -. Select the interval you want to use, or *Set a custom interval*. -+ -[role="screenshot"] -image::images/canvas-refresh-interval.png[Element data refresh interval] -+ -To manually refresh the data, click image:canvas/images/canvas-refresh-data.png[]. - -[float] -[[zoom-in-out]] -=== Use the zoom options - -To get a closer look at a portion of your workpad, use the zoom options. - -. Click *View > Zoom*. - -. Select the zoom option. -+ -[role="screenshot"] -image::images/canvas-zoom-controls.png[Zoom controls] - -[float] -[[add-more-pages]] -=== Add pages - -Organize your ideas onto separate pages by adding more pages. - -. Click *Page 1*, then click *+*. - -. On the *Page* editor panel, select the page transition from the *Transition* dropdown. -+ -[role="screenshot"] -image::images/canvas-add-pages.gif[Add pages] - -include::{kib-repo-dir}/canvas/canvas-elements.asciidoc[] - -include::{kib-repo-dir}/canvas/canvas-present-workpad.asciidoc[] - -include::{kib-repo-dir}/canvas/canvas-share-workpad.asciidoc[] diff --git a/docs/canvas/images/clone_element.gif b/docs/canvas/images/clone_element.gif new file mode 100644 index 000000000000000..ef8f44223d240c9 Binary files /dev/null and b/docs/canvas/images/clone_element.gif differ diff --git a/docs/canvas/images/copy_variable_syntax.png b/docs/canvas/images/copy_variable_syntax.png new file mode 100644 index 000000000000000..e583812cafb5caf Binary files /dev/null and b/docs/canvas/images/copy_variable_syntax.png differ diff --git a/docs/canvas/images/specify_variable_syntax.png b/docs/canvas/images/specify_variable_syntax.png new file mode 100644 index 000000000000000..98168c3d62ee52a Binary files /dev/null and b/docs/canvas/images/specify_variable_syntax.png differ diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index a03b1b74fc1ac2b..842f90b7047c898 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -81,6 +81,7 @@ readonly links: { readonly loadingData: string; readonly introduction: string; }; + readonly addData: string; readonly kibana: string; readonly siem: { readonly guide: string; diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.abortcontroller.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.abortcontroller.md deleted file mode 100644 index 0451a2254dc4031..000000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.abortcontroller.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [abortController](./kibana-plugin-plugins-data-public.searchinterceptor.abortcontroller.md) - -## SearchInterceptor.abortController property - -`abortController` used to signal all searches to abort. - -Signature: - -```typescript -protected abortController: AbortController; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.getpendingcount_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.getpendingcount_.md index db2c5d6957ad744..ef36b3f37b0c7c1 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.getpendingcount_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.getpendingcount_.md @@ -2,12 +2,16 @@ [Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [getPendingCount$](./kibana-plugin-plugins-data-public.searchinterceptor.getpendingcount_.md) -## SearchInterceptor.getPendingCount$ property +## SearchInterceptor.getPendingCount$() method Returns an `Observable` over the current number of pending searches. This could mean that one of the search requests is still in flight, or that it has only received partial responses. Signature: ```typescript -getPendingCount$: () => Observable; +getPendingCount$(): Observable; ``` +Returns: + +`Observable` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.hidetoast.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.hidetoast.md deleted file mode 100644 index 59938a755a99e49..000000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.hidetoast.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [hideToast](./kibana-plugin-plugins-data-public.searchinterceptor.hidetoast.md) - -## SearchInterceptor.hideToast property - -Signature: - -```typescript -protected hideToast: () => void; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.longrunningtoast.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.longrunningtoast.md deleted file mode 100644 index 5799039de91bc3c..000000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.longrunningtoast.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [longRunningToast](./kibana-plugin-plugins-data-public.searchinterceptor.longrunningtoast.md) - -## SearchInterceptor.longRunningToast property - -The current long-running toast (if there is one). - -Signature: - -```typescript -protected longRunningToast?: Toast; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md index b3b7da05326d066..32954927504aead 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md @@ -20,22 +20,15 @@ export declare class SearchInterceptor | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| [abortController](./kibana-plugin-plugins-data-public.searchinterceptor.abortcontroller.md) | | AbortController | abortController used to signal all searches to abort. | | [deps](./kibana-plugin-plugins-data-public.searchinterceptor.deps.md) | | SearchInterceptorDeps | | -| [getPendingCount$](./kibana-plugin-plugins-data-public.searchinterceptor.getpendingcount_.md) | | () => Observable<number> | Returns an Observable over the current number of pending searches. This could mean that one of the search requests is still in flight, or that it has only received partial responses. | -| [hideToast](./kibana-plugin-plugins-data-public.searchinterceptor.hidetoast.md) | | () => void | | -| [longRunningToast](./kibana-plugin-plugins-data-public.searchinterceptor.longrunningtoast.md) | | Toast | The current long-running toast (if there is one). | -| [pendingCount](./kibana-plugin-plugins-data-public.searchinterceptor.pendingcount.md) | | number | The number of pending search requests. | -| [pendingCount$](./kibana-plugin-plugins-data-public.searchinterceptor.pendingcount_.md) | | BehaviorSubject<number> | Observable that emits when the number of pending requests changes. | | [requestTimeout](./kibana-plugin-plugins-data-public.searchinterceptor.requesttimeout.md) | | number | undefined | | -| [showToast](./kibana-plugin-plugins-data-public.searchinterceptor.showtoast.md) | | () => void | | -| [timeoutSubscriptions](./kibana-plugin-plugins-data-public.searchinterceptor.timeoutsubscriptions.md) | | Subscription | The subscriptions from scheduling the automatic timeout for each request. | ## Methods | Method | Modifiers | Description | | --- | --- | --- | +| [getPendingCount$()](./kibana-plugin-plugins-data-public.searchinterceptor.getpendingcount_.md) | | Returns an Observable over the current number of pending searches. This could mean that one of the search requests is still in flight, or that it has only received partial responses. | | [runSearch(request, signal, strategy)](./kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md) | | | -| [search(request, options)](./kibana-plugin-plugins-data-public.searchinterceptor.search.md) | | Searches using the given search method. Overrides the AbortSignal with one that will abort either when cancelPending is called, when the request times out, or when the original AbortSignal is aborted. Updates the pendingCount when the request is started/finalized. | +| [search(request, options)](./kibana-plugin-plugins-data-public.searchinterceptor.search.md) | | Searches using the given search method. Overrides the AbortSignal with one that will abort either when cancelPending is called, when the request times out, or when the original AbortSignal is aborted. Updates pendingCount$ when the request is started/finalized. | | [setupTimers(options)](./kibana-plugin-plugins-data-public.searchinterceptor.setuptimers.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.pendingcount.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.pendingcount.md deleted file mode 100644 index 7dd2bd3e6703fc8..000000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.pendingcount.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [pendingCount](./kibana-plugin-plugins-data-public.searchinterceptor.pendingcount.md) - -## SearchInterceptor.pendingCount property - -The number of pending search requests. - -Signature: - -```typescript -protected pendingCount: number; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.pendingcount_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.pendingcount_.md deleted file mode 100644 index dad0fca0bfe08da..000000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.pendingcount_.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [pendingCount$](./kibana-plugin-plugins-data-public.searchinterceptor.pendingcount_.md) - -## SearchInterceptor.pendingCount$ property - -Observable that emits when the number of pending requests changes. - -Signature: - -```typescript -protected pendingCount$: BehaviorSubject; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.search.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.search.md index 38ddda7b4e184ff..1752d183a873774 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.search.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.search.md @@ -4,7 +4,7 @@ ## SearchInterceptor.search() method -Searches using the given `search` method. Overrides the `AbortSignal` with one that will abort either when `cancelPending` is called, when the request times out, or when the original `AbortSignal` is aborted. Updates the `pendingCount` when the request is started/finalized. +Searches using the given `search` method. Overrides the `AbortSignal` with one that will abort either when `cancelPending` is called, when the request times out, or when the original `AbortSignal` is aborted. Updates `pendingCount$` when the request is started/finalized. Signature: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.showtoast.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.showtoast.md deleted file mode 100644 index e495c72b57215df..000000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.showtoast.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [showToast](./kibana-plugin-plugins-data-public.searchinterceptor.showtoast.md) - -## SearchInterceptor.showToast property - -Signature: - -```typescript -protected showToast: () => void; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.timeoutsubscriptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.timeoutsubscriptions.md deleted file mode 100644 index 12f200e03778449..000000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.timeoutsubscriptions.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [timeoutSubscriptions](./kibana-plugin-plugins-data-public.searchinterceptor.timeoutsubscriptions.md) - -## SearchInterceptor.timeoutSubscriptions property - -The subscriptions from scheduling the automatic timeout for each request. - -Signature: - -```typescript -protected timeoutSubscriptions: Subscription; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.http.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.http.md index 1146179c13d63bd..66c31bb6fcf8054 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.http.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.http.md @@ -7,5 +7,5 @@ Signature: ```typescript -http: CoreStart['http']; +http: CoreSetup['http']; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md index 1291af5359887dd..63eb67ce48246c0 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md @@ -14,9 +14,9 @@ export interface SearchInterceptorDeps | Property | Type | Description | | --- | --- | --- | -| [application](./kibana-plugin-plugins-data-public.searchinterceptordeps.application.md) | ApplicationStart | | -| [http](./kibana-plugin-plugins-data-public.searchinterceptordeps.http.md) | CoreStart['http'] | | -| [toasts](./kibana-plugin-plugins-data-public.searchinterceptordeps.toasts.md) | ToastsStart | | -| [uiSettings](./kibana-plugin-plugins-data-public.searchinterceptordeps.uisettings.md) | CoreStart['uiSettings'] | | +| [http](./kibana-plugin-plugins-data-public.searchinterceptordeps.http.md) | CoreSetup['http'] | | +| [startServices](./kibana-plugin-plugins-data-public.searchinterceptordeps.startservices.md) | Promise<[CoreStart, any, unknown]> | | +| [toasts](./kibana-plugin-plugins-data-public.searchinterceptordeps.toasts.md) | ToastsSetup | | +| [uiSettings](./kibana-plugin-plugins-data-public.searchinterceptordeps.uisettings.md) | CoreSetup['uiSettings'] | | | [usageCollector](./kibana-plugin-plugins-data-public.searchinterceptordeps.usagecollector.md) | SearchUsageCollector | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.application.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.startservices.md similarity index 61% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.application.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.startservices.md index a8cd1b170a595ec..855d0652058b871 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.application.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.startservices.md @@ -1,11 +1,11 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) > [application](./kibana-plugin-plugins-data-public.searchinterceptordeps.application.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) > [startServices](./kibana-plugin-plugins-data-public.searchinterceptordeps.startservices.md) -## SearchInterceptorDeps.application property +## SearchInterceptorDeps.startServices property Signature: ```typescript -application: ApplicationStart; +startServices: Promise<[CoreStart, any, unknown]>; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.toasts.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.toasts.md index 0023b34af10c376..1f560dfa5cf7c3c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.toasts.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.toasts.md @@ -7,5 +7,5 @@ Signature: ```typescript -toasts: ToastsStart; +toasts: ToastsSetup; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.uisettings.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.uisettings.md index 425e177ec9300ad..a34d223c34ac2b5 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.uisettings.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.uisettings.md @@ -7,5 +7,5 @@ Signature: ```typescript -uiSettings: CoreStart['uiSettings']; +uiSettings: CoreSetup['uiSettings']; ``` diff --git a/docs/glossary.asciidoc b/docs/glossary.asciidoc index 51470513198b9a9..1edb33032418bfd 100644 --- a/docs/glossary.asciidoc +++ b/docs/glossary.asciidoc @@ -416,5 +416,5 @@ See // tag::workpad-def[] A workspace where you build presentations of your live data in <>. See -{kibana-ref}/create-canvas-workpad.html[Create a workpad]. +{kibana-ref}/canvas.html[Create a workpad]. // end::workpad-def[] diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index bffb3f97cd1b9c1..f750784c47043b1 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -97,7 +97,7 @@ Using a wildcard is the more popular approach. comparisons. + Kibana reads the index mapping and lists all fields that contain a timestamp. If your -index doesn't have time-based data, choose *I don't want to use the Time Filter*. +index doesn't have time-based data, choose *I don't want to use the time filter*. + You must select a time field to use global time filters on your dashboards. diff --git a/docs/user/canvas.asciidoc b/docs/user/canvas.asciidoc index 355684f7448a121..317ec67dd7c0af2 100644 --- a/docs/user/canvas.asciidoc +++ b/docs/user/canvas.asciidoc @@ -18,16 +18,170 @@ With Canvas, you can: * Focus the data you want to display with filters. [role="screenshot"] -image::images/canvas-gs-example.png[] +image::images/canvas-gs-example.png[Getting started example] For a quick overview of Canvas, watch link:https://www.youtube.com/watch?v=ZqvF_5-1xjQ[Stand out with Canvas]. -//When https://github.com/elastic/Video/issues/358 is resolved, update this link. + +[float] +[[create-workpads]] +== Create workpads + +A _workpad_ provides you with a space where you can build presentations of your live data. + +[float] +[[start-with-a-blank-workpad]] +=== Start with a blank workpad + +To use the background colors, images, and data of your choice, start with a blank workpad. + +. Open the menu, then go to *Canvas*. + +. On the *Canvas workpads* view, click *Create workpad*. + +. Add a *Name* to your workpad. + +. In the *Width* and *Height* fields, specify the size. + +. Select the layout. ++ +For example, click *720p* for a traditional presentation layout. + +. Click the *Background color* picker, then select the background color for your workpad. ++ +[role="screenshot"] +image::images/canvas-background-color-picker.png[Canvas color picker] + +[float] +[[create-workpads-from-templates]] +=== Create workpads from templates + +If you're unsure about where to start, you can use one of the preconfigured templates that come with Canvas. + +. Open the menu, then go to *Canvas*. + +. On the *Canvas workpads* view, select *Templates*. + +. Click the preconfigured template that you want to use. + +. Add your own *Name* to the workpad. + +[float] +[[import-existing-workpads]] +=== Import existing workpads + +When you want to use a workpad that someone else has already started, import the JSON file into Canvas. + +. Open the menu, then go to *Canvas*. + +. On the *Canvas workpads* view, click and drag the file to the *Import workpad JSON file* field. + +[float] +[[use-sample-data-workpads]] +=== Use sample data workpads + +Each of the sample data sets comes with a Canvas workpad that you can use for your own workpad inspiration. + +. Add a {kibana-ref}/add-sample-data.html[sample data set]. + +. On the *Add Data* page, click *View data*, then select *Canvas*. + +[float] +[[add-canvas-elements]] +== Add elements + +Create a story about your data by adding elements to your workpad that include images, text, charts, and more. + +[float] +[[create-elements]] +=== Create elements + +Choose the type of element you want to use, then use the preconfigured demo data to familiarize yourself with the element. When you're ready, connect the element to your own 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 begin, click *Add element*, then select the element you want to use. + +[role="screenshot"] +image::images/canvas-element-select.gif[Canvas elements] + +When you're ready 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 {ref}/sql-spec.html[SQL syntax]. + +* *{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, and pages and workpads often contain multiple data sources. + +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*. + +Elements are saved in *Add element > My elements*. + +[float] +[[add-existing-visuualizations]] +=== Add existing visualizations + +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] +[[add-your-own-images]] +=== Add your own images + +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[Add image to Canvas] + +[float] +[[add-more-pages]] +== Add pages + +Organize and separate your ideas by adding more pages. + +. Click *Page 1*, then click *+*. + +. On the *Page* editor panel, select the page transition from the *Transition* dropdown. ++ +[role="screenshot"] +image::images/canvas-add-pages.gif[Add pages] -- -include::{kib-repo-dir}/canvas/canvas-tutorial.asciidoc[] +include::{kib-repo-dir}/canvas/canvas-edit-workpads.asciidoc[] + +include::{kib-repo-dir}/canvas/canvas-present-workpad.asciidoc[] -include::{kib-repo-dir}/canvas/canvas-workpad.asciidoc[] +include::{kib-repo-dir}/canvas/canvas-share-workpad.asciidoc[] + +include::{kib-repo-dir}/canvas/canvas-tutorial.asciidoc[] include::{kib-repo-dir}/canvas/canvas-expression-lifecycle.asciidoc[] diff --git a/examples/embeddable_examples/kibana.json b/examples/embeddable_examples/kibana.json index 771c19cfdbd3dd9..0ac40ae1889de1d 100644 --- a/examples/embeddable_examples/kibana.json +++ b/examples/embeddable_examples/kibana.json @@ -4,7 +4,7 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "requiredPlugins": ["embeddable", "uiActions"], + "requiredPlugins": ["embeddable", "uiActions", "dashboard"], "optionalPlugins": [], "extraPublicDirs": ["public/todo", "public/hello_world", "public/todo/todo_ref_embeddable"], "requiredBundles": ["kibanaReact"] diff --git a/examples/embeddable_examples/public/book/add_book_to_library_action.tsx b/examples/embeddable_examples/public/book/add_book_to_library_action.tsx new file mode 100644 index 000000000000000..b74a1d56429825c --- /dev/null +++ b/examples/embeddable_examples/public/book/add_book_to_library_action.tsx @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { createAction, IncompatibleActionError } from '../../../../src/plugins/ui_actions/public'; +import { BookEmbeddable, BOOK_EMBEDDABLE } from './book_embeddable'; +import { ViewMode, isReferenceOrValueEmbeddable } from '../../../../src/plugins/embeddable/public'; + +interface ActionContext { + embeddable: BookEmbeddable; +} + +export const ACTION_ADD_BOOK_TO_LIBRARY = 'ACTION_ADD_BOOK_TO_LIBRARY'; + +export const createAddBookToLibraryAction = () => + createAction({ + getDisplayName: () => + i18n.translate('embeddableExamples.book.addToLibrary', { + defaultMessage: 'Add Book To Library', + }), + type: ACTION_ADD_BOOK_TO_LIBRARY, + order: 100, + getIconType: () => 'folderCheck', + isCompatible: async ({ embeddable }: ActionContext) => { + return ( + embeddable.type === BOOK_EMBEDDABLE && + embeddable.getInput().viewMode === ViewMode.EDIT && + isReferenceOrValueEmbeddable(embeddable) && + !embeddable.inputIsRefType(embeddable.getInput()) + ); + }, + execute: async ({ embeddable }: ActionContext) => { + if (!isReferenceOrValueEmbeddable(embeddable)) { + throw new IncompatibleActionError(); + } + const newInput = await embeddable.getInputAsRefType(); + embeddable.updateInput(newInput); + }, + }); diff --git a/examples/embeddable_examples/public/book/book_component.tsx b/examples/embeddable_examples/public/book/book_component.tsx index 064e13c131a0a7f..e46487641b913e7 100644 --- a/examples/embeddable_examples/public/book/book_component.tsx +++ b/examples/embeddable_examples/public/book/book_component.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { EuiFlexItem, EuiFlexGroup, EuiIcon } from '@elastic/eui'; import { EuiText } from '@elastic/eui'; -import { EuiFlexGrid } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { withEmbeddableSubscription } from '../../../../src/plugins/embeddable/public'; import { BookEmbeddableInput, BookEmbeddableOutput, BookEmbeddable } from './book_embeddable'; @@ -44,26 +44,32 @@ function wrapSearchTerms(task?: string, search?: string) { ); } -export function BookEmbeddableComponentInner({ input: { search }, output: { attributes } }: Props) { +export function BookEmbeddableComponentInner({ + input: { search }, + output: { attributes }, + embeddable, +}: Props) { const title = attributes?.title; const author = attributes?.author; const readIt = attributes?.readIt; + const byReference = embeddable.inputIsRefType(embeddable.getInput()); + return ( - + {title ? ( -

{wrapSearchTerms(title, search)},

+

{wrapSearchTerms(title, search)}

) : null} {author ? ( -
-{wrapSearchTerms(author, search)}
+ -{wrapSearchTerms(author, search)}
) : null} @@ -76,7 +82,21 @@ export function BookEmbeddableComponentInner({ input: { search }, output: { attr
)} - +
+ + + + {' '} + + {byReference + ? i18n.translate('embeddableExamples.book.byReferenceLabel', { + defaultMessage: 'Book is By Reference', + }) + : i18n.translate('embeddableExamples.book.byValueLabel', { + defaultMessage: 'Book is By Value', + })} + + ); diff --git a/examples/embeddable_examples/public/book/book_embeddable.tsx b/examples/embeddable_examples/public/book/book_embeddable.tsx index d49bd3280d97d9e..dd9418c0e8596db 100644 --- a/examples/embeddable_examples/public/book/book_embeddable.tsx +++ b/examples/embeddable_examples/public/book/book_embeddable.tsx @@ -25,10 +25,11 @@ import { IContainer, EmbeddableOutput, SavedObjectEmbeddableInput, - AttributeService, + ReferenceOrValueEmbeddable, } from '../../../../src/plugins/embeddable/public'; import { BookSavedObjectAttributes } from '../../common'; import { BookEmbeddableComponent } from './book_component'; +import { AttributeService } from '../../../../src/plugins/dashboard/public'; export const BOOK_EMBEDDABLE = 'book'; export type BookEmbeddableInput = BookByValueInput | BookByReferenceInput; @@ -59,7 +60,8 @@ function getHasMatch(search?: string, savedAttributes?: BookSavedObjectAttribute ); } -export class BookEmbeddable extends Embeddable { +export class BookEmbeddable extends Embeddable + implements ReferenceOrValueEmbeddable { public readonly type = BOOK_EMBEDDABLE; private subscription: Subscription; private node?: HTMLElement; @@ -96,6 +98,18 @@ export class BookEmbeddable extends Embeddable { + return this.attributeService.inputIsRefType(input); + }; + + getInputAsValueType = async (): Promise => { + return this.attributeService.getInputAsValueType(this.input); + }; + + getInputAsRefType = async (): Promise => { + return this.attributeService.getInputAsRefType(this.input, { showSaveModal: true }); + }; + public render(node: HTMLElement) { if (this.node) { ReactDOM.unmountComponentAtNode(this.node); @@ -113,6 +127,10 @@ export class BookEmbeddable extends Embeddable(this.type); } - return this.attributeService; + return this.attributeService!; } } diff --git a/examples/embeddable_examples/public/book/edit_book_action.tsx b/examples/embeddable_examples/public/book/edit_book_action.tsx index 222f70e0be60f72..b31d69696598e93 100644 --- a/examples/embeddable_examples/public/book/edit_book_action.tsx +++ b/examples/embeddable_examples/public/book/edit_book_action.tsx @@ -22,11 +22,7 @@ import { i18n } from '@kbn/i18n'; import { BookSavedObjectAttributes, BOOK_SAVED_OBJECT } from '../../common'; import { createAction } from '../../../../src/plugins/ui_actions/public'; import { toMountPoint } from '../../../../src/plugins/kibana_react/public'; -import { - ViewMode, - EmbeddableStart, - SavedObjectEmbeddableInput, -} from '../../../../src/plugins/embeddable/public'; +import { ViewMode, SavedObjectEmbeddableInput } from '../../../../src/plugins/embeddable/public'; import { BookEmbeddable, BOOK_EMBEDDABLE, @@ -34,10 +30,11 @@ import { BookByValueInput, } from './book_embeddable'; import { CreateEditBookComponent } from './create_edit_book_component'; +import { DashboardStart } from '../../../../src/plugins/dashboard/public'; interface StartServices { openModal: OverlayStart['openModal']; - getAttributeService: EmbeddableStart['getAttributeService']; + getAttributeService: DashboardStart['getAttributeService']; } interface ActionContext { diff --git a/examples/embeddable_examples/public/book/unlink_book_from_library_action.tsx b/examples/embeddable_examples/public/book/unlink_book_from_library_action.tsx new file mode 100644 index 000000000000000..cef77092a642ac5 --- /dev/null +++ b/examples/embeddable_examples/public/book/unlink_book_from_library_action.tsx @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { createAction, IncompatibleActionError } from '../../../../src/plugins/ui_actions/public'; +import { BookEmbeddable, BOOK_EMBEDDABLE } from './book_embeddable'; +import { ViewMode, isReferenceOrValueEmbeddable } from '../../../../src/plugins/embeddable/public'; + +interface ActionContext { + embeddable: BookEmbeddable; +} + +export const ACTION_UNLINK_BOOK_FROM_LIBRARY = 'ACTION_UNLINK_BOOK_FROM_LIBRARY'; + +export const createUnlinkBookFromLibraryAction = () => + createAction({ + getDisplayName: () => + i18n.translate('embeddableExamples.book.unlinkFromLibrary', { + defaultMessage: 'Unlink Book from Library Item', + }), + type: ACTION_UNLINK_BOOK_FROM_LIBRARY, + order: 100, + getIconType: () => 'folderExclamation', + isCompatible: async ({ embeddable }: ActionContext) => { + return ( + embeddable.type === BOOK_EMBEDDABLE && + embeddable.getInput().viewMode === ViewMode.EDIT && + isReferenceOrValueEmbeddable(embeddable) && + embeddable.inputIsRefType(embeddable.getInput()) + ); + }, + execute: async ({ embeddable }: ActionContext) => { + if (!isReferenceOrValueEmbeddable(embeddable)) { + throw new IncompatibleActionError(); + } + const newInput = await embeddable.getInputAsValueType(); + embeddable.updateInput(newInput); + }, + }); diff --git a/examples/embeddable_examples/public/plugin.ts b/examples/embeddable_examples/public/plugin.ts index 95f4f5b41e198ba..0c6ed1eb3be488c 100644 --- a/examples/embeddable_examples/public/plugin.ts +++ b/examples/embeddable_examples/public/plugin.ts @@ -58,6 +58,15 @@ import { BookEmbeddableFactoryDefinition, } from './book/book_embeddable_factory'; import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; +import { + ACTION_ADD_BOOK_TO_LIBRARY, + createAddBookToLibraryAction, +} from './book/add_book_to_library_action'; +import { DashboardStart } from '../../../src/plugins/dashboard/public'; +import { + ACTION_UNLINK_BOOK_FROM_LIBRARY, + createUnlinkBookFromLibraryAction, +} from './book/unlink_book_from_library_action'; export interface EmbeddableExamplesSetupDependencies { embeddable: EmbeddableSetup; @@ -66,6 +75,7 @@ export interface EmbeddableExamplesSetupDependencies { export interface EmbeddableExamplesStartDependencies { embeddable: EmbeddableStart; + dashboard: DashboardStart; } interface ExampleEmbeddableFactories { @@ -86,6 +96,8 @@ export interface EmbeddableExamplesStart { declare module '../../../src/plugins/ui_actions/public' { export interface ActionContextMapping { [ACTION_EDIT_BOOK]: { embeddable: BookEmbeddable }; + [ACTION_ADD_BOOK_TO_LIBRARY]: { embeddable: BookEmbeddable }; + [ACTION_UNLINK_BOOK_FROM_LIBRARY]: { embeddable: BookEmbeddable }; } } @@ -144,17 +156,25 @@ export class EmbeddableExamplesPlugin this.exampleEmbeddableFactories.getBookEmbeddableFactory = deps.embeddable.registerEmbeddableFactory( BOOK_EMBEDDABLE, new BookEmbeddableFactoryDefinition(async () => ({ - getAttributeService: (await core.getStartServices())[1].embeddable.getAttributeService, + getAttributeService: (await core.getStartServices())[1].dashboard.getAttributeService, openModal: (await core.getStartServices())[0].overlays.openModal, })) ); const editBookAction = createEditBookAction(async () => ({ - getAttributeService: (await core.getStartServices())[1].embeddable.getAttributeService, + getAttributeService: (await core.getStartServices())[1].dashboard.getAttributeService, openModal: (await core.getStartServices())[0].overlays.openModal, })); deps.uiActions.registerAction(editBookAction); deps.uiActions.attachAction(CONTEXT_MENU_TRIGGER, editBookAction.id); + + const addBookToLibraryAction = createAddBookToLibraryAction(); + deps.uiActions.registerAction(addBookToLibraryAction); + deps.uiActions.attachAction(CONTEXT_MENU_TRIGGER, addBookToLibraryAction.id); + + const unlinkBookFromLibraryAction = createUnlinkBookFromLibraryAction(); + deps.uiActions.registerAction(unlinkBookFromLibraryAction); + deps.uiActions.attachAction(CONTEXT_MENU_TRIGGER, unlinkBookFromLibraryAction.id); } public start( diff --git a/package.json b/package.json index becd670e4ddcf52..200aa41743f51c5 100644 --- a/package.json +++ b/package.json @@ -276,7 +276,6 @@ "url-loader": "2.2.0", "uuid": "3.3.2", "val-loader": "^1.1.1", - "validate-npm-package-name": "2.2.2", "vega": "^5.13.0", "vega-lite": "^4.13.1", "vega-schema-url-parser": "^1.1.0", diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index ee141e1d8ab7a4e..e411dcd472768d1 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -97,8 +97,6 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(511); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["buildProductionProjects"]; }); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); - /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(145); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "getProjects", function() { return _utils_projects__WEBPACK_IMPORTED_MODULE_2__["getProjects"]; }); @@ -15108,7 +15106,7 @@ function getMarkerLines(loc, source, opts) { column: 0, line: -1 }, loc.start); - const endLoc = Object.assign({}, startLoc, {}, loc.end); + const endLoc = Object.assign({}, startLoc, loc.end); const { linesAbove = 2, linesBelow = 3 @@ -15530,7 +15528,7 @@ function isIdentifierName(name) { } } - return true; + return !isFirst; } /***/ }), @@ -59477,9 +59475,6 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(512); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _build_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildProductionProjects"]; }); -/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(748); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); - /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -59500,7 +59495,6 @@ __webpack_require__.r(__webpack_exports__); */ - /***/ }), /* 512 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { @@ -90331,71 +90325,5 @@ NestedError.prototype.name = 'NestedError'; module.exports = NestedError; -/***/ }), -/* 748 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return prepareExternalProjectDependencies; }); -/* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(164); -/* harmony import */ var _utils_project__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(163); -/* - * 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. - */ - - -/** - * All external projects are located within `./plugins/{plugin}` relative - * to the Kibana root directory or `../kibana-extra/{plugin}` relative - * to Kibana itself. - */ - -const isKibanaDep = depVersion => // For ../kibana-extra/ directory (legacy only) -depVersion.includes('../../kibana/packages/') || // For plugins/ directory -depVersion.includes('../../packages/'); -/** - * This prepares the dependencies for an _external_ project. - */ - - -async function prepareExternalProjectDependencies(projectPath) { - const project = await _utils_project__WEBPACK_IMPORTED_MODULE_1__["Project"].fromPath(projectPath); - - if (!project.hasDependencies()) { - return; - } - - const deps = project.allDependencies; - - for (const depName of Object.keys(deps)) { - const depVersion = deps[depName]; // Kibana currently only supports `link:` dependencies on Kibana's own - // packages, as these are packaged into the `node_modules` folder when - // Kibana is built, so we don't need to take any action to enable - // `require(...)` to resolve for these packages. - - if (Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_0__["isLinkDependency"])(depVersion) && !isKibanaDep(depVersion)) { - // For non-Kibana packages we need to set up symlinks during the - // installation process, but this is not something we support yet. - throw new Error('This plugin is using `link:` dependencies for non-Kibana packages'); - } - } -} - /***/ }) /******/ ]); \ No newline at end of file diff --git a/packages/kbn-pm/src/index.ts b/packages/kbn-pm/src/index.ts index 0aa58adb4382fc9..27ce0a417fdebfe 100644 --- a/packages/kbn-pm/src/index.ts +++ b/packages/kbn-pm/src/index.ts @@ -18,7 +18,7 @@ */ export { run } from './cli'; -export { buildProductionProjects, prepareExternalProjectDependencies } from './production'; +export { buildProductionProjects } from './production'; export { getProjects } from './utils/projects'; export { Project } from './utils/project'; export { copyWorkspacePackages } from './utils/workspaces'; diff --git a/packages/kbn-pm/src/production/index.ts b/packages/kbn-pm/src/production/index.ts index 493af2beb648d27..f74ab8a4484f111 100644 --- a/packages/kbn-pm/src/production/index.ts +++ b/packages/kbn-pm/src/production/index.ts @@ -18,4 +18,3 @@ */ export { buildProductionProjects } from './build_production_projects'; -export { prepareExternalProjectDependencies } from './prepare_project_dependencies'; diff --git a/packages/kbn-pm/src/production/prepare_project_dependencies.test.ts b/packages/kbn-pm/src/production/prepare_project_dependencies.test.ts deleted file mode 100644 index 13ab8d56e019017..000000000000000 --- a/packages/kbn-pm/src/production/prepare_project_dependencies.test.ts +++ /dev/null @@ -1,40 +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 { join, resolve } from 'path'; - -import { prepareExternalProjectDependencies } from './prepare_project_dependencies'; - -const packagesFixtures = resolve(__dirname, '__fixtures__/external_packages'); - -test('does nothing when Kibana `link:` dependencies', async () => { - const projectPath = join(packagesFixtures, 'with_kibana_link_deps'); - - // We're checking for undefined, but we don't really care about what's - // returned, we only care about it resolving. - await expect(prepareExternalProjectDependencies(projectPath)).resolves.toBeUndefined(); -}); - -test('throws if non-Kibana `link` dependencies', async () => { - const projectPath = join(packagesFixtures, 'with_other_link_deps'); - - await expect(prepareExternalProjectDependencies(projectPath)).rejects.toThrow( - 'This plugin is using `link:` dependencies for non-Kibana packages' - ); -}); diff --git a/packages/kbn-pm/src/production/prepare_project_dependencies.ts b/packages/kbn-pm/src/production/prepare_project_dependencies.ts deleted file mode 100644 index 9817770166480dc..000000000000000 --- a/packages/kbn-pm/src/production/prepare_project_dependencies.ts +++ /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 { isLinkDependency } from '../utils/package_json'; -import { Project } from '../utils/project'; - -/** - * All external projects are located within `./plugins/{plugin}` relative - * to the Kibana root directory or `../kibana-extra/{plugin}` relative - * to Kibana itself. - */ -const isKibanaDep = (depVersion: string) => - // For ../kibana-extra/ directory (legacy only) - depVersion.includes('../../kibana/packages/') || - // For plugins/ directory - depVersion.includes('../../packages/'); - -/** - * This prepares the dependencies for an _external_ project. - */ -export async function prepareExternalProjectDependencies(projectPath: string) { - const project = await Project.fromPath(projectPath); - - if (!project.hasDependencies()) { - return; - } - - const deps = project.allDependencies; - - for (const depName of Object.keys(deps)) { - const depVersion = deps[depName]; - - // Kibana currently only supports `link:` dependencies on Kibana's own - // packages, as these are packaged into the `node_modules` folder when - // Kibana is built, so we don't need to take any action to enable - // `require(...)` to resolve for these packages. - if (isLinkDependency(depVersion) && !isKibanaDep(depVersion)) { - // For non-Kibana packages we need to set up symlinks during the - // installation process, but this is not something we support yet. - throw new Error('This plugin is using `link:` dependencies for non-Kibana packages'); - } - } -} diff --git a/src/cli_keystore/add.js b/src/cli_keystore/add.js index 87266702a26cc99..44737e387c2d299 100644 --- a/src/cli_keystore/add.js +++ b/src/cli_keystore/add.js @@ -17,7 +17,7 @@ * under the License. */ -import Logger from '../cli_plugin/lib/logger'; +import { Logger } from '../cli_plugin/lib/logger'; import { confirm, question } from '../legacy/server/utils'; import { createPromiseFromStreams, createConcatStream } from '../legacy/utils'; diff --git a/src/cli_keystore/add.test.js b/src/cli_keystore/add.test.js index 320581b470c2b42..b5d5009667eb47d 100644 --- a/src/cli_keystore/add.test.js +++ b/src/cli_keystore/add.test.js @@ -41,7 +41,7 @@ import { PassThrough } from 'stream'; import { Keystore } from '../legacy/server/keystore'; import { add } from './add'; -import Logger from '../cli_plugin/lib/logger'; +import { Logger } from '../cli_plugin/lib/logger'; import * as prompt from '../legacy/server/utils/prompt'; describe('Kibana keystore', () => { diff --git a/src/cli_keystore/create.js b/src/cli_keystore/create.js index 1af0959821f80d6..8be1eb36882f103 100644 --- a/src/cli_keystore/create.js +++ b/src/cli_keystore/create.js @@ -17,7 +17,7 @@ * under the License. */ -import Logger from '../cli_plugin/lib/logger'; +import { Logger } from '../cli_plugin/lib/logger'; import { confirm } from '../legacy/server/utils'; export async function create(keystore, command, options) { diff --git a/src/cli_keystore/create.test.js b/src/cli_keystore/create.test.js index 33b5aa4bd07d8eb..f48b3775ddfff70 100644 --- a/src/cli_keystore/create.test.js +++ b/src/cli_keystore/create.test.js @@ -40,7 +40,7 @@ import sinon from 'sinon'; import { Keystore } from '../legacy/server/keystore'; import { create } from './create'; -import Logger from '../cli_plugin/lib/logger'; +import { Logger } from '../cli_plugin/lib/logger'; import * as prompt from '../legacy/server/utils/prompt'; describe('Kibana keystore', () => { diff --git a/src/cli_keystore/get_keystore.js b/src/cli_keystore/get_keystore.js index c8ff2555563ad2f..e181efe9196b8e6 100644 --- a/src/cli_keystore/get_keystore.js +++ b/src/cli_keystore/get_keystore.js @@ -20,7 +20,7 @@ import { existsSync } from 'fs'; import { join } from 'path'; -import Logger from '../cli_plugin/lib/logger'; +import { Logger } from '../cli_plugin/lib/logger'; import { getConfigDirectory, getDataPath } from '../core/server/path'; export function getKeystore() { diff --git a/src/cli_keystore/get_keystore.test.js b/src/cli_keystore/get_keystore.test.js index 88102b8f51d5724..b1c42fca2f73ca8 100644 --- a/src/cli_keystore/get_keystore.test.js +++ b/src/cli_keystore/get_keystore.test.js @@ -18,7 +18,7 @@ */ import { getKeystore } from './get_keystore'; -import Logger from '../cli_plugin/lib/logger'; +import { Logger } from '../cli_plugin/lib/logger'; import fs from 'fs'; import sinon from 'sinon'; diff --git a/src/cli_keystore/list.js b/src/cli_keystore/list.js index e9158735a214f0c..4a99de271bc6a07 100644 --- a/src/cli_keystore/list.js +++ b/src/cli_keystore/list.js @@ -17,7 +17,7 @@ * under the License. */ -import Logger from '../cli_plugin/lib/logger'; +import { Logger } from '../cli_plugin/lib/logger'; export function list(keystore, command, options = {}) { const logger = new Logger(options); diff --git a/src/cli_keystore/list.test.js b/src/cli_keystore/list.test.js index 857991b5ae3b9c4..11c474f908216ec 100644 --- a/src/cli_keystore/list.test.js +++ b/src/cli_keystore/list.test.js @@ -38,7 +38,7 @@ jest.mock('fs', () => ({ import sinon from 'sinon'; import { Keystore } from '../legacy/server/keystore'; import { list } from './list'; -import Logger from '../cli_plugin/lib/logger'; +import { Logger } from '../cli_plugin/lib/logger'; describe('Kibana keystore', () => { describe('list', () => { diff --git a/src/cli_plugin/cli.js b/src/cli_plugin/cli.js index da1068b54b4b55e..e483385b5b9e8eb 100644 --- a/src/cli_plugin/cli.js +++ b/src/cli_plugin/cli.js @@ -17,12 +17,11 @@ * under the License. */ -import _ from 'lodash'; import { pkg } from '../core/server/utils'; import Command from '../cli/command'; -import listCommand from './list'; -import installCommand from './install'; -import removeCommand from './remove'; +import { listCommand } from './list'; +import { installCommand } from './install'; +import { removeCommand } from './remove'; const argv = process.env.kbnWorkerArgv ? JSON.parse(process.env.kbnWorkerArgv) @@ -44,8 +43,12 @@ program .command('help ') .description('get the help for a specific command') .action(function (cmdName) { - const cmd = _.find(program.commands, { _name: cmdName }); - if (!cmd) return program.error(`unknown command ${cmdName}`); + const cmd = program.commands.find((c) => c._name === cmdName); + + if (!cmd) { + return program.error(`unknown command ${cmdName}`); + } + cmd.help(); }); diff --git a/src/cli_plugin/install/__fixtures__/replies/invalid_name.zip b/src/cli_plugin/install/__fixtures__/replies/invalid_name.zip index 5de9a0677b6cb66..4d77ba0d389a699 100644 Binary files a/src/cli_plugin/install/__fixtures__/replies/invalid_name.zip and b/src/cli_plugin/install/__fixtures__/replies/invalid_name.zip differ diff --git a/src/cli_plugin/install/__fixtures__/replies/package.no_version.json b/src/cli_plugin/install/__fixtures__/replies/package.no_version.json deleted file mode 100644 index 9c4f574d894600c..000000000000000 --- a/src/cli_plugin/install/__fixtures__/replies/package.no_version.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "test-plugin" -} diff --git a/src/cli_plugin/install/__fixtures__/replies/test_plugin.zip b/src/cli_plugin/install/__fixtures__/replies/test_plugin.zip index 544abf86007e668..57f7455de10f844 100644 Binary files a/src/cli_plugin/install/__fixtures__/replies/test_plugin.zip and b/src/cli_plugin/install/__fixtures__/replies/test_plugin.zip differ diff --git a/src/cli_plugin/install/__fixtures__/replies/test_plugin_different_version.zip b/src/cli_plugin/install/__fixtures__/replies/test_plugin_different_version.zip index 12baa165fdb566d..b84473cebf954a4 100644 Binary files a/src/cli_plugin/install/__fixtures__/replies/test_plugin_different_version.zip and b/src/cli_plugin/install/__fixtures__/replies/test_plugin_different_version.zip differ diff --git a/src/cli_plugin/install/__fixtures__/replies/test_plugin_many.zip b/src/cli_plugin/install/__fixtures__/replies/test_plugin_many.zip index e4fc8d73feef86f..bc58c2bdb9dd7e1 100644 Binary files a/src/cli_plugin/install/__fixtures__/replies/test_plugin_many.zip and b/src/cli_plugin/install/__fixtures__/replies/test_plugin_many.zip differ diff --git a/src/cli_plugin/install/cleanup.js b/src/cli_plugin/install/cleanup.js index f31e028226c2779..38354bac4a3dfc2 100644 --- a/src/cli_plugin/install/cleanup.js +++ b/src/cli_plugin/install/cleanup.js @@ -45,6 +45,5 @@ export function cleanArtifacts(settings) { // At this point we're bailing, so swallow any errors on delete. try { del.sync(settings.workingPath); - del.sync(settings.plugins[0].path); } catch (e) {} // eslint-disable-line no-empty } diff --git a/src/cli_plugin/install/cleanup.test.js b/src/cli_plugin/install/cleanup.test.js index 46089f61d5e83e8..1a4cabbc82b5dd1 100644 --- a/src/cli_plugin/install/cleanup.test.js +++ b/src/cli_plugin/install/cleanup.test.js @@ -22,7 +22,7 @@ import fs from 'fs'; import del from 'del'; import { cleanPrevious, cleanArtifacts } from './cleanup'; -import Logger from '../lib/logger'; +import { Logger } from '../lib/logger'; describe('kibana cli', function () { describe('plugin installer', function () { diff --git a/src/cli_plugin/install/download.js b/src/cli_plugin/install/download.js index 10d20367c1b7ba6..b7f5fbec46edc38 100644 --- a/src/cli_plugin/install/download.js +++ b/src/cli_plugin/install/download.js @@ -17,11 +17,12 @@ * under the License. */ -import downloadHttpFile from './downloaders/http'; -import downloadLocalFile from './downloaders/file'; -import { UnsupportedProtocolError } from '../lib/errors'; import { parse } from 'url'; +import { UnsupportedProtocolError } from '../lib/errors'; +import { downloadHttpFile } from './downloaders/http'; +import { downloadLocalFile } from './downloaders/file'; + function _isWindows() { return /^win/.test(process.platform); } diff --git a/src/cli_plugin/install/download.test.js b/src/cli_plugin/install/download.test.js index 93e5e414fed7406..ae926b77f7d589c 100644 --- a/src/cli_plugin/install/download.test.js +++ b/src/cli_plugin/install/download.test.js @@ -17,16 +17,18 @@ * under the License. */ +import Fs from 'fs'; +import { join } from 'path'; +import http from 'http'; + import sinon from 'sinon'; import nock from 'nock'; import glob from 'glob-all'; import del from 'del'; -import Fs from 'fs'; -import Logger from '../lib/logger'; + +import { Logger } from '../lib/logger'; import { UnsupportedProtocolError } from '../lib/errors'; import { download, _downloadSingle, _getFilePath, _checkFilePathDeprecation } from './download'; -import { join } from 'path'; -import http from 'http'; describe('kibana cli', function () { describe('plugin downloader', function () { diff --git a/src/cli_plugin/install/downloaders/file.js b/src/cli_plugin/install/downloaders/file.js index 56f83b03d5a9074..c262f1010bbc827 100644 --- a/src/cli_plugin/install/downloaders/file.js +++ b/src/cli_plugin/install/downloaders/file.js @@ -17,9 +17,10 @@ * under the License. */ -import Progress from '../progress'; import { createWriteStream, createReadStream, statSync } from 'fs'; +import { Progress } from '../progress'; + function openSourceFile({ sourcePath }) { try { const fileInfo = statSync(sourcePath); @@ -58,7 +59,7 @@ async function copyFile({ readStream, writeStream, progress }) { /* // Responsible for managing local file transfers */ -export default async function copyLocalFile(logger, sourcePath, targetPath) { +export async function downloadLocalFile(logger, sourcePath, targetPath) { try { const { readStream, fileInfo } = openSourceFile({ sourcePath }); const writeStream = createWriteStream(targetPath); diff --git a/src/cli_plugin/install/downloaders/http.js b/src/cli_plugin/install/downloaders/http.js index 0fc01453f2b4c49..e9eafe3737ccb43 100644 --- a/src/cli_plugin/install/downloaders/http.js +++ b/src/cli_plugin/install/downloaders/http.js @@ -17,13 +17,15 @@ * under the License. */ -import Wreck from '@hapi/wreck'; -import Progress from '../progress'; import { createWriteStream } from 'fs'; + +import Wreck from '@hapi/wreck'; import HttpProxyAgent from 'http-proxy-agent'; import HttpsProxyAgent from 'https-proxy-agent'; import { getProxyForUrl } from 'proxy-from-env'; +import { Progress } from '../progress'; + function getProxyAgent(sourceUrl, logger) { const proxy = getProxyForUrl(sourceUrl); @@ -91,7 +93,7 @@ function downloadResponse({ resp, targetPath, progress }) { /* Responsible for managing http transfers */ -export default async function downloadUrl(logger, sourceUrl, targetPath, timeout) { +export async function downloadHttpFile(logger, sourceUrl, targetPath, timeout) { try { const { req, resp } = await sendRequest({ sourceUrl, timeout }, logger); diff --git a/src/cli_plugin/install/index.js b/src/cli_plugin/install/index.js index e3c465ea7a3f537..bc7e95b8489f0ca 100644 --- a/src/cli_plugin/install/index.js +++ b/src/cli_plugin/install/index.js @@ -17,13 +17,12 @@ * under the License. */ -import { fromRoot, pkg } from '../../core/server/utils'; -import install from './install'; -import Logger from '../lib/logger'; +import { pkg } from '../../core/server/utils'; +import { install } from './install'; +import { Logger } from '../lib/logger'; import { getConfigPath } from '../../core/server/path'; import { parse, parseMilliseconds } from './settings'; -import logWarnings from '../lib/log_warnings'; -import { warnIfUsingPluginDirOption } from '../lib/warn_if_plugin_dir_option'; +import { logWarnings } from '../lib/log_warnings'; function processCommand(command, options) { let settings; @@ -37,12 +36,11 @@ function processCommand(command, options) { const logger = new Logger(settings); - warnIfUsingPluginDirOption(settings, fromRoot('plugins'), logger); logWarnings(settings, logger); install(settings, logger); } -export default function pluginInstall(program) { +export function installCommand(program) { program .command('install ') .option('-q, --quiet', 'disable all process messaging except errors') @@ -53,15 +51,9 @@ export default function pluginInstall(program) { 'length of time before failing; 0 for never fail', parseMilliseconds ) - .option( - '-d, --plugin-dir ', - 'path to the directory where plugins are stored (DEPRECATED, known to not work for all plugins)', - fromRoot('plugins') - ) .description( 'install a plugin', `Common examples: - install x-pack install file:///Path/to/my/x-pack.zip install https://path.to/my/x-pack.zip` ) diff --git a/src/cli_plugin/install/index.test.js b/src/cli_plugin/install/index.test.js index 39352f52f20fdea..657ca0904041a7d 100644 --- a/src/cli_plugin/install/index.test.js +++ b/src/cli_plugin/install/index.test.js @@ -18,7 +18,8 @@ */ import sinon from 'sinon'; -import index from './index'; + +import { installCommand } from './index'; describe('kibana cli', function () { describe('plugin installer', function () { @@ -41,7 +42,7 @@ describe('kibana cli', function () { it('should define the command', function () { sinon.spy(program, 'command'); - index(program); + installCommand(program); expect(program.command.calledWith('install ')).toBe(true); program.command.restore(); @@ -50,7 +51,7 @@ describe('kibana cli', function () { it('should define the description', function () { sinon.spy(program, 'description'); - index(program); + installCommand(program); expect(program.description.calledWith('install a plugin')).toBe(true); program.description.restore(); @@ -59,9 +60,9 @@ describe('kibana cli', function () { it('should define the command line options', function () { const spy = sinon.spy(program, 'option'); - const options = [/-q/, /-s/, /-c/, /-t/, /-d/]; + const options = [/-q/, /-s/, /-c/, /-t/]; - index(program); + installCommand(program); for (let i = 0; i < spy.callCount; i++) { const call = spy.getCall(i); @@ -80,7 +81,7 @@ describe('kibana cli', function () { it('should call the action function', function () { sinon.spy(program, 'action'); - index(program); + installCommand(program); expect(program.action.calledOnce).toBe(true); program.action.restore(); diff --git a/src/cli_plugin/install/install.js b/src/cli_plugin/install/install.js index 92be2ac2503202e..b74b5894591120d 100644 --- a/src/cli_plugin/install/install.js +++ b/src/cli_plugin/install/install.js @@ -19,20 +19,20 @@ import Fs from 'fs'; import { promisify } from 'util'; +import path from 'path'; + +import del from 'del'; import { download } from './download'; -import path from 'path'; import { cleanPrevious, cleanArtifacts } from './cleanup'; import { extract, getPackData } from './pack'; import { renamePlugin } from './rename'; -import del from 'del'; import { errorIfXPackInstall } from '../lib/error_if_x_pack'; import { existingInstall, assertVersion } from './kibana'; -import { prepareExternalProjectDependencies } from '@kbn/pm'; const mkdir = promisify(Fs.mkdir); -export default async function install(settings, logger) { +export async function install(settings, logger) { try { errorIfXPackInstall(settings, logger); @@ -52,12 +52,8 @@ export default async function install(settings, logger) { assertVersion(settings); - await prepareExternalProjectDependencies(settings.workingPath); - - await renamePlugin( - settings.workingPath, - path.join(settings.pluginDir, settings.plugins[0].name) - ); + const targetDir = path.join(settings.pluginDir, settings.plugins[0].id); + await renamePlugin(settings.workingPath, targetDir); logger.log('Plugin installation complete'); } catch (err) { diff --git a/src/cli_plugin/install/kibana.js b/src/cli_plugin/install/kibana.js index edbcef3e7fed0eb..f093c1eee3db066 100644 --- a/src/cli_plugin/install/kibana.js +++ b/src/cli_plugin/install/kibana.js @@ -18,15 +18,16 @@ */ import path from 'path'; -import { versionSatisfies, cleanVersion } from '../../legacy/utils/version'; import { statSync } from 'fs'; +import { versionSatisfies, cleanVersion } from '../../legacy/utils/version'; + export function existingInstall(settings, logger) { try { - statSync(path.join(settings.pluginDir, settings.plugins[0].name)); + statSync(path.join(settings.pluginDir, settings.plugins[0].id)); logger.error( - `Plugin ${settings.plugins[0].name} already exists, please remove before installing a new version` + `Plugin ${settings.plugins[0].id} already exists, please remove before installing a new version` ); process.exit(70); } catch (e) { @@ -37,7 +38,7 @@ export function existingInstall(settings, logger) { export function assertVersion(settings) { if (!settings.plugins[0].kibanaVersion) { throw new Error( - `Plugin package.json is missing both a version property (required) and a kibana.version property (optional).` + `Plugin kibana.json is missing both a version property (required) and a kibanaVersion property (optional).` ); } @@ -45,7 +46,7 @@ export function assertVersion(settings) { const expected = cleanVersion(settings.version); if (!versionSatisfies(actual, expected)) { throw new Error( - `Plugin ${settings.plugins[0].name} [${actual}] is incompatible with Kibana [${expected}]` + `Plugin ${settings.plugins[0].id} [${actual}] is incompatible with Kibana [${expected}]` ); } } diff --git a/src/cli_plugin/install/kibana.test.js b/src/cli_plugin/install/kibana.test.js index 8c5dd00d099531e..ef3400be730695a 100644 --- a/src/cli_plugin/install/kibana.test.js +++ b/src/cli_plugin/install/kibana.test.js @@ -17,12 +17,14 @@ * under the License. */ -import sinon from 'sinon'; -import Logger from '../lib/logger'; import { join } from 'path'; -import del from 'del'; import fs from 'fs'; + +import sinon from 'sinon'; +import del from 'del'; + import { existingInstall, assertVersion } from './kibana'; +import { Logger } from '../lib/logger'; jest.spyOn(fs, 'statSync'); @@ -42,7 +44,7 @@ describe('kibana cli', function () { tempArchiveFile: tempArchiveFilePath, plugin: 'test-plugin', version: '1.0.0', - plugins: [{ name: 'foo' }], + plugins: [{ id: 'foo' }], pluginDir, }; @@ -69,7 +71,10 @@ describe('kibana cli', function () { plugin: 'test-plugin', version: '5.0.0-SNAPSHOT', plugins: [ - { name: 'foo', path: join(testWorkingPath, 'foo'), kibanaVersion: '5.0.0-SNAPSHOT' }, + { + id: 'foo', + kibanaVersion: '5.0.0-SNAPSHOT', + }, ], }; @@ -77,15 +82,17 @@ describe('kibana cli', function () { }); it('should throw an error if plugin is missing a kibana version.', function () { - expect(() => assertVersion(settings)).toThrow( - /plugin package\.json is missing both a version property/i + expect(() => assertVersion(settings)).toThrowErrorMatchingInlineSnapshot( + `"Plugin kibana.json is missing both a version property (required) and a kibanaVersion property (optional)."` ); }); it('should throw an error if plugin kibanaVersion does not match kibana version', function () { settings.plugins[0].kibanaVersion = '1.2.3.4'; - expect(() => assertVersion(settings)).toThrow(/incompatible with Kibana/i); + expect(() => assertVersion(settings)).toThrowErrorMatchingInlineSnapshot( + `"Plugin foo [1.2.3] is incompatible with Kibana [1.0.0]"` + ); }); it('should not throw an error if plugin kibanaVersion matches kibana version', function () { @@ -103,7 +110,9 @@ describe('kibana cli', function () { it('should ignore version info after the dash in checks on invalid version', function () { settings.plugins[0].kibanaVersion = '2.0.0-foo-bar-version-1.2.3'; - expect(() => assertVersion(settings)).toThrow(/incompatible with Kibana/i); + expect(() => assertVersion(settings)).toThrowErrorMatchingInlineSnapshot( + `"Plugin foo [2.0.0] is incompatible with Kibana [1.0.0]"` + ); }); }); diff --git a/src/cli_plugin/install/pack.js b/src/cli_plugin/install/pack.js index 87c94fce2b6777b..56d7be78e44bc52 100644 --- a/src/cli_plugin/install/pack.js +++ b/src/cli_plugin/install/pack.js @@ -18,7 +18,11 @@ */ import { analyzeArchive, extractArchive } from './zip'; -import validate from 'validate-npm-package-name'; + +const CAMEL_CASE_REG_EXP = /^[a-z]{1}([a-zA-Z0-9]{1,})$/; +export function isCamelCase(candidate) { + return CAMEL_CASE_REG_EXP.test(candidate); +} /** * Checks the plugin name. Will throw an exception if it does not meet @@ -27,9 +31,10 @@ import validate from 'validate-npm-package-name'; * @param {object} plugin - a package object from listPackages() */ function assertValidPackageName(plugin) { - const validation = validate(plugin.name); - if (!validation.validForNewPackages) { - throw new Error(`Invalid plugin name [${plugin.name}] in package.json`); + if (!isCamelCase(plugin.id)) { + throw new Error( + `Invalid plugin name [${plugin.id}] in kibana.json, expected it to be valid camelCase` + ); } } @@ -60,17 +65,13 @@ export async function getPackData(settings, logger) { /** * Extracts files from a zip archive to a file path using a filter function - * - * @param {string} archive - file path to a zip archive - * @param {string} targetDir - directory path to where the files should - * extracted */ export async function extract(settings, logger) { try { const plugin = settings.plugins[0]; logger.log('Extracting plugin archive'); - await extractArchive(settings.tempArchiveFile, settings.workingPath, plugin.archivePath); + await extractArchive(settings.tempArchiveFile, settings.workingPath, plugin.stripPrefix); logger.log('Extraction complete'); } catch (err) { logger.error(err.stack); diff --git a/src/cli_plugin/install/pack.test.js b/src/cli_plugin/install/pack.test.js index 05a60107f80ff82..c31437e61bebff2 100644 --- a/src/cli_plugin/install/pack.test.js +++ b/src/cli_plugin/install/pack.test.js @@ -18,14 +18,15 @@ */ import Fs from 'fs'; +import { join } from 'path'; import sinon from 'sinon'; import glob from 'glob-all'; import del from 'del'; -import Logger from '../lib/logger'; + +import { Logger } from '../lib/logger'; import { extract, getPackData } from './pack'; import { _downloadSingle } from './download'; -import { join } from 'path'; describe('kibana cli', function () { describe('pack', function () { @@ -73,133 +74,104 @@ describe('kibana cli', function () { return _downloadSingle(settings, logger, sourceUrl); } - function shouldReject() { - throw new Error('expected the promise to reject'); - } - describe('extract', function () { - //Also only extracts the content from the kibana folder. - //Ignores the others. - it('successfully extract a valid zip', function () { - return copyReplyFile('test_plugin.zip') - .then(() => { - return getPackData(settings, logger); - }) - .then(() => { - return extract(settings, logger); - }) - .then(() => { - const files = glob.sync('**/*', { cwd: testWorkingPath }); - const expected = [ - 'archive.part', - 'README.md', - 'index.js', - 'package.json', - 'public', - 'public/app.js', - 'extra file only in zip.txt', - ]; - expect(files.sort()).toEqual(expected.sort()); - }); + // Also only extracts the content from the kibana folder. + // Ignores the others. + it('successfully extract a valid zip', async () => { + await copyReplyFile('test_plugin.zip'); + await getPackData(settings, logger); + await extract(settings, logger); + + expect(glob.sync('**/*', { cwd: testWorkingPath })).toMatchInlineSnapshot(` + Array [ + "archive.part", + "bin", + "bin/executable", + "bin/not-executable", + "kibana.json", + "node_modules", + "node_modules/some-package", + "node_modules/some-package/index.js", + "node_modules/some-package/package.json", + "public", + "public/index.js", + ] + `); }); }); - describe('getPackData', function () { - it('populate settings.plugins', function () { - return copyReplyFile('test_plugin.zip') - .then(() => { - return getPackData(settings, logger); - }) - .then(() => { - expect(settings.plugins[0].name).toBe('test-plugin'); - expect(settings.plugins[0].archivePath).toBe('kibana/test-plugin'); - expect(settings.plugins[0].version).toBe('1.0.0'); - expect(settings.plugins[0].kibanaVersion).toBe('1.0.0'); - }); - }); - - it('populate settings.plugin.kibanaVersion', function () { - //kibana.version is defined in this package.json and is different than plugin version - return copyReplyFile('test_plugin_different_version.zip') - .then(() => { - return getPackData(settings, logger); - }) - .then(() => { - expect(settings.plugins[0].kibanaVersion).toBe('5.0.1'); - }); + describe('getPackData', () => { + it('populate settings.plugins', async () => { + await copyReplyFile('test_plugin.zip'); + await getPackData(settings, logger); + expect(settings.plugins).toMatchInlineSnapshot(` + Array [ + Object { + "id": "testPlugin", + "kibanaVersion": "1.0.0", + "stripPrefix": "kibana/test-plugin", + }, + ] + `); }); - it('populate settings.plugin.kibanaVersion (default to plugin version)', function () { - //kibana.version is not defined in this package.json, defaults to plugin version - return copyReplyFile('test_plugin.zip') - .then(() => { - return getPackData(settings, logger); - }) - .then(() => { - expect(settings.plugins[0].kibanaVersion).toBe('1.0.0'); - }); + it('populate settings.plugin.kibanaVersion', async () => { + await copyReplyFile('test_plugin_different_version.zip'); + await getPackData(settings, logger); + expect(settings.plugins).toMatchInlineSnapshot(` + Array [ + Object { + "id": "testPlugin", + "kibanaVersion": "5.0.1", + "stripPrefix": "kibana/test-plugin", + }, + ] + `); }); - it('populate settings.plugins with multiple plugins', function () { - return copyReplyFile('test_plugin_many.zip') - .then(() => { - return getPackData(settings, logger); - }) - .then(() => { - expect(settings.plugins[0].name).toBe('funger-plugin'); - expect(settings.plugins[0].archivePath).toBe('kibana/funger-plugin'); - expect(settings.plugins[0].version).toBe('1.0.0'); - - expect(settings.plugins[1].name).toBe('pdf'); - expect(settings.plugins[1].archivePath).toBe('kibana/pdf-linux'); - expect(settings.plugins[1].version).toBe('1.0.0'); - - expect(settings.plugins[2].name).toBe('pdf'); - expect(settings.plugins[2].archivePath).toBe('kibana/pdf-win32'); - expect(settings.plugins[2].version).toBe('1.0.0'); - - expect(settings.plugins[3].name).toBe('pdf'); - expect(settings.plugins[3].archivePath).toBe('kibana/pdf-win64'); - expect(settings.plugins[3].version).toBe('1.0.0'); - - expect(settings.plugins[4].name).toBe('pdf'); - expect(settings.plugins[4].archivePath).toBe('kibana/pdf'); - expect(settings.plugins[4].version).toBe('1.0.0'); - - expect(settings.plugins[5].name).toBe('test-plugin'); - expect(settings.plugins[5].archivePath).toBe('kibana/test-plugin'); - expect(settings.plugins[5].version).toBe('1.0.0'); - }); + it('populate settings.plugins with multiple plugins', async () => { + await copyReplyFile('test_plugin_many.zip'); + await getPackData(settings, logger); + expect(settings.plugins).toMatchInlineSnapshot(` + Array [ + Object { + "id": "fungerPlugin", + "kibanaVersion": "1.0.0", + "stripPrefix": "kibana/funger-plugin", + }, + Object { + "id": "pdf", + "kibanaVersion": "1.0.0", + "stripPrefix": "kibana/pdf", + }, + Object { + "id": "testPlugin", + "kibanaVersion": "1.0.0", + "stripPrefix": "kibana/test-plugin", + }, + ] + `); }); - it('throw an error if there is no kibana plugin', function () { - return copyReplyFile('test_plugin_no_kibana.zip') - .then(() => { - return getPackData(settings, logger); - }) - .then(shouldReject, (err) => { - expect(err.message).toMatch(/No kibana plugins found in archive/i); - }); + it('throw an error if there is no kibana plugin', async () => { + await copyReplyFile('test_plugin_no_kibana.zip'); + await expect(getPackData(settings, logger)).rejects.toThrowErrorMatchingInlineSnapshot( + `"No kibana plugins found in archive"` + ); }); - it('throw an error with a corrupt zip', function () { - return copyReplyFile('corrupt.zip') - .then(() => { - return getPackData(settings, logger); - }) - .then(shouldReject, (err) => { - expect(err.message).toMatch(/error retrieving/i); - }); + it('throw an error with a corrupt zip', async () => { + await copyReplyFile('corrupt.zip'); + await expect(getPackData(settings, logger)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Error retrieving metadata from plugin archive"` + ); }); - it('throw an error if there an invalid plugin name', function () { - return copyReplyFile('invalid_name.zip') - .then(() => { - return getPackData(settings, logger); - }) - .then(shouldReject, (err) => { - expect(err.message).toMatch(/invalid plugin name/i); - }); + it('throw an error if there an invalid plugin name', async () => { + await copyReplyFile('invalid_name.zip'); + await expect(getPackData(settings, logger)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Invalid plugin name [invalid name] in kibana.json, expected it to be valid camelCase"` + ); }); }); }); diff --git a/src/cli_plugin/install/progress.js b/src/cli_plugin/install/progress.js index e58e4472150b900..5c7d5074603dcf1 100644 --- a/src/cli_plugin/install/progress.js +++ b/src/cli_plugin/install/progress.js @@ -20,7 +20,7 @@ /** * Generates file transfer progress messages */ -export default class Progress { +export class Progress { constructor(logger) { const self = this; diff --git a/src/cli_plugin/install/progress.test.js b/src/cli_plugin/install/progress.test.js index 3b66e8b3dc86c9d..ef948bafca8367d 100644 --- a/src/cli_plugin/install/progress.test.js +++ b/src/cli_plugin/install/progress.test.js @@ -18,8 +18,9 @@ */ import sinon from 'sinon'; -import Progress from './progress'; -import Logger from '../lib/logger'; + +import { Progress } from './progress'; +import { Logger } from '../lib/logger'; describe('kibana cli', function () { describe('plugin installer', function () { diff --git a/src/cli_plugin/install/rename.js b/src/cli_plugin/install/rename.js index 1e5d94d47437532..897222a579a4afb 100644 --- a/src/cli_plugin/install/rename.js +++ b/src/cli_plugin/install/rename.js @@ -18,6 +18,7 @@ */ import { rename } from 'fs'; + import { delay } from 'lodash'; export function renamePlugin(workingPath, finalPath) { @@ -31,8 +32,12 @@ export function renamePlugin(workingPath, finalPath) { // Retry for up to retryTime seconds const windowsEPERM = process.platform === 'win32' && err.code === 'EPERM'; const retryAvailable = Date.now() - start < retryTime; - if (windowsEPERM && retryAvailable) - return delay(rename, retryDelay, workingPath, finalPath, retry); + + if (windowsEPERM && retryAvailable) { + delay(rename, retryDelay, workingPath, finalPath, retry); + return; + } + reject(err); } resolve(); diff --git a/src/cli_plugin/install/rename.test.js b/src/cli_plugin/install/rename.test.js index 40df75adc5efaf4..8525c367540f8f9 100644 --- a/src/cli_plugin/install/rename.test.js +++ b/src/cli_plugin/install/rename.test.js @@ -17,9 +17,10 @@ * under the License. */ -import sinon from 'sinon'; import fs from 'fs'; +import sinon from 'sinon'; + import { renamePlugin } from './rename'; describe('plugin folder rename', function () { diff --git a/src/cli_plugin/install/settings.js b/src/cli_plugin/install/settings.js index 40c845fc37a9e60..20a11479321eec4 100644 --- a/src/cli_plugin/install/settings.js +++ b/src/cli_plugin/install/settings.js @@ -17,9 +17,12 @@ * under the License. */ -import expiry from 'expiry-js'; import { resolve } from 'path'; +import expiry from 'expiry-js'; + +import { fromRoot } from '../../core/server/utils'; + function generateUrls({ version, plugin }) { return [ plugin, @@ -46,20 +49,14 @@ export function parse(command, options, kbnPackage) { quiet: options.quiet || false, silent: options.silent || false, config: options.config || '', - optimize: options.optimize, plugin: command, version: kbnPackage.version, - pluginDir: options.pluginDir || '', + pluginDir: fromRoot('plugins'), }; settings.urls = generateUrls(settings); settings.workingPath = resolve(settings.pluginDir, '.plugin.installing'); settings.tempArchiveFile = resolve(settings.workingPath, 'archive.part'); - settings.tempPackageFile = resolve(settings.workingPath, 'package.json'); - settings.setPlugin = function (plugin) { - settings.plugin = plugin; - settings.pluginPath = resolve(settings.pluginDir, settings.plugin.name); - }; return settings; } diff --git a/src/cli_plugin/install/settings.test.js b/src/cli_plugin/install/settings.test.js index 39ca07405ade25b..54ad453de9ef8cc 100644 --- a/src/cli_plugin/install/settings.test.js +++ b/src/cli_plugin/install/settings.test.js @@ -17,199 +17,82 @@ * under the License. */ +import { createAbsolutePathSerializer } from '@kbn/dev-utils'; + import { fromRoot } from '../../core/server/utils'; -import { resolve } from 'path'; import { parseMilliseconds, parse } from './settings'; -describe('kibana cli', function () { - describe('plugin installer', function () { - describe('command line option parsing', function () { - describe('parseMilliseconds function', function () { - it('should return 0 for an empty string', function () { - const value = ''; - const result = parseMilliseconds(value); - - expect(result).toBe(0); - }); - - it('should return 0 for a number with an invalid unit of measure', function () { - const result = parseMilliseconds('1gigablasts'); - expect(result).toBe(0); - }); - - it('should assume a number with no unit of measure is specified as milliseconds', function () { - const result = parseMilliseconds(1); - expect(result).toBe(1); - - const result2 = parseMilliseconds('1'); - expect(result2).toBe(1); - }); - - it('should interpret a number with "s" as the unit of measure as seconds', function () { - const result = parseMilliseconds('5s'); - expect(result).toBe(5 * 1000); - }); - - it('should interpret a number with "second" as the unit of measure as seconds', function () { - const result = parseMilliseconds('5second'); - expect(result).toBe(5 * 1000); - }); - - it('should interpret a number with "seconds" as the unit of measure as seconds', function () { - const result = parseMilliseconds('5seconds'); - expect(result).toBe(5 * 1000); - }); - - it('should interpret a number with "m" as the unit of measure as minutes', function () { - const result = parseMilliseconds('9m'); - expect(result).toBe(9 * 1000 * 60); - }); - - it('should interpret a number with "minute" as the unit of measure as minutes', function () { - const result = parseMilliseconds('9minute'); - expect(result).toBe(9 * 1000 * 60); - }); - - it('should interpret a number with "minutes" as the unit of measure as minutes', function () { - const result = parseMilliseconds('9minutes'); - expect(result).toBe(9 * 1000 * 60); - }); - }); - - describe('parse function', function () { - const command = 'plugin name'; - let options = {}; - const kbnPackage = { version: 1234 }; - beforeEach(function () { - options = { pluginDir: fromRoot('plugins') }; - }); - - describe('timeout option', function () { - it('should default to 0 (milliseconds)', function () { - const settings = parse(command, options, kbnPackage); - - expect(settings.timeout).toBe(0); - }); - - it('should set settings.timeout property', function () { - options.timeout = 1234; - const settings = parse(command, options, kbnPackage); - - expect(settings.timeout).toBe(1234); - }); - }); - - describe('quiet option', function () { - it('should default to false', function () { - const settings = parse(command, options, kbnPackage); - - expect(settings.quiet).toBe(false); - }); - - it('should set settings.quiet property to true', function () { - options.quiet = true; - const settings = parse(command, options, kbnPackage); - - expect(settings.quiet).toBe(true); - }); - }); - - describe('silent option', function () { - it('should default to false', function () { - const settings = parse(command, options, kbnPackage); - - expect(settings.silent).toBe(false); - }); - - it('should set settings.silent property to true', function () { - options.silent = true; - const settings = parse(command, options, kbnPackage); - - expect(settings.silent).toBe(true); - }); - }); - - describe('config option', function () { - it('should default to ZLS', function () { - const settings = parse(command, options, kbnPackage); - - expect(settings.config).toBe(''); - }); - - it('should set settings.config property', function () { - options.config = 'foo bar baz'; - const settings = parse(command, options, kbnPackage); - - expect(settings.config).toBe('foo bar baz'); - }); - }); - - describe('pluginDir option', function () { - it('should default to plugins', function () { - const settings = parse(command, options, kbnPackage); - - expect(settings.pluginDir).toBe(fromRoot('plugins')); - }); - - it('should set settings.config property', function () { - options.pluginDir = 'foo bar baz'; - const settings = parse(command, options, kbnPackage); - - expect(settings.pluginDir).toBe('foo bar baz'); - }); - }); - - describe('command value', function () { - it('should set settings.plugin property', function () { - const settings = parse(command, options, kbnPackage); - - expect(settings.plugin).toBe(command); - }); - }); - - describe('urls collection', function () { - it('should populate the settings.urls property', function () { - const settings = parse(command, options, kbnPackage); - - const expected = [ - command, - `https://artifacts.elastic.co/downloads/kibana-plugins/${command}/${command}-1234.zip`, - ]; - - expect(settings.urls).toEqual(expected); - }); - }); - - describe('workingPath value', function () { - it('should set settings.workingPath property', function () { - options.pluginDir = 'foo/bar/baz'; - const settings = parse(command, options, kbnPackage); - const expected = resolve('foo/bar/baz', '.plugin.installing'); - - expect(settings.workingPath).toBe(expected); - }); - }); - - describe('tempArchiveFile value', function () { - it('should set settings.tempArchiveFile property', function () { - options.pluginDir = 'foo/bar/baz'; - const settings = parse(command, options, kbnPackage); - const expected = resolve('foo/bar/baz', '.plugin.installing', 'archive.part'); - - expect(settings.tempArchiveFile).toBe(expected); - }); - }); +const SECOND = 1000; +const MINUTE = SECOND * 60; + +expect.addSnapshotSerializer(createAbsolutePathSerializer()); + +describe('parseMilliseconds function', function () { + it.each([ + ['', 0], + ['1gigablasts', 0], + [1, 1], + ['1', 1], + ['5s', 5 * SECOND], + ['5second', 5 * SECOND], + ['5seconds', 5 * SECOND], + ['9m', 9 * MINUTE], + ['9minute', 9 * MINUTE], + ['9minutes', 9 * MINUTE], + ])('should parse %j to %j', (input, result) => { + expect(parseMilliseconds(input)).toBe(result); + }); +}); - describe('tempPackageFile value', function () { - it('should set settings.tempPackageFile property', function () { - options.pluginDir = 'foo/bar/baz'; - const settings = parse(command, options, kbnPackage); - const expected = resolve('foo/bar/baz', '.plugin.installing', 'package.json'); +describe('parse function', function () { + const command = 'plugin name'; + const defaultOptions = { pluginDir: fromRoot('plugins') }; + const kbnPackage = { version: 1234 }; + + it('produces expected defaults', function () { + expect(parse(command, { ...defaultOptions }, kbnPackage)).toMatchInlineSnapshot(` + Object { + "config": "", + "plugin": "plugin name", + "pluginDir": /plugins, + "quiet": false, + "silent": false, + "tempArchiveFile": /plugins/.plugin.installing/archive.part, + "timeout": 0, + "urls": Array [ + "plugin name", + "https://artifacts.elastic.co/downloads/kibana-plugins/plugin name/plugin name-1234.zip", + ], + "version": 1234, + "workingPath": /plugins/.plugin.installing, + } + `); + }); - expect(settings.tempPackageFile).toBe(expected); - }); - }); - }); - }); + it('consumes overrides', function () { + const options = { + quiet: true, + silent: true, + config: 'foo bar baz', + ...defaultOptions, + }; + + expect(parse(command, options, kbnPackage)).toMatchInlineSnapshot(` + Object { + "config": "foo bar baz", + "plugin": "plugin name", + "pluginDir": /plugins, + "quiet": true, + "silent": true, + "tempArchiveFile": /plugins/.plugin.installing/archive.part, + "timeout": 0, + "urls": Array [ + "plugin name", + "https://artifacts.elastic.co/downloads/kibana-plugins/plugin name/plugin name-1234.zip", + ], + "version": 1234, + "workingPath": /plugins/.plugin.installing, + } + `); }); }); diff --git a/src/cli_plugin/install/zip.js b/src/cli_plugin/install/zip.js index 52eba2ea239a2c5..b906dd59a302b1e 100644 --- a/src/cli_plugin/install/zip.js +++ b/src/cli_plugin/install/zip.js @@ -17,21 +17,24 @@ * under the License. */ -import yauzl from 'yauzl'; import path from 'path'; import { createWriteStream, mkdir } from 'fs'; -import { get } from 'lodash'; + +import yauzl from 'yauzl'; + +const isDirectoryRegex = /(\/|\\)$/; +function isDirectory(filename) { + return isDirectoryRegex.test(filename); +} /** * Returns an array of package objects. There will be one for each of - * package.json files in the archive - * - * @param {string} archive - path to plugin archive zip file + * package.json files in the archive */ export function analyzeArchive(archive) { const plugins = []; - const regExp = new RegExp('(kibana[\\\\/][^\\\\/]+)[\\\\/]package.json', 'i'); + const regExp = new RegExp('(kibana[\\\\/][^\\\\/]+)[\\\\/]kibana.json', 'i'); return new Promise((resolve, reject) => { yauzl.open(archive, { lazyEntries: true }, function (err, zipfile) { @@ -47,31 +50,32 @@ export function analyzeArchive(archive) { return zipfile.readEntry(); } - zipfile.openReadStream(entry, function (err, readable) { + zipfile.openReadStream(entry, function (error, readable) { const chunks = []; - if (err) { - return reject(err); + if (error) { + return reject(error); } readable.on('data', (chunk) => chunks.push(chunk)); readable.on('end', function () { - const contents = Buffer.concat(chunks).toString(); - const pkg = JSON.parse(contents); - - plugins.push( - Object.assign(pkg, { - archivePath: match[1], - archive: archive, - - // Plugins must specify their version, and by default that version should match - // the version of kibana down to the patch level. If these two versions need - // to diverge, they can specify a kibana.version to indicate the version of - // kibana the plugin is intended to work with. - kibanaVersion: get(pkg, 'kibana.version', pkg.version), - }) - ); + const manifestJson = Buffer.concat(chunks).toString(); + const manifest = JSON.parse(manifestJson); + + plugins.push({ + id: manifest.id, + stripPrefix: match[1], + + // Plugins must specify their version, and by default that version in the plugin + // manifest should match the version of kibana down to the patch level. If these + // two versions need plugins can specify a kibanaVersion to indicate the version + // of kibana the plugin is intended to work with. + kibanaVersion: + typeof manifest.kibanaVersion === 'string' && manifest.kibanaVersion + ? manifest.kibanaVersion + : manifest.version, + }); zipfile.readEntry(); }); @@ -85,12 +89,7 @@ export function analyzeArchive(archive) { }); } -const isDirectoryRegex = /(\/|\\)$/; -export function _isDirectory(filename) { - return isDirectoryRegex.test(filename); -} - -export function extractArchive(archive, targetDir, extractPath) { +export function extractArchive(archive, targetDir, stripPrefix) { return new Promise((resolve, reject) => { yauzl.open(archive, { lazyEntries: true }, function (err, zipfile) { if (err) { @@ -102,8 +101,8 @@ export function extractArchive(archive, targetDir, extractPath) { zipfile.on('entry', function (entry) { let fileName = entry.fileName; - if (extractPath && fileName.startsWith(extractPath)) { - fileName = fileName.substring(extractPath.length); + if (stripPrefix && fileName.startsWith(stripPrefix)) { + fileName = fileName.substring(stripPrefix.length); } else { return zipfile.readEntry(); } @@ -112,30 +111,34 @@ export function extractArchive(archive, targetDir, extractPath) { fileName = path.join(targetDir, fileName); } - if (_isDirectory(fileName)) { - mkdir(fileName, { recursive: true }, function (err) { - if (err) { - return reject(err); + if (isDirectory(fileName)) { + mkdir(fileName, { recursive: true }, function (error) { + if (error) { + return reject(error); } zipfile.readEntry(); }); } else { // file entry - zipfile.openReadStream(entry, function (err, readStream) { - if (err) { - return reject(err); + zipfile.openReadStream(entry, function (error, readStream) { + if (error) { + return reject(error); } // ensure parent directory exists - mkdir(path.dirname(fileName), { recursive: true }, function (err) { - if (err) { - return reject(err); + mkdir(path.dirname(fileName), { recursive: true }, function (error2) { + if (error2) { + return reject(error2); } readStream.pipe( - createWriteStream(fileName, { mode: entry.externalFileAttributes >>> 16 }) + createWriteStream(fileName, { + // eslint-disable-next-line no-bitwise + mode: entry.externalFileAttributes >>> 16, + }) ); + readStream.on('end', function () { zipfile.readEntry(); }); diff --git a/src/cli_plugin/install/zip.test.js b/src/cli_plugin/install/zip.test.js index 28367e9e244531f..0f56c0d0322aaa8 100644 --- a/src/cli_plugin/install/zip.test.js +++ b/src/cli_plugin/install/zip.test.js @@ -17,12 +17,16 @@ * under the License. */ -import del from 'del'; import path from 'path'; import os from 'os'; -import glob from 'glob'; import fs from 'fs'; -import { analyzeArchive, extractArchive, _isDirectory } from './zip'; + +import del from 'del'; +import glob from 'glob'; + +import { analyzeArchive, extractArchive } from './zip'; + +const getMode = (path) => (fs.statSync(path).mode & parseInt('777', 8)).toString(8); describe('kibana cli', function () { describe('zip', function () { @@ -43,32 +47,37 @@ describe('kibana cli', function () { describe('analyzeArchive', function () { it('returns array of plugins', async () => { const packages = await analyzeArchive(archivePath); - const plugin = packages[0]; - - expect(packages).toBeInstanceOf(Array); - expect(plugin.name).toBe('test-plugin'); - expect(plugin.archivePath).toBe('kibana/test-plugin'); - expect(plugin.archive).toBe(archivePath); - expect(plugin.kibanaVersion).toBe('1.0.0'); + expect(packages).toMatchInlineSnapshot(` + Array [ + Object { + "id": "testPlugin", + "kibanaVersion": "1.0.0", + "stripPrefix": "kibana/test-plugin", + }, + ] + `); }); }); describe('extractArchive', () => { it('extracts files using the extractPath filter', async () => { - const archive = path.resolve(repliesPath, 'test_plugin_many.zip'); - + const archive = path.resolve(repliesPath, 'test_plugin.zip'); await extractArchive(archive, tempPath, 'kibana/test-plugin'); - const files = await glob.sync('**/*', { cwd: tempPath }); - - const expected = [ - 'extra file only in zip.txt', - 'index.js', - 'package.json', - 'public', - 'public/app.js', - 'README.md', - ]; - expect(files.sort()).toEqual(expected.sort()); + + expect(glob.sync('**/*', { cwd: tempPath })).toMatchInlineSnapshot(` + Array [ + "bin", + "bin/executable", + "bin/not-executable", + "kibana.json", + "node_modules", + "node_modules/some-package", + "node_modules/some-package/index.js", + "node_modules/some-package/package.json", + "public", + "public/index.js", + ] + `); }); }); @@ -76,49 +85,26 @@ describe('kibana cli', function () { it('verify consistency of modes of files', async () => { const archivePath = path.resolve(repliesPath, 'test_plugin.zip'); - await extractArchive(archivePath, tempPath, 'kibana/libs'); - const files = await glob.sync('**/*', { cwd: tempPath }); - - const expected = ['executable', 'unexecutable']; - expect(files.sort()).toEqual(expected.sort()); + await extractArchive(archivePath, tempPath, 'kibana/test-plugin/bin'); - const executableMode = - '0' + - (fs.statSync(path.resolve(tempPath, 'executable')).mode & parseInt('777', 8)).toString(8); - const unExecutableMode = - '0' + - (fs.statSync(path.resolve(tempPath, 'unexecutable')).mode & parseInt('777', 8)).toString( - 8 - ); + expect(glob.sync('**/*', { cwd: tempPath })).toMatchInlineSnapshot(` + Array [ + "executable", + "not-executable", + ] + `); - expect(executableMode).toEqual('0755'); - expect(unExecutableMode).toEqual('0644'); + expect(getMode(path.resolve(tempPath, 'executable'))).toEqual('755'); + expect(getMode(path.resolve(tempPath, 'not-executable'))).toEqual('644'); }); }); it('handles a corrupt zip archive', async () => { - try { - await extractArchive(path.resolve(repliesPath, 'corrupt.zip')); - throw new Error('This should have failed'); - } catch (e) { - return; - } - }); - }); - - describe('_isDirectory', () => { - it('should check for a forward slash', () => { - expect(_isDirectory('/foo/bar/')).toBe(true); - }); - - it('should check for a backslash', () => { - expect(_isDirectory('\\foo\\bar\\')).toBe(true); - }); - - it('should return false for files', () => { - expect(_isDirectory('foo.txt')).toBe(false); - expect(_isDirectory('\\path\\to\\foo.txt')).toBe(false); - expect(_isDirectory('/path/to/foo.txt')).toBe(false); + await expect( + extractArchive(path.resolve(repliesPath, 'corrupt.zip')) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"end of central directory record signature not found"` + ); }); }); }); diff --git a/src/cli_plugin/lib/error_if_x_pack.js b/src/cli_plugin/lib/error_if_x_pack.js index d6624f5308ec02e..4ea7ceb37478ec7 100644 --- a/src/cli_plugin/lib/error_if_x_pack.js +++ b/src/cli_plugin/lib/error_if_x_pack.js @@ -17,7 +17,7 @@ * under the License. */ -import { isOSS } from './is_oss'; +import { isOss } from './is_oss'; function isXPack(plugin) { return /x-pack/.test(plugin); @@ -25,7 +25,7 @@ function isXPack(plugin) { export function errorIfXPackInstall(settings) { if (isXPack(settings.plugin)) { - if (isOSS()) { + if (isOss()) { throw new Error( 'You are using the OSS-only distribution of Kibana. ' + 'As of version 6.3+ X-Pack is bundled in the standard distribution of this software by default; ' + @@ -40,7 +40,7 @@ export function errorIfXPackInstall(settings) { } export function errorIfXPackRemove(settings) { - if (isXPack(settings.plugin) && !isOSS()) { + if (isXPack(settings.plugin) && !isOss()) { throw new Error( 'You are using the standard distribution of Kibana. Please install the OSS-only distribution to remove X-Pack features.' ); diff --git a/src/cli_plugin/lib/is_oss.js b/src/cli_plugin/lib/is_oss.js index 3f2190d8346ec0c..53f19a41228d68a 100644 --- a/src/cli_plugin/lib/is_oss.js +++ b/src/cli_plugin/lib/is_oss.js @@ -20,6 +20,6 @@ import fs from 'fs'; import path from 'path'; -export function isOSS() { +export function isOss() { return !fs.existsSync(path.resolve(__dirname, '../../../x-pack')); } diff --git a/src/cli_plugin/lib/is_oss.test.js b/src/cli_plugin/lib/is_oss.test.js index a4673710c63ce91..636e1616e7d3ca2 100644 --- a/src/cli_plugin/lib/is_oss.test.js +++ b/src/cli_plugin/lib/is_oss.test.js @@ -17,12 +17,12 @@ * under the License. */ -import { isOSS } from './is_oss'; +import { isOss } from './is_oss'; describe('is_oss', () => { describe('x-pack installed', () => { it('should return false', () => { - expect(isOSS()).toEqual(false); + expect(isOss()).toEqual(false); }); }); }); diff --git a/src/cli_plugin/lib/log_warnings.js b/src/cli_plugin/lib/log_warnings.js index b4542acecb3050d..f31c3d4bd2e9acb 100644 --- a/src/cli_plugin/lib/log_warnings.js +++ b/src/cli_plugin/lib/log_warnings.js @@ -17,7 +17,7 @@ * under the License. */ -export default function (settings, logger) { +export function logWarnings(logger) { process.on('warning', (warning) => { // deprecation warnings do no reflect a current problem for // the user and therefor should be filtered out. diff --git a/src/cli_plugin/lib/logger.js b/src/cli_plugin/lib/logger.js index efd6130322c3864..592618fbecfc87c 100644 --- a/src/cli_plugin/lib/logger.js +++ b/src/cli_plugin/lib/logger.js @@ -20,7 +20,7 @@ /** * Logs messages and errors */ -export default class Logger { +export class Logger { constructor(settings = {}) { this.previousLineEnded = true; this.silent = !!settings.silent; diff --git a/src/cli_plugin/lib/logger.test.js b/src/cli_plugin/lib/logger.test.js index 00cad1a9bbb114e..7ff683ea50c9599 100644 --- a/src/cli_plugin/lib/logger.test.js +++ b/src/cli_plugin/lib/logger.test.js @@ -18,7 +18,8 @@ */ import sinon from 'sinon'; -import Logger from './logger'; + +import { Logger } from './logger'; describe('kibana cli', function () { describe('plugin installer', function () { diff --git a/src/cli_plugin/list/index.js b/src/cli_plugin/list/index.js index c0f708b8ccf8332..a3cf1a54a8a6247 100644 --- a/src/cli_plugin/list/index.js +++ b/src/cli_plugin/list/index.js @@ -18,37 +18,16 @@ */ import { fromRoot } from '../../core/server/utils'; -import list from './list'; -import Logger from '../lib/logger'; -import { parse } from './settings'; -import logWarnings from '../lib/log_warnings'; -import { warnIfUsingPluginDirOption } from '../lib/warn_if_plugin_dir_option'; +import { list } from './list'; +import { Logger } from '../lib/logger'; +import { logWarnings } from '../lib/log_warnings'; -function processCommand(command, options) { - let settings; - try { - settings = parse(command, options); - } catch (ex) { - //The logger has not yet been initialized. - console.error(ex.message); - process.exit(64); // eslint-disable-line no-process-exit - } - - const logger = new Logger(settings); - - warnIfUsingPluginDirOption(settings, fromRoot('plugins'), logger); - logWarnings(settings, logger); - list(settings, logger); +function processCommand() { + const logger = new Logger(); + logWarnings(logger); + list(fromRoot('plugins'), logger); } -export default function pluginList(program) { - program - .command('list') - .option( - '-d, --plugin-dir ', - 'path to the directory where plugins are stored (DEPRECATED, known to not work for all plugins)', - fromRoot('plugins') - ) - .description('list installed plugins') - .action(processCommand); +export function listCommand(program) { + program.command('list').description('list installed plugins').action(processCommand); } diff --git a/src/cli_plugin/list/list.js b/src/cli_plugin/list/list.js index b34631e5dfd0851..bf6a082a91878ff 100644 --- a/src/cli_plugin/list/list.js +++ b/src/cli_plugin/list/list.js @@ -20,19 +20,20 @@ import { statSync, readdirSync, readFileSync } from 'fs'; import { join } from 'path'; -export default function list(settings, logger) { - readdirSync(settings.pluginDir).forEach((filename) => { - const stat = statSync(join(settings.pluginDir, filename)); +export function list(pluginDir, logger) { + readdirSync(pluginDir).forEach((name) => { + const stat = statSync(join(pluginDir, name)); - if (stat.isDirectory() && filename[0] !== '.') { + if (stat.isDirectory() && name[0] !== '.') { try { - const packagePath = join(settings.pluginDir, filename, 'package.json'); - const { version } = JSON.parse(readFileSync(packagePath, 'utf8')); - logger.log(filename + '@' + version); + const packagePath = join(pluginDir, name, 'kibana.json'); + const pkg = JSON.parse(readFileSync(packagePath, 'utf8')); + logger.log(pkg.id + '@' + pkg.version); } catch (e) { - throw new Error('Unable to read package.json file for plugin ' + filename); + throw new Error('Unable to read kibana.json file for plugin ' + name); } } }); + logger.log(''); //intentional blank line for aesthetics } diff --git a/src/cli_plugin/list/list.test.js b/src/cli_plugin/list/list.test.js index 071a253fa87fe0c..b1b5d1cde6a35d2 100644 --- a/src/cli_plugin/list/list.test.js +++ b/src/cli_plugin/list/list.test.js @@ -17,78 +17,67 @@ * under the License. */ -import sinon from 'sinon'; -import del from 'del'; -import Logger from '../lib/logger'; -import list from './list'; import { join } from 'path'; -import { writeFileSync, appendFileSync, mkdirSync } from 'fs'; +import { writeFileSync, mkdirSync } from 'fs'; + +import del from 'del'; + +import { list } from './list'; function createPlugin(name, version, pluginBaseDir) { const pluginDir = join(pluginBaseDir, name); mkdirSync(pluginDir, { recursive: true }); - appendFileSync(join(pluginDir, 'package.json'), '{"version": "' + version + '"}'); + writeFileSync( + join(pluginDir, 'kibana.json'), + JSON.stringify({ + id: name, + version, + }) + ); } +const logger = { + messages: [], + log(msg) { + this.messages.push(`log: ${msg}`); + }, + error(msg) { + this.messages.push(`error: ${msg}`); + }, +}; + describe('kibana cli', function () { describe('plugin lister', function () { const pluginDir = join(__dirname, '.test.data.list'); - let logger; - - const settings = { - pluginDir: pluginDir, - }; beforeEach(function () { - logger = new Logger(settings); - sinon.stub(logger, 'log'); - sinon.stub(logger, 'error'); + logger.messages.length = 0; del.sync(pluginDir); mkdirSync(pluginDir, { recursive: true }); }); afterEach(function () { - logger.log.restore(); - logger.error.restore(); del.sync(pluginDir); }); - it('list all of the folders in the plugin folder', function () { - createPlugin('plugin1', '5.0.0-alpha2', pluginDir); - createPlugin('plugin2', '3.2.1', pluginDir); - createPlugin('plugin3', '1.2.3', pluginDir); - - list(settings, logger); - - expect(logger.log.calledWith('plugin1@5.0.0-alpha2')).toBe(true); - expect(logger.log.calledWith('plugin2@3.2.1')).toBe(true); - expect(logger.log.calledWith('plugin3@1.2.3')).toBe(true); - }); - - it('ignore folders that start with a period', function () { + it('list all of the folders in the plugin folder, ignoring dot prefixed plugins and regular files', function () { createPlugin('.foo', '1.0.0', pluginDir); createPlugin('plugin1', '5.0.0-alpha2', pluginDir); createPlugin('plugin2', '3.2.1', pluginDir); createPlugin('plugin3', '1.2.3', pluginDir); createPlugin('.bar', '1.0.0', pluginDir); - - list(settings, logger); - - expect(logger.log.calledWith('.foo@1.0.0')).toBe(false); - expect(logger.log.calledWith('.bar@1.0.0')).toBe(false); - }); - - it('list should only list folders', function () { - createPlugin('plugin1', '1.0.0', pluginDir); - createPlugin('plugin2', '1.0.0', pluginDir); - createPlugin('plugin3', '1.0.0', pluginDir); writeFileSync(join(pluginDir, 'plugin4'), 'This is a file, and not a folder.'); - list(settings, logger); + list(pluginDir, logger); - expect(logger.log.calledWith('plugin1@1.0.0')).toBe(true); - expect(logger.log.calledWith('plugin2@1.0.0')).toBe(true); - expect(logger.log.calledWith('plugin3@1.0.0')).toBe(true); + expect(logger.messages).toMatchInlineSnapshot(` + Array [ + "log: plugin1@5.0.0-alpha2", + "log: plugin2@3.2.1", + "log: plugin3@1.2.3", + "log: ", + ] + `); }); it('list should throw an exception if a plugin does not have a package.json', function () { @@ -96,19 +85,23 @@ describe('kibana cli', function () { mkdirSync(join(pluginDir, 'empty-plugin'), { recursive: true }); expect(function () { - list(settings, logger); - }).toThrowError('Unable to read package.json file for plugin empty-plugin'); + list(pluginDir, logger); + }).toThrowErrorMatchingInlineSnapshot( + `"Unable to read kibana.json file for plugin empty-plugin"` + ); }); it('list should throw an exception if a plugin have an empty package.json', function () { createPlugin('plugin1', '1.0.0', pluginDir); const invalidPluginDir = join(pluginDir, 'invalid-plugin'); mkdirSync(invalidPluginDir, { recursive: true }); - appendFileSync(join(invalidPluginDir, 'package.json'), ''); + writeFileSync(join(invalidPluginDir, 'package.json'), ''); expect(function () { - list(settings, logger); - }).toThrowError('Unable to read package.json file for plugin invalid-plugin'); + list(pluginDir, logger); + }).toThrowErrorMatchingInlineSnapshot( + `"Unable to read kibana.json file for plugin invalid-plugin"` + ); }); }); }); diff --git a/src/cli_plugin/list/settings.test.js b/src/cli_plugin/list/settings.test.js deleted file mode 100644 index 85e6cb88e82fd72..000000000000000 --- a/src/cli_plugin/list/settings.test.js +++ /dev/null @@ -1,50 +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 { fromRoot } from '../../core/server/utils'; -import { parse } from './settings'; - -describe('kibana cli', function () { - describe('plugin installer', function () { - describe('command line option parsing', function () { - describe('parse function', function () { - let command; - const options = {}; - beforeEach(function () { - command = { pluginDir: fromRoot('plugins') }; - }); - - describe('pluginDir option', function () { - it('should default to plugins', function () { - const settings = parse(command, options); - - expect(settings.pluginDir).toBe(fromRoot('plugins')); - }); - - it('should set settings.config property', function () { - command.pluginDir = 'foo bar baz'; - const settings = parse(command, options); - - expect(settings.pluginDir).toBe('foo bar baz'); - }); - }); - }); - }); - }); -}); diff --git a/src/cli_plugin/remove/index.js b/src/cli_plugin/remove/index.js index 9ff06e0e760bfc9..c3bd96086db9b97 100644 --- a/src/cli_plugin/remove/index.js +++ b/src/cli_plugin/remove/index.js @@ -17,46 +17,34 @@ * under the License. */ -import { fromRoot } from '../../core/server/utils'; -import remove from './remove'; -import Logger from '../lib/logger'; +import { remove } from './remove'; +import { Logger } from '../lib/logger'; import { parse } from './settings'; import { getConfigPath } from '../../core/server/path'; -import logWarnings from '../lib/log_warnings'; -import { warnIfUsingPluginDirOption } from '../lib/warn_if_plugin_dir_option'; +import { logWarnings } from '../lib/log_warnings'; function processCommand(command, options) { let settings; try { settings = parse(command, options); } catch (ex) { - //The logger has not yet been initialized. + // The logger has not yet been initialized. console.error(ex.message); process.exit(64); // eslint-disable-line no-process-exit } const logger = new Logger(settings); - warnIfUsingPluginDirOption(settings, fromRoot('plugins'), logger); logWarnings(settings, logger); remove(settings, logger); } -export default function pluginRemove(program) { +export function removeCommand(program) { program .command('remove ') .option('-q, --quiet', 'disable all process messaging except errors') .option('-s, --silent', 'disable all process messaging') .option('-c, --config ', 'path to the config file', getConfigPath()) - .option( - '-d, --plugin-dir ', - 'path to the directory where plugins are stored (DEPRECATED, known to not work for all plugins)', - fromRoot('plugins') - ) - .description( - 'remove a plugin', - `common examples: - remove x-pack` - ) + .description('remove a plugin') .action(processCommand); } diff --git a/src/cli_plugin/remove/remove.js b/src/cli_plugin/remove/remove.js index 353e592390ff40d..0c218301242be77 100644 --- a/src/cli_plugin/remove/remove.js +++ b/src/cli_plugin/remove/remove.js @@ -18,11 +18,12 @@ */ import { statSync } from 'fs'; -import { errorIfXPackRemove } from '../lib/error_if_x_pack'; import del from 'del'; -export default function remove(settings, logger) { +import { errorIfXPackRemove } from '../lib/error_if_x_pack'; + +export function remove(settings, logger) { try { let stat; try { diff --git a/src/cli_plugin/remove/remove.test.js b/src/cli_plugin/remove/remove.test.js index 4bf061820aa0501..44c66468bbb557f 100644 --- a/src/cli_plugin/remove/remove.test.js +++ b/src/cli_plugin/remove/remove.test.js @@ -17,13 +17,15 @@ * under the License. */ +import { join } from 'path'; +import { writeFileSync, existsSync, mkdirSync } from 'fs'; + import sinon from 'sinon'; import glob from 'glob-all'; import del from 'del'; -import Logger from '../lib/logger'; -import remove from './remove'; -import { join } from 'path'; -import { writeFileSync, existsSync, mkdirSync } from 'fs'; + +import { Logger } from '../lib/logger'; +import { remove } from './remove'; describe('kibana cli', function () { describe('plugin remover', function () { diff --git a/src/cli_plugin/remove/settings.js b/src/cli_plugin/remove/settings.js index dc8d3c87e6eab3a..8a7d41b35ae57eb 100644 --- a/src/cli_plugin/remove/settings.js +++ b/src/cli_plugin/remove/settings.js @@ -19,12 +19,14 @@ import { resolve } from 'path'; +import { fromRoot } from '../../core/server/utils'; + export function parse(command, options) { const settings = { quiet: options.quiet || false, silent: options.silent || false, config: options.config || '', - pluginDir: options.pluginDir || '', + pluginDir: fromRoot('plugins'), plugin: command, }; diff --git a/src/cli_plugin/remove/settings.test.js b/src/cli_plugin/remove/settings.test.js index b3110e1ff0499ac..9ae555d79cd1a8c 100644 --- a/src/cli_plugin/remove/settings.test.js +++ b/src/cli_plugin/remove/settings.test.js @@ -17,88 +17,42 @@ * under the License. */ -import { fromRoot } from '../../core/server/utils'; -import { parse } from './settings'; - -describe('kibana cli', function () { - describe('plugin installer', function () { - describe('command line option parsing', function () { - describe('parse function', function () { - const command = 'plugin name'; - let options = {}; - const kbnPackage = { version: 1234 }; - beforeEach(function () { - options = { pluginDir: fromRoot('plugins') }; - }); - - describe('quiet option', function () { - it('should default to false', function () { - const settings = parse(command, options, kbnPackage); - - expect(settings.quiet).toBe(false); - }); - - it('should set settings.quiet property to true', function () { - options.quiet = true; - const settings = parse(command, options, kbnPackage); - - expect(settings.quiet).toBe(true); - }); - }); - - describe('silent option', function () { - it('should default to false', function () { - const settings = parse(command, options, kbnPackage); - - expect(settings.silent).toBe(false); - }); - - it('should set settings.silent property to true', function () { - options.silent = true; - const settings = parse(command, options, kbnPackage); - - expect(settings.silent).toBe(true); - }); - }); +import { createAbsolutePathSerializer } from '@kbn/dev-utils'; - describe('config option', function () { - it('should default to ZLS', function () { - const settings = parse(command, options, kbnPackage); - - expect(settings.config).toBe(''); - }); - - it('should set settings.config property', function () { - options.config = 'foo bar baz'; - const settings = parse(command, options, kbnPackage); - - expect(settings.config).toBe('foo bar baz'); - }); - }); - - describe('pluginDir option', function () { - it('should default to plugins', function () { - const settings = parse(command, options, kbnPackage); - - expect(settings.pluginDir).toBe(fromRoot('plugins')); - }); - - it('should set settings.config property', function () { - options.pluginDir = 'foo bar baz'; - const settings = parse(command, options, kbnPackage); - - expect(settings.pluginDir).toBe('foo bar baz'); - }); - }); +import { parse } from './settings'; - describe('command value', function () { - it('should set settings.plugin property', function () { - const settings = parse(command, options, kbnPackage); +const command = 'plugin name'; + +expect.addSnapshotSerializer(createAbsolutePathSerializer()); + +it('produces the defaults', () => { + expect(parse(command, {})).toMatchInlineSnapshot(` + Object { + "config": "", + "plugin": "plugin name", + "pluginDir": /plugins, + "pluginPath": /plugins/plugin name, + "quiet": false, + "silent": false, + } + `); +}); - expect(settings.plugin).toBe(command); - }); - }); - }); - }); - }); +it('overrides the defaults with the parsed cli options', () => { + expect( + parse(command, { + quiet: true, + silent: true, + config: 'foo/bar', + }) + ).toMatchInlineSnapshot(` + Object { + "config": "foo/bar", + "plugin": "plugin name", + "pluginDir": /plugins, + "pluginPath": /plugins/plugin name, + "quiet": true, + "silent": true, + } + `); }); diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index eb54983d0be1359..8853d951819947a 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -109,6 +109,7 @@ export class DocLinksService { loadingData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/tutorial-load-dataset.html`, introduction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-patterns.html`, }, + addData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/connect-to-elasticsearch.html`, kibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index.html`, siem: { guide: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/index.html`, @@ -209,6 +210,7 @@ export interface DocLinksStart { readonly loadingData: string; readonly introduction: string; }; + readonly addData: string; readonly kibana: string; readonly siem: { readonly guide: string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 9b421e0172df0c8..0e879d16b4637a0 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -576,6 +576,7 @@ export interface DocLinksStart { readonly loadingData: string; readonly introduction: string; }; + readonly addData: string; readonly kibana: string; readonly siem: { readonly guide: string; diff --git a/src/cli_plugin/list/settings.js b/src/dev/build/lib/__mocks__/get_build_number.ts similarity index 87% rename from src/cli_plugin/list/settings.js rename to src/dev/build/lib/__mocks__/get_build_number.ts index d17ce5deaec304c..60cfd3d82557a3c 100644 --- a/src/cli_plugin/list/settings.js +++ b/src/dev/build/lib/__mocks__/get_build_number.ts @@ -17,10 +17,6 @@ * under the License. */ -export function parse(command) { - const settings = { - pluginDir: command.pluginDir || '', - }; - - return settings; +export function getBuildNumber() { + return 12345; } diff --git a/src/dev/build/lib/get_build_number.ts b/src/dev/build/lib/get_build_number.ts new file mode 100644 index 000000000000000..c512042592002a1 --- /dev/null +++ b/src/dev/build/lib/get_build_number.ts @@ -0,0 +1,34 @@ +/* + * 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 os from 'os'; +import execa from 'execa'; + +export async function getBuildNumber() { + if (/^win/.test(os.platform())) { + // Windows does not have the wc process and `find /C /V ""` does not consistently work + const log = await execa('git', ['log', '--format="%h"']); + return log.stdout.split('\n').length; + } + + const wc = await execa.command('git log --format="%h" | wc -l', { + shell: true, + }); + return parseFloat(wc.stdout.trim()); +} diff --git a/src/dev/build/lib/version_info.test.ts b/src/dev/build/lib/version_info.test.ts index 1b0c71bf9220ee9..ec8c363ddaccf12 100644 --- a/src/dev/build/lib/version_info.test.ts +++ b/src/dev/build/lib/version_info.test.ts @@ -20,6 +20,8 @@ import pkg from '../../../../package.json'; import { getVersionInfo } from './version_info'; +jest.mock('./get_build_number'); + describe('isRelease = true', () => { it('returns unchanged package.version, build sha, and build number', async () => { const versionInfo = await getVersionInfo({ diff --git a/src/dev/build/lib/version_info.ts b/src/dev/build/lib/version_info.ts index 958112c524bac7a..fb3530b4c4edf24 100644 --- a/src/dev/build/lib/version_info.ts +++ b/src/dev/build/lib/version_info.ts @@ -17,22 +17,8 @@ * under the License. */ -import os from 'os'; - import execa from 'execa'; - -async function getBuildNumber() { - if (/^win/.test(os.platform())) { - // Windows does not have the wc process and `find /C /V ""` does not consistently work - const log = await execa('git', ['log', '--format="%h"']); - return log.stdout.split('\n').length; - } - - const wc = await execa.command('git log --format="%h" | wc -l', { - shell: true, - }); - return parseFloat(wc.stdout.trim()); -} +import { getBuildNumber } from './get_build_number'; interface Options { isRelease: boolean; diff --git a/src/dev/build/tasks/nodejs/download_node_builds_task.test.ts b/src/dev/build/tasks/nodejs/download_node_builds_task.test.ts index 6f08c8aa6975067..413bf95cde8773c 100644 --- a/src/dev/build/tasks/nodejs/download_node_builds_task.test.ts +++ b/src/dev/build/tasks/nodejs/download_node_builds_task.test.ts @@ -26,13 +26,10 @@ import { import { Config, Platform } from '../../lib'; import { DownloadNodeBuilds } from './download_node_builds_task'; -// import * as NodeShasumsNS from '../node_shasums'; -// import * as NodeDownloadInfoNS from '../node_download_info'; -// import * as DownloadNS from '../../../lib/download'; -// import { DownloadNodeBuilds } from '../download_node_builds_task'; jest.mock('./node_shasums'); jest.mock('./node_download_info'); jest.mock('../../lib/download'); +jest.mock('../../lib/get_build_number'); expect.addSnapshotSerializer(createAnyInstanceSerializer(ToolingLog)); diff --git a/src/dev/build/tasks/nodejs/extract_node_builds_task.test.ts b/src/dev/build/tasks/nodejs/extract_node_builds_task.test.ts index 94c421f7c9a6265..f1700ef7b578c1e 100644 --- a/src/dev/build/tasks/nodejs/extract_node_builds_task.test.ts +++ b/src/dev/build/tasks/nodejs/extract_node_builds_task.test.ts @@ -27,6 +27,7 @@ import { Config } from '../../lib'; import { ExtractNodeBuilds } from './extract_node_builds_task'; jest.mock('../../lib/fs'); +jest.mock('../../lib/get_build_number'); const Fs = jest.requireMock('../../lib/fs'); diff --git a/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts b/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts index f24b7ffc59c1480..19416963d5eddc9 100644 --- a/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts +++ b/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts @@ -29,6 +29,7 @@ import { VerifyExistingNodeBuilds } from './verify_existing_node_builds_task'; jest.mock('./node_shasums'); jest.mock('./node_download_info'); jest.mock('../../lib/fs'); +jest.mock('../../lib/get_build_number'); const { getNodeShasums } = jest.requireMock('./node_shasums'); const { getNodeDownloadInfo } = jest.requireMock('./node_download_info'); diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx new file mode 100644 index 000000000000000..c2f529fe399f3fc --- /dev/null +++ b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx @@ -0,0 +1,156 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EmbeddableInput, + SavedObjectEmbeddableInput, + isSavedObjectEmbeddableInput, + IEmbeddable, +} from '../embeddable_plugin'; +import { + SavedObjectsClientContract, + SimpleSavedObject, + I18nStart, + NotificationsStart, +} from '../../../../core/public'; +import { + SavedObjectSaveModal, + showSaveModal, + OnSaveProps, + SaveResult, +} from '../../../saved_objects/public'; + +/** + * The attribute service is a shared, generic service that embeddables can use to provide the functionality + * required to fulfill the requirements of the ReferenceOrValueEmbeddable interface. The attribute_service + * can also be used as a higher level wrapper to transform an embeddable input shape that references a saved object + * into an embeddable input shape that contains that saved object's attributes by value. + */ +export class AttributeService< + SavedObjectAttributes extends { title: string }, + ValType extends EmbeddableInput & { attributes: SavedObjectAttributes }, + RefType extends SavedObjectEmbeddableInput +> { + constructor( + private type: string, + private savedObjectsClient: SavedObjectsClientContract, + private i18nContext: I18nStart['Context'], + private toasts: NotificationsStart['toasts'] + ) {} + + public async unwrapAttributes(input: RefType | ValType): Promise { + if (this.inputIsRefType(input)) { + const savedObject: SimpleSavedObject = await this.savedObjectsClient.get< + SavedObjectAttributes + >(this.type, input.savedObjectId); + return savedObject.attributes; + } + return input.attributes; + } + + public async wrapAttributes( + newAttributes: SavedObjectAttributes, + useRefType: boolean, + embeddable?: IEmbeddable + ): Promise> { + const savedObjectId = + embeddable && isSavedObjectEmbeddableInput(embeddable.getInput()) + ? (embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId + : undefined; + if (!useRefType) { + return { attributes: newAttributes } as ValType; + } else { + try { + if (savedObjectId) { + await this.savedObjectsClient.update(this.type, savedObjectId, newAttributes); + return { savedObjectId } as RefType; + } else { + const savedItem = await this.savedObjectsClient.create(this.type, newAttributes); + return { savedObjectId: savedItem.id } as RefType; + } + } catch (error) { + this.toasts.addDanger({ + title: i18n.translate('dashboard.attributeService.saveToLibraryError', { + defaultMessage: `Panel was not saved to the library. Error: {errorMessage}`, + values: { + errorMessage: error.message, + }, + }), + 'data-test-subj': 'saveDashboardFailure', + }); + return Promise.reject({ error }); + } + } + } + + inputIsRefType = (input: ValType | RefType): input is RefType => { + return isSavedObjectEmbeddableInput(input); + }; + + getInputAsValueType = async (input: ValType | RefType): Promise => { + if (!this.inputIsRefType(input)) { + return input; + } + const attributes = await this.unwrapAttributes(input); + return { + ...input, + savedObjectId: undefined, + attributes, + }; + }; + + getInputAsRefType = async ( + input: ValType | RefType, + saveOptions?: { showSaveModal: boolean } | { title: string } + ): Promise => { + if (this.inputIsRefType(input)) { + return input; + } + + return new Promise((resolve, reject) => { + const onSave = async (props: OnSaveProps): Promise => { + try { + input.attributes.title = props.newTitle; + const wrappedInput = (await this.wrapAttributes(input.attributes, true)) as RefType; + resolve(wrappedInput); + return { id: wrappedInput.savedObjectId }; + } catch (error) { + reject(); + return { error }; + } + }; + + if (saveOptions && (saveOptions as { showSaveModal: boolean }).showSaveModal) { + showSaveModal( + reject()} + title={input.attributes.title} + showCopyOnSave={false} + objectType={this.type} + showDescription={false} + />, + this.i18nContext + ); + } + }); + }; +} diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts index dcfde67cd9f1312..8a9954cc77a2e53 100644 --- a/src/plugins/dashboard/public/index.ts +++ b/src/plugins/dashboard/public/index.ts @@ -40,6 +40,7 @@ export { export { addEmbeddableToDashboardUrl } from './url_utils/url_helper'; export { SavedObjectDashboard } from './saved_dashboards'; export { SavedDashboardPanel } from './types'; +export { AttributeService } from './attribute_service/attribute_service'; export function plugin(initializerContext: PluginInitializerContext) { return new DashboardPlugin(initializerContext); diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index f1319665d258b41..3b0863a9f465165 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -34,7 +34,13 @@ import { ScopedHistory, } from 'src/core/public'; import { UsageCollectionSetup } from '../../usage_collection/public'; -import { CONTEXT_MENU_TRIGGER, EmbeddableSetup, EmbeddableStart } from '../../embeddable/public'; +import { + CONTEXT_MENU_TRIGGER, + EmbeddableSetup, + EmbeddableStart, + SavedObjectEmbeddableInput, + EmbeddableInput, +} from '../../embeddable/public'; import { DataPublicPluginSetup, DataPublicPluginStart, esFilters } from '../../data/public'; import { SharePluginSetup, SharePluginStart, UrlGeneratorContract } from '../../share/public'; import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public'; @@ -85,6 +91,7 @@ import { DashboardConstants } from './dashboard_constants'; import { addEmbeddableToDashboardUrl } from './url_utils/url_helper'; import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder'; import { UrlGeneratorState } from '../../share/public'; +import { AttributeService } from '.'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -131,6 +138,13 @@ export interface DashboardStart { dashboardUrlGenerator?: DashboardUrlGenerator; dashboardFeatureFlagConfig: DashboardFeatureFlagConfig; DashboardContainerByValueRenderer: ReturnType; + getAttributeService: < + A extends { title: string }, + V extends EmbeddableInput & { attributes: A }, + R extends SavedObjectEmbeddableInput + >( + type: string + ) => AttributeService; } declare module '../../../plugins/ui_actions/public' { @@ -420,6 +434,13 @@ export class DashboardPlugin DashboardContainerByValueRenderer: createDashboardContainerByValueRenderer({ factory: dashboardContainerFactory, }), + getAttributeService: (type: string) => + new AttributeService( + type, + core.savedObjects.client, + core.i18n.Context, + core.notifications.toasts + ), }; } diff --git a/src/plugins/data/public/index_patterns/index_patterns/redirect_no_index_pattern.tsx b/src/plugins/data/public/index_patterns/index_patterns/redirect_no_index_pattern.tsx index 7ed6525db6350e6..ac015f8dd33afbb 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/redirect_no_index_pattern.tsx +++ b/src/plugins/data/public/index_patterns/index_patterns/redirect_no_index_pattern.tsx @@ -40,7 +40,7 @@ export const onRedirectNoIndexPattern = ( const bannerMessage = i18n.translate('data.indexPatterns.ensureDefaultIndexPattern.bannerLabel', { defaultMessage: - "In order to visualize and explore data in Kibana, you'll need to create an index pattern to retrieve data from Elasticsearch.", + 'To visualize and explore data in Kibana, you must create an index pattern to retrieve data from Elasticsearch.', }); // Avoid being hostile to new users who don't have an index pattern setup yet diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 135b6121d1dd5e6..3fc1e6454829d66 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -44,6 +44,7 @@ const createSetupContract = (): Setup => { search: searchServiceMock.createSetupContract(), fieldFormats: fieldFormatsServiceMock.createSetupContract(), query: querySetupMock, + __enhance: jest.fn(), }; }; diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 68c0f506f121d94..e950434b287a756 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -34,6 +34,7 @@ import { DataSetupDependencies, DataStartDependencies, InternalStartServices, + DataPublicPluginEnhancements, } from './types'; import { AutocompleteService } from './autocomplete'; import { SearchService } from './search/search_service'; @@ -156,16 +157,21 @@ export class DataPublicPlugin })) ); + const searchService = this.searchService.setup(core, { + expressions, + usageCollection, + getInternalStartServices, + packageInfo: this.packageInfo, + }); + return { autocomplete: this.autocomplete.setup(core), - search: this.searchService.setup(core, { - expressions, - usageCollection, - getInternalStartServices, - packageInfo: this.packageInfo, - }), + search: searchService, fieldFormats: this.fieldFormatsService.setup(core), query: queryService, + __enhance: (enhancements: DataPublicPluginEnhancements) => { + searchService.__enhance(enhancements.search); + }, }; } diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 6225d74fb1b3106..a61334905e9f58f 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -8,12 +8,12 @@ import { $Values } from '@kbn/utility-types'; import _ from 'lodash'; import { Action } from 'history'; import { ApiResponse } from '@elastic/elasticsearch/lib/Transport'; -import { ApplicationStart } from 'kibana/public'; import { Assign } from '@kbn/utility-types'; import { BehaviorSubject } from 'rxjs'; import Boom from 'boom'; import { Component } from 'react'; import { CoreSetup } from 'src/core/public'; +import { CoreSetup as CoreSetup_2 } from 'kibana/public'; import { CoreStart } from 'kibana/public'; import { CoreStart as CoreStart_2 } from 'src/core/public'; import { Ensure } from '@kbn/utility-types'; @@ -65,7 +65,7 @@ import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/ex import { Subscription } from 'rxjs'; import { Toast } from 'kibana/public'; import { ToastInputFields } from 'src/core/public/notifications'; -import { ToastsStart } from 'kibana/public'; +import { ToastsSetup } from 'kibana/public'; import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; @@ -222,6 +222,10 @@ export type CustomFilter = Filter & { // // @public (undocumented) export interface DataPublicPluginSetup { + // Warning: (ae-forgotten-export) The symbol "DataPublicPluginEnhancements" needs to be exported by the entry point index.d.ts + // + // @internal (undocumented) + __enhance: (enhancements: DataPublicPluginEnhancements) => void; // Warning: (ae-forgotten-export) The symbol "AutocompleteSetup" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1714,15 +1718,19 @@ export interface SearchError { // @public (undocumented) export class SearchInterceptor { constructor(deps: SearchInterceptorDeps, requestTimeout?: number | undefined); + // @internal protected abortController: AbortController; + // @internal (undocumented) + protected application: CoreStart['application']; // (undocumented) protected readonly deps: SearchInterceptorDeps; - getPendingCount$: () => Observable; - // (undocumented) + getPendingCount$(): Observable; + // @internal (undocumented) protected hideToast: () => void; + // @internal protected longRunningToast?: Toast; + // @internal protected pendingCount$: BehaviorSubject; - protected pendingCount: number; // (undocumented) protected readonly requestTimeout?: number | undefined; // (undocumented) @@ -1733,8 +1741,9 @@ export class SearchInterceptor { combinedSignal: AbortSignal; cleanup: () => void; }; - // (undocumented) + // @internal (undocumented) protected showToast: () => void; + // @internal protected timeoutSubscriptions: Subscription; } @@ -1743,13 +1752,13 @@ export class SearchInterceptor { // @public (undocumented) export interface SearchInterceptorDeps { // (undocumented) - application: ApplicationStart; + http: CoreSetup_2['http']; // (undocumented) - http: CoreStart['http']; + startServices: Promise<[CoreStart, any, unknown]>; // (undocumented) - toasts: ToastsStart; + toasts: ToastsSetup; // (undocumented) - uiSettings: CoreStart['uiSettings']; + uiSettings: CoreSetup_2['uiSettings']; // Warning: (ae-forgotten-export) The symbol "SearchUsageCollector" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1980,9 +1989,9 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:393:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:396:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:45:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/types.ts:54:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/types.ts:55:5 - (ae-forgotten-export) The symbol "createFiltersFromRangeSelectAction" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/types.ts:63:5 - (ae-forgotten-export) The symbol "IndexPatternSelectProps" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/types.ts:62:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/types.ts:63:5 - (ae-forgotten-export) The symbol "createFiltersFromRangeSelectAction" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/types.ts:71:5 - (ae-forgotten-export) The symbol "IndexPatternSelectProps" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index 96445e5367147de..ae028df31e401e5 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -21,7 +21,14 @@ export * from './aggs'; export * from './expressions'; export * from './tabify'; -export { ISearch, ISearchOptions, ISearchGeneric, ISearchSetup, ISearchStart } from './types'; +export { + ISearch, + ISearchOptions, + ISearchGeneric, + ISearchSetup, + ISearchStart, + SearchEnhancements, +} from './types'; export { IEsSearchResponse, IEsSearchRequest, ES_SEARCH_STRATEGY } from '../../common/search'; diff --git a/src/plugins/data/public/search/mocks.ts b/src/plugins/data/public/search/mocks.ts index c56331baffed2e7..8ccf46fe7c97d28 100644 --- a/src/plugins/data/public/search/mocks.ts +++ b/src/plugins/data/public/search/mocks.ts @@ -26,13 +26,13 @@ export * from './search_source/mocks'; function createSetupContract(): jest.Mocked { return { aggs: searchAggsSetupMock(), + __enhance: jest.fn(), }; } function createStartContract(): jest.Mocked { return { aggs: searchAggsStartMock(), - setInterceptor: jest.fn(), search: jest.fn(), searchSource: searchSourceMock, __LEGACY: { diff --git a/src/plugins/data/public/search/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor.test.ts index f4c5de2bcaf3135..2eded17bda88c81 100644 --- a/src/plugins/data/public/search/search_interceptor.test.ts +++ b/src/plugins/data/public/search/search_interceptor.test.ts @@ -17,27 +17,27 @@ * under the License. */ -import { CoreStart } from '../../../../core/public'; +import { CoreSetup } from '../../../../core/public'; import { coreMock } from '../../../../core/public/mocks'; import { IEsSearchRequest } from '../../common/search'; import { SearchInterceptor } from './search_interceptor'; import { AbortError } from '../../common'; let searchInterceptor: SearchInterceptor; -let mockCoreStart: MockedKeys; +let mockCoreSetup: MockedKeys; const flushPromises = () => new Promise((resolve) => setImmediate(resolve)); jest.useFakeTimers(); describe('SearchInterceptor', () => { beforeEach(() => { - mockCoreStart = coreMock.createStart(); + mockCoreSetup = coreMock.createSetup(); searchInterceptor = new SearchInterceptor( { - toasts: mockCoreStart.notifications.toasts, - application: mockCoreStart.application, - uiSettings: mockCoreStart.uiSettings, - http: mockCoreStart.http, + toasts: mockCoreSetup.notifications.toasts, + startServices: mockCoreSetup.getStartServices(), + uiSettings: mockCoreSetup.uiSettings, + http: mockCoreSetup.http, }, 1000 ); @@ -46,7 +46,7 @@ describe('SearchInterceptor', () => { describe('search', () => { test('Observable should resolve if fetch is successful', async () => { const mockResponse: any = { result: 200 }; - mockCoreStart.http.fetch.mockResolvedValueOnce(mockResponse); + mockCoreSetup.http.fetch.mockResolvedValueOnce(mockResponse); const mockRequest: IEsSearchRequest = { params: {}, }; @@ -58,7 +58,7 @@ describe('SearchInterceptor', () => { test('Observable should fail if fetch has an error', async () => { const mockResponse: any = { result: 500 }; - mockCoreStart.http.fetch.mockRejectedValueOnce(mockResponse); + mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse); const mockRequest: IEsSearchRequest = { params: {}, }; @@ -72,7 +72,7 @@ describe('SearchInterceptor', () => { }); test('Observable should fail if fetch times out (test merged signal)', async () => { - mockCoreStart.http.fetch.mockImplementationOnce((options: any) => { + mockCoreSetup.http.fetch.mockImplementationOnce((options: any) => { return new Promise((resolve, reject) => { options.signal.addEventListener('abort', () => { reject(new AbortError()); @@ -100,7 +100,7 @@ describe('SearchInterceptor', () => { test('Observable should fail if user aborts (test merged signal)', async () => { const abortController = new AbortController(); - mockCoreStart.http.fetch.mockImplementationOnce((options: any) => { + mockCoreSetup.http.fetch.mockImplementationOnce((options: any) => { return new Promise((resolve, reject) => { options.signal.addEventListener('abort', () => { reject(new AbortError()); @@ -136,7 +136,7 @@ describe('SearchInterceptor', () => { const error = (e: any) => { expect(e).toBeInstanceOf(AbortError); - expect(mockCoreStart.http.fetch).not.toBeCalled(); + expect(mockCoreSetup.http.fetch).not.toBeCalled(); done(); }; response.subscribe({ error }); @@ -150,7 +150,7 @@ describe('SearchInterceptor', () => { pendingCount$.subscribe(pendingNext); const mockResponse: any = { result: 200 }; - mockCoreStart.http.fetch.mockResolvedValue(mockResponse); + mockCoreSetup.http.fetch.mockResolvedValue(mockResponse); const mockRequest: IEsSearchRequest = { params: {}, }; @@ -169,7 +169,7 @@ describe('SearchInterceptor', () => { pendingCount$.subscribe(pendingNext); const mockResponse: any = { result: 500 }; - mockCoreStart.http.fetch.mockRejectedValue(mockResponse); + mockCoreSetup.http.fetch.mockRejectedValue(mockResponse); const mockRequest: IEsSearchRequest = { params: {}, }; diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index d6fcde8e986f365..99fccda7fddf351 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -20,7 +20,7 @@ import { trimEnd } from 'lodash'; import { BehaviorSubject, throwError, timer, Subscription, defer, from, Observable } from 'rxjs'; import { finalize, filter } from 'rxjs/operators'; -import { ApplicationStart, Toast, ToastsStart, CoreStart } from 'kibana/public'; +import { Toast, CoreStart, ToastsSetup, CoreSetup } from 'kibana/public'; import { getCombinedSignal, AbortError } from '../../common/utils'; import { IEsSearchRequest, IEsSearchResponse, ES_SEARCH_STRATEGY } from '../../common/search'; import { ISearchOptions } from './types'; @@ -30,39 +30,43 @@ import { SearchUsageCollector } from './collectors'; const LONG_QUERY_NOTIFICATION_DELAY = 10000; export interface SearchInterceptorDeps { - toasts: ToastsStart; - application: ApplicationStart; - http: CoreStart['http']; - uiSettings: CoreStart['uiSettings']; + toasts: ToastsSetup; + http: CoreSetup['http']; + uiSettings: CoreSetup['uiSettings']; + startServices: Promise<[CoreStart, any, unknown]>; usageCollector?: SearchUsageCollector; } export class SearchInterceptor { /** * `abortController` used to signal all searches to abort. + * @internal */ protected abortController = new AbortController(); - /** - * The number of pending search requests. - */ - protected pendingCount = 0; - /** * Observable that emits when the number of pending requests changes. + * @internal */ - protected pendingCount$ = new BehaviorSubject(this.pendingCount); + protected pendingCount$ = new BehaviorSubject(0); /** * The subscriptions from scheduling the automatic timeout for each request. + * @internal */ protected timeoutSubscriptions: Subscription = new Subscription(); /** * The current long-running toast (if there is one). + * @internal */ protected longRunningToast?: Toast; + /** + * @internal + */ + protected application!: CoreStart['application']; + /** * This class should be instantiated with a `requestTimeout` corresponding with how many ms after * requests are initiated that they should automatically cancel. @@ -76,6 +80,10 @@ export class SearchInterceptor { ) { this.deps.http.addLoadingCountSource(this.pendingCount$); + this.deps.startServices.then(([coreStart]) => { + this.application = coreStart.application; + }); + // When search requests go out, a notification is scheduled allowing users to continue the // request past the timeout. When all search requests complete, we remove the notification. this.getPendingCount$() @@ -87,9 +95,9 @@ export class SearchInterceptor { * Returns an `Observable` over the current number of pending searches. This could mean that one * of the search requests is still in flight, or that it has only received partial responses. */ - public getPendingCount$ = () => { + public getPendingCount$() { return this.pendingCount$.asObservable(); - }; + } protected runSearch( request: IEsSearchRequest, @@ -112,7 +120,7 @@ export class SearchInterceptor { /** * Searches using the given `search` method. Overrides the `AbortSignal` with one that will abort * either when `cancelPending` is called, when the request times out, or when the original - * `AbortSignal` is aborted. Updates the `pendingCount` when the request is started/finalized. + * `AbortSignal` is aborted. Updates `pendingCount$` when the request is started/finalized. */ public search( request: IEsSearchRequest, @@ -125,11 +133,11 @@ export class SearchInterceptor { } const { combinedSignal, cleanup } = this.setupTimers(options); - this.pendingCount$.next(++this.pendingCount); + this.pendingCount$.next(this.pendingCount$.getValue() + 1); return this.runSearch(request, combinedSignal, options?.strategy).pipe( finalize(() => { - this.pendingCount$.next(--this.pendingCount); + this.pendingCount$.next(this.pendingCount$.getValue() - 1); cleanup(); }) ); @@ -173,13 +181,16 @@ export class SearchInterceptor { }; } + /** + * @internal + */ protected showToast = () => { if (this.longRunningToast) return; this.longRunningToast = this.deps.toasts.addInfo( { title: 'Your query is taking a while', text: getLongQueryNotification({ - application: this.deps.application, + application: this.application, }), }, { @@ -188,6 +199,9 @@ export class SearchInterceptor { ); }; + /** + * @internal + */ protected hideToast = () => { if (this.longRunningToast) { this.deps.toasts.remove(this.longRunningToast); @@ -198,3 +212,5 @@ export class SearchInterceptor { } }; } + +export type ISearchInterceptor = PublicMethodsOf; diff --git a/src/plugins/data/public/search/search_service.test.ts b/src/plugins/data/public/search/search_service.test.ts index 55d31db19173381..f0a017847e06aa7 100644 --- a/src/plugins/data/public/search/search_service.test.ts +++ b/src/plugins/data/public/search/search_service.test.ts @@ -41,6 +41,7 @@ describe('Search service', () => { expressions: expressionsPluginMock.createSetupContract(), } as any); expect(setup).toHaveProperty('aggs'); + expect(setup).toHaveProperty('__enhance'); }); }); @@ -49,7 +50,6 @@ describe('Search service', () => { const start = searchService.start(mockCoreStart, { indexPatterns: {}, } as any); - expect(start).toHaveProperty('setInterceptor'); expect(start).toHaveProperty('search'); }); }); diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 064e16014cb70ac..4c94925b66d6e26 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -18,7 +18,7 @@ */ import { Plugin, CoreSetup, CoreStart, PackageInfo } from '../../../../core/public'; -import { ISearchSetup, ISearchStart } from './types'; +import { ISearchSetup, ISearchStart, SearchEnhancements } from './types'; import { ExpressionsSetup } from '../../../../plugins/expressions/public'; import { createSearchSource, SearchSource, SearchSourceDependencies } from './search_source'; @@ -28,7 +28,7 @@ import { calculateBounds, TimeRange } from '../../common/query'; import { IndexPatternsContract } from '../index_patterns/index_patterns'; import { GetInternalStartServicesFn } from '../types'; -import { SearchInterceptor } from './search_interceptor'; +import { ISearchInterceptor, SearchInterceptor } from './search_interceptor'; import { getAggTypes, getAggTypesFunctions, @@ -54,7 +54,7 @@ interface SearchServiceStartDependencies { export class SearchService implements Plugin { private esClient?: LegacyApiCaller; private readonly aggTypesRegistry = new AggTypesRegistry(); - private searchInterceptor!: SearchInterceptor; + private searchInterceptor!: ISearchInterceptor; private usageCollector?: SearchUsageCollector; /** @@ -91,15 +91,6 @@ export class SearchService implements Plugin { const aggFunctions = getAggTypesFunctions(); aggFunctions.forEach((fn) => expressions.registerFunction(fn)); - return { - aggs: { - calculateAutoTimeExpression: getCalculateAutoTimeExpression(core.uiSettings), - types: aggTypesSetup, - }, - }; - } - - public start(core: CoreStart, dependencies: SearchServiceStartDependencies): ISearchStart { /** * A global object that intercepts all searches and provides convenience methods for cancelling * all pending search requests, as well as getting the number of pending search requests. @@ -109,14 +100,27 @@ export class SearchService implements Plugin { this.searchInterceptor = new SearchInterceptor( { toasts: core.notifications.toasts, - application: core.application, http: core.http, uiSettings: core.uiSettings, + startServices: core.getStartServices(), usageCollector: this.usageCollector!, }, core.injectedMetadata.getInjectedVar('esRequestTimeout') as number ); + return { + usageCollector: this.usageCollector!, + __enhance: (enhancements: SearchEnhancements) => { + this.searchInterceptor = enhancements.searchInterceptor; + }, + aggs: { + calculateAutoTimeExpression: getCalculateAutoTimeExpression(core.uiSettings), + types: aggTypesSetup, + }, + }; + } + + public start(core: CoreStart, dependencies: SearchServiceStartDependencies): ISearchStart { const aggTypesStart = this.aggTypesRegistry.start(); const search: ISearchGeneric = (request, options) => { @@ -145,17 +149,12 @@ export class SearchService implements Plugin { types: aggTypesStart, }, search, - usageCollector: this.usageCollector!, searchSource: { create: createSearchSource(dependencies.indexPatterns, searchSourceDependencies), createEmpty: () => { return new SearchSource({}, searchSourceDependencies); }, }, - setInterceptor: (searchInterceptor: SearchInterceptor) => { - // TODO: should an intercepror have a destroy method? - this.searchInterceptor = searchInterceptor; - }, __LEGACY: legacySearch, }; } diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index f80a13d048a684c..d85d4c4e5c9354b 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -21,7 +21,7 @@ import { Observable } from 'rxjs'; import { PackageInfo } from 'kibana/server'; import { SearchAggsSetup, SearchAggsStart } from './aggs'; import { LegacyApiCaller } from './legacy/es_client'; -import { SearchInterceptor } from './search_interceptor'; +import { ISearchInterceptor } from './search_interceptor'; import { ISearchSource, SearchSourceFields } from './search_source'; import { SearchUsageCollector } from './collectors'; import { @@ -54,23 +54,33 @@ export interface ISearchStartLegacy { esClient: LegacyApiCaller; } +export interface SearchEnhancements { + searchInterceptor: ISearchInterceptor; +} /** * The setup contract exposed by the Search plugin exposes the search strategy extension * point. */ export interface ISearchSetup { aggs: SearchAggsSetup; + usageCollector?: SearchUsageCollector; + /** + * @internal + */ + __enhance: (enhancements: SearchEnhancements) => void; } export interface ISearchStart { aggs: SearchAggsStart; - setInterceptor: (searchInterceptor: SearchInterceptor) => void; search: ISearchGeneric; searchSource: { create: (fields?: SearchSourceFields) => Promise; createEmpty: () => ISearchSource; }; - usageCollector?: SearchUsageCollector; + /** + * @deprecated + * @internal + */ __LEGACY: ISearchStartLegacy; } diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index 6d671272514244d..c39b7d355d4954e 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -25,13 +25,17 @@ import { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public'; import { AutocompleteSetup, AutocompleteStart } from './autocomplete'; import { FieldFormatsSetup, FieldFormatsStart } from './field_formats'; import { createFiltersFromRangeSelectAction, createFiltersFromValueClickAction } from './actions'; -import { ISearchSetup, ISearchStart } from './search'; +import { ISearchSetup, ISearchStart, SearchEnhancements } from './search'; import { QuerySetup, QueryStart } from './query'; import { IndexPatternSelectProps } from './ui/index_pattern_select'; import { IndexPatternsContract } from './index_patterns'; import { StatefulSearchBarProps } from './ui/search_bar/create_search_bar'; import { UsageCollectionSetup } from '../../usage_collection/public'; +export interface DataPublicPluginEnhancements { + search: SearchEnhancements; +} + export interface DataSetupDependencies { expressions: ExpressionsSetup; uiActions: UiActionsSetup; @@ -47,6 +51,10 @@ export interface DataPublicPluginSetup { search: ISearchSetup; fieldFormats: FieldFormatsSetup; query: QuerySetup; + /** + * @internal + */ + __enhance: (enhancements: DataPublicPluginEnhancements) => void; } export interface DataPublicPluginStart { diff --git a/src/plugins/data/server/saved_objects/index_patterns.ts b/src/plugins/data/server/saved_objects/index_patterns.ts index 44d2813f6e3e81c..6aabcdf7c1c01b0 100644 --- a/src/plugins/data/server/saved_objects/index_patterns.ts +++ b/src/plugins/data/server/saved_objects/index_patterns.ts @@ -42,16 +42,10 @@ export const indexPatternSavedObjectType: SavedObjectsType = { }, }, mappings: { + dynamic: false, properties: { - fieldFormatMap: { type: 'text' }, - fields: { type: 'text' }, - intervalName: { type: 'keyword' }, - notExpandable: { type: 'boolean' }, - sourceFilters: { type: 'text' }, - timeFieldName: { type: 'keyword' }, title: { type: 'text' }, type: { type: 'keyword' }, - typeMeta: { type: 'keyword' }, }, }, migrations: indexPatternSavedObjectTypeMigrations as any, diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index fafbdda148de87e..57253c1f741abc8 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -28,7 +28,8 @@ export { ACTION_EDIT_PANEL, Adapters, AddPanelAction, - AttributeService, + ReferenceOrValueEmbeddable, + isReferenceOrValueEmbeddable, ChartActionContext, Container, ContainerInput, diff --git a/src/plugins/embeddable/public/lib/embeddables/attribute_service.ts b/src/plugins/embeddable/public/lib/embeddables/attribute_service.ts deleted file mode 100644 index a33f592350d9a85..000000000000000 --- a/src/plugins/embeddable/public/lib/embeddables/attribute_service.ts +++ /dev/null @@ -1,68 +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 { SavedObjectsClientContract } from '../../../../../core/public'; -import { - SavedObjectEmbeddableInput, - isSavedObjectEmbeddableInput, - EmbeddableInput, - IEmbeddable, -} from '.'; -import { SimpleSavedObject } from '../../../../../core/public'; - -export class AttributeService< - SavedObjectAttributes, - ValType extends EmbeddableInput & { attributes: SavedObjectAttributes }, - RefType extends SavedObjectEmbeddableInput -> { - constructor(private type: string, private savedObjectsClient: SavedObjectsClientContract) {} - - public async unwrapAttributes(input: RefType | ValType): Promise { - if (isSavedObjectEmbeddableInput(input)) { - const savedObject: SimpleSavedObject = await this.savedObjectsClient.get< - SavedObjectAttributes - >(this.type, input.savedObjectId); - return savedObject.attributes; - } - return input.attributes; - } - - public async wrapAttributes( - newAttributes: SavedObjectAttributes, - useRefType: boolean, - embeddable?: IEmbeddable - ): Promise> { - const savedObjectId = - embeddable && isSavedObjectEmbeddableInput(embeddable.getInput()) - ? (embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId - : undefined; - - if (useRefType) { - if (savedObjectId) { - await this.savedObjectsClient.update(this.type, savedObjectId, newAttributes); - return { savedObjectId } as RefType; - } else { - const savedItem = await this.savedObjectsClient.create(this.type, newAttributes); - return { savedObjectId: savedItem.id } as RefType; - } - } else { - return { attributes: newAttributes } as ValType; - } - } -} diff --git a/src/plugins/embeddable/public/lib/embeddables/index.ts b/src/plugins/embeddable/public/lib/embeddables/index.ts index 06cb6e322acf39a..5bab5ac27f3cc9a 100644 --- a/src/plugins/embeddable/public/lib/embeddables/index.ts +++ b/src/plugins/embeddable/public/lib/embeddables/index.ts @@ -25,5 +25,4 @@ export { ErrorEmbeddable, isErrorEmbeddable } from './error_embeddable'; export { withEmbeddableSubscription } from './with_subscription'; export { EmbeddableRoot } from './embeddable_root'; export * from './saved_object_embeddable'; -export { AttributeService } from './attribute_service'; export { EmbeddableRenderer, EmbeddableRendererProps } from './embeddable_renderer'; diff --git a/src/plugins/embeddable/public/lib/index.ts b/src/plugins/embeddable/public/lib/index.ts index b757fa59a7f3a6d..aef4c33ee1078c9 100644 --- a/src/plugins/embeddable/public/lib/index.ts +++ b/src/plugins/embeddable/public/lib/index.ts @@ -25,3 +25,4 @@ export * from './triggers'; export * from './containers'; export * from './panel'; export * from './state_transfer'; +export * from './reference_or_value_embeddable'; diff --git a/src/cli_plugin/lib/warn_if_plugin_dir_option.js b/src/plugins/embeddable/public/lib/reference_or_value_embeddable/index.ts similarity index 72% rename from src/cli_plugin/lib/warn_if_plugin_dir_option.js rename to src/plugins/embeddable/public/lib/reference_or_value_embeddable/index.ts index eb85ba063c3c951..e9b8521a35ba5b7 100644 --- a/src/cli_plugin/lib/warn_if_plugin_dir_option.js +++ b/src/plugins/embeddable/public/lib/reference_or_value_embeddable/index.ts @@ -17,11 +17,4 @@ * under the License. */ -export function warnIfUsingPluginDirOption(options, defaultValue, logger) { - if (options && options.pluginDir !== defaultValue) { - logger.log( - 'Warning: Using the -d, --plugin-dir option is deprecated, and is ' + - 'known to not work for all plugins, including X-Pack.' - ); - } -} +export { ReferenceOrValueEmbeddable, isReferenceOrValueEmbeddable } from './types'; diff --git a/src/plugins/embeddable/public/lib/reference_or_value_embeddable/types.ts b/src/plugins/embeddable/public/lib/reference_or_value_embeddable/types.ts new file mode 100644 index 000000000000000..eaf5c94a09138dc --- /dev/null +++ b/src/plugins/embeddable/public/lib/reference_or_value_embeddable/types.ts @@ -0,0 +1,56 @@ +/* + * 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 { EmbeddableInput, SavedObjectEmbeddableInput } from '..'; + +/** + * Any embeddable that implements this interface will be able to use input that is + * either by reference (backed by a saved object) OR by value, (provided + * by the container). + * @public + */ +export interface ReferenceOrValueEmbeddable< + ValTypeInput extends EmbeddableInput = EmbeddableInput, + RefTypeInput extends SavedObjectEmbeddableInput = SavedObjectEmbeddableInput +> { + /** + * determines whether the input is by value or by reference. + */ + inputIsRefType: (input: ValTypeInput | RefTypeInput) => input is RefTypeInput; + + /** + * Gets the embeddable's current input as its Value type + */ + getInputAsValueType: () => Promise; + + /** + * Gets the embeddable's current input as its Reference type + */ + getInputAsRefType: () => Promise; +} + +export function isReferenceOrValueEmbeddable( + incoming: unknown +): incoming is ReferenceOrValueEmbeddable { + return ( + !!(incoming as ReferenceOrValueEmbeddable).inputIsRefType && + !!(incoming as ReferenceOrValueEmbeddable).getInputAsValueType && + !!(incoming as ReferenceOrValueEmbeddable).getInputAsRefType + ); +} diff --git a/src/plugins/embeddable/public/mocks.tsx b/src/plugins/embeddable/public/mocks.tsx index 94aa980e446ca43..fa79af909a42796 100644 --- a/src/plugins/embeddable/public/mocks.tsx +++ b/src/plugins/embeddable/public/mocks.tsx @@ -97,7 +97,6 @@ const createStartContract = (): Start => { getEmbeddableFactories: jest.fn(), getEmbeddableFactory: jest.fn(), EmbeddablePanel: jest.fn(), - getAttributeService: jest.fn(), getEmbeddablePanel: jest.fn(), getStateTransfer: jest.fn(() => createEmbeddableStateTransferMock() as EmbeddableStateTransfer), }; diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index 319cbf8ec44b477..3cbd49279564f37 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -37,10 +37,8 @@ import { defaultEmbeddableFactoryProvider, IEmbeddable, EmbeddablePanel, - SavedObjectEmbeddableInput, } from './lib'; import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition'; -import { AttributeService } from './lib/embeddables/attribute_service'; import { EmbeddableStateTransfer } from './lib/state_transfer'; export interface EmbeddableSetupDependencies { @@ -75,14 +73,6 @@ export interface EmbeddableStart { embeddableFactoryId: string ) => EmbeddableFactory | undefined; getEmbeddableFactories: () => IterableIterator; - getAttributeService: < - A, - V extends EmbeddableInput & { attributes: A }, - R extends SavedObjectEmbeddableInput - >( - type: string - ) => AttributeService; - EmbeddablePanel: EmbeddablePanelHOC; getEmbeddablePanel: (stateTransfer?: EmbeddableStateTransfer) => EmbeddablePanelHOC; getStateTransfer: (history?: ScopedHistory) => EmbeddableStateTransfer; @@ -159,7 +149,6 @@ export class EmbeddablePublicPlugin implements Plugin new AttributeService(type, core.savedObjects.client), getStateTransfer: (history?: ScopedHistory) => { return history ? new EmbeddableStateTransfer(core.application.navigateToApp, history) diff --git a/src/plugins/index_pattern_management/public/_templates.scss b/src/plugins/index_pattern_management/public/_templates.scss new file mode 100644 index 000000000000000..5303537bddabcbc --- /dev/null +++ b/src/plugins/index_pattern_management/public/_templates.scss @@ -0,0 +1,11 @@ +%inp-empty-state-footer { + background: $euiColorLightestShade; + margin: 0 (-$euiSizeL) (-$euiSizeL); + padding: $euiSizeL; + border-radius: 0 0 $euiBorderRadius $euiBorderRadius; + + // sass-lint:disable-block mixins-before-declarations + @include euiBreakpoint('xs', 's') { + text-align: center; + } +} diff --git a/src/plugins/index_pattern_management/public/_variables.scss b/src/plugins/index_pattern_management/public/_variables.scss new file mode 100644 index 000000000000000..5da25a91bd77c58 --- /dev/null +++ b/src/plugins/index_pattern_management/public/_variables.scss @@ -0,0 +1 @@ +$inpEmptyStateMaxWidth: $euiSizeXXL * 19; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_prompt/index.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_prompt/index.tsx deleted file mode 100644 index ab3b90177bcfda4..000000000000000 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_prompt/index.tsx +++ /dev/null @@ -1,108 +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 { - EuiDescriptionList, - EuiDescriptionListDescription, - EuiDescriptionListTitle, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiHorizontalRule, - EuiSpacer, - EuiText, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; - -export const CreateIndexPatternPrompt = ({ onClose }: { onClose: () => void }) => ( - - - -

- -

-
-
- - -

- -

-
- - -

- -

-
- - - - - - - - - - - - - - - - - - - - - -
-
-); diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap index 6d79515c172fe14..0e5fc0582f72c56 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap @@ -62,10 +62,37 @@ exports[`CreateIndexPatternWizard renders index pattern step when there are indi exports[`CreateIndexPatternWizard renders the empty state when there are no indices 1`] = ` - + +
+ + + - + +
+ + + - - } - > -

- - - , - "learnHowLink": - - , - "needToIndex": - - , - } - } - /> -

- - - -
- -`; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/empty_state/empty_state.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/empty_state/empty_state.tsx deleted file mode 100644 index 43c3bf79026feb1..000000000000000 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/empty_state/empty_state.tsx +++ /dev/null @@ -1,90 +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 React from 'react'; - -import { EuiCallOut, EuiTextColor, EuiLink, EuiButton } from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n/react'; -import { IBasePath } from 'kibana/public'; - -export const EmptyState = ({ - onRefresh, - prependBasePath, -}: { - onRefresh: () => void; - prependBasePath: IBasePath['prepend']; -}) => ( -
- - } - > -

- - - - ), - learnHowLink: ( - - - - ), - getStartedLink: ( - - - - ), - }} - /> -

- - - - -
-
-); diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/__snapshots__/header.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/__snapshots__/header.test.tsx.snap index c4f735558b1f219..851e5cc4c2a7622 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/__snapshots__/header.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/__snapshots__/header.test.tsx.snap @@ -7,7 +7,7 @@ exports[`Header should mark the input as invalid 1`] = ` >

@@ -119,7 +119,7 @@ exports[`Header should render normally 1`] = ` >

diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/header.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/header.tsx index f1bf0d54a1cbf79..8efa44decf3c601 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/header.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/header.tsx @@ -66,7 +66,7 @@ export const Header: React.FC = ({

@@ -127,6 +127,7 @@ export const Header: React.FC = ({ id="checkboxShowSystemIndices" checked={isIncludingSystemIndices} onChange={onChangeIncludingSystemIndices} + data-test-subj="showSystemAndHiddenIndices" /> ) : null} diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/__snapshots__/indices_list.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/__snapshots__/indices_list.test.tsx.snap index 598de4d90e89327..6631a9bbd1d02bc 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/__snapshots__/indices_list.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/__snapshots__/indices_list.test.tsx.snap @@ -2,7 +2,10 @@ exports[`IndicesList should change pages 1`] = `
- + - + - + - + - + - + {rows} diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/loading_indices/__snapshots__/loading_indices.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/loading_indices/__snapshots__/loading_indices.test.tsx.snap index 9d67ca913d415d4..a5517f6d4b6167b 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/loading_indices/__snapshots__/loading_indices.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/loading_indices/__snapshots__/loading_indices.test.tsx.snap @@ -3,47 +3,33 @@ exports[`LoadingIndices should render normally 1`] = ` - - - - - - - - - - - - +

+ + + + `; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/loading_indices/loading_indices.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/loading_indices/loading_indices.tsx index 16e8d1a9f3e9867..82603fd59859617 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/loading_indices/loading_indices.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/loading_indices/loading_indices.tsx @@ -19,34 +19,30 @@ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTextColor, EuiLoadingSpinner } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiLoadingSpinner } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; export const LoadingIndices = ({ ...rest }) => ( - + - - - - - - + +

- - - - - - - - +

+
+
+ +
); diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/status_message/status_message.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/status_message/status_message.tsx index 22b75071b93bb92..c2c4c84b516836c 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/status_message/status_message.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/status_message/status_message.tsx @@ -132,7 +132,7 @@ export const StatusMessage: React.FC = ({ /> ); - } else if (allIndicesLength) { + } else { statusIcon = undefined; statusColor = 'warning'; statusMessage = ( diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx index fab638509313dd5..d8555d71d6ec01c 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx @@ -159,7 +159,7 @@ export class StepIndexPattern extends Component indexPatternCreationType.getIndexTags(indexName), query, this.state.isIncludingSystemIndices ) @@ -175,13 +175,13 @@ export class StepIndexPattern extends Component indexPatternCreationType.getIndexTags(indexName), `${query}*`, this.state.isIncludingSystemIndices ), getIndices( this.context.services.http, - indexPatternCreationType, + (indexName: string) => indexPatternCreationType.getIndexTags(indexName), query, this.state.isIncludingSystemIndices ), @@ -227,7 +227,13 @@ export class StepIndexPattern extends Component; + return ( + <> + + + + + ); } renderStatusMessage(matchedIndices: { diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/advanced_options/__snapshots__/advanced_options.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/advanced_options/__snapshots__/advanced_options.test.tsx.snap index d1b10fb532020a4..a2d2023ea060167 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/advanced_options/__snapshots__/advanced_options.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/advanced_options/__snapshots__/advanced_options.test.tsx.snap @@ -7,7 +7,7 @@ exports[`AdvancedOptions should hide if not showing 1`] = ` onClick={[Function]} > @@ -25,7 +25,7 @@ exports[`AdvancedOptions should render normally 1`] = ` onClick={[Function]} > diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/advanced_options/advanced_options.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/advanced_options/advanced_options.tsx index b8f34095743ba09..752fcbcd42b5c4b 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/advanced_options/advanced_options.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/advanced_options/advanced_options.tsx @@ -45,12 +45,12 @@ export const AdvancedOptions: React.FC = ({ {isVisible ? ( ) : ( )} diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/header/__snapshots__/header.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/header/__snapshots__/header.test.tsx.snap index 2ac243780b31db3..9efda4fdac7f911 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/header/__snapshots__/header.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/header/__snapshots__/header.test.tsx.snap @@ -17,9 +17,18 @@ exports[`Header should render normally 1`] = ` size="m" /> - - ki* - + + ki* + , + "indexPatternName": "ki*", + } + } + /> `; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/header/header.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/header/header.tsx index c17b356e159f6a4..530d0688b61ca6d 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/header/header.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/header/header.tsx @@ -40,7 +40,14 @@ export const Header: React.FC = ({ indexPattern, indexPatternName } - {indexPattern} + {indexPattern}, + indexPatternName, + }} + /> ); diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/time_field.scss b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/time_field.scss deleted file mode 100644 index 5bd60e8b76afc40..000000000000000 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/time_field.scss +++ /dev/null @@ -1,7 +0,0 @@ -/** - * 1. Bring the line-height down or else this link expands its container when it becomes visible. - */ - -.timeFieldRefreshButton { - line-height: 1 !important; /* 1 */ -} diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/time_field.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/time_field.tsx index 7a3d72551f46417..1eae1055ac5efae 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/time_field.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/time_field.tsx @@ -17,8 +17,6 @@ * under the License. */ -import './time_field.scss'; - import React from 'react'; import { diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.test.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.test.tsx index b14cd526d7e2766..af5618424bbc085 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.test.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.test.tsx @@ -27,7 +27,6 @@ jest.mock('./components/step_index_pattern', () => ({ StepIndexPattern: 'StepInd jest.mock('./components/step_time_field', () => ({ StepTimeField: 'StepTimeField' })); jest.mock('./components/header', () => ({ Header: 'Header' })); jest.mock('./components/loading_state', () => ({ LoadingState: 'LoadingState' })); -jest.mock('./components/empty_state', () => ({ EmptyState: 'EmptyState' })); jest.mock('./lib/get_indices', () => ({ getIndices: () => { return [{ name: 'kibana' }]; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx index cd76ca09ccb741d..a789ebbfadbceef 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx @@ -33,7 +33,6 @@ import { StepIndexPattern } from './components/step_index_pattern'; import { StepTimeField } from './components/step_time_field'; import { Header } from './components/header'; import { LoadingState } from './components/loading_state'; -import { EmptyState } from './components/empty_state'; import { context as contextType } from '../../../../kibana_react/public'; import { getCreateBreadcrumbs } from '../breadcrumbs'; @@ -125,7 +124,13 @@ export class CreateIndexPatternWizard extends Component< // query local and remote indices, updating state independently ensureMinimumTime( this.catchAndWarn( - getIndices(this.context.services.http, this.state.indexPatternCreationType, `*`, false), + getIndices( + this.context.services.http, + (indexName: string) => this.state.indexPatternCreationType.getIndexTags(indexName), + `*`, + false + ), + [], indicesFailMsg ) @@ -136,7 +141,13 @@ export class CreateIndexPatternWizard extends Component< this.catchAndWarn( // if we get an error from remote cluster query, supply fallback value that allows user entry. // ['a'] is fallback value - getIndices(this.context.services.http, this.state.indexPatternCreationType, `*:*`, false), + getIndices( + this.context.services.http, + (indexName: string) => this.state.indexPatternCreationType.getIndexTags(indexName), + `*:*`, + false + ), + ['a'], clustersFailMsg ).then((remoteIndices: string[] | MatchedItem[]) => @@ -200,39 +211,24 @@ export class CreateIndexPatternWizard extends Component< }; renderHeader() { + const { docLinks, indexPatternCreationType } = this.state; return (
); } renderContent() { - const { - allIndices, - isInitiallyLoadingIndices, - step, - indexPattern, - remoteClustersExist, - } = this.state; + const { allIndices, isInitiallyLoadingIndices, step, indexPattern } = this.state; if (isInitiallyLoadingIndices) { return ; } - const hasDataIndices = allIndices.some(({ name }: MatchedItem) => !name.startsWith('.')); - if (!hasDataIndices && !remoteClustersExist) { - return ( - - ); - } - const header = this.renderHeader(); if (step === 1) { diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.test.ts b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.test.ts index 4cd28090420a7e0..8f3765b7b5dcc57 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.test.ts +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.test.ts @@ -37,7 +37,7 @@ describe('extractTimeFields', () => { expect(extractTimeFields(fields)).toEqual([ { display: '@timestamp', fieldName: '@timestamp' }, { isDisabled: true, display: '───', fieldName: '' }, - { display: `I don't want to use the Time Filter`, fieldName: undefined }, + { display: `I don't want to use the time filter`, fieldName: undefined }, ]); }); }); diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.ts b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.ts index b7056ad17343aff..efac21245c2578a 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.ts +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.ts @@ -45,7 +45,7 @@ export function extractTimeFields(fields: IFieldType[]) { const noTimeFieldLabel = i18n.translate( 'indexPatternManagement.createIndexPattern.stepTime.noTimeFieldOptionLabel', { - defaultMessage: "I don't want to use the Time Filter", + defaultMessage: "I don't want to use the time filter", } ); const noTimeFieldOption = { diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.test.ts b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.test.ts index 8e4dd37284333ce..44a2d1a3be0d053 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.test.ts +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.test.ts @@ -18,7 +18,6 @@ */ import { getIndices, responseToItemArray } from './get_indices'; -import { IndexPatternCreationConfig } from '../../../../../index_pattern_management/public'; import { httpServiceMock } from '../../../../../../core/public/mocks'; import { ResolveIndexResponseItemIndexAttrs } from '../types'; @@ -44,33 +43,27 @@ export const successfulResponse = { ], }; -const mockIndexPatternCreationType = new IndexPatternCreationConfig({ - type: 'default', - name: 'name', - showSystemIndices: false, - httpClient: {}, - isBeta: false, -}); +const mockGetTags = () => []; const http = httpServiceMock.createStartContract(); http.get.mockResolvedValue(successfulResponse); describe('getIndices', () => { it('should work in a basic case', async () => { - const result = await getIndices(http, mockIndexPatternCreationType, 'kibana', false); + const result = await getIndices(http, mockGetTags, 'kibana', false); expect(result.length).toBe(3); expect(result[0].name).toBe('f-alias'); expect(result[1].name).toBe('foo'); }); it('should ignore ccs query-all', async () => { - expect((await getIndices(http, mockIndexPatternCreationType, '*:', false)).length).toBe(0); + expect((await getIndices(http, mockGetTags, '*:', false)).length).toBe(0); }); it('should ignore a single comma', async () => { - expect((await getIndices(http, mockIndexPatternCreationType, ',', false)).length).toBe(0); - expect((await getIndices(http, mockIndexPatternCreationType, ',*', false)).length).toBe(0); - expect((await getIndices(http, mockIndexPatternCreationType, ',foobar', false)).length).toBe(0); + expect((await getIndices(http, mockGetTags, ',', false)).length).toBe(0); + expect((await getIndices(http, mockGetTags, ',*', false)).length).toBe(0); + expect((await getIndices(http, mockGetTags, ',foobar', false)).length).toBe(0); }); it('response object to item array', () => { @@ -98,8 +91,8 @@ describe('getIndices', () => { }, ], }; - expect(responseToItemArray(result, mockIndexPatternCreationType)).toMatchSnapshot(); - expect(responseToItemArray({}, mockIndexPatternCreationType)).toEqual([]); + expect(responseToItemArray(result, mockGetTags)).toMatchSnapshot(); + expect(responseToItemArray({}, mockGetTags)).toEqual([]); }); describe('errors', () => { @@ -107,7 +100,7 @@ describe('getIndices', () => { http.get.mockImplementationOnce(() => { throw new Error('Test error'); }); - const result = await getIndices(http, mockIndexPatternCreationType, 'kibana', false); + const result = await getIndices(http, mockGetTags, 'kibana', false); expect(result.length).toBe(0); }); }); diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.ts b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.ts index c6a11de1bc4fc0c..6844e90316e226f 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.ts +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.ts @@ -38,7 +38,7 @@ const frozenLabel = i18n.translate('indexPatternManagement.frozenLabel', { export async function getIndices( http: HttpStart, - indexPatternCreationType: IndexPatternCreationConfig, + getIndexTags: IndexPatternCreationConfig['getIndexTags'], rawPattern: string, showAllIndices: boolean ): Promise { @@ -73,7 +73,7 @@ export async function getIndices( return []; } - return responseToItemArray(response, indexPatternCreationType); + return responseToItemArray(response, getIndexTags); } catch { return []; } @@ -81,7 +81,7 @@ export async function getIndices( export const responseToItemArray = ( response: ResolveIndexResponse, - indexPatternCreationType: IndexPatternCreationConfig + getIndexTags: IndexPatternCreationConfig['getIndexTags'] ): MatchedItem[] => { const source: MatchedItem[] = []; @@ -89,7 +89,7 @@ export const responseToItemArray = ( const tags: MatchedItem['tags'] = [{ key: 'index', name: indexLabel, color: 'default' }]; const isFrozen = (index.attributes || []).includes(ResolveIndexResponseItemIndexAttrs.FROZEN); - tags.push(...indexPatternCreationType.getIndexTags(index.name)); + tags.push(...getIndexTags(index.name)); if (isFrozen) { tags.push({ name: frozenLabel, key: 'frozen', color: 'danger' }); } diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx index 4b538af7c5fe79c..987e8f0dae3a0cf 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -27,7 +27,6 @@ import { EuiBadge, EuiText, EuiLink, - EuiIcon, EuiCallOut, EuiPanel, } from '@elastic/eui'; @@ -162,7 +161,7 @@ export const EditIndexPattern = withRouter( const timeFilterHeader = i18n.translate( 'indexPatternManagement.editIndexPattern.timeFilterHeader', { - defaultMessage: "Time Filter field name: '{timeFieldName}'", + defaultMessage: "Time field: '{timeFieldName}'", values: { timeFieldName: indexPattern.timeFieldName }, } ); @@ -187,62 +186,55 @@ export const EditIndexPattern = withRouter( return (
- - - - - {showTagsSection && ( - - {Boolean(indexPattern.timeFieldName) && ( - - {timeFilterHeader} - - )} - {tags.map((tag: any) => ( - - {tag.name} - - ))} - + + + {showTagsSection && ( + + {Boolean(indexPattern.timeFieldName) && ( + + {timeFilterHeader} + )} - - -

- {indexPattern.title} }} - />{' '} - - {mappingAPILink} - - -

-
- {conflictedFields.length > 0 && ( - -

{mappingConflictLabel}

-
- )} -
- - - -
+ {tags.map((tag: any) => ( + + {tag.name} + + ))} + + )} + + +

+ {indexPattern.title} }} + />{' '} + + {mappingAPILink} + +

+
+ {conflictedFields.length > 0 && ( + <> + + +

{mappingConflictLabel}

+
+ + )} + +
); diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx index 4cf43d63d583972..8ca8c6453c7e96d 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx @@ -19,14 +19,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { - EuiFlexGroup, - EuiToolTip, - EuiFlexItem, - EuiIcon, - EuiTitle, - EuiButtonIcon, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiToolTip, EuiFlexItem, EuiTitle, EuiButtonIcon } from '@elastic/eui'; import { IIndexPattern } from 'src/plugins/data/public'; interface IndexHeaderProps { @@ -77,22 +70,13 @@ export function IndexHeader({ return ( - - {defaultIndex === indexPattern.id && ( - - - - )} - - -

{indexPattern.title}

-
-
-
+ +

{indexPattern.title}

+
- - {setDefault && ( + + {defaultIndex !== indexPattern.id && setDefault && ( setFieldFilter(e.target.value)} diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_help/test_script.scss b/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_help/test_script.scss index 34e8a60d070745b..ca230711827dc79 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_help/test_script.scss +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_help/test_script.scss @@ -1,5 +1,5 @@ .testScript__searchBar { - .globalQueryBar { + .globalQueryBar { padding: $euiSize 0 0; } } diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/__snapshots__/empty_index_pattern_prompt.test.tsx.snap b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/__snapshots__/empty_index_pattern_prompt.test.tsx.snap new file mode 100644 index 000000000000000..c5e6d1220d8bf8d --- /dev/null +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/__snapshots__/empty_index_pattern_prompt.test.tsx.snap @@ -0,0 +1,99 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EmptyIndexPatternPrompt should render normally 1`] = ` + + + + + + + +

+ +
+ +

+

+ +

+ + + +
+
+
+ + + + + + + + + + + +
+`; diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/assets/index_pattern_illustration.scss b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/assets/index_pattern_illustration.scss new file mode 100644 index 000000000000000..cd0477aba7adf24 --- /dev/null +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/assets/index_pattern_illustration.scss @@ -0,0 +1,9 @@ +.indexPatternIllustration { + &__verticalStripes { + fill: $euiColorFullShade; + } + + &__dots { + fill: $euiColorLightShade; + } +} diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/assets/index_pattern_illustration.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/assets/index_pattern_illustration.tsx new file mode 100644 index 000000000000000..2461c0f5df9194e --- /dev/null +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/assets/index_pattern_illustration.tsx @@ -0,0 +1,551 @@ +/* + * 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 './index_pattern_illustration.scss'; +import React from 'react'; + +const IndexPatternIllustration = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); + +export const Illustration = IndexPatternIllustration; diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/empty_index_pattern_prompt.scss b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/empty_index_pattern_prompt.scss new file mode 100644 index 000000000000000..11ac55b098a57c3 --- /dev/null +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/empty_index_pattern_prompt.scss @@ -0,0 +1,31 @@ +@import '../../../variables'; +@import '../../../templates'; + +.inpEmptyIndexPatternPrompt { + // override EUI specificity + max-width: $inpEmptyStateMaxWidth !important; // sass-lint:disable-line no-important +} + +.inpEmptyIndexPatternPrompt__footer { + @extend %inp-empty-state-footer; + // override EUI specificity + align-items: baseline !important; // sass-lint:disable-line no-important +} + +.inpEmptyIndexPatternPrompt__title { + // override EUI specificity + width: auto !important; // sass-lint:disable-line no-important +} + +@include euiBreakpoint('xs', 's') { + .inpEmptyIndexPatternPrompt__illustration > svg { + width: $euiSize * 12; + height: auto; + margin: 0 auto; + } + + .inpEmptyIndexPatternPrompt__text { + text-align: center; + align-items: center; + } +} diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/empty_state/empty_state.test.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/empty_index_pattern_prompt.test.tsx similarity index 57% rename from src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/empty_state/empty_state.test.tsx rename to src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/empty_index_pattern_prompt.test.tsx index 7fa39363e3ef72d..83eb803333afcfc 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/empty_state/empty_state.test.tsx +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/empty_index_pattern_prompt.test.tsx @@ -18,30 +18,20 @@ */ import React from 'react'; -import { EmptyState } from '../empty_state'; -import { shallow } from 'enzyme'; -import sinon from 'sinon'; +import { EmptyIndexPatternPrompt } from '../empty_index_pattern_prompt'; +import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; -describe('EmptyState', () => { +describe('EmptyIndexPatternPrompt', () => { it('should render normally', () => { - const component = shallow( {}} prependBasePath={(x) => x} />); + const component = shallowWithI18nProvider( + {} }]} + docLinksIndexPatternIntro={'testUrl'} + setBreadcrumbs={() => {}} + /> + ); expect(component).toMatchSnapshot(); }); - - describe('props', () => { - describe('onRefresh', () => { - it('is called when refresh button is clicked', () => { - const onRefreshHandler = sinon.stub(); - - const component = shallow( - x} /> - ); - - component.find('[data-test-subj="refreshIndicesButton"]').simulate('click'); - - sinon.assert.calledOnce(onRefreshHandler); - }); - }); - }); }); diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/empty_index_pattern_prompt.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/empty_index_pattern_prompt.tsx new file mode 100644 index 000000000000000..de389097fd4ba51 --- /dev/null +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/empty_index_pattern_prompt.tsx @@ -0,0 +1,111 @@ +/* + * 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 './empty_index_pattern_prompt.scss'; + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { EuiPageContent, EuiSpacer, EuiText, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { EuiDescriptionListTitle } from '@elastic/eui'; +import { EuiDescriptionListDescription, EuiDescriptionList } from '@elastic/eui'; +import { EuiLink } from '@elastic/eui'; +import { getListBreadcrumbs } from '../../breadcrumbs'; +import { IndexPatternCreationOption } from '../../types'; +import { CreateButton } from '../../create_button'; +import { Illustration } from './assets/index_pattern_illustration'; +import { ManagementAppMountParams } from '../../../../../management/public'; + +interface Props { + canSave: boolean; + creationOptions: IndexPatternCreationOption[]; + docLinksIndexPatternIntro: string; + setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; +} + +export const EmptyIndexPatternPrompt = ({ + canSave, + creationOptions, + docLinksIndexPatternIntro, + setBreadcrumbs, +}: Props) => { + setBreadcrumbs(getListBreadcrumbs()); + + return ( + + + + + + + +

+ +
+ +

+

+ +

+ {canSave && ( + + + + )} +
+
+
+ + + + + + + + + + + +
+ ); +}; diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/index.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/index.tsx new file mode 100644 index 000000000000000..239bb272b23abcc --- /dev/null +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/index.tsx @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { EmptyIndexPatternPrompt } from './empty_index_pattern_prompt'; diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap new file mode 100644 index 000000000000000..645694371f90596 --- /dev/null +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap @@ -0,0 +1,216 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EmptyState should render normally 1`] = ` + + + + + +

+ +

+
+
+
+ + + + + + } + icon={ + + } + onClick={[Function]} + title={ + + } + /> + + + + } + icon={ + + } + isDisabled={false} + onClick={[Function]} + title={ + + } + /> + + + + } + icon={ + + } + onClick={[Function]} + title={ + + } + /> + + + +
+ + + + + , + "title": , + }, + ] + } + /> + + + + + + + , + "title": , + }, + ] + } + /> + + +
+
+
+ + + + + , + } + } + /> + +
+`; diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.scss b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.scss new file mode 100644 index 000000000000000..37889b9d7c48344 --- /dev/null +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.scss @@ -0,0 +1,23 @@ +@import '../../../variables'; +@import '../../../templates'; + +.inpEmptyState { + // override EUI specificity + max-width: $inpEmptyStateMaxWidth !important; // sass-lint:disable-line no-important +} + +.inpEmptyState__cardGrid { + justify-content: center; +} + +.inpEmptyState__card { + min-width: $euiSizeXL * 6; +} + +.inpEmptyState__footer { + @extend %inp-empty-state-footer; +} + +.inpEmptyState__footerFlexItem { + min-width: $euiSizeXL * 7; +} diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.test.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.test.tsx new file mode 100644 index 000000000000000..7b2cc0f4c3c60e4 --- /dev/null +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.test.tsx @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EmptyState } from '../empty_state'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; +// @ts-expect-error +import { findTestSubject } from '@elastic/eui/lib/test'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { docLinksServiceMock } from '../../../../../../core/public/mocks'; +import { MlCardState } from '../../../types'; + +const docLinks = docLinksServiceMock.createStartContract(); + +jest.mock('react-router-dom', () => ({ + useHistory: () => ({ + createHref: jest.fn(), + }), +})); + +describe('EmptyState', () => { + it('should render normally', () => { + const component = shallow( + {}} + navigateToApp={async () => {}} + getMlCardState={() => MlCardState.ENABLED} + canSave={true} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + describe('props', () => { + describe('onRefresh', () => { + it('is called when refresh button is clicked', () => { + const onRefreshHandler = sinon.stub(); + + const component = mountWithIntl( + {}} + getMlCardState={() => MlCardState.ENABLED} + canSave={true} + /> + ); + + findTestSubject(component, 'refreshIndicesButton').simulate('click'); + + sinon.assert.calledOnce(onRefreshHandler); + }); + }); + }); +}); diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx new file mode 100644 index 000000000000000..e758184f0f14b70 --- /dev/null +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx @@ -0,0 +1,234 @@ +/* + * 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 './empty_state.scss'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { DocLinksStart, ApplicationStart } from 'kibana/public'; +import { + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiTitle, + EuiPageContentBody, + EuiPageContent, + EuiIcon, + EuiSpacer, + EuiFlexItem, + EuiDescriptionList, + EuiFlexGrid, + EuiCard, + EuiLink, + EuiText, +} from '@elastic/eui'; +import { useHistory } from 'react-router-dom'; +import { reactRouterNavigate } from '../../../../../../plugins/kibana_react/public'; +import { MlCardState } from '../../../types'; + +export const EmptyState = ({ + onRefresh, + navigateToApp, + docLinks, + getMlCardState, + canSave, +}: { + onRefresh: () => void; + navigateToApp: ApplicationStart['navigateToApp']; + docLinks: DocLinksStart; + getMlCardState: () => MlCardState; + canSave: boolean; +}) => { + const mlCard = ( + + navigateToApp('ml', { path: '#/filedatavisualizer' })} + className="inpEmptyState__card" + betaBadgeLabel={ + getMlCardState() === MlCardState.ENABLED + ? undefined + : i18n.translate( + 'indexPatternManagement.createIndexPattern.emptyState.basicLicenseLabel', + { + defaultMessage: 'Basic', + } + ) + } + betaBadgeTooltipContent={i18n.translate( + 'indexPatternManagement.createIndexPattern.emptyState.basicLicenseDescription', + { + defaultMessage: 'This feature requires a Basic license.', + } + )} + isDisabled={getMlCardState() === MlCardState.DISABLED} + icon={} + title={ + + } + description={ + + } + /> + + ); + + const createAnyway = ( + + + + + ), + }} + /> + + ); + + return ( + <> + + + + +

+ +

+
+
+
+ + + + + navigateToApp('home', { path: '#/tutorial_directory' })} + icon={} + title={ + + } + description={ + + } + /> + + {getMlCardState() !== MlCardState.HIDDEN ? mlCard : <>} + + navigateToApp('home', { path: '#/tutorial_directory/sampleData' })} + icon={} + title={ + + } + description={ + + } + /> + + + +
+ + + + ), + description: ( + + + + ), + }, + ]} + /> + + + + ), + description: ( + + {' '} + + + ), + }, + ]} + /> + + +
+
+
+ + {canSave && createAnyway} + + ); +}; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/empty_state/index.ts b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/empty_state/index.ts rename to src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/index.ts diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx index 947882b8df495b2..2768314a7586044 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx @@ -20,14 +20,14 @@ import { EuiBadge, EuiButtonEmpty, - EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, - EuiPanel, EuiSpacer, EuiText, EuiBadgeGroup, + EuiPageContent, + EuiTitle, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { withRouter, RouteComponentProps } from 'react-router-dom'; @@ -36,10 +36,13 @@ import { i18n } from '@kbn/i18n'; import { reactRouterNavigate, useKibana } from '../../../../../plugins/kibana_react/public'; import { IndexPatternManagmentContext } from '../../types'; import { CreateButton } from '../create_button'; -import { CreateIndexPatternPrompt } from '../create_index_pattern_prompt'; import { IndexPatternTableItem, IndexPatternCreationOption } from '../types'; import { getIndexPatterns } from '../utils'; import { getListBreadcrumbs } from '../breadcrumbs'; +import { EmptyState } from './empty_state'; +import { MatchedItem, ResolveIndexResponseItemAlias } from '../create_index_pattern_wizard/types'; +import { EmptyIndexPatternPrompt } from './empty_index_pattern_prompt'; +import { getIndices } from '../create_index_pattern_wizard/lib'; const pagination = { initialPageSize: 10, @@ -81,13 +84,19 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { uiSettings, indexPatternManagementStart, chrome, + docLinks, + application, + http, + getMlCardState, } = useKibana().services; - const [showFlyout, setShowFlyout] = useState(false); const [indexPatterns, setIndexPatterns] = useState([]); const [creationOptions, setCreationOptions] = useState([]); + const [sources, setSources] = useState([]); + const [remoteClustersExist, setRemoteClustersExist] = useState(false); + const [isLoadingSources, setIsLoadingSources] = useState(true); + const [isLoadingIndexPatterns, setIsLoadingIndexPatterns] = useState(true); setBreadcrumbs(getListBreadcrumbs()); - useEffect(() => { (async function () { const options = await indexPatternManagementStart.creation.getIndexPatternCreationOptions( @@ -98,9 +107,9 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { uiSettings.get('defaultIndex'), indexPatternManagementStart ); + setIsLoadingIndexPatterns(false); setCreationOptions(options); setIndexPatterns(gettedIndexPatterns); - setShowFlyout(gettedIndexPatterns.length === 0); })(); }, [ history.push, @@ -110,6 +119,28 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { savedObjects.client, ]); + const removeAliases = (item: MatchedItem) => + !((item as unknown) as ResolveIndexResponseItemAlias).indices; + + const loadSources = () => { + getIndices(http, () => [], '*', false).then((dataSources) => + setSources(dataSources.filter(removeAliases)) + ); + getIndices(http, () => [], '*:*', false).then((dataSources) => + setRemoteClustersExist(!!dataSources.filter(removeAliases).length) + ); + }; + + useEffect(() => { + getIndices(http, () => [], '*', false).then((dataSources) => { + setSources(dataSources.filter(removeAliases)); + setIsLoadingSources(false); + }); + getIndices(http, () => [], '*:*', false).then((dataSources) => + setRemoteClustersExist(!!dataSources.filter(removeAliases).length) + ); + }, [http, creationOptions]); + chrome.docTitle.change(title); const columns = [ @@ -130,12 +161,11 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { {name} +   {index.tags && index.tags.map(({ key: tagKey, name: tagName }) => ( - - {tagName} - + {tagName} ))} @@ -156,31 +186,51 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { <> ); + if (isLoadingSources || isLoadingIndexPatterns) { + return <>; + } + + const hasDataIndices = sources.some(({ name }: MatchedItem) => !name.startsWith('.')); + + if (!indexPatterns.length) { + if (!hasDataIndices && !remoteClustersExist) { + return ( + + ); + } else { + return ( + + ); + } + } + return ( - - {showFlyout && setShowFlyout(false)} />} + - - - - -

{title}

-
-
- - setShowFlyout(true)} - aria-label="Help" + + +

{title}

+
+ + +

+ - - +

+
{createButton}
@@ -195,7 +245,7 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { sorting={sorting} search={search} /> -
+ ); }; diff --git a/src/plugins/index_pattern_management/public/index.ts b/src/plugins/index_pattern_management/public/index.ts index 2d6db13757eea57..9a0fd39fb4fd919 100644 --- a/src/plugins/index_pattern_management/public/index.ts +++ b/src/plugins/index_pattern_management/public/index.ts @@ -41,3 +41,5 @@ export { IndexPatternCreationOption, IndexPatternListConfig, } from './service'; + +export { MlCardState } from './types'; diff --git a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx index bcabd55c8797500..add45a07e0c5fbb 100644 --- a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx +++ b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx @@ -34,7 +34,7 @@ import { CreateIndexPatternWizardWithRouter, } from '../components'; import { IndexPatternManagementStartDependencies, IndexPatternManagementStart } from '../plugin'; -import { IndexPatternManagmentContext } from '../types'; +import { IndexPatternManagmentContext, MlCardState } from '../types'; const readOnlyBadge = { text: i18n.translate('indexPatternManagement.indexPatterns.badge.readOnly.text', { @@ -48,7 +48,8 @@ const readOnlyBadge = { export async function mountManagementSection( getStartServices: StartServicesAccessor, - params: ManagementAppMountParams + params: ManagementAppMountParams, + getMlCardState: () => MlCardState ) { const [ { chrome, application, savedObjects, uiSettings, notifications, overlays, http, docLinks }, @@ -73,6 +74,7 @@ export async function mountManagementSection( data, indexPatternManagementStart: indexPatternManagementStart as IndexPatternManagementStart, setBreadcrumbs: params.setBreadcrumbs, + getMlCardState, }; ReactDOM.render( diff --git a/src/plugins/index_pattern_management/public/mocks.ts b/src/plugins/index_pattern_management/public/mocks.ts index ec8100db4208514..6a9ef23e3732e66 100644 --- a/src/plugins/index_pattern_management/public/mocks.ts +++ b/src/plugins/index_pattern_management/public/mocks.ts @@ -39,6 +39,9 @@ const createSetupContract = (): IndexPatternManagementSetup => ({ getAll: jest.fn(), getById: jest.fn(), } as any, + environment: { + update: jest.fn(), + }, }); const createStartContract = (): IndexPatternManagementStart => ({ diff --git a/src/plugins/index_pattern_management/public/plugin.ts b/src/plugins/index_pattern_management/public/plugin.ts index fe680eff8657e2e..ee1e00fcafd9804 100644 --- a/src/plugins/index_pattern_management/public/plugin.ts +++ b/src/plugins/index_pattern_management/public/plugin.ts @@ -86,7 +86,9 @@ export class IndexPatternManagementPlugin mount: async (params) => { const { mountManagementSection } = await import('./management_app'); - return mountManagementSection(core.getStartServices, params); + return mountManagementSection(core.getStartServices, params, () => + this.indexPatternManagementService.environmentService.getEnvironment().ml() + ); }, }); diff --git a/src/plugins/index_pattern_management/public/service/environment/environment.mock.ts b/src/plugins/index_pattern_management/public/service/environment/environment.mock.ts new file mode 100644 index 000000000000000..2c2c68b8ead2d64 --- /dev/null +++ b/src/plugins/index_pattern_management/public/service/environment/environment.mock.ts @@ -0,0 +1,44 @@ +/* + * 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 { EnvironmentService, EnvironmentServiceSetup } from './environment'; +import { MlCardState } from '../../types'; + +const createSetupMock = (): jest.Mocked => { + const setup = { + update: jest.fn(), + }; + return setup; +}; + +const createMock = (): jest.Mocked> => { + const service = { + setup: jest.fn(), + getEnvironment: jest.fn(() => ({ + ml: () => MlCardState.ENABLED, + })), + }; + service.setup.mockImplementation(createSetupMock); + return service; +}; + +export const environmentServiceMock = { + createSetup: createSetupMock, + create: createMock, +}; diff --git a/src/plugins/index_pattern_management/public/service/environment/environment.test.ts b/src/plugins/index_pattern_management/public/service/environment/environment.test.ts new file mode 100644 index 000000000000000..1aa67ba751b8119 --- /dev/null +++ b/src/plugins/index_pattern_management/public/service/environment/environment.test.ts @@ -0,0 +1,49 @@ +/* + * 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 { EnvironmentService } from './environment'; +import { MlCardState } from '../../types'; + +describe('EnvironmentService', () => { + describe('setup', () => { + test('allows multiple update calls', () => { + const setup = new EnvironmentService().setup(); + expect(() => { + setup.update({ ml: () => MlCardState.ENABLED }); + }).not.toThrow(); + }); + }); + + describe('getEnvironment', () => { + test('returns default values', () => { + const service = new EnvironmentService(); + expect(service.getEnvironment().ml()).toEqual(MlCardState.DISABLED); + }); + + test('returns last state of update calls', () => { + let cardState = MlCardState.DISABLED; + const service = new EnvironmentService(); + const setup = service.setup(); + setup.update({ ml: () => cardState }); + expect(service.getEnvironment().ml()).toEqual(MlCardState.DISABLED); + cardState = MlCardState.ENABLED; + expect(service.getEnvironment().ml()).toEqual(MlCardState.ENABLED); + }); + }); +}); diff --git a/src/plugins/index_pattern_management/public/service/environment/environment.ts b/src/plugins/index_pattern_management/public/service/environment/environment.ts new file mode 100644 index 000000000000000..f40ce3589fa76ab --- /dev/null +++ b/src/plugins/index_pattern_management/public/service/environment/environment.ts @@ -0,0 +1,52 @@ +/* + * 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 { MlCardState } from '../../types'; + +/** @public */ +export interface Environment { + /** + * Flag whether ml features should be advertised + */ + readonly ml: () => MlCardState; +} + +export class EnvironmentService { + private environment = { + ml: () => MlCardState.DISABLED, + }; + + public setup() { + return { + /** + * Update the environment to influence how available features are presented. + * @param update + */ + update: (update: Partial) => { + this.environment = Object.assign({}, this.environment, update); + }, + }; + } + + public getEnvironment() { + return this.environment; + } +} + +export type EnvironmentServiceSetup = ReturnType; diff --git a/src/plugins/index_pattern_management/public/service/environment/index.ts b/src/plugins/index_pattern_management/public/service/environment/index.ts new file mode 100644 index 000000000000000..91d14c358e7db8e --- /dev/null +++ b/src/plugins/index_pattern_management/public/service/environment/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { EnvironmentService, Environment, EnvironmentServiceSetup } from './environment'; diff --git a/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts index d4cc9c95e76a7ee..06b9b83b1b60161 100644 --- a/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts +++ b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts @@ -21,6 +21,7 @@ import { HttpSetup } from '../../../../core/public'; import { IndexPatternCreationManager, IndexPatternCreationConfig } from './creation'; import { IndexPatternListManager, IndexPatternListConfig } from './list'; import { FieldFormatEditors } from './field_format_editors'; +import { EnvironmentService } from './environment'; import { BytesFormatEditor, @@ -49,11 +50,13 @@ export class IndexPatternManagementService { indexPatternCreationManager: IndexPatternCreationManager; indexPatternListConfig: IndexPatternListManager; fieldFormatEditors: FieldFormatEditors; + environmentService: EnvironmentService; constructor() { this.indexPatternCreationManager = new IndexPatternCreationManager(); this.indexPatternListConfig = new IndexPatternListManager(); this.fieldFormatEditors = new FieldFormatEditors(); + this.environmentService = new EnvironmentService(); } public setup({ httpClient }: SetupDependencies) { @@ -83,6 +86,7 @@ export class IndexPatternManagementService { creation: creationManagerSetup, list: indexPatternListConfigSetup, fieldFormatEditors: fieldFormatEditorsSetup, + environment: this.environmentService.setup(), }; } diff --git a/src/plugins/index_pattern_management/public/types.ts b/src/plugins/index_pattern_management/public/types.ts index 97941687e652dea..2876bd622735072 100644 --- a/src/plugins/index_pattern_management/public/types.ts +++ b/src/plugins/index_pattern_management/public/types.ts @@ -44,8 +44,15 @@ export interface IndexPatternManagmentContext { data: DataPublicPluginStart; indexPatternManagementStart: IndexPatternManagementStart; setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; + getMlCardState: () => MlCardState; } export type IndexPatternManagmentContextValue = KibanaReactContextValue< IndexPatternManagmentContext >; + +export enum MlCardState { + HIDDEN, + DISABLED, + ENABLED, +} diff --git a/src/plugins/vis_type_vega/public/_vega_vis.scss b/src/plugins/vis_type_vega/public/_vega_vis.scss index f9468d677eeed9e..12108c7ba3de0b5 100644 --- a/src/plugins/vis_type_vega/public/_vega_vis.scss +++ b/src/plugins/vis_type_vega/public/_vega_vis.scss @@ -107,6 +107,7 @@ .vgaVis__tooltip { max-width: 100%; + position: fixed; h2 { margin-bottom: $euiSizeS; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js b/src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js index 01386fd91abc5ae..7b0274478cea2df 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js @@ -85,12 +85,12 @@ export class TooltipHandler { let anchorBounds; if (item.bounds.width() > this.centerOnMark || item.bounds.height() > this.centerOnMark) { // I would expect clientX/Y, but that shows incorrectly - anchorBounds = createRect(event.pageX, event.pageY, 0, 0); + anchorBounds = createRect(event.clientX, event.clientY, 0, 0); } else { const containerBox = this.container.getBoundingClientRect(); anchorBounds = createRect( - containerBox.left + view._origin[0] + item.bounds.x1 + window.pageXOffset, - containerBox.top + view._origin[1] + item.bounds.y1 + window.pageYOffset, + containerBox.left + view._origin[0] + item.bounds.x1, + containerBox.top + view._origin[1] + item.bounds.y1, item.bounds.width(), item.bounds.height() ); diff --git a/test/functional/apps/management/_index_patterns_empty.ts b/test/functional/apps/management/_index_patterns_empty.ts new file mode 100644 index 000000000000000..4ae2e7836ac376e --- /dev/null +++ b/test/functional/apps/management/_index_patterns_empty.ts @@ -0,0 +1,66 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['common', 'settings']); + const testSubjects = getService('testSubjects'); + const globalNav = getService('globalNav'); + const es = getService('legacyEs'); + + describe('index pattern empty view', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + await kibanaServer.uiSettings.replace({}); + await PageObjects.settings.navigateTo(); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + await esArchiver.loadIfNeeded('makelogs'); + }); + + // create index pattern and return to verify list + it(`shows empty views`, async () => { + // @ts-expect-error + await es.transport.request({ + path: '/_all', + method: 'DELETE', + }); + await PageObjects.settings.clickKibanaIndexPatterns(); + await testSubjects.existOrFail('createAnyway'); + // @ts-expect-error + await es.transport.request({ + path: '/logstash-a/_doc', + method: 'POST', + body: { user: 'matt', message: 20 }, + }); + await testSubjects.click('refreshIndicesButton'); + await testSubjects.existOrFail('createIndexPatternButton', { timeout: 5000 }); + await PageObjects.settings.createIndexPattern('logstash-*', ''); + }); + + it(`doesn't show read-only badge`, async () => { + await globalNav.badgeMissingOrFail(); + }); + }); +} diff --git a/test/functional/apps/management/_kibana_settings.js b/test/functional/apps/management/_kibana_settings.js index 2a488a94c688962..e2b20bacc0b3977 100644 --- a/test/functional/apps/management/_kibana_settings.js +++ b/test/functional/apps/management/_kibana_settings.js @@ -28,7 +28,7 @@ export default function ({ getService, getPageObjects }) { before(async function () { // delete .kibana index and then wait for Kibana to re-create it await kibanaServer.uiSettings.replace({}); - await PageObjects.settings.createIndexPattern(); + await PageObjects.settings.createIndexPattern('logstash-*'); await PageObjects.settings.navigateTo(); }); diff --git a/test/functional/apps/management/index.js b/test/functional/apps/management/index.js index 97e7314f9678e73..d5f0c286af7a554 100644 --- a/test/functional/apps/management/index.js +++ b/test/functional/apps/management/index.js @@ -43,6 +43,7 @@ export default function ({ getService, loadTestFile }) { loadTestFile(require.resolve('./_scripted_fields')); loadTestFile(require.resolve('./_scripted_fields_preview')); loadTestFile(require.resolve('./_mgmt_import_saved_objects')); + loadTestFile(require.resolve('./_index_patterns_empty')); }); describe('', function () { diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 4b80647c8749dd5..a4285a5f94d51e2 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -55,15 +55,6 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider await testSubjects.click('indexPatterns'); await PageObjects.header.waitUntilLoadingHasFinished(); - - // check for the index pattern info flyout that covers the - // create index pattern button on smaller screens - // @ts-ignore - await retry.waitFor('index pattern info flyout', async () => { - if (await testSubjects.exists('CreateIndexPatternPrompt')) { - await testSubjects.click('CreateIndexPatternPrompt > euiFlyoutCloseButton'); - } else return true; - }); } async getAdvancedSettings(propertyName: string) { @@ -311,9 +302,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider } async isIndexPatternListEmpty() { - await testSubjects.existOrFail('indexPatternTable', { timeout: 5000 }); - const indexPatternList = await this.getIndexPatternList(); - return indexPatternList.length === 0; + return !(await testSubjects.exists('indexPatternTable', { timeout: 5000 })); } async removeLogstashIndexPatternIfExist() { diff --git a/x-pack/package.json b/x-pack/package.json index b426e790c2d47d7..2b52646e0f74832 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -255,8 +255,8 @@ "cronstrue": "^1.51.0", "cytoscape": "^3.10.0", "d3": "3.5.17", - "d3-scale": "1.0.7", "d3-array": "1.2.4", + "d3-scale": "1.0.7", "dedent": "^0.7.0", "del": "^5.1.0", "dragselect": "1.13.1", @@ -267,7 +267,7 @@ "font-awesome": "4.7.0", "formsy-react": "^1.1.5", "fp-ts": "^2.3.1", - "get-port": "4.2.0", + "get-port": "^4.2.0", "getos": "^3.1.0", "git-url-parse": "11.1.2", "github-markdown-css": "^2.10.0", diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts index bdf3f6a0acf90c7..7f6e3feac0671b0 100644 --- a/x-pack/plugins/data_enhanced/public/plugin.ts +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -31,20 +31,26 @@ export class DataEnhancedPlugin KUERY_LANGUAGE_NAME, setupKqlQuerySuggestionProvider(core) ); - } - public start(core: CoreStart, plugins: DataEnhancedStartDependencies) { - setAutocompleteService(plugins.data.autocomplete); const enhancedSearchInterceptor = new EnhancedSearchInterceptor( { toasts: core.notifications.toasts, - application: core.application, http: core.http, uiSettings: core.uiSettings, - usageCollector: plugins.data.search.usageCollector, + startServices: core.getStartServices(), + usageCollector: data.search.usageCollector, }, core.injectedMetadata.getInjectedVar('esRequestTimeout') as number ); - plugins.data.search.setInterceptor(enhancedSearchInterceptor); + + data.__enhance({ + search: { + searchInterceptor: enhancedSearchInterceptor, + }, + }); + } + + public start(core: CoreStart, plugins: DataEnhancedStartDependencies) { + setAutocompleteService(plugins.data.autocomplete); } } diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts index d004511fa467498..fe954f1602cd313 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts @@ -6,7 +6,7 @@ import { coreMock } from '../../../../../src/core/public/mocks'; import { EnhancedSearchInterceptor } from './search_interceptor'; -import { CoreStart } from 'kibana/public'; +import { CoreSetup, CoreStart } from 'kibana/public'; import { AbortError } from '../../../../../src/plugins/data/common'; const timeTravel = (msToRun = 0) => { @@ -19,13 +19,14 @@ const error = jest.fn(); const complete = jest.fn(); let searchInterceptor: EnhancedSearchInterceptor; +let mockCoreSetup: MockedKeys; let mockCoreStart: MockedKeys; jest.useFakeTimers(); function mockFetchImplementation(responses: any[]) { let i = 0; - mockCoreStart.http.fetch.mockImplementation(() => { + mockCoreSetup.http.fetch.mockImplementation(() => { const { time = 0, value = {}, isError = false } = responses[i++]; return new Promise((resolve, reject) => setTimeout(() => { @@ -39,6 +40,7 @@ describe('EnhancedSearchInterceptor', () => { let mockUsageCollector: any; beforeEach(() => { + mockCoreSetup = coreMock.createSetup(); mockCoreStart = coreMock.createStart(); next.mockClear(); @@ -54,12 +56,20 @@ describe('EnhancedSearchInterceptor', () => { trackLongQueryRunBeyondTimeout: jest.fn(), }; + const mockPromise = new Promise((resolve) => { + resolve([ + { + application: mockCoreStart.application, + }, + ]); + }); + searchInterceptor = new EnhancedSearchInterceptor( { - toasts: mockCoreStart.notifications.toasts, - application: mockCoreStart.application, - http: mockCoreStart.http, - uiSettings: mockCoreStart.uiSettings, + toasts: mockCoreSetup.notifications.toasts, + startServices: mockPromise as any, + http: mockCoreSetup.http, + uiSettings: mockCoreSetup.uiSettings, usageCollector: mockUsageCollector, }, 1000 @@ -229,8 +239,8 @@ describe('EnhancedSearchInterceptor', () => { expect(error).toHaveBeenCalled(); expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError); - expect(mockCoreStart.http.fetch).toHaveBeenCalledTimes(2); - expect(mockCoreStart.http.delete).toHaveBeenCalled(); + expect(mockCoreSetup.http.fetch).toHaveBeenCalledTimes(2); + expect(mockCoreSetup.http.delete).toHaveBeenCalled(); }); test('should not DELETE a running async search on async timeout prior to first response', async () => { @@ -253,8 +263,8 @@ describe('EnhancedSearchInterceptor', () => { expect(error).toHaveBeenCalled(); expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError); - expect(mockCoreStart.http.fetch).toHaveBeenCalled(); - expect(mockCoreStart.http.delete).not.toHaveBeenCalled(); + expect(mockCoreSetup.http.fetch).toHaveBeenCalled(); + expect(mockCoreSetup.http.delete).not.toHaveBeenCalled(); }); test('should DELETE a running async search on async timeout after first response', async () => { @@ -285,16 +295,16 @@ describe('EnhancedSearchInterceptor', () => { expect(next).toHaveBeenCalled(); expect(error).not.toHaveBeenCalled(); - expect(mockCoreStart.http.fetch).toHaveBeenCalled(); - expect(mockCoreStart.http.delete).not.toHaveBeenCalled(); + expect(mockCoreSetup.http.fetch).toHaveBeenCalled(); + expect(mockCoreSetup.http.delete).not.toHaveBeenCalled(); // Long enough to reach the timeout but not long enough to reach the next response await timeTravel(1000); expect(error).toHaveBeenCalled(); expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError); - expect(mockCoreStart.http.fetch).toHaveBeenCalledTimes(2); - expect(mockCoreStart.http.delete).toHaveBeenCalled(); + expect(mockCoreSetup.http.fetch).toHaveBeenCalledTimes(2); + expect(mockCoreSetup.http.delete).toHaveBeenCalled(); }); test('should DELETE a running async search on async timeout on error from fetch', async () => { @@ -327,16 +337,16 @@ describe('EnhancedSearchInterceptor', () => { expect(next).toHaveBeenCalled(); expect(error).not.toHaveBeenCalled(); - expect(mockCoreStart.http.fetch).toHaveBeenCalled(); - expect(mockCoreStart.http.delete).not.toHaveBeenCalled(); + expect(mockCoreSetup.http.fetch).toHaveBeenCalled(); + expect(mockCoreSetup.http.delete).not.toHaveBeenCalled(); // Long enough to reach the timeout but not long enough to reach the next response await timeTravel(10); expect(error).toHaveBeenCalled(); expect(error.mock.calls[0][0]).toBe(responses[1].value); - expect(mockCoreStart.http.fetch).toHaveBeenCalledTimes(2); - expect(mockCoreStart.http.delete).toHaveBeenCalled(); + expect(mockCoreSetup.http.fetch).toHaveBeenCalledTimes(2); + expect(mockCoreSetup.http.delete).toHaveBeenCalled(); }); }); @@ -367,7 +377,7 @@ describe('EnhancedSearchInterceptor', () => { await timeTravel(); - const areAllRequestsAborted = mockCoreStart.http.fetch.mock.calls.every( + const areAllRequestsAborted = mockCoreSetup.http.fetch.mock.calls.every( ([{ signal }]) => signal?.aborted ); expect(areAllRequestsAborted).toBe(true); diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts index bff9e2cb9048c83..ae6dddf33536f00 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -20,8 +20,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { /** * This class should be instantiated with a `requestTimeout` corresponding with how many ms after * requests are initiated that they should automatically cancel. - * @param toasts The `core.notifications.toasts` service - * @param application The `core.application` service + * @param deps `SearchInterceptorDeps` * @param requestTimeout Usually config value `elasticsearch.requestTimeout` */ constructor(deps: SearchInterceptorDeps, requestTimeout?: number) { @@ -78,7 +77,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { const { combinedSignal, cleanup } = this.setupTimers(options); const aborted$ = from(toPromise(combinedSignal)); - this.pendingCount$.next(++this.pendingCount); + this.pendingCount$.next(this.pendingCount$.getValue() + 1); return this.runSearch(request, combinedSignal, options?.strategy).pipe( expand((response) => { @@ -113,7 +112,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { }, }), finalize(() => { - this.pendingCount$.next(--this.pendingCount); + this.pendingCount$.next(this.pendingCount$.getValue() - 1); cleanup(); }) ); diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts index 6f82946c0ea1452..e999d40a3f8e660 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export { mockHistory } from './react_router_history.mock'; +export { mockHistory, mockLocation } from './react_router_history.mock'; export { mockKibanaContext } from './kibana_context.mock'; export { mockLicenseContext } from './license_context.mock'; export { diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts index fd422465d87f1bd..779eb1a043e8c8a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts @@ -5,7 +5,7 @@ */ /** - * NOTE: This variable name MUST start with 'mock*' in order for + * NOTE: These variable names MUST start with 'mock*' in order for * Jest to accept its use within a jest.mock() */ export const mockHistory = { @@ -15,9 +15,17 @@ export const mockHistory = { pathname: '/current-path', }, }; +export const mockLocation = { + key: 'someKey', + pathname: '/current-path', + search: '?query=something', + hash: '#hash', + state: {}, +}; jest.mock('react-router-dom', () => ({ useHistory: jest.fn(() => mockHistory), + useLocation: jest.fn(() => mockLocation), })); /** diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_state.tsx index d6c38629d814361..4d2b790e7fb9758 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_state.tsx @@ -5,7 +5,7 @@ */ import React, { useContext } from 'react'; -import { EuiPage, EuiPageBody, EuiPageContent, EuiEmptyPrompt, EuiButton } from '@elastic/eui'; +import { EuiPageContent, EuiEmptyPrompt, EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { sendTelemetry } from '../../../shared/telemetry'; @@ -32,43 +32,40 @@ export const EmptyState: React.FC = () => { }; return ( - + <> - - - - - - -

- } - titleSize="l" - body={ -

- -

- } - actions={ - - - - } - /> - - - + + + + + + } + titleSize="l" + body={ +

+ +

+ } + actions={ + + + + } + /> +
+ ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx index 3753ad5433e8fd0..c5a5f1fbb921f94 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; +import { EuiPageContent } from '@elastic/eui'; import { ErrorStatePrompt } from '../../../shared/error_state'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; @@ -16,16 +16,14 @@ import './empty_states.scss'; export const ErrorState: React.FC = () => { return ( - + <> - - - - - - - + + + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/loading_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/loading_state.tsx index 533dca7d0ab7956..221091b79dc54f7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/loading_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/loading_state.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { EuiPage, EuiPageBody, EuiPageContent, EuiSpacer, EuiLoadingContent } from '@elastic/eui'; +import { EuiPageContent, EuiSpacer, EuiLoadingContent } from '@elastic/eui'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { EngineOverviewHeader } from '../engine_overview_header'; @@ -14,17 +14,14 @@ import './empty_states.scss'; export const LoadingState: React.FC = () => { return ( - + <> - - - - - - - - - - + + + + + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.scss index 2c7f7de6458e2a8..e39bbbc95564b4f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.scss @@ -4,18 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -/** - * Engine Overview - */ .engineOverview { - width: 100%; - - &__body { - padding: $euiSize; + padding: $euiSize; - @include euiBreakpoint('m', 'l', 'xl') { - padding: $euiSizeXL; - } + @include euiBreakpoint('m', 'l', 'xl') { + padding: $euiSizeXL; } } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx index 286c32b2a443be8..acac5d17665b710 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx @@ -6,8 +6,6 @@ import React, { useContext, useEffect, useState } from 'react'; import { - EuiPage, - EuiPageBody, EuiPageContent, EuiPageContentHeader, EuiPageContentBody, @@ -92,64 +90,61 @@ export const EngineOverview: React.FC = () => { if (!engines.length) return ; return ( - + <> - - - - - - -

- - -

-
-
- - - - - {metaEngines.length > 0 && ( - <> - - - -

- - -

-
-
- - - - - )} -
-
-
+ + + + +

+ + +

+
+
+ + + + + {metaEngines.length > 0 && ( + <> + + + +

+ + +

+
+
+ + + + + )} +
+ ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 45e318ca0f9d958..9e660d10053ec40 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -10,37 +10,33 @@ import React, { useContext } from 'react'; import { Redirect } from 'react-router-dom'; import { shallow } from 'enzyme'; -import { SetupGuide } from './components/setup_guide'; -import { EngineOverview } from './components/engine_overview'; - -import { AppSearch } from './'; - -describe('App Search Routes', () => { - describe('/', () => { - it('redirects to Setup Guide when enterpriseSearchUrl is not set', () => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ enterpriseSearchUrl: '' })); - const wrapper = shallow(); - - expect(wrapper.find(Redirect)).toHaveLength(1); - expect(wrapper.find(EngineOverview)).toHaveLength(0); - }); - - it('renders Engine Overview when enterpriseSearchUrl is set', () => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ - enterpriseSearchUrl: 'https://foo.bar', - })); - const wrapper = shallow(); - - expect(wrapper.find(EngineOverview)).toHaveLength(1); - expect(wrapper.find(Redirect)).toHaveLength(0); - }); +import { Layout, SideNav, SideNavLink } from '../shared/layout'; +import { AppSearch, AppSearchNav } from './'; + +describe('AppSearch', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(Layout)).toHaveLength(1); + }); + + it('redirects to Setup Guide when enterpriseSearchUrl is not set', () => { + (useContext as jest.Mock).mockImplementationOnce(() => ({ enterpriseSearchUrl: '' })); + const wrapper = shallow(); + + expect(wrapper.find(Redirect)).toHaveLength(1); + expect(wrapper.find(Layout)).toHaveLength(0); }); +}); - describe('/setup_guide', () => { - it('renders', () => { - const wrapper = shallow(); +describe('AppSearchNav', () => { + it('renders', () => { + const wrapper = shallow(); - expect(wrapper.find(SetupGuide)).toHaveLength(1); - }); + expect(wrapper.find(SideNav)).toHaveLength(1); + expect(wrapper.find(SideNavLink).first().prop('to')).toEqual('/engines'); + expect(wrapper.find(SideNavLink).last().prop('to')).toEqual( + 'http://localhost:3002/as#/role-mappings' + ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index 8f7142f1631a958..d69b3ba29b0cab2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -5,24 +5,80 @@ */ import React, { useContext } from 'react'; -import { Route, Redirect } from 'react-router-dom'; +import { Route, Redirect, Switch } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; + +import { APP_SEARCH_PLUGIN } from '../../../common/constants'; import { KibanaContext, IKibanaContext } from '../index'; +import { Layout, SideNav, SideNavLink } from '../shared/layout'; import { SetupGuide } from './components/setup_guide'; import { EngineOverview } from './components/engine_overview'; export const AppSearch: React.FC = () => { const { enterpriseSearchUrl } = useContext(KibanaContext) as IKibanaContext; + if (!enterpriseSearchUrl) + return ( + + + + + + + {/* Kibana displays a blank page on redirect if this isn't included */} + + + ); return ( - <> - - {!enterpriseSearchUrl ? : } - - + + - + + }> + + + {/* For some reason a Redirect to /engines just doesn't work here - it shows a blank page */} + + + + + + + + + + ); +}; + +export const AppSearchNav: React.FC = () => { + const { enterpriseSearchUrl } = useContext(KibanaContext) as IKibanaContext; + const externalUrl = `${enterpriseSearchUrl}/as#`; + + return ( + + + {i18n.translate('xpack.enterpriseSearch.appSearch.nav.engines', { + defaultMessage: 'Engines', + })} + + + {i18n.translate('xpack.enterpriseSearch.appSearch.nav.settings', { + defaultMessage: 'Account Settings', + })} + + + {i18n.translate('xpack.enterpriseSearch.appSearch.nav.credentials', { + defaultMessage: 'Credentials', + })} + + + {i18n.translate('xpack.enterpriseSearch.appSearch.nav.roleMappings', { + defaultMessage: 'Role Mappings', + })} + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/layout/index.ts new file mode 100644 index 000000000000000..2211cdee6c73075 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { Layout } from './layout'; +export { SideNav, SideNavLink } from './side_nav'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.scss b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.scss new file mode 100644 index 000000000000000..c73a527147961a2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.scss @@ -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. + */ + +.enterpriseSearchLayout { + $sideBarWidth: $euiSize * 15; + $sideBarMobileHeight: $euiSize * 4.75; + + display: block; + position: relative; + left: $sideBarWidth; + width: calc(100% - #{$sideBarWidth}); + padding: 0; + + @include euiBreakpoint('xs', 's', 'm') { + left: auto; + width: 100%; + } + + &__sideBarToggle { + display: none; + + @include euiBreakpoint('xs', 's', 'm') { + display: block; + + position: absolute; + right: $euiSize; + top: $sideBarMobileHeight / 2; + transform: translateY(-50%) scale(0.9); + + .euiButton { + min-width: 0; + } + } + } + + &__sideBar { + z-index: $euiZNavigation; + position: fixed; + margin-left: -1 * $sideBarWidth; + margin-right: 0; + overflow-y: auto; + overflow-x: hidden; + + $kibanaHeader: 49px; // NOTE: Keep an eye on this for changes + height: calc(100vh - #{$kibanaHeader}); + width: $sideBarWidth; + + background-color: $euiColorLightestShade; + box-shadow: inset (-1 * $euiSizeXS) 0 $euiSizeS (-1 * $euiSizeXS) rgba($euiShadowColor, 0.25); + + @include euiBreakpoint('xs', 's', 'm') { + position: relative; + width: 100%; + height: $sideBarMobileHeight; + margin-left: 0; + overflow-y: hidden; + + border-bottom: $euiBorderThin; + box-shadow: none; + + &--isOpen { + height: auto; + overflow-y: auto; + } + } + } + + &__body { + padding: $euiSizeXXL; + + @include euiBreakpoint('m') { + padding: $euiSizeL; + } + @include euiBreakpoint('xs', 's') { + padding: $euiSize; + } + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx new file mode 100644 index 000000000000000..4053f2f4bb613e6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiPageSideBar, EuiButton } from '@elastic/eui'; + +import { Layout, INavContext } from './layout'; + +describe('Layout', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find('.enterpriseSearchLayout')).toHaveLength(1); + }); + + it('renders navigation', () => { + const wrapper = shallow(Hello World} />); + + expect(wrapper.find('.enterpriseSearchLayout__sideBar')).toHaveLength(1); + expect(wrapper.find('.nav-test')).toHaveLength(1); + }); + + it('renders navigation toggle state', () => { + const wrapper = shallow(Hello World} />); + expect(wrapper.find(EuiPageSideBar).prop('className')).not.toContain('--isOpen'); + expect(wrapper.find(EuiButton).prop('iconType')).toEqual('arrowRight'); + + const toggle = wrapper.find('[data-test-subj="enterpriseSearchNavToggle"]'); + toggle.simulate('click'); + + expect(wrapper.find(EuiPageSideBar).prop('className')).toContain('--isOpen'); + expect(wrapper.find(EuiButton).prop('iconType')).toEqual('arrowDown'); + }); + + it('passes down NavContext to navigation links', () => { + const wrapper = shallow(} />); + + const toggle = wrapper.find('[data-test-subj="enterpriseSearchNavToggle"]'); + toggle.simulate('click'); + expect(wrapper.find(EuiPageSideBar).prop('className')).toContain('--isOpen'); + + const context = (wrapper.find('ContextProvider').prop('value') as unknown) as INavContext; + context.closeNavigation(); + expect(wrapper.find(EuiPageSideBar).prop('className')).not.toContain('--isOpen'); + }); + + it('renders children', () => { + const wrapper = shallow( + +
Test
+
+ ); + + expect(wrapper.find('.enterpriseSearchLayout__body')).toHaveLength(1); + expect(wrapper.find('.testing')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx new file mode 100644 index 000000000000000..b4497140b65b7e4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import classNames from 'classnames'; + +import { EuiPage, EuiPageSideBar, EuiPageBody, EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import './layout.scss'; + +interface ILayoutProps { + navigation: React.ReactNode; +} + +export interface INavContext { + closeNavigation(): void; +} +export const NavContext = React.createContext({}); + +export const Layout: React.FC = ({ children, navigation }) => { + const [isNavOpen, setIsNavOpen] = useState(false); + const toggleNavigation = () => setIsNavOpen(!isNavOpen); + const closeNavigation = () => setIsNavOpen(false); + + const navClasses = classNames('enterpriseSearchLayout__sideBar', { + 'enterpriseSearchLayout__sideBar--isOpen': isNavOpen, // eslint-disable-line @typescript-eslint/naming-convention + }); + + return ( + + +
+ + {i18n.translate('xpack.enterpriseSearch.nav.menu', { + defaultMessage: 'Menu', + })} + +
+ {navigation} +
+ {children} +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.scss b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.scss new file mode 100644 index 000000000000000..d673542ba198337 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.scss @@ -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. + */ + +$euiSizeML: $euiSize * 1.25; // 20px - between medium and large ¯\_(ツ)_/¯ + +.enterpriseSearchProduct { + display: flex; + align-items: center; + padding: $euiSizeML; + + background-image: url('./side_nav_bg.svg'); + background-repeat: no-repeat; + + @include euiBreakpoint('xs', 's', 'm') { + padding: $euiSize $euiSizeML; + } + + &__icon { + display: flex; + align-items: center; + justify-content: center; + + width: $euiSizeXXL; + height: $euiSizeXXL; + margin-right: $euiSizeS; + + background-color: $euiColorEmptyShade; + border-radius: 50%; + @include euiSlightShadow(); + + .euiIcon { + width: $euiSizeML; + height: $euiSizeML; + } + } + + &__title { + .euiText { + font-weight: $euiFontWeightMedium; + } + } +} + +.enterpriseSearchNavLinks { + &__item { + display: block; + padding: $euiSizeM $euiSizeML; + font-size: $euiFontSizeS; + font-weight: $euiFontWeightMedium; + line-height: $euiFontSizeM; + + $activeBgColor: rgba($euiColorFullShade, 0.05); + + &--isActive { + background-color: $activeBgColor; + } + + &.euiLink { + color: $euiTextColor; + font-weight: $euiFontWeightMedium; + + &:hover { + color: $euiTextColor; + } + + &:focus { + outline: solid 0 $activeBgColor; + background-color: $activeBgColor; + } + } + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx new file mode 100644 index 000000000000000..c117fa404a16b80 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx @@ -0,0 +1,92 @@ +/* + * 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 '../../__mocks__/react_router_history.mock'; + +import React from 'react'; +import { useLocation } from 'react-router-dom'; +import { shallow } from 'enzyme'; + +import { EuiLink as EuiLinkExternal } from '@elastic/eui'; +import { EuiLink } from '../react_router_helpers'; +import { ENTERPRISE_SEARCH_PLUGIN, APP_SEARCH_PLUGIN } from '../../../../common/constants'; + +import { SideNav, SideNavLink } from './'; + +describe('SideNav', () => { + it('renders link children', () => { + const wrapper = shallow( + +
Hello World
+
+ ); + + expect(wrapper.type()).toEqual('nav'); + expect(wrapper.find('.enterpriseSearchNavLinks')).toHaveLength(1); + expect(wrapper.find('.testing')).toHaveLength(1); + }); + + it('renders a custom product', () => { + const wrapper = shallow(); + + expect(wrapper.find('h3').text()).toEqual('App Search'); + expect(wrapper.find('.enterpriseSearchProduct--appSearch')).toHaveLength(1); + }); +}); + +describe('SideNavLink', () => { + it('renders', () => { + const wrapper = shallow(Link); + + expect(wrapper.type()).toEqual('li'); + expect(wrapper.find(EuiLink)).toHaveLength(1); + expect(wrapper.find('.enterpriseSearchNavLinks__item')).toHaveLength(1); + }); + + it('renders an external link if isExternal is true', () => { + const wrapper = shallow( + + Link + + ); + const externalLink = wrapper.find(EuiLinkExternal); + + expect(externalLink).toHaveLength(1); + expect(externalLink.prop('href')).toEqual('http://website.com'); + expect(externalLink.prop('target')).toEqual('_blank'); + }); + + it('sets an active class if the current path matches the nav link', () => { + (useLocation as jest.Mock).mockImplementationOnce(() => ({ pathname: '/test/' })); + + const wrapper = shallow(Link); + + expect(wrapper.find('.enterpriseSearchNavLinks__item--isActive')).toHaveLength(1); + }); + + it('sets an active class if the current path is / and the link isRoot', () => { + (useLocation as jest.Mock).mockImplementationOnce(() => ({ pathname: '/' })); + + const wrapper = shallow( + + Link + + ); + + expect(wrapper.find('.enterpriseSearchNavLinks__item--isActive')).toHaveLength(1); + }); + + it('passes down custom classes and props', () => { + const wrapper = shallow( + + Link + + ); + + expect(wrapper.find('.testing')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="testing"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx new file mode 100644 index 000000000000000..5969fa7806a446e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx @@ -0,0 +1,107 @@ +/* + * 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, { useContext } from 'react'; +import { useLocation } from 'react-router-dom'; +import classNames from 'classnames'; + +import { i18n } from '@kbn/i18n'; +import { EuiIcon, EuiTitle, EuiText, EuiLink as EuiLinkExternal } from '@elastic/eui'; // TODO: Remove EuiLinkExternal after full Kibana transition +import { EuiLink } from '../react_router_helpers'; + +import { ENTERPRISE_SEARCH_PLUGIN } from '../../../../common/constants'; + +import { NavContext, INavContext } from './layout'; + +import './side_nav.scss'; + +/** + * Side navigation - product & icon + links wrapper + */ + +interface ISideNavProps { + // Expects product plugin constants (@see common/constants.ts) + product: { + NAME: string; + ID: string; + }; +} + +export const SideNav: React.FC = ({ product, children }) => { + return ( + + ); +}; + +/** + * Side navigation link item + */ + +interface ISideNavLinkProps { + to: string; + isExternal?: boolean; + className?: string; + isRoot?: boolean; +} + +export const SideNavLink: React.FC = ({ + isExternal, + to, + children, + className, + isRoot, + ...rest +}) => { + const { closeNavigation } = useContext(NavContext) as INavContext; + + const { pathname } = useLocation(); + const currentPath = pathname.endsWith('/') ? pathname.slice(0, -1) : pathname; + const isActive = currentPath === to || (isRoot && currentPath === ''); + + const classes = classNames('enterpriseSearchNavLinks__item', className, { + 'enterpriseSearchNavLinks__item--isActive': !isExternal && isActive, // eslint-disable-line @typescript-eslint/naming-convention + }); + + return ( +
  • + {isExternal ? ( + + {children} + + ) : ( + + {children} + + )} +
  • + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav_bg.svg b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav_bg.svg new file mode 100644 index 000000000000000..a19227ab7b7eb30 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav_bg.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx index 7d4c068b211555d..76ee8293f2c8b9b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx @@ -73,5 +73,14 @@ describe('EUI & React Router Component Helpers', () => { expect(mockHistory.push).not.toHaveBeenCalled(); }); + + it('calls inherited onClick actions in addition to default navigation', () => { + const customOnClick = jest.fn(); // Can be anything from telemetry to a state reset + const wrapper = mount(); + + wrapper.find(EuiLink).simulate('click', { shiftKey: true }); + + expect(customOnClick).toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx index f486e432bae76ab..b53b2f2b3b650a9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx @@ -19,13 +19,15 @@ import { letBrowserHandleEvent } from './link_events'; interface IEuiReactRouterProps { to: string; + onClick?(): void; } -export const EuiReactRouterHelper: React.FC = ({ to, children }) => { +export const EuiReactRouterHelper: React.FC = ({ to, onClick, children }) => { const history = useHistory(); - const onClick = (event: React.MouseEvent) => { - if (letBrowserHandleEvent(event)) return; + const reactRouterLinkClick = (event: React.MouseEvent) => { + if (onClick) onClick(); // Run any passed click events (e.g. telemetry) + if (letBrowserHandleEvent(event)) return; // Return early if the link behavior shouldn't be handled by React Router // Prevent regular link behavior, which causes a browser refresh. event.preventDefault(); @@ -37,21 +39,29 @@ export const EuiReactRouterHelper: React.FC = ({ to, child // Generate the correct link href (with basename etc. accounted for) const href = history.createHref({ pathname: to }); - const reactRouterProps = { href, onClick }; + const reactRouterProps = { href, onClick: reactRouterLinkClick }; return React.cloneElement(children as React.ReactElement, reactRouterProps); }; type TEuiReactRouterLinkProps = EuiLinkAnchorProps & IEuiReactRouterProps; type TEuiReactRouterButtonProps = EuiButtonProps & IEuiReactRouterProps; -export const EuiReactRouterLink: React.FC = ({ to, ...rest }) => ( - +export const EuiReactRouterLink: React.FC = ({ + to, + onClick, + ...rest +}) => ( + ); -export const EuiReactRouterButton: React.FC = ({ to, ...rest }) => ( - +export const EuiReactRouterButton: React.FC = ({ + to, + onClick, + ...rest +}) => ( + ); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/index.ts index 6f7b55a3ea4b027..6ce9eefd26445a7 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/index.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { OnXJsonEditorUpdateHandler, XJsonEditor } from './xjson_editor'; +export { XJsonEditor } from './xjson_editor'; +export { TextEditor } from './text_editor'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/text_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/text_editor.tsx new file mode 100644 index 000000000000000..1d0e36c0d526c7f --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/text_editor.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiPanel } from '@elastic/eui'; +import React, { FunctionComponent } from 'react'; +import { EuiFormRow } from '@elastic/eui'; +import { + CodeEditor, + FieldHook, + getFieldValidityAndErrorMessage, +} from '../../../../../../shared_imports'; + +interface Props { + field: FieldHook; + editorProps: { [key: string]: any }; +} + +export const TextEditor: FunctionComponent = ({ field, editorProps }) => { + const { value, helpText, setValue, label } = field; + const { errorMessage } = getFieldValidityAndErrorMessage(field); + + return ( + + + + + + ); +}; 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 a8456ad0ffd72ca..228094c0dfac583 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 @@ -4,25 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiPanel } from '@elastic/eui'; import { XJsonLang } from '@kbn/monaco'; import React, { FunctionComponent, useCallback } from 'react'; -import { EuiFormRow } from '@elastic/eui'; -import { - CodeEditor, - FieldHook, - getFieldValidityAndErrorMessage, - Monaco, -} from '../../../../../../shared_imports'; +import { FieldHook, Monaco } from '../../../../../../shared_imports'; -export type OnXJsonEditorUpdateHandler = (arg: { - data: { - raw: string; - format(): T; - }; - validate(): boolean; - isValid: boolean | undefined; -}) => void; +import { TextEditor } from './text_editor'; interface Props { field: FieldHook; @@ -30,9 +16,8 @@ interface Props { } export const XJsonEditor: FunctionComponent = ({ field, editorProps }) => { - const { value, helpText, setValue, label } = field; + const { value, setValue } = field; const { xJson, setXJson, convertToJson } = Monaco.useXJsonMode(value); - const { errorMessage } = getFieldValidityAndErrorMessage(field); const onChange = useCallback( (s) => { @@ -42,25 +27,18 @@ export const XJsonEditor: FunctionComponent = ({ field, editorProps }) => [setValue, setXJson, convertToJson] ); return ( - - - { - XJsonLang.registerGrammarChecker(m); - }} - options={{ minimap: { enabled: false } }} - onChange={onChange} - {...(editorProps as any)} - /> - - + { + XJsonLang.registerGrammarChecker(m); + }, + onChange, + ...editorProps, + }} + /> ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/manage_processor_form.container.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/manage_processor_form.container.tsx index ea137b87e66d5dc..84551ce152099ab 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/manage_processor_form.container.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/manage_processor_form.container.tsx @@ -40,18 +40,19 @@ export const ManageProcessorForm: FunctionComponent = ({ const handleSubmit = useCallback( async (data: FormData, isValid: boolean) => { if (isValid) { - const { type, customOptions, ...options } = data; + const { type, customOptions, fields } = data; onSubmit({ type, - options: customOptions ? customOptions : options, + options: customOptions ? customOptions : fields, }); } }, [onSubmit] ); + const maybeProcessorOptions = processor?.options; const { form } = useForm({ - defaultValue: processor?.options, + defaultValue: { fields: maybeProcessorOptions ?? {} }, onSubmit: handleSubmit, }); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/manage_processor_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/manage_processor_form.tsx index 4e172cce630276e..ad6d191be802df6 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/manage_processor_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/manage_processor_form.tsx @@ -14,12 +14,12 @@ import { EuiFlyoutHeader, EuiFlyoutBody, EuiFlyoutFooter, - EuiSpacer, EuiTabs, EuiTab, EuiTitle, EuiFlexGroup, EuiFlexItem, + EuiSpacer, } from '@elastic/eui'; import { Form, FormDataProvider, FormHook } from '../../../../../shared_imports'; 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 6b2568bad3afc17..a6447bc30ac00f9 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 @@ -5,7 +5,7 @@ */ import React, { FunctionComponent } from 'react'; -import { EuiHorizontalRule } from '@elastic/eui'; +import { EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; import { FormDataProvider } from '../../../../../shared_imports'; import { ProcessorInternal } from '../../types'; @@ -36,6 +36,7 @@ export const ProcessorSettingsFields: FunctionComponent = ({ processor }) return ( <> + ); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/append.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/append.tsx new file mode 100644 index 000000000000000..8eb484b56bafe0b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/append.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * 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, + ComboBoxField, +} from '../../../../../../shared_imports'; + +import { FieldsConfig } from './shared'; +import { FieldNameField } from './common_fields/field_name_field'; + +const { emptyField } = fieldValidators; + +const fieldsConfig: FieldsConfig = { + value: { + type: FIELD_TYPES.COMBO_BOX, + deserializer: (v) => (Array.isArray(v) ? v : [String(v)]), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.appendForm.valueFieldLabel', { + defaultMessage: 'Value', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.appendForm.valueFieldHelpText', { + defaultMessage: 'The value to be appended by this processor.', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.appendForm.valueRequiredError', { + defaultMessage: 'A value to set is required.', + }) + ), + }, + ], + }, +}; + +export const Append: FunctionComponent = () => { + return ( + <> + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/bytes.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/bytes.tsx new file mode 100644 index 000000000000000..64a501f03d454ee --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/bytes.tsx @@ -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 React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { FIELD_TYPES, UseField, Field } from '../../../../../../shared_imports'; + +import { FieldsConfig } from './shared'; +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; +import { FieldNameField } from './common_fields/field_name_field'; + +const fieldsConfig: FieldsConfig = { + target_field: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.bytesForm.targetFieldLabel', { + defaultMessage: 'Target field (optional)', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.bytesForm.targetFieldHelpText', { + defaultMessage: 'The field to assign the converted value to', + }), + }, +}; + +export const Bytes: FunctionComponent = () => { + return ( + <> + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/circle.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/circle.tsx new file mode 100644 index 000000000000000..3a39e597cb8dce4 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/circle.tsx @@ -0,0 +1,140 @@ +/* + * 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, + SelectField, + NumericField, +} from '../../../../../../shared_imports'; + +import { FieldsConfig } from './shared'; +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; +import { FieldNameField } from './common_fields/field_name_field'; + +const { emptyField } = fieldValidators; + +const fieldsConfig: FieldsConfig = { + target_field: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.circleForm.targetFieldLabel', { + defaultMessage: 'Target field (optional)', + }), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.circleForm.targetFieldHelpText', + { + defaultMessage: 'By default field is updated in-place.', + } + ), + }, + error_distance: { + type: FIELD_TYPES.NUMBER, + deserializer: (v) => (typeof v === 'number' && !isNaN(v) ? v : 1.0), + serializer: Number, + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceFieldLabel', + { + defaultMessage: 'Error distance', + } + ), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceHelpText', + { + defaultMessage: + 'The difference between the resulting inscribed distance from center to side and the circle’s radius (measured in meters for geo_shape, unit-less for shape).', + } + ), + validations: [ + { + validator: ({ value }) => { + return isNaN(Number(value)) + ? { + message: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceError', + { + defaultMessage: 'An error distance value is required.', + } + ), + } + : undefined; + }, + }, + ], + }, + shape_type: { + type: FIELD_TYPES.SELECT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeFieldLabel', { + defaultMessage: 'Shape type', + }), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeFieldHelpText', + { defaultMessage: 'Which field mapping type is to be used.' } + ), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeRequiredError', { + defaultMessage: 'A shape type value is required.', + }) + ), + }, + ], + }, +}; + +export const Circle: FunctionComponent = () => { + return ( + <> + + + + + + + + + + + ); +}; 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 4802653f9e68074..7ae7b82c31a43e1 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 @@ -15,41 +15,64 @@ import { ToggleField, } from '../../../../../../../shared_imports'; +import { TextEditor } from '../../field_components'; + const ignoreFailureConfig: FieldConfig = { defaultValue: false, + serializer: (v) => (v === false ? undefined : v), + deserializer: (v) => (typeof v === 'boolean' ? v : undefined), label: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.commonFields.ignoreFailureFieldLabel', { defaultMessage: 'Ignore failure', } ), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.commonFields.ignoreFailureHelpText', + { defaultMessage: 'Ignore failures for this processor.' } + ), type: FIELD_TYPES.TOGGLE, }; const ifConfig: FieldConfig = { - defaultValue: undefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.ifFieldLabel', { defaultMessage: 'Condition (optional)', }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.ifFieldHelpText', { + defaultMessage: 'Conditionally execute this processor.', + }), type: FIELD_TYPES.TEXT, }; const tagConfig: FieldConfig = { - defaultValue: undefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.tagFieldLabel', { defaultMessage: 'Tag (optional)', }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.tagFieldHelpText', { + defaultMessage: 'An identifier for this processor. Useful for debugging and metrics.', + }), type: FIELD_TYPES.TEXT, }; export const CommonProcessorFields: FunctionComponent = () => { return ( - <> - +
    + - + - - + +
    ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/field_name_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/field_name_field.tsx new file mode 100644 index 000000000000000..7ef5ba6768c190f --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/field_name_field.tsx @@ -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 React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + FIELD_TYPES, + UseField, + Field, + fieldValidators, + ValidationConfig, +} from '../../../../../../../shared_imports'; + +import { FieldsConfig } from '../shared'; + +const { emptyField } = fieldValidators; + +export const fieldsConfig: FieldsConfig = { + field: { + type: FIELD_TYPES.TEXT, + deserializer: String, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.fieldFieldLabel', { + defaultMessage: 'Field', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.fieldRequiredError', { + defaultMessage: 'A field value is required.', + }) + ), + }, + ], + }, +}; + +interface Props { + helpText?: React.ReactNode; + /** + * The field name requires a value. Processor specific validation + * checks can be added here. + */ + additionalValidations?: ValidationConfig[]; +} + +export const FieldNameField: FunctionComponent = ({ helpText, additionalValidations }) => ( + +); 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 new file mode 100644 index 000000000000000..08eb0a425ef3384 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx @@ -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. + */ +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FIELD_TYPES, UseField, ToggleField } from '../../../../../../../shared_imports'; + +import { FieldsConfig } from '../shared'; + +export const fieldsConfig: FieldsConfig = { + ignore_missing: { + type: FIELD_TYPES.TOGGLE, + defaultValue: false, + serializer: (v) => (v === false ? undefined : v), + deserializer: (v) => (typeof v === 'boolean' ? v : undefined), + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.commonFields.ignoreMissingFieldLabel', + { + defaultMessage: 'Ignore missing', + } + ), + }, +}; + +interface Props { + helpText?: string; +} + +export const IgnoreMissingField: FunctionComponent = ({ helpText }) => ( + +); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/processor_type_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/processor_type_field.tsx index 71ee4a714a28e3e..e4ad90f61af0a13 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/processor_type_field.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/processor_type_field.tsx @@ -46,20 +46,12 @@ interface Props { const { emptyField } = fieldValidators; -const typeConfig: FieldConfig = { +const typeConfig: FieldConfig = { type: FIELD_TYPES.COMBO_BOX, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.typeField.typeFieldLabel', { defaultMessage: 'Processor', }), - deserializer: (value: string | undefined) => { - if (value) { - return [value]; - } - return []; - }, - serializer: (value: string[]) => { - return value[0]; - }, + deserializer: String, validations: [ { validator: emptyField( @@ -73,11 +65,11 @@ const typeConfig: FieldConfig = { export const ProcessorTypeField: FunctionComponent = ({ initialType }) => { return ( - + config={typeConfig} defaultValue={initialType} path="type"> {(typeField) => { let selectedOptions: ProcessorTypeAndLabel[]; - if ((typeField.value as string[]).length) { - const [type] = typeField.value as string[]; + if (typeField.value?.length) { + const type = typeField.value; const descriptor = getProcessorDescriptor(type); selectedOptions = descriptor ? [{ label: descriptor.label, value: type }] @@ -103,9 +95,7 @@ export const ProcessorTypeField: FunctionComponent = ({ initialType }) => return false; } - const newValue = [...(typeField.value as string[]), value]; - - typeField.setValue(newValue); + typeField.setValue(value); }; return ( @@ -131,8 +121,9 @@ export const ProcessorTypeField: FunctionComponent = ({ initialType }) => options={processorTypesAndLabels} selectedOptions={selectedOptions} onCreateOption={onCreateComboOption} - onChange={(options: EuiComboBoxOptionOption[]) => { - typeField.setValue(options.map(({ value }) => value)); + onChange={(options: Array>) => { + const [selection] = options; + typeField.setValue(selection?.value! ?? ''); }} noSuggestions={false} singleSelection={{ diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/convert.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/convert.tsx new file mode 100644 index 000000000000000..b45f589bf0f9225 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/convert.tsx @@ -0,0 +1,136 @@ +/* + * 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, + SelectField, +} from '../../../../../../shared_imports'; + +import { FieldsConfig } from './shared'; +import { FieldNameField } from './common_fields/field_name_field'; +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; + +const { emptyField } = fieldValidators; + +const fieldsConfig: FieldsConfig = { + /* Required fields config */ + type: { + type: FIELD_TYPES.TEXT, + defaultValue: '', + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.convertForm.typeFieldLabel', { + defaultMessage: 'Type', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.convertForm.typeFieldHelpText', { + defaultMessage: 'The type to convert the existing value to.', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.convertForm.typeRequiredError', { + defaultMessage: 'A type value is required.', + }) + ), + }, + ], + }, + /* Optional fields config */ + target_field: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.convertForm.targetFieldLabel', { + defaultMessage: 'Target field (optional)', + }), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.convertForm.targetFieldHelpText', + { + defaultMessage: 'The field to assign the converted value to.', + } + ), + }, +}; + +export const Convert: FunctionComponent = () => { + return ( + <> + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/csv.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/csv.tsx new file mode 100644 index 000000000000000..3ac0179ca02a695 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/csv.tsx @@ -0,0 +1,164 @@ +/* + * 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 { EuiCode } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + FIELD_TYPES, + fieldValidators, + UseField, + Field, + ToggleField, + ComboBoxField, + ValidationFunc, +} from '../../../../../../shared_imports'; + +import { FieldsConfig } from './shared'; +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; +import { FieldNameField } from './common_fields/field_name_field'; + +import { isArrayOfStrings } from './shared'; + +const { minLengthField } = fieldValidators; + +/** + * Allow empty strings ('') to pass this validation. + */ +const isStringLengthOne: ValidationFunc = ({ value }) => { + return typeof value === 'string' && value !== '' && value.length !== 1 + ? { + message: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.convertForm.separatorLengthError', + { + defaultMessage: 'A separator value must be 1 character.', + } + ), + } + : undefined; +}; + +const fieldsConfig: FieldsConfig = { + /* Required fields config */ + target_fields: { + type: FIELD_TYPES.COMBO_BOX, + deserializer: (v) => { + return isArrayOfStrings(v) ? v : []; + }, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.csvForm.targetFieldsFieldLabel', { + defaultMessage: 'Target fields', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.csvForm.targetFieldsHelpText', { + defaultMessage: 'The array of fields to assign extracted values to.', + }), + validations: [ + { + validator: minLengthField({ + length: 1, + message: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.csvForm.targetFieldRequiredError', + { + defaultMessage: 'A target fields value is required.', + } + ), + }), + }, + ], + }, + /* Optional fields config */ + separator: { + type: FIELD_TYPES.TEXT, + serializer: (v) => (v ? v : undefined), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.convertForm.separatorFieldLabel', { + defaultMessage: 'Separator (optional)', + }), + validations: [ + { + validator: isStringLengthOne, + }, + ], + helpText: ( + {','} }} + /> + ), + }, + quote: { + type: FIELD_TYPES.TEXT, + serializer: (v) => (v ? v : undefined), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.convertForm.quoteFieldLabel', { + defaultMessage: 'Quote (optional)', + }), + validations: [ + { + validator: isStringLengthOne, + }, + ], + helpText: ( + {'"'} }} + /> + ), + }, + trim: { + type: FIELD_TYPES.TOGGLE, + defaultValue: false, + deserializer: (v) => (typeof v === 'boolean' ? v : undefined), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.csvForm.trimFieldLabel', { + defaultMessage: 'Trim', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.csvForm.trimFieldHelpText', { + defaultMessage: 'Trim whitespaces in unquoted fields', + }), + }, + empty_value: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.convertForm.emptyValueFieldLabel', { + defaultMessage: 'Empty value (optional)', + }), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.convertForm.emptyValueFieldHelpText', + { + defaultMessage: + 'Value used to fill empty fields, empty fields will be skipped if this is not provided.', + } + ), + }, +}; + +export const CSV: FunctionComponent = () => { + return ( + <> + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date.tsx new file mode 100644 index 000000000000000..424e84058ac3fcb --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date.tsx @@ -0,0 +1,122 @@ +/* + * 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 { EuiCode } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + FIELD_TYPES, + fieldValidators, + UseField, + Field, + ComboBoxField, +} from '../../../../../../shared_imports'; + +import { FieldsConfig, isArrayOfStrings } from './shared'; +import { FieldNameField } from './common_fields/field_name_field'; + +const { minLengthField } = fieldValidators; + +const fieldsConfig: FieldsConfig = { + /* Required fields config */ + formats: { + type: FIELD_TYPES.COMBO_BOX, + deserializer: (v) => { + return isArrayOfStrings(v) ? v : []; + }, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.dateForm.formatsFieldLabel', { + defaultMessage: 'Formats', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.dateForm.formatsFieldHelpText', { + defaultMessage: + 'An array of the expected date formats. Can be a java time pattern or one of the following formats: ISO8601, UNIX, UNIX_MS, or TAI64N.', + }), + validations: [ + { + validator: minLengthField({ + length: 1, + message: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dateForm.formatsRequiredError', + { + defaultMessage: 'A value for formats is required.', + } + ), + }), + }, + ], + }, + /* Optional fields config */ + target_field: { + type: FIELD_TYPES.TEXT, + serializer: (v) => (v ? undefined : v), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.dateForm.targetFieldFieldLabel', { + defaultMessage: 'Target field (optional)', + }), + helpText: ( + {'@timestamp'}, + }} + /> + ), + }, + timezone: { + type: FIELD_TYPES.TEXT, + serializer: (v) => (v ? v : undefined), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.dateForm.timezoneFieldLabel', { + defaultMessage: 'Timezone (optional)', + }), + helpText: ( + {'UTC'} }} + /> + ), + }, + locale: { + type: FIELD_TYPES.TEXT, + serializer: (v) => (v ? v : undefined), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.dateForm.localeFieldLabel', { + defaultMessage: 'Locale (optional)', + }), + helpText: ( + {'ENGLISH'} }} + /> + ), + }, +}; + +/** + * Disambiguate from global Date object + */ +export const DateProcessor: FunctionComponent = () => { + return ( + <> + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date_index_name.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date_index_name.tsx new file mode 100644 index 000000000000000..387c9ff4e0b46aa --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date_index_name.tsx @@ -0,0 +1,242 @@ +/* + * 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 { EuiCode } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + FIELD_TYPES, + fieldValidators, + UseField, + Field, + ComboBoxField, + SelectField, +} from '../../../../../../shared_imports'; + +import { FieldsConfig, isArrayOfStrings } from './shared'; +import { FieldNameField } from './common_fields/field_name_field'; + +const { emptyField } = fieldValidators; + +const fieldsConfig: FieldsConfig = { + /* Required fields config */ + date_rounding: { + type: FIELD_TYPES.SELECT, + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRoundingFieldLabel', + { + defaultMessage: 'Date rounding', + } + ), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRoundingFieldHelpText', + { + defaultMessage: 'How to round the date when formatting the date into the index name.', + } + ), + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRoundingRequiredError', + { + defaultMessage: 'A field value is required.', + } + ) + ), + }, + ], + }, + /* Optional fields config */ + index_name_prefix: { + type: FIELD_TYPES.TEXT, + serializer: (v) => (v ? v : undefined), + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.indexNamePrefixFieldLabel', + { + defaultMessage: 'Index name prefix (optional)', + } + ), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.indexNamePrefixFieldHelpText', + { defaultMessage: 'A prefix of the index name to be prepended before the printed date.' } + ), + }, + index_name_format: { + type: FIELD_TYPES.TEXT, + serializer: (v) => (v ? v : undefined), + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.indexNameFormatFieldLabel', + { + defaultMessage: 'Index name format (optional)', + } + ), + helpText: ( + {'yyyy-MM-dd'} }} + /> + ), + }, + date_formats: { + type: FIELD_TYPES.COMBO_BOX, + serializer: (v: string[]) => { + return v.length ? v : undefined; + }, + deserializer: (v) => { + return isArrayOfStrings(v) ? v : []; + }, + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateFormatsFieldLabel', + { + defaultMessage: 'Date formats (optional)', + } + ), + helpText: ( + {"yyyy-MM-dd'T'HH:mm:ss.SSSXX"} }} + /> + ), + }, + timezone: { + type: FIELD_TYPES.TEXT, + serializer: (v) => (v ? v : undefined), + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.timezoneFieldLabel', + { + defaultMessage: 'Timezone (optional)', + } + ), + helpText: ( + {'UTC'} }} + /> + ), + }, + locale: { + type: FIELD_TYPES.TEXT, + serializer: (v) => (v ? v : undefined), + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.localeFieldLabel', + { + defaultMessage: 'Locale (optional)', + } + ), + helpText: ( + {'ENGLISH'} }} + /> + ), + }, +}; + +/** + * Disambiguate from global Date object + */ +export const DateIndexName: FunctionComponent = () => { + return ( + <> + + + + + + + + + + + + + + + ); +}; 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 new file mode 100644 index 000000000000000..5f9f55ced1a2563 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dissect.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiCode } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { TextEditor } from '../field_components'; + +import { + FieldConfig, + FIELD_TYPES, + fieldValidators, + UseField, + Field, +} from '../../../../../../shared_imports'; + +import { FieldNameField } from './common_fields/field_name_field'; +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; + +const { emptyField } = fieldValidators; + +const fieldsConfig: Record = { + /* Required field config */ + pattern: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.dissectForm.patternFieldLabel', { + defaultMessage: 'Pattern', + }), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dissectForm.patternFieldHelpText', + { + defaultMessage: 'The pattern to apply to the field.', + } + ), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.dissectForm.patternRequiredError', { + defaultMessage: 'A pattern value is required.', + }) + ), + }, + ], + }, + /* Optional field config */ + append_separator: { + type: FIELD_TYPES.TEXT, + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dissectForm.appendSeparatorparaotrFieldLabel', + { + defaultMessage: 'Append separator (optional)', + } + ), + helpText: ( + {'""'} }} + /> + ), + }, +}; + +export const Dissect: FunctionComponent = () => { + return ( + <> + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dot_expander.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dot_expander.tsx new file mode 100644 index 000000000000000..4e50c61ac930c34 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dot_expander.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { FieldConfig, FIELD_TYPES, UseField, Field } from '../../../../../../shared_imports'; + +import { FieldNameField } from './common_fields/field_name_field'; + +const fieldsConfig: Record = { + path: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.dotExpanderForm.pathFieldLabel', { + defaultMessage: 'Path', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.dotExpanderForm.pathHelpText', { + defaultMessage: 'Only required if the field to expand is part another object field.', + }), + }, +}; + +export const DotExpander: FunctionComponent = () => { + return ( + <> + { + if (typeof value === 'string' && value.length) { + return !value.includes('.') + ? { + message: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dotExpanderForm.fieldNameRequiresDotError', + { defaultMessage: 'A field value requires at least one dot character.' } + ), + } + : undefined; + } + }, + }, + ]} + /> + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/drop.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/drop.tsx new file mode 100644 index 000000000000000..87b6cb76cdccece --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/drop.tsx @@ -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 { FunctionComponent } from 'react'; + +/** + * This fields component has no unique fields + */ +export const Drop: FunctionComponent = () => { + return null; +}; 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 77f85e61eff6b42..3148022adaa9807 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 @@ -15,6 +15,7 @@ import { UseField, Field, } from '../../../../../../shared_imports'; +import { TextEditor } from '../field_components'; const { emptyField } = fieldValidators; @@ -84,15 +85,25 @@ const ignoreMissingConfig: FieldConfig = { export const Gsub: FunctionComponent = () => { return ( <> - + - + - + - + - + ); }; 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 new file mode 100644 index 000000000000000..6996deb2d861c5b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/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. + */ + +export { Append } from './append'; +export { Bytes } from './bytes'; +export { Circle } from './circle'; +export { Convert } from './convert'; +export { CSV } from './csv'; +export { DateProcessor } from './date'; +export { DateIndexName } from './date_index_name'; +export { Dissect } from './dissect'; +export { DotExpander } from './dot_expander'; +export { Drop } from './drop'; 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 1ba6a14d0448d85..88cea620ae804e1 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 @@ -64,11 +64,11 @@ 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/shared.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/shared.ts new file mode 100644 index 000000000000000..a0a31dd3a8e934c --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/shared.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 * as rt from 'io-ts'; +import { isRight } from 'fp-ts/lib/Either'; +import { flow } from 'fp-ts/lib/function'; + +import { FieldConfig } from '../../../../../../shared_imports'; + +export const arrayOfStrings = rt.array(rt.string); +export const isArrayOfStrings = flow(arrayOfStrings.decode, isRight); + +export type FieldsConfig = Record; 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 7055721fc8b07c1..502045084b24de1 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 @@ -7,6 +7,19 @@ import { i18n } from '@kbn/i18n'; import { FunctionComponent } from 'react'; +import { + Append, + Bytes, + Circle, + Convert, + CSV, + DateProcessor, + DateIndexName, + Dissect, + DotExpander, + Drop, +} from '../manage_processor_form/processors'; + // import { SetProcessor } from './processors/set'; // import { Gsub } from './processors/gsub'; @@ -23,70 +36,70 @@ type MapProcessorTypeToDescriptor = Record; export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { append: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Append, docLinkPath: '/append-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.append', { defaultMessage: 'Append', }), }, bytes: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Bytes, docLinkPath: '/bytes-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.bytes', { defaultMessage: 'Bytes', }), }, circle: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Circle, docLinkPath: '/ingest-circle-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.circle', { defaultMessage: 'Circle', }), }, convert: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Convert, docLinkPath: '/convert-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.convert', { defaultMessage: 'Convert', }), }, csv: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: CSV, docLinkPath: '/csv-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.csv', { defaultMessage: 'CSV', }), }, date: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: DateProcessor, docLinkPath: '/date-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.date', { defaultMessage: 'Date', }), }, date_index_name: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: DateIndexName, docLinkPath: '/date-index-name-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.dateIndexName', { defaultMessage: 'Date Index Name', }), }, dissect: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Dissect, docLinkPath: '/dissect-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.dissect', { defaultMessage: 'Dissect', }), }, dot_expander: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: DotExpander, docLinkPath: '/dot-expand-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.dotExpander', { defaultMessage: 'Dot Expander', }), }, drop: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Drop, docLinkPath: '/drop-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.drop', { defaultMessage: 'Drop', diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts index d2c4b73a4876797..936db37f0c62928 100644 --- a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -43,6 +43,8 @@ export { FieldConfig, FieldHook, getFieldValidityAndErrorMessage, + ValidationFunc, + ValidationConfig, } from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; export { @@ -57,6 +59,9 @@ export { FormRow, ToggleField, ComboBoxField, + RadioGroupField, + NumericField, + SelectField, } from '../../../../src/plugins/es_ui_shared/static/forms/components'; export { diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.mock.ts index 0450849931b3005..da22e33dc7b524e 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.mock.ts @@ -8,6 +8,7 @@ import { COMMENTS, DESCRIPTION, ENTRIES, + ITEM_ID, ITEM_TYPE, LIST_ID, META, @@ -32,3 +33,26 @@ export const getCreateExceptionListItemSchemaMock = (): CreateExceptionListItemS tags: TAGS, type: ITEM_TYPE, }); + +/** + * Useful for end to end testing + */ +export const getCreateExceptionListItemMinimalSchemaMock = (): CreateExceptionListItemSchema => ({ + description: DESCRIPTION, + entries: ENTRIES, + item_id: ITEM_ID, + list_id: LIST_ID, + name: NAME, + type: ITEM_TYPE, +}); + +/** + * Useful for end to end testing + */ +export const getCreateExceptionListItemMinimalSchemaMockWithoutId = (): CreateExceptionListItemSchema => ({ + description: DESCRIPTION, + entries: ENTRIES, + list_id: LIST_ID, + name: NAME, + type: ITEM_TYPE, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.mock.ts index d9c04746103690a..f8431fcce1bf76d 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.mock.ts @@ -7,6 +7,7 @@ import { DESCRIPTION, ENDPOINT_TYPE, + LIST_ID, META, NAME, NAMESPACE_TYPE, @@ -26,3 +27,22 @@ export const getCreateExceptionListSchemaMock = (): CreateExceptionListSchema => type: ENDPOINT_TYPE, version: VERSION, }); + +/** + * Useful for end to end testing + */ +export const getCreateExceptionListMinimalSchemaMock = (): CreateExceptionListSchema => ({ + description: DESCRIPTION, + list_id: LIST_ID, + name: NAME, + type: ENDPOINT_TYPE, +}); + +/** + * Useful for end to end testing + */ +export const getCreateExceptionListMinimalSchemaMockWithoutId = (): CreateExceptionListSchema => ({ + description: DESCRIPTION, + name: NAME, + type: ENDPOINT_TYPE, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.mock.ts index 90d70c273f490ec..4673c0fe7629d26 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.mock.ts @@ -9,6 +9,7 @@ import { DESCRIPTION, ENTRIES, ID, + ITEM_ID, ITEM_TYPE, LIST_ITEM_ID, META, @@ -34,3 +35,15 @@ export const getUpdateExceptionListItemSchemaMock = (): UpdateExceptionListItemS tags: TAGS, type: ITEM_TYPE, }); + +/** + * Useful for end to end tests and other mechanisms which want to fill in the values + * after doing a get of the structure. + */ +export const getUpdateMinimalExceptionListItemSchemaMock = (): UpdateExceptionListItemSchema => ({ + description: DESCRIPTION, + entries: ENTRIES, + item_id: ITEM_ID, + name: NAME, + type: ITEM_TYPE, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.mock.ts index 22af29e6af0b787..b7dc2d9e0c94879 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.mock.ts @@ -20,3 +20,14 @@ export const getUpdateExceptionListSchemaMock = (): UpdateExceptionListSchema => tags: ['malware'], type: 'endpoint', }); + +/** + * Useful for end to end tests and other mechanisms which want to fill in the values + * after doing a get of the structure. + */ +export const getUpdateMinimalExceptionListSchemaMock = (): UpdateExceptionListSchema => ({ + description: DESCRIPTION, + list_id: LIST_ID, + name: NAME, + type: 'endpoint', +}); diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts index c0d04c9823ca3be..1a8f21a5232f80a 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts @@ -7,8 +7,11 @@ import { COMMENTS, DATE_NOW, DESCRIPTION, + ELASTIC_USER, ENTRIES, + ITEM_ID, ITEM_TYPE, + LIST_ID, META, NAME, NAMESPACE_TYPE, @@ -38,3 +41,24 @@ export const getExceptionListItemSchemaMock = (): ExceptionListItemSchema => ({ updated_at: DATE_NOW, updated_by: USER, }); + +/** + * This is useful for end to end tests where we remove the auto generated parts for comparisons + * such as created_at, updated_at, and id. + */ +export const getExceptionListItemResponseMockWithoutAutoGeneratedValues = (): Partial< + ExceptionListItemSchema +> => ({ + _tags: [], + comments: [], + created_by: ELASTIC_USER, + description: DESCRIPTION, + entries: ENTRIES, + item_id: ITEM_ID, + list_id: LIST_ID, + name: NAME, + namespace_type: 'single', + tags: [], + type: ITEM_TYPE, + updated_by: ELASTIC_USER, +}); diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts index 2655b09631b23cc..e2f0a7c06b40048 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts @@ -7,9 +7,12 @@ import { DATE_NOW, DESCRIPTION, + ELASTIC_USER, ENDPOINT_TYPE, IMMUTABLE, + LIST_ID, META, + NAME, TIE_BREAKER, USER, VERSION, @@ -18,6 +21,7 @@ import { import { ENDPOINT_LIST_ID } from '../..'; import { ExceptionListSchema } from './exception_list_schema'; + export const getExceptionListSchemaMock = (): ExceptionListSchema => ({ _tags: ['endpoint', 'process', 'malware', 'os:linux'], _version: _VERSION, @@ -37,3 +41,23 @@ export const getExceptionListSchemaMock = (): ExceptionListSchema => ({ updated_by: 'user_name', version: VERSION, }); + +/** + * This is useful for end to end tests where we remove the auto generated parts for comparisons + * such as created_at, updated_at, and id. + */ +export const getExceptionResponseMockWithoutAutoGeneratedValues = (): Partial< + ExceptionListSchema +> => ({ + _tags: [], + created_by: ELASTIC_USER, + description: DESCRIPTION, + immutable: IMMUTABLE, + list_id: LIST_ID, + name: NAME, + namespace_type: 'single', + tags: [], + type: ENDPOINT_TYPE, + updated_by: ELASTIC_USER, + version: VERSION, +}); diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts index fc0473b2b37040b..f092aec82a8f3e7 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts @@ -57,7 +57,7 @@ export const createExceptionListItemRoute = (router: IRouter): void => { }); if (exceptionList == null) { return siemResponse.error({ - body: `list id: "${listId}" does not exist`, + body: `exception list id: "${listId}" does not exist`, statusCode: 404, }); } else { diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts index 88643e53ff0a721..103cba700013f98 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts @@ -62,7 +62,7 @@ export const findExceptionListItemRoute = (router: IRouter): void => { }); if (exceptionListItems == null) { return siemResponse.error({ - body: `list id: "${listId}" does not exist`, + body: `exception list id: "${listId}" does not exist`, statusCode: 404, }); } diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts index 7e15f694aee13c7..745ad0735a17478 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts @@ -54,39 +54,46 @@ export const updateExceptionListItemRoute = (router: IRouter): void => { namespace_type: namespaceType, tags, } = request.body; - const exceptionLists = getExceptionListClient(context); - const exceptionListItem = await exceptionLists.updateExceptionListItem({ - _tags, - _version, - comments, - description, - entries, - id, - itemId, - meta, - name, - namespaceType, - tags, - type, - }); - if (exceptionListItem == null) { - if (id != null) { - return siemResponse.error({ - body: `list item id: "${id}" not found`, - statusCode: 404, - }); - } else { - return siemResponse.error({ - body: `list item item_id: "${itemId}" not found`, - statusCode: 404, - }); - } + if (id == null && itemId == null) { + return siemResponse.error({ + body: 'either id or item_id need to be defined', + statusCode: 404, + }); } else { - const [validated, errors] = validate(exceptionListItem, exceptionListItemSchema); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); + const exceptionLists = getExceptionListClient(context); + const exceptionListItem = await exceptionLists.updateExceptionListItem({ + _tags, + _version, + comments, + description, + entries, + id, + itemId, + meta, + name, + namespaceType, + tags, + type, + }); + if (exceptionListItem == null) { + if (id != null) { + return siemResponse.error({ + body: `exception list item id: "${id}" does not exist`, + statusCode: 404, + }); + } else { + return siemResponse.error({ + body: `exception list item item_id: "${itemId}" does not exist`, + statusCode: 404, + }); + } } else { - return response.ok({ body: validated ?? {} }); + const [validated, errors] = validate(exceptionListItem, exceptionListItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } } } } catch (err) { diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts index 8102210b8430d7e..1903d0f601d1d10 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts @@ -15,7 +15,7 @@ import { updateExceptionListSchema, } from '../../common/schemas'; -import { getExceptionListClient } from './utils'; +import { getErrorMessageExceptionList, getExceptionListClient } from './utils'; export const updateExceptionListRoute = (router: IRouter): void => { router.put( @@ -50,7 +50,7 @@ export const updateExceptionListRoute = (router: IRouter): void => { const exceptionLists = getExceptionListClient(context); if (id == null && listId == null) { return siemResponse.error({ - body: `either id or list_id need to be defined`, + body: 'either id or list_id need to be defined', statusCode: 404, }); } else { @@ -69,7 +69,7 @@ export const updateExceptionListRoute = (router: IRouter): void => { }); if (list == null) { return siemResponse.error({ - body: `exception list id: "${id}" not found`, + body: getErrorMessageExceptionList({ id, listId }), statusCode: 404, }); } else { diff --git a/x-pack/plugins/lists/server/routes/utils/get_error_message_exception_list.ts b/x-pack/plugins/lists/server/routes/utils/get_error_message_exception_list.ts index 665a7540184a030..7db3bedd9ec84cc 100644 --- a/x-pack/plugins/lists/server/routes/utils/get_error_message_exception_list.ts +++ b/x-pack/plugins/lists/server/routes/utils/get_error_message_exception_list.ts @@ -12,10 +12,10 @@ export const getErrorMessageExceptionList = ({ listId: string | undefined; }): string => { if (id != null) { - return `Exception list id: "${id}" does not exist`; + return `exception list id: "${id}" does not exist`; } else if (listId != null) { - return `Exception list list_id: "${listId}" does not exist`; + return `exception list list_id: "${listId}" does not exist`; } else { - return 'Exception list does not exist'; + return 'exception list does not exist'; } }; diff --git a/x-pack/plugins/lists/server/routes/utils/get_error_message_exception_list_item.ts b/x-pack/plugins/lists/server/routes/utils/get_error_message_exception_list_item.ts index 8e6730ef3db5cd0..efb6c0e59ade561 100644 --- a/x-pack/plugins/lists/server/routes/utils/get_error_message_exception_list_item.ts +++ b/x-pack/plugins/lists/server/routes/utils/get_error_message_exception_list_item.ts @@ -12,10 +12,10 @@ export const getErrorMessageExceptionListItem = ({ itemId: string | undefined; }): string => { if (id != null) { - return `Exception list item id: "${id}" does not exist`; + return `exception list item id: "${id}" does not exist`; } else if (itemId != null) { - return `Exception list item list_id: "${itemId}" does not exist`; + return `exception list item item_id: "${itemId}" does not exist`; } else { - return 'Exception list item does not exist'; + return 'exception list item does not exist'; } }; diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts index 2102673060273f9..d0bce8508e82e79 100644 --- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts @@ -36,8 +36,8 @@ export interface MlSummaryJob { export interface AuditMessage { job_id: string; msgTime: number; - level: number; - highestLevel: number; + level: string; + highestLevel: string; highestLevelText: string; text: string; } diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index c61db9fb1ad8daf..7b4ea5458f4a610 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -16,7 +16,8 @@ "share", "embeddable", "uiActions", - "kibanaLegacy" + "kibanaLegacy", + "indexPatternManagement" ], "optionalPlugins": [ "security", diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index 42c462fa9d8697b..c281dc4e9ae0594 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -21,7 +21,8 @@ import { MlRouter } from './routing'; import { mlApiServicesProvider } from './services/ml_api_service'; import { HttpService } from './services/http_service'; -export type MlDependencies = Omit & MlStartDependencies; +export type MlDependencies = Omit & + MlStartDependencies; interface AppProps { coreStart: CoreStart; diff --git a/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js b/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js index 1b33d680422958c..329863fdc998629 100644 --- a/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js +++ b/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js @@ -62,7 +62,10 @@ describe('FieldTitleBar', () => { expect(hasClassName).toBeTruthy(); }); - test(`tooltip hovering`, (done) => { + test(`tooltip hovering`, () => { + // Use fake timers so we don't have to wait for the EuiToolTip timeout + jest.useFakeTimers(); + const props = { card: { fieldName: 'foo', type: 'bar' } }; const wrapper = mountWithIntl(); const container = wrapper.find({ className: 'field-name' }); @@ -70,14 +73,22 @@ describe('FieldTitleBar', () => { expect(wrapper.find('EuiToolTip').children()).toHaveLength(1); container.simulate('mouseover'); - // EuiToolTip mounts children after a 250ms delay - setTimeout(() => { - wrapper.update(); - expect(wrapper.find('EuiToolTip').children()).toHaveLength(2); - container.simulate('mouseout'); - wrapper.update(); - expect(wrapper.find('EuiToolTip').children()).toHaveLength(1); - done(); - }, 250); + + // Run the timers so the EuiTooltip will be visible + jest.runAllTimers(); + + wrapper.update(); + expect(wrapper.find('EuiToolTip').children()).toHaveLength(2); + + container.simulate('mouseout'); + + // Run the timers so the EuiTooltip will be hidden again + jest.runAllTimers(); + + wrapper.update(); + expect(wrapper.find('EuiToolTip').children()).toHaveLength(1); + + // Clearing all mocks will also reset fake timers. + jest.clearAllMocks(); }); }); diff --git a/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js b/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js index 7e37dc10ade33bd..d4200c2f8366b40 100644 --- a/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js +++ b/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js @@ -27,6 +27,9 @@ describe('FieldTypeIcon', () => { }); test(`render with tooltip and test hovering`, () => { + // Use fake timers so we don't have to wait for the EuiToolTip timeout + jest.useFakeTimers(); + const typeIconComponent = mount( ); @@ -35,11 +38,23 @@ describe('FieldTypeIcon', () => { expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(1); container.simulate('mouseover'); - // EuiToolTip mounts children after a 250ms delay - setTimeout(() => expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(2), 250); + + // Run the timers so the EuiTooltip will be visible + jest.runAllTimers(); + + typeIconComponent.update(); + expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(2); container.simulate('mouseout'); + + // Run the timers so the EuiTooltip will be hidden again + jest.runAllTimers(); + + typeIconComponent.update(); expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(1); + + // Clearing all mocks will also reset fake timers. + jest.clearAllMocks(); }); test(`update component`, () => { diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index aa6163379f9c0a1..ff59d46de758d94 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -20,8 +20,10 @@ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { HomePublicPluginSetup } from 'src/plugins/home/public'; +import { IndexPatternManagementSetup } from 'src/plugins/index_pattern_management/public'; import { EmbeddableSetup } from 'src/plugins/embeddable/public'; import { AppStatus, AppUpdater, DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; +import { MlCardState } from '../../../../src/plugins/index_pattern_management/public'; import { SecurityPluginSetup } from '../../security/public'; import { LicensingPluginSetup } from '../../licensing/public'; import { registerManagementSection } from './application/management'; @@ -53,6 +55,7 @@ export interface MlSetupDependencies { uiActions: UiActionsSetup; kibanaVersion: string; share: SharePluginSetup; + indexPatternManagement: IndexPatternManagementSetup; } export type MlCoreSetup = CoreSetup; @@ -104,11 +107,20 @@ export class MlPlugin implements Plugin { }); const licensing = pluginsSetup.licensing.license$.pipe(take(1)); - licensing.subscribe((license) => { + licensing.subscribe(async (license) => { + const [coreStart] = await core.getStartServices(); if (isMlEnabled(license)) { // add ML to home page registerFeature(pluginsSetup.home); + // register ML for the index pattern management no data screen. + pluginsSetup.indexPatternManagement.environment.update({ + ml: () => + coreStart.application.capabilities.ml.canFindFileStructure + ? MlCardState.ENABLED + : MlCardState.HIDDEN, + }); + // register various ML plugin features which require a full license if (isFullLicense(license)) { registerManagementSection(pluginsSetup.management, core); diff --git a/x-pack/plugins/ml/public/shared.ts b/x-pack/plugins/ml/public/shared.ts index 4b1d7ee733dcfd6..ec884bfac5351cc 100644 --- a/x-pack/plugins/ml/public/shared.ts +++ b/x-pack/plugins/ml/public/shared.ts @@ -9,6 +9,7 @@ export * from '../common/constants/anomalies'; export * from '../common/types/data_recognizer'; export * from '../common/types/capabilities'; export * from '../common/types/anomalies'; +export * from '../common/types/anomaly_detection_jobs'; export * from '../common/types/modules'; export * from '../common/types/audit_message'; diff --git a/x-pack/plugins/observability/public/components/app/ingest_manager_panel/index.tsx b/x-pack/plugins/observability/public/components/app/ingest_manager_panel/index.tsx index 1ab9f75632d9dc0..5d0c8a40ed3de99 100644 --- a/x-pack/plugins/observability/public/components/app/ingest_manager_panel/index.tsx +++ b/x-pack/plugins/observability/public/components/app/ingest_manager_panel/index.tsx @@ -11,13 +11,16 @@ import { EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EuiText } from '@elastic/eui'; import { EuiLink } from '@elastic/eui'; +import { usePluginContext } from '../../../hooks/use_plugin_context'; export function IngestManagerPanel() { + const { core } = usePluginContext(); + return ( @@ -25,7 +28,7 @@ export function IngestManagerPanel() {

    - {i18n.translate('xpack.observability.ingestManafer.title', { + {i18n.translate('xpack.observability.ingestManager.title', { defaultMessage: 'Have you seen our new Ingest Manager?', })}

    @@ -33,15 +36,15 @@ export function IngestManagerPanel() {
    - {i18n.translate('xpack.observability.ingestManafer.text', { + {i18n.translate('xpack.observability.ingestManager.text', { defaultMessage: 'The Elastic Agent provides a simple, unified way to add monitoring for logs, metrics, and other types of data to your hosts. You no longer need to install multiple Beats and other agents, making it easier and faster to deploy configurations across your infrastructure.', })} - - {i18n.translate('xpack.observability.ingestManafer.button', { + + {i18n.translate('xpack.observability.ingestManager.button', { defaultMessage: 'Try Ingest Manager Beta', })} diff --git a/x-pack/plugins/reporting/server/routes/jobs.ts b/x-pack/plugins/reporting/server/routes/jobs.ts index e8eac9e577beb5b..db62c0cc403fc05 100644 --- a/x-pack/plugins/reporting/server/routes/jobs.ts +++ b/x-pack/plugins/reporting/server/routes/jobs.ts @@ -15,11 +15,6 @@ import { downloadJobResponseHandlerFactory, } from './lib/job_response_handler'; -interface ListQuery { - page: string; - size: string; - ids?: string; // optional field forbids us from extending RequestQuery -} const MAIN_ENTRY = `${API_BASE_URL}/jobs`; const handleUnavailable = (res: any) => { @@ -52,11 +47,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { const { management: { jobTypes = [] }, } = await reporting.getLicenseInfo(); - const { - page: queryPage = '0', - size: querySize = '10', - ids: queryIds = null, - } = req.query as ListQuery; // NOTE: type inference is not working here. userHandler breaks it? + const { page: queryPage = '0', size: querySize = '10', ids: queryIds = null } = req.query; const page = parseInt(queryPage, 10) || 0; const size = Math.min(100, parseInt(querySize, 10) || 10); const jobIds = queryIds ? queryIds.split(',') : null; @@ -116,7 +107,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { return handleUnavailable(res); } - const { docId } = req.params as { docId: string }; + const { docId } = req.params; const { management: { jobTypes = [] }, } = await reporting.getLicenseInfo(); @@ -161,7 +152,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { return res.custom({ statusCode: 503 }); } - const { docId } = req.params as { docId: string }; + const { docId } = req.params; const { management: { jobTypes = [] }, } = await reporting.getLicenseInfo(); @@ -213,7 +204,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { return handleUnavailable(res); } - const { docId } = req.params as { docId: string }; + const { docId } = req.params; const { management: { jobTypes = [] }, } = await reporting.getLicenseInfo(); @@ -239,7 +230,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { return handleUnavailable(res); } - const { docId } = req.params as { docId: string }; + const { docId } = req.params; const { management: { jobTypes = [] }, } = await reporting.getLicenseInfo(); diff --git a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts index 3758eafc6d718c0..e2f393aad57d23c 100644 --- a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts +++ b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts @@ -12,7 +12,7 @@ import { getUserFactory } from './get_user'; type ReportingUser = AuthenticatedUser | null; const superuserRole = 'superuser'; -export type RequestHandlerUser = RequestHandler extends (...a: infer U) => infer R +export type RequestHandlerUser = RequestHandler extends (...a: infer U) => infer R ? (user: ReportingUser, ...a: U) => R : never; @@ -21,7 +21,7 @@ export const authorizedUserPreRoutingFactory = function authorizedUserPreRouting ) { const setupDeps = reporting.getPluginSetupDeps(); const getUser = getUserFactory(setupDeps.security); - return (handler: RequestHandlerUser): RequestHandler => { + return (handler: RequestHandlerUser): RequestHandler => { return (context, req, res) => { let user: ReportingUser = null; if (setupDeps.security && setupDeps.security.license.isEnabled()) { diff --git a/x-pack/plugins/security_solution/common/machine_learning/has_ml_license.test.ts b/x-pack/plugins/security_solution/common/machine_learning/has_ml_license.test.ts new file mode 100644 index 000000000000000..1ffc2e16b78f751 --- /dev/null +++ b/x-pack/plugins/security_solution/common/machine_learning/has_ml_license.test.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 { emptyMlCapabilities } from './empty_ml_capabilities'; +import { hasMlLicense } from './has_ml_license'; + +describe('hasMlLicense', () => { + test('it returns false when license is not platinum or trial', () => { + const capabilities = { ...emptyMlCapabilities, isPlatinumOrTrialLicense: false }; + expect(hasMlLicense(capabilities)).toEqual(false); + }); + + test('it returns true when license is platinum or trial', () => { + const capabilities = { ...emptyMlCapabilities, isPlatinumOrTrialLicense: true }; + expect(hasMlLicense(capabilities)).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/common/machine_learning/has_ml_license.ts b/x-pack/plugins/security_solution/common/machine_learning/has_ml_license.ts new file mode 100644 index 000000000000000..c0b6862ac30fe56 --- /dev/null +++ b/x-pack/plugins/security_solution/common/machine_learning/has_ml_license.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MlCapabilitiesResponse } from '../../../ml/common/types/capabilities'; + +export const hasMlLicense = (capabilities: MlCapabilitiesResponse): boolean => + capabilities.isPlatinumOrTrialLicense; diff --git a/x-pack/plugins/security_solution/common/machine_learning/is_security_job.ts b/x-pack/plugins/security_solution/common/machine_learning/is_security_job.ts index 43cfa4ad599640c..f5783fc9b3973af 100644 --- a/x-pack/plugins/security_solution/common/machine_learning/is_security_job.ts +++ b/x-pack/plugins/security_solution/common/machine_learning/is_security_job.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MlSummaryJob } from '../../../ml/common/types/anomaly_detection_jobs'; import { ML_GROUP_IDS } from '../constants'; -export const isSecurityJob = (job: MlSummaryJob): boolean => +export const isSecurityJob = (job: { groups: string[] }): boolean => job.groups.some((group) => ML_GROUP_IDS.includes(group)); diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.test.tsx index 6fb693e47560d62..56daa9a8364f6b1 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.test.tsx @@ -80,6 +80,9 @@ describe('Configuration button', () => { }); test('it shows the tooltip when hovering the button', () => { + // Use fake timers so we don't have to wait for the EuiToolTip timeout + jest.useFakeTimers(); + const msgTooltip = 'My message tooltip'; const titleTooltip = 'My title'; @@ -96,11 +99,14 @@ describe('Configuration button', () => { ); newWrapper.find('[data-test-subj="configure-case-button"]').first().simulate('mouseOver'); - // EuiToolTip mounts children after a 250ms delay - setTimeout( - () => - expect(newWrapper.find('.euiToolTipPopover').text()).toBe(`${titleTooltip}${msgTooltip}`), - 250 - ); + + // Run the timers so the EuiTooltip will be visible + jest.runAllTimers(); + + newWrapper.update(); + expect(newWrapper.find('.euiToolTipPopover').text()).toBe(`${titleTooltip}${msgTooltip}`); + + // Clearing all mocks will also reset fake timers. + jest.clearAllMocks(); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts index 6fbb308672e5d75..e6597de892bff37 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts @@ -9,13 +9,11 @@ import { useState, useEffect, useMemo } from 'react'; import { DEFAULT_ANOMALY_SCORE } from '../../../../../common/constants'; import { anomaliesTableData } from '../api/anomalies_table_data'; import { InfluencerInput, Anomalies, CriteriaFields } from '../types'; -import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; -import { useSiemJobs } from '../../ml_popover/hooks/use_siem_jobs'; -import { useMlCapabilities } from '../../ml_popover/hooks/use_ml_capabilities'; -import { useStateToaster, errorToToaster } from '../../toasters'; import * as i18n from './translations'; import { useTimeZone, useUiSetting$ } from '../../../lib/kibana'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; +import { useInstalledSecurityJobs } from '../hooks/use_installed_security_jobs'; interface Args { influencers?: InfluencerInput[]; @@ -58,15 +56,13 @@ export const useAnomaliesTableData = ({ skip = false, }: Args): Return => { const [tableData, setTableData] = useState(null); - const [, siemJobs] = useSiemJobs(true); + const { isMlUser, jobs } = useInstalledSecurityJobs(); const [loading, setLoading] = useState(true); - const capabilities = useMlCapabilities(); - const userPermissions = hasMlUserPermissions(capabilities); - const [, dispatchToaster] = useStateToaster(); + const { addError } = useAppToasts(); const timeZone = useTimeZone(); const [anomalyScore] = useUiSetting$(DEFAULT_ANOMALY_SCORE); - const siemJobIds = siemJobs.filter((job) => job.isInstalled).map((job) => job.id); + const jobIds = jobs.map((job) => job.id); const startDateMs = useMemo(() => new Date(startDate).getTime(), [startDate]); const endDateMs = useMemo(() => new Date(endDate).getTime(), [endDate]); @@ -81,11 +77,11 @@ export const useAnomaliesTableData = ({ earliestMs: number, latestMs: number ) { - if (userPermissions && !skip && siemJobIds.length > 0) { + if (isMlUser && !skip && jobIds.length > 0) { try { const data = await anomaliesTableData( { - jobIds: siemJobIds, + jobIds, criteriaFields: criteriaFieldsInput, aggregationInterval: 'auto', threshold: getThreshold(anomalyScore, threshold), @@ -104,13 +100,13 @@ export const useAnomaliesTableData = ({ } } catch (error) { if (isSubscribed) { - errorToToaster({ title: i18n.SIEM_TABLE_FETCH_FAILURE, error, dispatchToaster }); + addError(error, { title: i18n.SIEM_TABLE_FETCH_FAILURE }); setLoading(false); } } - } else if (!userPermissions && isSubscribed) { + } else if (!isMlUser && isSubscribed) { setLoading(false); - } else if (siemJobIds.length === 0 && isSubscribed) { + } else if (jobIds.length === 0 && isSubscribed) { setLoading(false); } else if (isSubscribed) { setTableData(null); @@ -132,9 +128,9 @@ export const useAnomaliesTableData = ({ startDateMs, endDateMs, skip, - userPermissions, + isMlUser, // eslint-disable-next-line react-hooks/exhaustive-deps - siemJobIds.sort().join(), + jobIds.sort().join(), ]); return [loading, tableData]; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/api/get_jobs_summary.ts b/x-pack/plugins/security_solution/public/common/components/ml/api/get_jobs_summary.ts new file mode 100644 index 000000000000000..15f823814d7fc87 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/api/get_jobs_summary.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpSetup } from '../../../../../../../../src/core/public'; +import { MlSummaryJob } from '../../../../../../ml/public'; + +export interface GetJobsSummaryArgs { + http: HttpSetup; + jobIds?: string[]; + signal: AbortSignal; +} + +/** + * Fetches a summary of all ML jobs currently installed + * + * @param http HTTP Service + * @param jobIds Array of job IDs to filter against + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const getJobsSummary = async ({ + http, + jobIds, + signal, +}: GetJobsSummaryArgs): Promise => + http.fetch('/api/ml/jobs/jobs_summary', { + method: 'POST', + body: JSON.stringify({ jobIds: jobIds ?? [] }), + asSystemRequest: true, + signal, + }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/api/get_ml_capabilities.ts b/x-pack/plugins/security_solution/public/common/components/ml/api/get_ml_capabilities.ts index 32f6f888ab8d712..8ee765c1dea4763 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/api/get_ml_capabilities.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/api/get_ml_capabilities.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { HttpSetup } from '../../../../../../../../src/core/public'; import { MlCapabilitiesResponse } from '../../../../../../ml/public'; -import { KibanaServices } from '../../../lib/kibana'; import { InfluencerInput } from '../types'; export interface Body { @@ -21,10 +21,15 @@ export interface Body { maxExamples: number; } -export const getMlCapabilities = async (signal: AbortSignal): Promise => { - return KibanaServices.get().http.fetch('/api/ml/ml_capabilities', { +export const getMlCapabilities = async ({ + http, + signal, +}: { + http: HttpSetup; + signal: AbortSignal; +}): Promise => + http.fetch('/api/ml/ml_capabilities', { method: 'GET', asSystemRequest: true, signal, }); -}; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_jobs_summary.ts b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_jobs_summary.ts new file mode 100644 index 000000000000000..a80bfb59649cb48 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_jobs_summary.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useAsync, withOptionalSignal } from '../../../../shared_imports'; +import { getJobsSummary } from '../api/get_jobs_summary'; + +const _getJobsSummary = withOptionalSignal(getJobsSummary); + +export const useGetJobsSummary = () => useAsync(_getJobsSummary); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_ml_capabilities.ts b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_ml_capabilities.ts new file mode 100644 index 000000000000000..aabd8c7b62e85fd --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_ml_capabilities.ts @@ -0,0 +1,12 @@ +/* + * 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 { getMlCapabilities } from '../api/get_ml_capabilities'; +import { useAsync, withOptionalSignal } from '../../../../shared_imports'; + +const _getMlCapabilities = withOptionalSignal(getMlCapabilities); + +export const useGetMlCapabilities = () => useAsync(_getMlCapabilities); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.test.ts new file mode 100644 index 000000000000000..72690a17739266f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.test.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook } from '@testing-library/react-hooks'; + +import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; +import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license'; +import { isSecurityJob } from '../../../../../common/machine_learning/is_security_job'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; +import { useAppToastsMock } from '../../../hooks/use_app_toasts.mock'; +import { mockJobsSummaryResponse } from '../../ml_popover/api.mock'; +import { getJobsSummary } from '../api/get_jobs_summary'; +import { useInstalledSecurityJobs } from './use_installed_security_jobs'; + +jest.mock('../../../../../common/machine_learning/has_ml_user_permissions'); +jest.mock('../../../../../common/machine_learning/has_ml_license'); +jest.mock('../../../hooks/use_app_toasts'); +jest.mock('../api/get_jobs_summary'); + +describe('useInstalledSecurityJobs', () => { + let appToastsMock: jest.Mocked>; + + beforeEach(() => { + appToastsMock = useAppToastsMock.create(); + (useAppToasts as jest.Mock).mockReturnValue(appToastsMock); + (getJobsSummary as jest.Mock).mockResolvedValue(mockJobsSummaryResponse); + }); + + describe('when the user has permissions', () => { + beforeEach(() => { + (hasMlUserPermissions as jest.Mock).mockReturnValue(true); + (hasMlLicense as jest.Mock).mockReturnValue(true); + }); + + it('returns jobs and permissions', async () => { + const { result, waitForNextUpdate } = renderHook(() => useInstalledSecurityJobs()); + await waitForNextUpdate(); + + expect(result.current.jobs).toHaveLength(3); + expect(result.current.jobs).toEqual( + expect.arrayContaining([ + { + datafeedId: 'datafeed-siem-api-rare_process_linux_ecs', + datafeedIndices: ['auditbeat-*'], + datafeedState: 'stopped', + description: 'SIEM Auditbeat: Detect unusually rare processes on Linux (beta)', + earliestTimestampMs: 1557353420495, + groups: ['siem'], + hasDatafeed: true, + id: 'siem-api-rare_process_linux_ecs', + isSingleMetricViewerJob: true, + jobState: 'closed', + latestTimestampMs: 1557434782207, + memory_status: 'hard_limit', + processed_record_count: 582251, + }, + ]) + ); + expect(result.current.isMlUser).toEqual(true); + expect(result.current.isLicensed).toEqual(true); + }); + + it('filters out non-security jobs', async () => { + const { result, waitForNextUpdate } = renderHook(() => useInstalledSecurityJobs()); + await waitForNextUpdate(); + + expect(result.current.jobs.length).toBeGreaterThan(0); + expect(result.current.jobs.every(isSecurityJob)).toEqual(true); + }); + + it('renders a toast error if the ML call fails', async () => { + (getJobsSummary as jest.Mock).mockRejectedValue('whoops'); + const { waitForNextUpdate } = renderHook(() => useInstalledSecurityJobs()); + await waitForNextUpdate(); + + expect(appToastsMock.addError).toHaveBeenCalledWith('whoops', { + title: 'Security job fetch failure', + }); + }); + }); + + describe('when the user does not have valid permissions', () => { + beforeEach(() => { + (hasMlUserPermissions as jest.Mock).mockReturnValue(false); + (hasMlLicense as jest.Mock).mockReturnValue(false); + }); + + it('returns empty jobs and false predicates', () => { + const { result } = renderHook(() => useInstalledSecurityJobs()); + + expect(result.current.jobs).toEqual([]); + expect(result.current.isMlUser).toEqual(false); + expect(result.current.isLicensed).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts new file mode 100644 index 000000000000000..a9a728f81cc6cd3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState } from 'react'; + +import { MlSummaryJob } from '../../../../../../ml/public'; +import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; +import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license'; +import { isSecurityJob } from '../../../../../common/machine_learning/is_security_job'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; +import { useHttp } from '../../../lib/kibana'; +import { useMlCapabilities } from './use_ml_capabilities'; +import * as i18n from '../translations'; +import { useGetJobsSummary } from './use_get_jobs_summary'; + +export interface UseInstalledSecurityJobsReturn { + loading: boolean; + jobs: MlSummaryJob[]; + isMlUser: boolean; + isLicensed: boolean; +} + +/** + * Returns a collection of installed ML jobs (MlSummaryJob) relevant to + * Security Solution, i.e. all installed jobs in the `security` ML group. + * Use the corresponding helper functions to filter the job list as + * necessary (running jobs, etc). + * + */ +export const useInstalledSecurityJobs = (): UseInstalledSecurityJobsReturn => { + const [jobs, setJobs] = useState([]); + const { addError } = useAppToasts(); + const mlCapabilities = useMlCapabilities(); + const http = useHttp(); + const { error, loading, result, start } = useGetJobsSummary(); + + const isMlUser = hasMlUserPermissions(mlCapabilities); + const isLicensed = hasMlLicense(mlCapabilities); + + useEffect(() => { + if (isMlUser && isLicensed) { + start({ http }); + } + }, [http, isMlUser, isLicensed, start]); + + useEffect(() => { + if (result) { + const securityJobs = result.filter(isSecurityJob); + setJobs(securityJobs); + } + }, [result]); + + useEffect(() => { + if (error) { + addError(error, { title: i18n.SIEM_JOB_FETCH_FAILURE }); + } + }, [addError, error]); + + return { isLicensed, isMlUser, jobs, loading }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_ml_capabilities.tsx b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_ml_capabilities.ts similarity index 80% rename from x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_ml_capabilities.tsx rename to x-pack/plugins/security_solution/public/common/components/ml/hooks/use_ml_capabilities.ts index d897b2554b4fddb..4f804a355e4b556 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_ml_capabilities.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_ml_capabilities.ts @@ -6,6 +6,6 @@ import { useContext } from 'react'; -import { MlCapabilitiesContext } from '../../ml/permissions/ml_capabilities_provider'; +import { MlCapabilitiesContext } from '../permissions/ml_capabilities_provider'; export const useMlCapabilities = () => useContext(MlCapabilitiesContext); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/permissions/ml_capabilities_provider.tsx b/x-pack/plugins/security_solution/public/common/components/ml/permissions/ml_capabilities_provider.tsx index c83271a56be5a89..c12c8d78da714e4 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/permissions/ml_capabilities_provider.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/permissions/ml_capabilities_provider.tsx @@ -8,9 +8,9 @@ import React, { useState, useEffect } from 'react'; import { MlCapabilitiesResponse } from '../../../../../../ml/public'; import { emptyMlCapabilities } from '../../../../../common/machine_learning/empty_ml_capabilities'; -import { getMlCapabilities } from '../api/get_ml_capabilities'; -import { errorToToaster, useStateToaster } from '../../toasters'; - +import { useAppToasts } from '../../../hooks/use_app_toasts'; +import { useHttp } from '../../../lib/kibana'; +import { useGetMlCapabilities } from '../hooks/use_get_ml_capabilities'; import * as i18n from './translations'; interface MlCapabilitiesProvider extends MlCapabilitiesResponse { @@ -32,36 +32,27 @@ export const MlCapabilitiesProvider = React.memo<{ children: JSX.Element }>(({ c const [capabilities, setCapabilities] = useState( emptyMlCapabilitiesProvider ); - const [, dispatchToaster] = useStateToaster(); + const http = useHttp(); + const { addError } = useAppToasts(); + const { start, result, error } = useGetMlCapabilities(); useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); + start({ http }); + }, [http, start]); - async function fetchMlCapabilities() { - try { - const mlCapabilities = await getMlCapabilities(abortCtrl.signal); - if (isSubscribed) { - setCapabilities({ ...mlCapabilities, capabilitiesFetched: true }); - } - } catch (error) { - if (isSubscribed) { - errorToToaster({ - title: i18n.MACHINE_LEARNING_PERMISSIONS_FAILURE, - error, - dispatchToaster, - }); - } - } + useEffect(() => { + if (result) { + setCapabilities({ ...result, capabilitiesFetched: true }); } + }, [result]); - fetchMlCapabilities(); - return () => { - isSubscribed = false; - abortCtrl.abort(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + useEffect(() => { + if (error) { + addError(error, { + title: i18n.MACHINE_LEARNING_PERMISSIONS_FAILURE, + }); + } + }, [addError, error]); return ( {children} diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx index 9bfae686b1a594b..7fdf41e6b6500de 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx @@ -16,7 +16,7 @@ import { convertAnomaliesToHosts } from './convert_anomalies_to_hosts'; import { Loader } from '../../loader'; import { getIntervalFromAnomalies } from '../anomaly/get_interval_from_anomalies'; import { AnomaliesHostTableProps } from '../types'; -import { useMlCapabilities } from '../../ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../hooks/use_ml_capabilities'; import { BasicTable } from './basic_table'; import { hostEquality } from './host_equality'; import { getCriteriaFromHostType } from '../criteria/get_criteria_from_host_type'; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx index af27d411b990d99..124d8d9a794c1ac 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx @@ -14,7 +14,7 @@ import { convertAnomaliesToNetwork } from './convert_anomalies_to_network'; import { Loader } from '../../loader'; import { AnomaliesNetworkTableProps } from '../types'; import { getAnomaliesNetworkTableColumnsCurated } from './get_anomalies_network_table_columns'; -import { useMlCapabilities } from '../../ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../hooks/use_ml_capabilities'; import { BasicTable } from './basic_table'; import { networkEquality } from './network_equality'; import { getCriteriaFromNetworkType } from '../criteria/get_criteria_from_network_type'; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/translations.ts b/x-pack/plugins/security_solution/public/common/components/ml/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/translations.ts rename to x-pack/plugins/security_solution/public/common/components/ml/translations.ts diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/__mocks__/api.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/api.mock.ts similarity index 99% rename from x-pack/plugins/security_solution/public/common/components/ml_popover/__mocks__/api.tsx rename to x-pack/plugins/security_solution/public/common/components/ml_popover/api.mock.ts index 54bb0a96207e148..0e8f033ff0cf355 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/__mocks__/api.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/api.mock.ts @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ +import { MlSummaryJob } from '../../../../../ml/public'; import { Group, - JobSummary, Module, RecognizerModule, SetupMlResponse, - SiemJob, + SecurityJob, StartDatafeedResponse, StopDatafeedResponse, -} from '../types'; +} from './types'; export const mockGroupsResponse: Group[] = [ { @@ -31,7 +31,7 @@ export const mockGroupsResponse: Group[] = [ { id: 'suricata', jobIds: ['suricata_alert_rate'], calendarIds: [] }, ]; -export const mockOpenedJob: JobSummary = { +export const mockOpenedJob: MlSummaryJob = { datafeedId: 'datafeed-siem-api-rare_process_linux_ecs', datafeedIndices: ['auditbeat-*'], datafeedState: 'started', @@ -48,7 +48,7 @@ export const mockOpenedJob: JobSummary = { processed_record_count: 3425264, }; -export const mockJobsSummaryResponse: JobSummary[] = [ +export const mockJobsSummaryResponse: MlSummaryJob[] = [ { id: 'rc-rare-process-windows-5', description: @@ -491,7 +491,7 @@ export const mockStopDatafeedsSuccess: StopDatafeedResponse = { 'datafeed-linux_anomalous_network_service': { stopped: true }, }; -export const mockSiemJobs: SiemJob[] = [ +export const mockSecurityJobs: SecurityJob[] = [ { id: 'linux_anomalous_network_activity_ecs', description: diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/api.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/api.ts similarity index 89% rename from x-pack/plugins/security_solution/public/common/components/ml_popover/api.tsx rename to x-pack/plugins/security_solution/public/common/components/ml_popover/api.ts index 7c72098209a066e..dd0fb33fd2bc67f 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/api.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/api.ts @@ -9,7 +9,6 @@ import { CloseJobsResponse, ErrorResponse, GetModulesProps, - JobSummary, MlSetupArgs, Module, RecognizerModule, @@ -165,21 +164,3 @@ export const stopDatafeeds = async ({ return [stopDatafeedsResponse, closeJobsResponse]; }; - -/** - * Fetches a summary of all ML jobs currently installed - * - * NOTE: If not sending jobIds in the body, you must at least send an empty body or the server will - * return a 500 - * - * @param signal to cancel request - * - * @throws An error if response is not OK - */ -export const getJobsSummary = async (signal: AbortSignal): Promise => - KibanaServices.get().http.fetch('/api/ml/jobs/jobs_summary', { - method: 'POST', - body: JSON.stringify({}), - asSystemRequest: true, - signal, - }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.test.tsx index 0b8da6be57e1b55..2a2db46d4230775 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.test.tsx @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mockSiemJobs } from './__mocks__/api'; +import { mockSecurityJobs } from './api.mock'; import { filterJobs, getStablePatternTitles, searchFilter } from './helpers'; describe('helpers', () => { describe('filterJobs', () => { test('returns all jobs when no filter is suplied', () => { const filteredJobs = filterJobs({ - jobs: mockSiemJobs, + jobs: mockSecurityJobs, selectedGroups: [], showCustomJobs: false, showElasticJobs: false, @@ -23,17 +23,17 @@ describe('helpers', () => { describe('searchFilter', () => { test('returns all jobs when nullfilterQuery is provided', () => { - const jobsToDisplay = searchFilter(mockSiemJobs); - expect(jobsToDisplay.length).toEqual(mockSiemJobs.length); + const jobsToDisplay = searchFilter(mockSecurityJobs); + expect(jobsToDisplay.length).toEqual(mockSecurityJobs.length); }); test('returns correct DisplayJobs when filterQuery matches job.id', () => { - const jobsToDisplay = searchFilter(mockSiemJobs, 'rare_process'); + const jobsToDisplay = searchFilter(mockSecurityJobs, 'rare_process'); expect(jobsToDisplay.length).toEqual(2); }); test('returns correct DisplayJobs when filterQuery matches job.description', () => { - const jobsToDisplay = searchFilter(mockSiemJobs, 'Detect unusually'); + const jobsToDisplay = searchFilter(mockSecurityJobs, 'Detect unusually'); expect(jobsToDisplay.length).toEqual(2); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.tsx index 5989d052f7cd251..daf9da855c0f949 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SiemJob } from './types'; +import { SecurityJob } from './types'; /** * Returns a filtered array of Jobs according to JobsTableFilters selections @@ -22,12 +22,12 @@ export const filterJobs = ({ showElasticJobs, filterQuery, }: { - jobs: SiemJob[]; + jobs: SecurityJob[]; selectedGroups: string[]; showCustomJobs: boolean; showElasticJobs: boolean; filterQuery: string; -}): SiemJob[] => +}): SecurityJob[] => searchFilter( jobs .filter((job) => !showCustomJobs || (showCustomJobs && !job.isElasticJob)) @@ -44,7 +44,7 @@ export const filterJobs = ({ * @param jobs to filter * @param filterQuery user-provided search string to filter for occurrence in job names/description */ -export const searchFilter = (jobs: SiemJob[], filterQuery?: string): SiemJob[] => +export const searchFilter = (jobs: SecurityJob[], filterQuery?: string): SecurityJob[] => jobs.filter((job) => filterQuery == null ? true diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.test.ts b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.test.ts new file mode 100644 index 000000000000000..80f50912a84f289 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.test.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook } from '@testing-library/react-hooks'; + +import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions'; +import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; +import { useAppToastsMock } from '../../../hooks/use_app_toasts.mock'; +import { getJobsSummary } from '../../ml/api/get_jobs_summary'; +import { checkRecognizer, getModules } from '../api'; +import { SecurityJob } from '../types'; +import { + mockJobsSummaryResponse, + mockGetModuleResponse, + checkRecognizerSuccess, +} from '../api.mock'; +import { useSecurityJobs } from './use_security_jobs'; + +jest.mock('../../../../../common/machine_learning/has_ml_admin_permissions'); +jest.mock('../../../../../common/machine_learning/has_ml_license'); +jest.mock('../../../lib/kibana'); +jest.mock('../../../hooks/use_app_toasts'); +jest.mock('../../ml/hooks/use_ml_capabilities'); +jest.mock('../../ml/api/get_jobs_summary'); +jest.mock('../api'); + +describe('useSecurityJobs', () => { + let appToastsMock: jest.Mocked>; + + beforeEach(() => { + appToastsMock = useAppToastsMock.create(); + (useAppToasts as jest.Mock).mockReturnValue(appToastsMock); + }); + + describe('when user has valid permissions', () => { + beforeEach(() => { + (hasMlAdminPermissions as jest.Mock).mockReturnValue(true); + (hasMlLicense as jest.Mock).mockReturnValue(true); + (getJobsSummary as jest.Mock).mockResolvedValue(mockJobsSummaryResponse); + (getModules as jest.Mock).mockResolvedValue(mockGetModuleResponse); + (checkRecognizer as jest.Mock).mockResolvedValue(checkRecognizerSuccess); + }); + + it('combines multiple ML calls into an array of SecurityJobs', async () => { + const expectedSecurityJob: SecurityJob = { + datafeedId: 'datafeed-siem-api-rare_process_linux_ecs', + datafeedIndices: ['auditbeat-*'], + datafeedState: 'stopped', + defaultIndexPattern: '', + description: 'SIEM Auditbeat: Detect unusually rare processes on Linux (beta)', + earliestTimestampMs: 1557353420495, + groups: ['siem'], + hasDatafeed: true, + id: 'siem-api-rare_process_linux_ecs', + isCompatible: true, + isElasticJob: false, + isInstalled: true, + isSingleMetricViewerJob: true, + jobState: 'closed', + latestTimestampMs: 1557434782207, + memory_status: 'hard_limit', + moduleId: '', + processed_record_count: 582251, + }; + + const { result, waitForNextUpdate } = renderHook(() => useSecurityJobs(false)); + await waitForNextUpdate(); + + expect(result.current.jobs).toHaveLength(6); + expect(result.current.jobs).toEqual(expect.arrayContaining([expectedSecurityJob])); + }); + + it('returns those permissions', async () => { + const { result, waitForNextUpdate } = renderHook(() => useSecurityJobs(false)); + await waitForNextUpdate(); + + expect(result.current.isMlAdmin).toEqual(true); + expect(result.current.isLicensed).toEqual(true); + }); + + it('renders a toast error if an ML call fails', async () => { + (getModules as jest.Mock).mockRejectedValue('whoops'); + const { waitForNextUpdate } = renderHook(() => useSecurityJobs(false)); + await waitForNextUpdate(); + + expect(appToastsMock.addError).toHaveBeenCalledWith('whoops', { + title: 'Security job fetch failure', + }); + }); + }); + + describe('when the user does not have valid permissions', () => { + beforeEach(() => { + (hasMlAdminPermissions as jest.Mock).mockReturnValue(false); + (hasMlLicense as jest.Mock).mockReturnValue(false); + }); + + it('returns empty jobs and false predicates', () => { + const { result } = renderHook(() => useSecurityJobs(false)); + + expect(result.current.jobs).toEqual([]); + expect(result.current.isMlAdmin).toEqual(false); + expect(result.current.isLicensed).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.ts b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.ts new file mode 100644 index 000000000000000..e8809e8366eed45 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.ts @@ -0,0 +1,95 @@ +/* + * 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 } from 'react'; + +import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; +import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions'; +import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; +import { useUiSetting$, useHttp } from '../../../lib/kibana'; +import { checkRecognizer, getModules } from '../api'; +import { SecurityJob } from '../types'; +import { createSecurityJobs } from './use_security_jobs_helpers'; +import { useMlCapabilities } from '../../ml/hooks/use_ml_capabilities'; +import * as i18n from '../../ml/translations'; +import { getJobsSummary } from '../../ml/api/get_jobs_summary'; + +export interface UseSecurityJobsReturn { + loading: boolean; + jobs: SecurityJob[]; + isMlAdmin: boolean; + isLicensed: boolean; +} + +/** + * Compiles a collection of SecurityJobs, which are a list of all jobs relevant to the Security Solution App. This + * includes all installed jobs in the `Security` ML group, and all jobs within ML Modules defined in + * ml_module (whether installed or not). Use the corresponding helper functions to filter the job + * list as necessary. E.g. installed jobs, running jobs, etc. + * + * NOTE: If the user is not an ml admin, jobs will be empty and isMlAdmin will be false. + * + * @param refetchData + */ +export const useSecurityJobs = (refetchData: boolean): UseSecurityJobsReturn => { + const [jobs, setJobs] = useState([]); + const [loading, setLoading] = useState(true); + const mlCapabilities = useMlCapabilities(); + const [siemDefaultIndex] = useUiSetting$(DEFAULT_INDEX_KEY); + const http = useHttp(); + const { addError } = useAppToasts(); + + const isMlAdmin = hasMlAdminPermissions(mlCapabilities); + const isLicensed = hasMlLicense(mlCapabilities); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + setLoading(true); + + async function fetchSecurityJobIdsFromGroupsData() { + if (isMlAdmin && isLicensed) { + try { + // Batch fetch all installed jobs, ML modules, and check which modules are compatible with siemDefaultIndex + const [jobSummaryData, modulesData, compatibleModules] = await Promise.all([ + getJobsSummary({ http, signal: abortCtrl.signal }), + getModules({ signal: abortCtrl.signal }), + checkRecognizer({ + indexPatternName: siemDefaultIndex, + signal: abortCtrl.signal, + }), + ]); + + const compositeSecurityJobs = createSecurityJobs( + jobSummaryData, + modulesData, + compatibleModules + ); + + if (isSubscribed) { + setJobs(compositeSecurityJobs); + } + } catch (error) { + if (isSubscribed) { + addError(error, { title: i18n.SIEM_JOB_FETCH_FAILURE }); + } + } + } + if (isSubscribed) { + setLoading(false); + } + } + + fetchSecurityJobIdsFromGroupsData(); + return () => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [refetchData, isMlAdmin, isLicensed, siemDefaultIndex, addError, http]); + + return { isLicensed, isMlAdmin, jobs, loading }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs_helpers.test.tsx similarity index 83% rename from x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.test.tsx rename to x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs_helpers.test.tsx index fc9f369a305aa79..7fb4e6f59d9f7fe 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs_helpers.test.tsx @@ -6,29 +6,29 @@ import { composeModuleAndInstalledJobs, - createSiemJobs, + createSecurityJobs, getAugmentedFields, getInstalledJobs, getModuleJobs, - moduleToSiemJob, -} from './use_siem_jobs_helpers'; + moduleToSecurityJob, +} from './use_security_jobs_helpers'; import { checkRecognizerSuccess, mockGetModuleResponse, mockJobsSummaryResponse, -} from '../__mocks__/api'; +} from '../api.mock'; // TODO: Expand test coverage -describe('useSiemJobsHelpers', () => { - describe('moduleToSiemJob', () => { - test('correctly converts module to SiemJob', () => { - const siemJob = moduleToSiemJob( +describe('useSecurityJobsHelpers', () => { + describe('moduleToSecurityJob', () => { + test('correctly converts module to SecurityJob', () => { + const securityJob = moduleToSecurityJob( mockGetModuleResponse[0], mockGetModuleResponse[0].jobs[0], false ); - expect(siemJob).toEqual({ + expect(securityJob).toEqual({ datafeedId: '', datafeedIndices: [], datafeedState: '', @@ -86,19 +86,19 @@ describe('useSiemJobsHelpers', () => { const installedJobs = getInstalledJobs(mockJobsSummaryResponse, moduleJobs, [ 'siem_auditbeat', ]); - const siemJobs = composeModuleAndInstalledJobs(installedJobs, moduleJobs); - expect(siemJobs.length).toEqual(6); + const securityJobs = composeModuleAndInstalledJobs(installedJobs, moduleJobs); + expect(securityJobs.length).toEqual(6); }); }); - describe('createSiemJobs', () => { + describe('createSecurityJobs', () => { test('returns correct number of jobs when creating jobs with successful responses', () => { - const siemJobs = createSiemJobs( + const securityJobs = createSecurityJobs( mockJobsSummaryResponse, mockGetModuleResponse, checkRecognizerSuccess ); - expect(siemJobs.length).toEqual(6); + expect(securityJobs.length).toEqual(6); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs_helpers.tsx similarity index 59% rename from x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.tsx rename to x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs_helpers.tsx index adbd712ffeb3e06..d0109fd29b5fb87 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs_helpers.tsx @@ -5,26 +5,26 @@ */ import { - AugmentedSiemJobFields, - JobSummary, + AugmentedSecurityJobFields, Module, ModuleJob, RecognizerModule, - SiemJob, + SecurityJob, } from '../types'; import { mlModules } from '../ml_modules'; +import { MlSummaryJob } from '../../../../../../ml/public'; /** - * Helper function for converting from ModuleJob -> SiemJob + * Helper function for converting from ModuleJob -> SecurityJob * @param module * @param moduleJob * @param isCompatible */ -export const moduleToSiemJob = ( +export const moduleToSecurityJob = ( module: Module, moduleJob: ModuleJob, isCompatible: boolean -): SiemJob => { +): SecurityJob => { return { datafeedId: '', datafeedIndices: [], @@ -46,7 +46,7 @@ export const moduleToSiemJob = ( }; /** - * Returns fields necessary to augment a ModuleJob to a SiemJob + * Returns fields necessary to augment a ModuleJob to a SecurityJob * * @param jobId * @param moduleJobs @@ -54,9 +54,9 @@ export const moduleToSiemJob = ( */ export const getAugmentedFields = ( jobId: string, - moduleJobs: SiemJob[], + moduleJobs: SecurityJob[], compatibleModuleIds: string[] -): AugmentedSiemJobFields => { +): AugmentedSecurityJobFields => { const moduleJob = moduleJobs.find((mj) => mj.id === jobId); return moduleJob !== undefined ? { @@ -74,24 +74,27 @@ export const getAugmentedFields = ( }; /** - * Process Modules[] from the `get_module` ML API into SiemJobs[] by filtering to SIEM specific + * Process Modules[] from the `get_module` ML API into SecurityJobs[] by filtering to Security specific * modules and unpacking jobs from each module * * @param modulesData * @param compatibleModuleIds */ -export const getModuleJobs = (modulesData: Module[], compatibleModuleIds: string[]): SiemJob[] => +export const getModuleJobs = ( + modulesData: Module[], + compatibleModuleIds: string[] +): SecurityJob[] => modulesData .filter((module) => mlModules.includes(module.id)) .map((module) => [ ...module.jobs.map((moduleJob) => - moduleToSiemJob(module, moduleJob, compatibleModuleIds.includes(module.id)) + moduleToSecurityJob(module, moduleJob, compatibleModuleIds.includes(module.id)) ), ]) .flat(); /** - * Process JobSummary[] from the `jobs_summary` ML API into SiemJobs[] by filtering to to SIEM jobs + * Process data from the `jobs_summary` ML API into SecurityJobs[] by filtering to Security jobs * and augmenting with moduleId/defaultIndexPattern/isCompatible * * @param jobSummaryData @@ -99,57 +102,57 @@ export const getModuleJobs = (modulesData: Module[], compatibleModuleIds: string * @param compatibleModuleIds */ export const getInstalledJobs = ( - jobSummaryData: JobSummary[], - moduleJobs: SiemJob[], + jobSummaryData: MlSummaryJob[], + moduleJobs: SecurityJob[], compatibleModuleIds: string[] -): SiemJob[] => +): SecurityJob[] => jobSummaryData .filter(({ groups }) => groups.includes('siem') || groups.includes('security')) - .map((jobSummary) => ({ + .map((jobSummary) => ({ ...jobSummary, ...getAugmentedFields(jobSummary.id, moduleJobs, compatibleModuleIds), isInstalled: true, })); /** - * Combines installed jobs + moduleSiemJobs that don't overlap and sorts by name asc + * Combines installed jobs + moduleSecurityJobs that don't overlap and sorts by name asc * * @param installedJobs - * @param moduleSiemJobs + * @param moduleSecurityJobs */ export const composeModuleAndInstalledJobs = ( - installedJobs: SiemJob[], - moduleSiemJobs: SiemJob[] -): SiemJob[] => { + installedJobs: SecurityJob[], + moduleSecurityJobs: SecurityJob[] +): SecurityJob[] => { const installedJobsIds = installedJobs.map((installedJob) => installedJob.id); return [ ...installedJobs, - ...moduleSiemJobs.filter((mj) => !installedJobsIds.includes(mj.id)), + ...moduleSecurityJobs.filter((mj) => !installedJobsIds.includes(mj.id)), ].sort((a, b) => a.id.localeCompare(b.id)); }; /** - * Creates a list of SiemJobs by composing JobSummary jobs (installed jobs) and Module - * jobs (pre-packaged SIEM jobs) into a single job object that can be used throughout the SIEM app + * Creates a list of SecurityJobs by composing jobs summaries (installed jobs) and Module + * jobs (pre-packaged Security jobs) into a single job object that can be used throughout the Security app * * @param jobSummaryData * @param modulesData * @param compatibleModules */ -export const createSiemJobs = ( - jobSummaryData: JobSummary[], +export const createSecurityJobs = ( + jobSummaryData: MlSummaryJob[], modulesData: Module[], compatibleModules: RecognizerModule[] -): SiemJob[] => { +): SecurityJob[] => { // Create lookup of compatible modules const compatibleModuleIds = compatibleModules.map((module) => module.id); - // Process modulesData: Filter to SIEM specific modules, and unpack jobs from modules - const moduleSiemJobs = getModuleJobs(modulesData, compatibleModuleIds); + // Process modulesData: Filter to Security specific modules, and unpack jobs from modules + const moduleSecurityJobs = getModuleJobs(modulesData, compatibleModuleIds); - // Process jobSummaryData: Filter to SIEM jobs, and augment with moduleId/defaultIndexPattern/isCompatible - const installedJobs = getInstalledJobs(jobSummaryData, moduleSiemJobs, compatibleModuleIds); + // Process jobSummaryData: Filter to Security jobs, and augment with moduleId/defaultIndexPattern/isCompatible + const installedJobs = getInstalledJobs(jobSummaryData, moduleSecurityJobs, compatibleModuleIds); - // Combine installed jobs + moduleSiemJobs that don't overlap, and sort by name asc - return composeModuleAndInstalledJobs(installedJobs, moduleSiemJobs); + // Combine installed jobs + moduleSecurityJobs that don't overlap, and sort by name asc + return composeModuleAndInstalledJobs(installedJobs, moduleSecurityJobs); }; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs.tsx deleted file mode 100644 index 7f0a8dea1913e42..000000000000000 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs.tsx +++ /dev/null @@ -1,81 +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 { useEffect, useState } from 'react'; - -import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; -import { checkRecognizer, getJobsSummary, getModules } from '../api'; -import { SiemJob } from '../types'; -import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; -import { errorToToaster, useStateToaster } from '../../toasters'; -import { useUiSetting$ } from '../../../lib/kibana'; - -import * as i18n from './translations'; -import { createSiemJobs } from './use_siem_jobs_helpers'; -import { useMlCapabilities } from './use_ml_capabilities'; - -type Return = [boolean, SiemJob[]]; - -/** - * Compiles a collection of SiemJobs, which are a list of all jobs relevant to the SIEM App. This - * includes all installed jobs in the `SIEM` ML group, and all jobs within ML Modules defined in - * ml_module (whether installed or not). Use the corresponding helper functions to filter the job - * list as necessary. E.g. installed jobs, running jobs, etc. - * - * @param refetchData - */ -export const useSiemJobs = (refetchData: boolean): Return => { - const [siemJobs, setSiemJobs] = useState([]); - const [loading, setLoading] = useState(true); - const mlCapabilities = useMlCapabilities(); - const userPermissions = hasMlUserPermissions(mlCapabilities); - const [siemDefaultIndex] = useUiSetting$(DEFAULT_INDEX_KEY); - const [, dispatchToaster] = useStateToaster(); - - useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); - setLoading(true); - - async function fetchSiemJobIdsFromGroupsData() { - if (userPermissions) { - try { - // Batch fetch all installed jobs, ML modules, and check which modules are compatible with siemDefaultIndex - const [jobSummaryData, modulesData, compatibleModules] = await Promise.all([ - getJobsSummary(abortCtrl.signal), - getModules({ signal: abortCtrl.signal }), - checkRecognizer({ - indexPatternName: siemDefaultIndex, - signal: abortCtrl.signal, - }), - ]); - - const compositeSiemJobs = createSiemJobs(jobSummaryData, modulesData, compatibleModules); - - if (isSubscribed) { - setSiemJobs(compositeSiemJobs); - } - } catch (error) { - if (isSubscribed) { - errorToToaster({ title: i18n.SIEM_JOB_FETCH_FAILURE, error, dispatchToaster }); - } - } - } - if (isSubscribed) { - setLoading(false); - } - } - - fetchSiemJobIdsFromGroupsData(); - return () => { - isSubscribed = false; - abortCtrl.abort(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [refetchData, userPermissions]); - - return [loading, siemJobs]; -}; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap index 747ac63551b55ba..9bee321e9fbde46 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap @@ -25,7 +25,7 @@ exports[`JobsTableFilters renders correctly against snapshot 1`] = ` { - let siemJobs: SiemJob[]; + let securityJobs: SecurityJob[]; beforeEach(() => { - siemJobs = cloneDeep(mockSiemJobs); + securityJobs = cloneDeep(mockSecurityJobs); }); test('renders correctly against snapshot', () => { const wrapper = shallow( - + ); expect(wrapper).toMatchSnapshot(); }); @@ -29,7 +32,7 @@ describe('GroupsFilterPopover', () => { const mockOnSelectedGroupsChanged = jest.fn(); const wrapper = mount( ); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx index d879942b8b10147..362fb94dc1ec4fe 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx @@ -15,30 +15,30 @@ import { EuiSpacer, } from '@elastic/eui'; import * as i18n from './translations'; -import { SiemJob } from '../../types'; +import { SecurityJob } from '../../types'; import { toggleSelectedGroup } from './toggle_selected_group'; interface GroupsFilterPopoverProps { - siemJobs: SiemJob[]; + securityJobs: SecurityJob[]; onSelectedGroupsChanged: Dispatch>; } /** - * Popover for selecting which SiemJob groups to filter on. Component extracts unique groups and - * their counts from the provided SiemJobs. The 'siem' & 'security' groups are filtered out as all jobs will be + * Popover for selecting which SecurityJob groups to filter on. Component extracts unique groups and + * their counts from the provided SecurityJobs. The 'siem' & 'security' groups are filtered out as all jobs will be * siem/security jobs * - * @param siemJobs jobs to fetch groups from to display for filtering + * @param securityJobs jobs to fetch groups from to display for filtering * @param onSelectedGroupsChanged change listener to be notified when group selection changes */ export const GroupsFilterPopoverComponent = ({ - siemJobs, + securityJobs, onSelectedGroupsChanged, }: GroupsFilterPopoverProps) => { const [isGroupPopoverOpen, setIsGroupPopoverOpen] = useState(false); const [selectedGroups, setSelectedGroups] = useState([]); - const groups = siemJobs + const groups = securityJobs .map((j) => j.groups) .flat() .filter((g) => g !== 'siem' && g !== 'security'); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx index 5b656adc3e58176..6b7699d57aedf9f 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx @@ -7,20 +7,20 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; import { JobsTableFiltersComponent } from './jobs_table_filters'; -import { SiemJob } from '../../types'; +import { SecurityJob } from '../../types'; import { cloneDeep } from 'lodash/fp'; -import { mockSiemJobs } from '../../__mocks__/api'; +import { mockSecurityJobs } from '../../api.mock'; describe('JobsTableFilters', () => { - let siemJobs: SiemJob[]; + let securityJobs: SecurityJob[]; beforeEach(() => { - siemJobs = cloneDeep(mockSiemJobs); + securityJobs = cloneDeep(mockSecurityJobs); }); test('renders correctly against snapshot', () => { const wrapper = shallow( - + ); expect(wrapper).toMatchSnapshot(); }); @@ -28,7 +28,7 @@ describe('JobsTableFilters', () => { test('when you click Elastic Jobs filter, state is updated and it is selected', () => { const onFilterChanged = jest.fn(); const wrapper = mount( - + ); wrapper.find('[data-test-subj="show-elastic-jobs-filter-button"]').first().simulate('click'); @@ -45,7 +45,7 @@ describe('JobsTableFilters', () => { test('when you click Custom Jobs filter, state is updated and it is selected', () => { const onFilterChanged = jest.fn(); const wrapper = mount( - + ); wrapper.find('[data-test-subj="show-custom-jobs-filter-button"]').first().simulate('click'); @@ -62,7 +62,7 @@ describe('JobsTableFilters', () => { test('when you click Custom Jobs filter once, then Elastic Jobs filter, state is updated and selected changed', () => { const onFilterChanged = jest.fn(); const wrapper = mount( - + ); wrapper.find('[data-test-subj="show-custom-jobs-filter-button"]').first().simulate('click'); @@ -88,7 +88,7 @@ describe('JobsTableFilters', () => { test('when you click Custom Jobs filter twice, state is updated and it is revert', () => { const onFilterChanged = jest.fn(); const wrapper = mount( - + ); wrapper.find('[data-test-subj="show-custom-jobs-filter-button"]').first().simulate('click'); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx index 4cfb7f8ad2b5bb7..f25ea667b34118a 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx @@ -15,11 +15,11 @@ import { } from '@elastic/eui'; import { EuiSearchBarQuery } from '../../../../../timelines/components/open_timeline/types'; import * as i18n from './translations'; -import { JobsFilters, SiemJob } from '../../types'; +import { JobsFilters, SecurityJob } from '../../types'; import { GroupsFilterPopover } from './groups_filter_popover'; interface JobsTableFiltersProps { - siemJobs: SiemJob[]; + securityJobs: SecurityJob[]; onFilterChanged: Dispatch>; } @@ -27,10 +27,13 @@ interface JobsTableFiltersProps { * Collection of filters for filtering data within the JobsTable. Contains search bar, Elastic/Custom * Jobs filter button toggle, and groups selection * - * @param siemJobs jobs to fetch groups from to display for filtering + * @param securityJobs jobs to fetch groups from to display for filtering * @param onFilterChanged change listener to be notified on filter changes */ -export const JobsTableFiltersComponent = ({ siemJobs, onFilterChanged }: JobsTableFiltersProps) => { +export const JobsTableFiltersComponent = ({ + securityJobs, + onFilterChanged, +}: JobsTableFiltersProps) => { const [filterQuery, setFilterQuery] = useState(''); const [selectedGroups, setSelectedGroups] = useState([]); const [showCustomJobs, setShowCustomJobs] = useState(false); @@ -71,7 +74,10 @@ export const JobsTableFiltersComponent = ({ siemJobs, onFilterChanged }: JobsTab - + diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.test.tsx index ade8c6fe805257c..e58d76bd1dde00c 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.test.tsx @@ -9,22 +9,22 @@ import React from 'react'; import { JobSwitchComponent } from './job_switch'; import { cloneDeep } from 'lodash/fp'; -import { mockSiemJobs } from '../__mocks__/api'; -import { SiemJob } from '../types'; +import { mockSecurityJobs } from '../api.mock'; +import { SecurityJob } from '../types'; describe('JobSwitch', () => { - let siemJobs: SiemJob[]; + let securityJobs: SecurityJob[]; let onJobStateChangeMock = jest.fn(); beforeEach(() => { - siemJobs = cloneDeep(mockSiemJobs); + securityJobs = cloneDeep(mockSecurityJobs); onJobStateChangeMock = jest.fn(); }); test('renders correctly against snapshot', () => { const wrapper = shallow( ); @@ -34,8 +34,8 @@ describe('JobSwitch', () => { test('should call onJobStateChange when the switch is clicked to be true/open', () => { const wrapper = mount( ); @@ -57,8 +57,8 @@ describe('JobSwitch', () => { test('should have a switch when it is not in the loading state', () => { const wrapper = mount( ); @@ -68,8 +68,8 @@ describe('JobSwitch', () => { test('should not have a switch when it is in the loading state', () => { const wrapper = mount( ); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.tsx index d370d475bd6e57c..3ad71ee6b6919cf 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.tsx @@ -12,7 +12,7 @@ import { isJobFailed, isJobStarted, } from '../../../../../common/machine_learning/helpers'; -import { SiemJob } from '../types'; +import { SecurityJob } from '../types'; const StaticSwitch = styled(EuiSwitch)` .euiSwitch__thumb, @@ -24,14 +24,14 @@ const StaticSwitch = styled(EuiSwitch)` StaticSwitch.displayName = 'StaticSwitch'; export interface JobSwitchProps { - job: SiemJob; - isSiemJobsLoading: boolean; - onJobStateChange: (job: SiemJob, latestTimestampMs: number, enable: boolean) => Promise; + job: SecurityJob; + isSecurityJobsLoading: boolean; + onJobStateChange: (job: SecurityJob, latestTimestampMs: number, enable: boolean) => Promise; } export const JobSwitchComponent = ({ job, - isSiemJobsLoading, + isSecurityJobsLoading, onJobStateChange, }: JobSwitchProps) => { const [isLoading, setIsLoading] = useState(false); @@ -47,7 +47,7 @@ export const JobSwitchComponent = ({ return ( - {isSiemJobsLoading || isLoading || isJobLoading(job.jobState, job.datafeedState) ? ( + {isSecurityJobsLoading || isLoading || isJobLoading(job.jobState, job.datafeedState) ? ( ) : ( { - let siemJobs: SiemJob[]; + let securityJobs: SecurityJob[]; let onJobStateChangeMock = jest.fn(); beforeEach(() => { - siemJobs = cloneDeep(mockSiemJobs); + securityJobs = cloneDeep(mockSecurityJobs); onJobStateChangeMock = jest.fn(); }); @@ -25,7 +25,7 @@ describe('JobsTableComponent', () => { const wrapper = shallow( ); @@ -36,7 +36,7 @@ describe('JobsTableComponent', () => { const wrapper = mount( ); @@ -46,11 +46,11 @@ describe('JobsTableComponent', () => { }); test('should render the hyperlink with URI encodings which points specifically to the job id', () => { - siemJobs[0].id = 'job id with spaces'; + securityJobs[0].id = 'job id with spaces'; const wrapper = mount( ); @@ -63,7 +63,7 @@ describe('JobsTableComponent', () => { const wrapper = mount( ); @@ -73,14 +73,14 @@ describe('JobsTableComponent', () => { .simulate('click', { target: { checked: true }, }); - expect(onJobStateChangeMock.mock.calls[0]).toEqual([siemJobs[0], 1571022859393, true]); + expect(onJobStateChangeMock.mock.calls[0]).toEqual([securityJobs[0], 1571022859393, true]); }); test('should have a switch when it is not in the loading state', () => { const wrapper = mount( ); @@ -91,7 +91,7 @@ describe('JobsTableComponent', () => { const wrapper = mount( ); 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 f28a99c9947d540..be911a1cd853786 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 @@ -25,7 +25,7 @@ import styled from 'styled-components'; import { useBasePath } from '../../../lib/kibana'; import * as i18n from './translations'; import { JobSwitch } from './job_switch'; -import { SiemJob } from '../types'; +import { SecurityJob } from '../types'; const JobNameWrapper = styled.div` margin: 5px 0; @@ -38,12 +38,12 @@ const truncateThreshold = 200; const getJobsTableColumns = ( isLoading: boolean, - onJobStateChange: (job: SiemJob, latestTimestampMs: number, enable: boolean) => Promise, + onJobStateChange: (job: SecurityJob, latestTimestampMs: number, enable: boolean) => Promise, basePath: string ) => [ { name: i18n.COLUMN_JOB_NAME, - render: ({ id, description }: SiemJob) => ( + render: ({ id, description }: SecurityJob) => ( ( + render: ({ groups }: SecurityJob) => ( {groups.map((group) => ( @@ -76,9 +76,13 @@ const getJobsTableColumns = ( { name: i18n.COLUMN_RUN_JOB, - render: (job: SiemJob) => + render: (job: SecurityJob) => job.isCompatible ? ( - + ) : ( ), @@ -87,13 +91,16 @@ const getJobsTableColumns = ( } as const, ]; -const getPaginatedItems = (items: SiemJob[], pageIndex: number, pageSize: number): SiemJob[] => - items.slice(pageIndex * pageSize, pageIndex * pageSize + pageSize); +const getPaginatedItems = ( + items: SecurityJob[], + pageIndex: number, + pageSize: number +): SecurityJob[] => items.slice(pageIndex * pageSize, pageIndex * pageSize + pageSize); export interface JobTableProps { isLoading: boolean; - jobs: SiemJob[]; - onJobStateChange: (job: SiemJob, latestTimestampMs: number, enable: boolean) => Promise; + jobs: SecurityJob[]; + onJobStateChange: (job: SecurityJob, latestTimestampMs: number, enable: boolean) => Promise; } export const JobsTableComponent = ({ isLoading, jobs, onJobStateChange }: JobTableProps) => { diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx index 0ebf3674718482c..f2bf2273c4b3fb8 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx @@ -12,19 +12,17 @@ import styled from 'styled-components'; import { useKibana } from '../../lib/kibana'; import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../lib/telemetry'; -import { hasMlAdminPermissions } from '../../../../common/machine_learning/has_ml_admin_permissions'; import { errorToToaster, useStateToaster, ActionToaster } from '../toasters'; import { setupMlJob, startDatafeeds, stopDatafeeds } from './api'; import { filterJobs } from './helpers'; -import { useSiemJobs } from './hooks/use_siem_jobs'; import { JobsTableFilters } from './jobs_table/filters/jobs_table_filters'; import { JobsTable } from './jobs_table/jobs_table'; import { ShowingCount } from './jobs_table/showing_count'; import { PopoverDescription } from './popover_description'; import * as i18n from './translations'; -import { JobsFilters, SiemJob } from './types'; +import { JobsFilters, SecurityJob } from './types'; import { UpgradeContents } from './upgrade_contents'; -import { useMlCapabilities } from './hooks/use_ml_capabilities'; +import { useSecurityJobs } from './hooks/use_security_jobs'; const PopoverContentsDiv = styled.div` max-width: 684px; @@ -87,24 +85,25 @@ export const MlPopover = React.memo(() => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [filterProperties, setFilterProperties] = useState(defaultFilterProps); - const [isLoadingSiemJobs, siemJobs] = useSiemJobs(refreshToggle); + const { isMlAdmin, isLicensed, loading: isLoadingSecurityJobs, jobs } = useSecurityJobs( + refreshToggle + ); const [, dispatchToaster] = useStateToaster(); - const capabilities = useMlCapabilities(); const docLinks = useKibana().services.docLinks; const handleJobStateChange = useCallback( - (job: SiemJob, latestTimestampMs: number, enable: boolean) => + (job: SecurityJob, latestTimestampMs: number, enable: boolean) => enableDatafeed(job, latestTimestampMs, enable, dispatch, dispatchToaster), [dispatch, dispatchToaster] ); const filteredJobs = filterJobs({ - jobs: siemJobs, + jobs, ...filterProperties, }); - const incompatibleJobCount = siemJobs.filter((j) => !j.isCompatible).length; + const incompatibleJobCount = jobs.filter((j) => !j.isCompatible).length; - if (!capabilities.isPlatinumOrTrialLicense) { + if (!isLicensed) { // If the user does not have platinum show upgrade UI return ( { ); - } else if (hasMlAdminPermissions(capabilities)) { + } else if (isMlAdmin) { // If the user has Platinum License & ML Admin Permissions, show Anomaly Detection button & full config UI return ( { - + @@ -194,7 +193,7 @@ export const MlPopover = React.memo(() => { )} @@ -209,7 +208,7 @@ export const MlPopover = React.memo(() => { // Enable/Disable Job & Datafeed -- passed to JobsTable for use as callback on JobSwitch const enableDatafeed = async ( - job: SiemJob, + job: SecurityJob, latestTimestampMs: number, enable: boolean, dispatch: Dispatch, @@ -257,7 +256,7 @@ const enableDatafeed = async ( dispatch({ type: 'refresh' }); }; -const submitTelemetry = (job: SiemJob, enabled: boolean) => { +const submitTelemetry = (job: SecurityJob, enabled: boolean) => { // Report type of job enabled/disabled track( METRIC_TYPE.COUNT, diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/types.ts b/x-pack/plugins/security_solution/public/common/components/ml_popover/types.ts index f39daa0b9a7fbeb..c839f5110fe7fdf 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/types.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AuditMessageBase } from '../../../../../ml/public'; import { MlError } from '../ml/types'; +import { MlSummaryJob } from '../../../../../ml/public'; export interface Group { id: string; @@ -98,28 +98,6 @@ export interface MlSetupArgs { prefix?: string; } -/** - * Representation of an ML Job as returned from the `ml/jobs/jobs_summary` API - */ -export interface JobSummary { - auditMessage?: AuditMessageBase; - datafeedId: string; - datafeedIndices: string[]; - datafeedState: string; - description: string; - earliestTimestampMs?: number; - latestResultsTimestampMs?: number; - groups: string[]; - hasDatafeed: boolean; - id: string; - isSingleMetricViewerJob: boolean; - jobState: string; - latestTimestampMs?: number; - memory_status: string; - nodeName?: string; - processed_record_count: number; -} - export interface Detector { detector_description: string; function: string; @@ -133,10 +111,10 @@ export interface CustomURL { } /** - * Representation of an ML Job as used by the SIEM App -- a composition of ModuleJob and JobSummary + * Representation of an ML Job as used by the SIEM App -- a composition of ModuleJob and MlSummaryJob * that includes necessary metadata like moduleName, defaultIndexPattern, etc. */ -export interface SiemJob extends JobSummary { +export interface SecurityJob extends MlSummaryJob { moduleId: string; defaultIndexPattern: string; isCompatible: boolean; @@ -144,7 +122,7 @@ export interface SiemJob extends JobSummary { isElasticJob: boolean; } -export interface AugmentedSiemJobFields { +export interface AugmentedSecurityJobFields { moduleId: string; defaultIndexPattern: string; isCompatible: boolean; diff --git a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx index 76270a7c08cd638..94019b26c180b0b 100644 --- a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx @@ -9,7 +9,7 @@ import React, { useEffect } from 'react'; import { DEFAULT_ANOMALY_SCORE } from '../../../../../common/constants'; import { AnomaliesQueryTabBodyProps } from './types'; import { getAnomaliesFilterQuery } from './utils'; -import { useSiemJobs } from '../../../components/ml_popover/hooks/use_siem_jobs'; +import { useInstalledSecurityJobs } from '../../../components/ml/hooks/use_installed_security_jobs'; import { useUiSetting$ } from '../../../lib/kibana'; import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; import { histogramConfigs } from './histogram_configs'; @@ -38,13 +38,13 @@ export const AnomaliesQueryTabBody = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const [, siemJobs] = useSiemJobs(true); + const { jobs } = useInstalledSecurityJobs(); const [anomalyScore] = useUiSetting$(DEFAULT_ANOMALY_SCORE); const mergedFilterQuery = getAnomaliesFilterQuery( filterQuery, anomaliesFilterQuery, - siemJobs, + jobs, anomalyScore, flowTarget, ip diff --git a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/utils.ts b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/utils.ts index 10d5d1c60a6c2b4..5248801d723b60e 100644 --- a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/utils.ts +++ b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/utils.ts @@ -6,21 +6,20 @@ import deepmerge from 'deepmerge'; +import { MlSummaryJob } from '../../../../../../ml/public'; import { ESTermQuery } from '../../../../../common/typed_json'; import { createFilter } from '../../helpers'; -import { SiemJob } from '../../../components/ml_popover/types'; import { FlowTarget } from '../../../../graphql/types'; export const getAnomaliesFilterQuery = ( filterQuery: string | ESTermQuery | undefined, anomaliesFilterQuery: object = {}, - siemJobs: SiemJob[] = [], + securityJobs: MlSummaryJob[] = [], anomalyScore: number, flowTarget?: FlowTarget, ip?: string ): string => { - const siemJobIds = siemJobs - .filter((job) => job.isInstalled) + const securityJobIds = securityJobs .map((job) => job.id) .map((jobId) => ({ match_phrase: { @@ -38,7 +37,7 @@ export const getAnomaliesFilterQuery = ( filter: [ { bool: { - should: siemJobIds, + should: securityJobIds, minimum_should_match: 1, }, }, diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts new file mode 100644 index 000000000000000..1af4ba3ba9233c0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.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. + */ + +const createAppToastsMock = () => ({ + addError: jest.fn(), + addSuccess: jest.fn(), +}); + +export const useAppToastsMock = { + create: createAppToastsMock, +}; diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts index 2c52acd3ec747cd..5f4285f2747ae1b 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts @@ -17,6 +17,7 @@ export const KibanaServices = { get: jest.fn(), getKibanaVersion: jest.fn(() => export const useKibana = jest.fn(createUseKibanaMock()); export const useUiSetting = jest.fn(createUseUiSettingMock()); export const useUiSetting$ = jest.fn(createUseUiSetting$Mock()); +export const useHttp = jest.fn(() => useKibana().services.http); export const useTimeZone = jest.fn(); export const useDateFormat = jest.fn(); export const useBasePath = jest.fn(() => '/test/base/path'); diff --git a/x-pack/plugins/security_solution/public/common/mock/kibana_core.ts b/x-pack/plugins/security_solution/public/common/mock/kibana_core.ts index 13b3c4b249bfef3..f8eed75cf9bf1a6 100644 --- a/x-pack/plugins/security_solution/public/common/mock/kibana_core.ts +++ b/x-pack/plugins/security_solution/public/common/mock/kibana_core.ts @@ -6,8 +6,10 @@ import { coreMock } from '../../../../../../src/core/public/mocks'; import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; +import { securityMock } from '../../../../../plugins/security/public/mocks'; export const createKibanaCoreStartMock = () => coreMock.createStart(); export const createKibanaPluginsStartMock = () => ({ data: dataPluginMock.createStartContract(), + security: securityMock.createSetup(), }); diff --git a/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts b/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts index c5d50e1379482b8..bdb8ca85b0d7774 100644 --- a/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts +++ b/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts @@ -96,28 +96,10 @@ export const createUseKibanaMock = () => { export const createStartServices = () => { const core = createKibanaCoreStartMock(); const plugins = createKibanaPluginsStartMock(); - const security = { - authc: { - getCurrentUser: jest.fn(), - areAPIKeysEnabled: jest.fn(), - }, - sessionTimeout: { - start: jest.fn(), - stop: jest.fn(), - extend: jest.fn(), - }, - license: { - isEnabled: jest.fn(), - getFeatures: jest.fn(), - features$: jest.fn(), - }, - __legacyCompat: { logoutUrl: 'logoutUrl', tenant: 'tenant' }, - }; const services = ({ ...core, ...plugins, - security, } as unknown) as StartServices; return services; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index 47c12d193417403..00141c9a453d82f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -38,7 +38,7 @@ import { buildRuleTypeDescription, buildThresholdDescription, } from './helpers'; -import { useSiemJobs } from '../../../../common/components/ml_popover/hooks/use_siem_jobs'; +import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/use_security_jobs'; import { buildMlJobDescription } from './ml_job_description'; import { buildActionsDescription } from './actions_description'; import { buildThrottleDescription } from './throttle_description'; @@ -67,7 +67,7 @@ export const StepRuleDescriptionComponent: React.FC = }) => { const kibana = useKibana(); const [filterManager] = useState(new FilterManager(kibana.services.uiSettings)); - const [, siemJobs] = useSiemJobs(true); + const { jobs } = useSecurityJobs(false); const keys = Object.keys(schema); const listItems = keys.reduce((acc: ListItems[], key: string) => { @@ -77,7 +77,7 @@ export const StepRuleDescriptionComponent: React.FC = buildMlJobDescription( get(key, data) as string, (get(key, schema) as { label: string }).label, - siemJobs + jobs ), ]; } diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx index c82a465f08c3a72..3152fef12c6523a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx @@ -7,31 +7,14 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { mockOpenedJob } from '../../../../common/components/ml_popover/api.mock'; import { MlJobDescription, AuditIcon, JobStatusBadge } from './ml_job_description'; -jest.mock('../../../../common/lib/kibana'); -const job = { - moduleId: 'moduleId', - defaultIndexPattern: 'defaultIndexPattern', - isCompatible: true, - isInstalled: true, - isElasticJob: true, - datafeedId: 'datafeedId', - datafeedIndices: [], - datafeedState: 'datafeedState', - description: 'description', - groups: [], - hasDatafeed: true, - id: 'id', - isSingleMetricViewerJob: false, - jobState: 'jobState', - memory_status: 'memory_status', - processed_record_count: 0, -}; +jest.mock('../../../../common/lib/kibana'); describe('MlJobDescription', () => { it('renders correctly', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find('[data-test-subj="machineLearningJobId"]')).toHaveLength(1); }); @@ -47,7 +30,7 @@ describe('AuditIcon', () => { describe('JobStatusBadge', () => { it('renders correctly', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find('EuiBadge')).toHaveLength(1); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx index d7e06511e79373b..6baa2abab33d1a9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx @@ -8,9 +8,9 @@ import React from 'react'; import styled from 'styled-components'; import { EuiBadge, EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; +import { MlSummaryJob } from '../../../../../../ml/public'; import { isJobStarted } from '../../../../../common/machine_learning/helpers'; import { useKibana } from '../../../../common/lib/kibana'; -import { SiemJob } from '../../../../common/components/ml_popover/types'; import { ListItems } from './types'; import { ML_JOB_STARTED, ML_JOB_STOPPED } from './translations'; @@ -21,7 +21,7 @@ enum MessageLevels { } const AuditIconComponent: React.FC<{ - message: SiemJob['auditMessage']; + message: MlSummaryJob['auditMessage']; }> = ({ message }) => { if (!message) { return null; @@ -47,7 +47,7 @@ const AuditIconComponent: React.FC<{ export const AuditIcon = React.memo(AuditIconComponent); -const JobStatusBadgeComponent: React.FC<{ job: SiemJob }> = ({ job }) => { +const JobStatusBadgeComponent: React.FC<{ job: MlSummaryJob }> = ({ job }) => { const isStarted = isJobStarted(job.jobState, job.datafeedState); const color = isStarted ? 'secondary' : 'danger'; const text = isStarted ? ML_JOB_STARTED : ML_JOB_STOPPED; @@ -69,7 +69,7 @@ const Wrapper = styled.div` overflow: hidden; `; -const MlJobDescriptionComponent: React.FC<{ job: SiemJob }> = ({ job }) => { +const MlJobDescriptionComponent: React.FC<{ job: MlSummaryJob }> = ({ job }) => { const jobUrl = useKibana().services.application.getUrlForApp( `ml#/jobs?mlManagement=(jobId:${encodeURI(job.id)})` ); @@ -92,12 +92,12 @@ export const MlJobDescription = React.memo(MlJobDescriptionComponent); export const buildMlJobDescription = ( jobId: string, label: string, - siemJobs: SiemJob[] + jobs: MlSummaryJob[] ): ListItems => { - const siemJob = siemJobs.find((job) => job.id === jobId); + const job = jobs.find(({ id }) => id === jobId); return { title: label, - description: siemJob ? : jobId, + description: job ? : jobId, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.test.tsx index 6f6581e4de1c375..4a08adbedab3f60 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.test.tsx @@ -8,14 +8,14 @@ import React from 'react'; import { shallow } from 'enzyme'; import { MlJobSelect } from './index'; -import { useSiemJobs } from '../../../../common/components/ml_popover/hooks/use_siem_jobs'; +import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/use_security_jobs'; import { useFormFieldMock } from '../../../../common/mock'; -jest.mock('../../../../common/components/ml_popover/hooks/use_siem_jobs'); +jest.mock('../../../../common/components/ml_popover/hooks/use_security_jobs'); jest.mock('../../../../common/lib/kibana'); describe('MlJobSelect', () => { beforeAll(() => { - (useSiemJobs as jest.Mock).mockReturnValue([false, []]); + (useSecurityJobs as jest.Mock).mockReturnValue({ loading: false, jobs: [] }); }); it('renders correctly', () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.tsx index cdfdf4ca6b66bf0..b0aa0329fe8f40d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.tsx @@ -19,7 +19,7 @@ import { import styled from 'styled-components'; import { isJobStarted } from '../../../../../common/machine_learning/helpers'; import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; -import { useSiemJobs } from '../../../../common/components/ml_popover/hooks/use_siem_jobs'; +import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/use_security_jobs'; import { useKibana } from '../../../../common/lib/kibana'; import { ML_JOB_SELECT_PLACEHOLDER_TEXT, @@ -81,7 +81,7 @@ interface MlJobSelectProps { export const MlJobSelect: React.FC = ({ describedByIds = [], field }) => { const jobId = field.value as string; const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - const [isLoading, siemJobs] = useSiemJobs(false); + const { loading, jobs } = useSecurityJobs(false); const mlUrl = useKibana().services.application.getUrlForApp('ml'); const handleJobChange = useCallback( (machineLearningJobId: string) => { @@ -96,7 +96,7 @@ export const MlJobSelect: React.FC = ({ describedByIds = [], f disabled: true, }; - const jobOptions = siemJobs.map((job) => ({ + const jobOptions = jobs.map((job) => ({ value: job.id, inputDisplay: job.id, dropdownDisplay: , @@ -107,9 +107,9 @@ export const MlJobSelect: React.FC = ({ describedByIds = [], f const isJobRunning = useMemo(() => { // If the selected job is not found in the list, it means the placeholder is selected // and so we don't want to show the warning, thus isJobRunning will be true when 'job == null' - const job = siemJobs.find((j) => j.id === jobId); + const job = jobs.find(({ id }) => id === jobId); return job == null || isJobStarted(job.jobState, job.datafeedState); - }, [siemJobs, jobId]); + }, [jobs, jobId]); return ( @@ -126,7 +126,7 @@ export const MlJobSelect: React.FC = ({ describedByIds = [], f = ({ componentProps={{ describedByIds: ['detectionEngineStepDefineRuleType'], isReadOnly: isUpdateView, - hasValidLicense: mlCapabilities.isPlatinumOrTrialLicense, + hasValidLicense: hasMlLicense(mlCapabilities), isMlAdmin: hasMlAdminPermissions(mlCapabilities), }} /> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx index 85dce907084e8a5..110691328b13b5a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx @@ -47,8 +47,9 @@ import { getColumns, getMonitoringColumns } from './columns'; import { showRulesTable } from './helpers'; import { allRulesReducer, State } from './reducer'; import { RulesTableFilters } from './rules_table_filters/rules_table_filters'; -import { useMlCapabilities } from '../../../../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../../../../../common/components/ml/hooks/use_ml_capabilities'; import { hasMlAdminPermissions } from '../../../../../../common/machine_learning/has_ml_admin_permissions'; +import { hasMlLicense } from '../../../../../../common/machine_learning/has_ml_license'; import { SecurityPageName } from '../../../../../app/types'; import { useFormatUrl } from '../../../../../common/components/link_to'; @@ -145,8 +146,7 @@ export const AllRules = React.memo( const { formatUrl } = useFormatUrl(SecurityPageName.detections); // TODO: Refactor license check + hasMlAdminPermissions to common check - const hasMlPermissions = - mlCapabilities.isPlatinumOrTrialLicense && hasMlAdminPermissions(mlCapabilities); + const hasMlPermissions = hasMlLicense(mlCapabilities) && hasMlAdminPermissions(mlCapabilities); const setRules = useCallback((newRules: Rule[], newPagination: Partial) => { dispatch({ diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 016d0c7c67a9e85..8a969a4cf098cb0 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -71,8 +71,9 @@ import { RuleActionsOverflow } from '../../../../components/rules/rule_actions_o import { RuleStatusFailedCallOut } from './status_failed_callout'; import { FailureHistory } from './failure_history'; import { RuleStatus } from '../../../../components/rules//rule_status'; -import { useMlCapabilities } from '../../../../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../../../../../common/components/ml/hooks/use_ml_capabilities'; import { hasMlAdminPermissions } from '../../../../../../common/machine_learning/has_ml_admin_permissions'; +import { hasMlLicense } from '../../../../../../common/machine_learning/has_ml_license'; import { SecurityPageName } from '../../../../../app/types'; import { LinkButton } from '../../../../../common/components/links'; import { useFormatUrl } from '../../../../../common/components/link_to'; @@ -161,8 +162,7 @@ export const RuleDetailsPageComponent: FC = ({ const { globalFullScreen } = useFullScreen(); // TODO: Refactor license check + hasMlAdminPermissions to common check - const hasMlPermissions = - mlCapabilities.isPlatinumOrTrialLicense && hasMlAdminPermissions(mlCapabilities); + const hasMlPermissions = hasMlLicense(mlCapabilities) && hasMlAdminPermissions(mlCapabilities); const ruleDetailTabs = getRuleDetailsTabs(rule); // persist rule until refresh is complete diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index 34840b28266268b..67f563e944f42f9 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -17,7 +17,7 @@ import { LastEventTime } from '../../../common/components/last_event_time'; import { AnomalyTableProvider } from '../../../common/components/ml/anomaly/anomaly_table_provider'; import { hostToCriteria } from '../../../common/components/ml/criteria/host_to_criteria'; import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml_user_permissions'; -import { useMlCapabilities } from '../../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities'; import { scoreIntervalToDateTime } from '../../../common/components/ml/score/score_interval_to_datetime'; import { SiemNavigation } from '../../../common/components/navigation'; import { KpiHostsComponent } from '../../components/kpi_hosts'; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index e4e69443c510d23..2b19249dc426fd2 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -34,7 +34,7 @@ import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from import { SpyRoute } from '../../common/utils/route/spy_routes'; import { esQuery } from '../../../../../../src/plugins/data/public'; -import { useMlCapabilities } from '../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../../common/components/ml/hooks/use_ml_capabilities'; import { OverviewEmpty } from '../../overview/components/overview_empty'; import { Display } from './display'; import { HostsTabs } from './hosts_tabs'; diff --git a/x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx b/x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx index cf08b084d21979e..d6dfe1769308e97 100644 --- a/x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx @@ -30,7 +30,7 @@ import { DescriptionListStyled, OverviewWrapper } from '../../../common/componen import { Loader } from '../../../common/components/loader'; import { Anomalies, NarrowDateRange } from '../../../common/components/ml/types'; import { AnomalyScores } from '../../../common/components/ml/score/anomaly_scores'; -import { useMlCapabilities } from '../../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities'; import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml_user_permissions'; import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect'; diff --git a/x-pack/plugins/security_solution/public/network/pages/index.tsx b/x-pack/plugins/security_solution/public/network/pages/index.tsx index 9ac05cc98bb454b..07abe7bc8c209fe 100644 --- a/x-pack/plugins/security_solution/public/network/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/index.tsx @@ -7,7 +7,7 @@ import React, { useMemo } from 'react'; import { Route, Switch, RouteComponentProps, useHistory } from 'react-router-dom'; -import { useMlCapabilities } from '../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../../common/components/ml/hooks/use_ml_capabilities'; import { hasMlUserPermissions } from '../../../common/machine_learning/has_ml_user_permissions'; import { FlowTarget } from '../../graphql/types'; diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx index 0a15b039b96af22..c7aba6fcc8a5b38 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx @@ -23,7 +23,7 @@ import { HostItem } from '../../../graphql/types'; import { Loader } from '../../../common/components/loader'; import { IPDetailsLink } from '../../../common/components/links'; import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml_user_permissions'; -import { useMlCapabilities } from '../../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities'; import { AnomalyScores } from '../../../common/components/ml/score/anomaly_scores'; import { Anomalies, NarrowDateRange } from '../../../common/components/ml/types'; import { DescriptionListStyled, OverviewWrapper } from '../../../common/components/page'; 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 d8a9f2790bd35fe..1f291e8b7478df6 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 @@ -169,12 +169,30 @@ describe('Resolver, when analyzing a tree that has two related events for the or button.simulate('click'); } }); - it('should open the submenu', async () => { + it('should open the submenu and display exactly one option with the correct count', async () => { await expect( simulator.map(() => simulator.testSubject('resolver:map:node-submenu-item').map((node) => node.text()) ) ).toYieldEqualTo(['2 registry']); + await expect( + simulator.map(() => simulator.processNodeSubmenuItems().length) + ).toYieldEqualTo(1); + }); + }); + describe('and when the related events button is clicked again', () => { + beforeEach(async () => { + const button = await simulator.resolveWrapper(() => + simulator.processNodeRelatedEventButton(entityIDs.origin) + ); + if (button) { + button.simulate('click'); + } + }); + it('should close the submenu', async () => { + await expect( + simulator.map(() => simulator.processNodeSubmenuItems().length) + ).toYieldEqualTo(0); }); }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx index 9f310bb1cc0d65d..061dfce64b4e442 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx @@ -127,7 +127,6 @@ const EdgeLineComponent = React.memo( return ( void) => { - const disabled = selectedItems?.some((item) => item.status === TimelineStatus.immutable); + const disabled = selectedItems == null || selectedItems.length === 0; return ( <> , ); }, - // eslint-disable-next-line react-hooks/exhaustive-deps [ selectedItems, deleteTimelines, diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx index 9de3242c5e3038a..3d5c5f60d1d9b05 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx @@ -9,6 +9,7 @@ import { cloneDeep } from 'lodash/fp'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ThemeProvider } from 'styled-components'; +import { act } from '@testing-library/react'; import '../../../common/mock/match_media'; import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines_page'; @@ -51,7 +52,7 @@ describe('OpenTimeline', () => { title, timelineType: TimelineType.default, timelineStatus: TimelineStatus.active, - templateTimelineFilter: [
    ], + templateTimelineFilter: [
    ,
    ], totalSearchResultsCount: mockSearchResults.length, }); @@ -279,6 +280,86 @@ describe('OpenTimeline', () => { expect(wrapper.find('[data-test-subj="utility-bar-action"]').exists()).toEqual(true); }); + test('it should disable export-timeline if no timeline is selected', async () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: null, + selectedItems: [], + }; + const wrapper = mountWithIntl( + + + + ); + + wrapper.find('[data-test-subj="utility-bar-action"]').find('EuiLink').simulate('click'); + await act(async () => { + expect( + wrapper.find('[data-test-subj="export-timeline-action"]').first().prop('disabled') + ).toEqual(true); + }); + }); + + test('it should disable delete timeline if no timeline is selected', async () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: null, + selectedItems: [], + }; + const wrapper = mountWithIntl( + + + + ); + + wrapper.find('[data-test-subj="utility-bar-action"]').find('EuiLink').simulate('click'); + await act(async () => { + expect( + wrapper.find('[data-test-subj="delete-timeline-action"]').first().prop('disabled') + ).toEqual(true); + }); + }); + + test('it should enable export-timeline if a timeline is selected', async () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: null, + selectedItems: [{}], + }; + const wrapper = mountWithIntl( + + + + ); + + wrapper.find('[data-test-subj="utility-bar-action"]').find('EuiLink').simulate('click'); + await act(async () => { + expect( + wrapper.find('[data-test-subj="export-timeline-action"]').first().prop('disabled') + ).toEqual(false); + }); + }); + + test('it should enable delete timeline if a timeline is selected', async () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: null, + selectedItems: [{}], + }; + const wrapper = mountWithIntl( + + + + ); + + wrapper.find('[data-test-subj="utility-bar-action"]').find('EuiLink').simulate('click'); + await act(async () => { + expect( + wrapper.find('[data-test-subj="delete-timeline-action"]').first().prop('disabled') + ).toEqual(false); + }); + }); + test("it should render a selectable timeline table if timelineStatus is active (selecting custom templates' tab)", () => { const defaultProps = { ...getDefaultTestProps(mockResults), diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx index c9495c46d4acf3f..1f5f0ccca3b708f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx @@ -160,7 +160,6 @@ export const OpenTimeline = React.memo( }, [onDeleteSelected, deleteTimelines, timelineStatus]); const SearchRowContent = useMemo(() => <>{templateTimelineFilter}, [templateTimelineFilter]); - return ( <> { + test('it filters out running tasks', async () => { const taskManagerId = uuid.v1(); const claimOwnershipUntil = new Date(Date.now()); const runAt = new Date(); @@ -641,7 +641,7 @@ if (doc['task.runAt'].size()!=0) { taskType: 'foo', schedule: undefined, attempts: 0, - status: 'idle', + status: 'claiming', params: '{ "hello": "world" }', state: '{ "baby": "Henhen" }', user: 'jimbo', @@ -715,7 +715,103 @@ if (doc['task.runAt'].size()!=0) { runAt, scope: ['reporting'], state: { baby: 'Henhen' }, - status: 'idle', + status: 'claiming', + taskType: 'foo', + user: 'jimbo', + ownerId: taskManagerId, + }, + ]); + }); + + test('it returns task objects', async () => { + const taskManagerId = uuid.v1(); + const claimOwnershipUntil = new Date(Date.now()); + const runAt = new Date(); + const tasks = [ + { + _id: 'aaa', + _source: { + type: 'task', + task: { + runAt, + taskType: 'foo', + schedule: undefined, + attempts: 0, + status: 'claiming', + params: '{ "hello": "world" }', + state: '{ "baby": "Henhen" }', + user: 'jimbo', + scope: ['reporting'], + ownerId: taskManagerId, + }, + }, + _seq_no: 1, + _primary_term: 2, + sort: ['a', 1], + }, + { + _id: 'bbb', + _source: { + type: 'task', + task: { + runAt, + taskType: 'bar', + schedule: { interval: '5m' }, + attempts: 2, + status: 'claiming', + params: '{ "shazm": 1 }', + state: '{ "henry": "The 8th" }', + user: 'dabo', + scope: ['reporting', 'ceo'], + ownerId: taskManagerId, + }, + }, + _seq_no: 3, + _primary_term: 4, + sort: ['b', 2], + }, + ]; + const { + result: { docs }, + args: { + search: { + body: { query }, + }, + }, + } = await testClaimAvailableTasks({ + opts: { + taskManagerId, + }, + claimingOpts: { + claimOwnershipUntil, + size: 10, + }, + hits: tasks, + }); + + expect(query.bool.must).toContainEqual({ + bool: { + must: [ + { + term: { + 'task.ownerId': taskManagerId, + }, + }, + { term: { 'task.status': 'claiming' } }, + ], + }, + }); + + expect(docs).toMatchObject([ + { + attempts: 0, + id: 'aaa', + schedule: undefined, + params: { hello: 'world' }, + runAt, + scope: ['reporting'], + state: { baby: 'Henhen' }, + status: 'claiming', taskType: 'foo', user: 'jimbo', ownerId: taskManagerId, @@ -728,7 +824,7 @@ if (doc['task.runAt'].size()!=0) { runAt, scope: ['reporting', 'ceo'], state: { henry: 'The 8th' }, - status: 'running', + status: 'claiming', taskType: 'bar', user: 'dabo', ownerId: taskManagerId, diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index a18fb57b35b3d7b..f2da41053e6ab65 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -217,48 +217,39 @@ export class TaskStore { claimTasksByIdWithRawIds, size ); + const docs = numberOfTasksClaimed > 0 ? await this.sweepForClaimedTasks(claimTasksByIdWithRawIds, size) : []; - // emit success/fail events for claimed tasks by id - if (claimTasksById && claimTasksById.length) { - const [documentsReturnedById, documentsClaimedBySchedule] = partition(docs, (doc) => - claimTasksById.includes(doc.id) - ); - - const [documentsClaimedById, documentsRequestedButNotClaimed] = partition( - documentsReturnedById, - // we filter the schduled tasks down by status is 'claiming' in the esearch, - // but we do not apply this limitation on tasks claimed by ID so that we can - // provide more detailed error messages when we fail to claim them - (doc) => doc.status === TaskStatus.Claiming - ); - - const documentsRequestedButNotReturned = difference( - claimTasksById, - map(documentsReturnedById, 'id') - ); + const [documentsReturnedById, documentsClaimedBySchedule] = partition(docs, (doc) => + claimTasksById.includes(doc.id) + ); - this.emitEvents( - [...documentsClaimedById, ...documentsClaimedBySchedule].map((doc) => - asTaskClaimEvent(doc.id, asOk(doc)) - ) - ); + const [documentsClaimedById, documentsRequestedButNotClaimed] = partition( + documentsReturnedById, + // we filter the schduled tasks down by status is 'claiming' in the esearch, + // but we do not apply this limitation on tasks claimed by ID so that we can + // provide more detailed error messages when we fail to claim them + (doc) => doc.status === TaskStatus.Claiming + ); - this.emitEvents( - documentsRequestedButNotClaimed.map((doc) => asTaskClaimEvent(doc.id, asErr(some(doc)))) - ); + const documentsRequestedButNotReturned = difference( + claimTasksById, + map(documentsReturnedById, 'id') + ); - this.emitEvents( - documentsRequestedButNotReturned.map((id) => asTaskClaimEvent(id, asErr(none))) - ); - } + this.emitEvents([ + ...documentsClaimedById.map((doc) => asTaskClaimEvent(doc.id, asOk(doc))), + ...documentsClaimedBySchedule.map((doc) => asTaskClaimEvent(doc.id, asOk(doc))), + ...documentsRequestedButNotClaimed.map((doc) => asTaskClaimEvent(doc.id, asErr(some(doc)))), + ...documentsRequestedButNotReturned.map((id) => asTaskClaimEvent(id, asErr(none))), + ]); return { - claimedTasks: numberOfTasksClaimed, - docs, + claimedTasks: documentsClaimedById.length + documentsClaimedBySchedule.length, + docs: docs.filter((doc) => doc.status === TaskStatus.Claiming), }; }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e5cd46b330ca8bf..81c06cf5c381f6b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2388,11 +2388,6 @@ "indexPatternManagement.createIndexPattern.description": "インデックスパターンは、{single}または{multiple}データソース、{star}と一致します。", "indexPatternManagement.createIndexPattern.documentation": "ドキュメンテーションを表示", "indexPatternManagement.createIndexPattern.emptyState.checkDataButton": "新規データを確認", - "indexPatternManagement.createIndexPattern.emptyStateHeader": "Elasticsearchデータが見つかりませんでした", - "indexPatternManagement.createIndexPattern.emptyStateLabel.emptyStateDetail": "{needToIndex} {learnHowLink}または{getStartedLink}", - "indexPatternManagement.createIndexPattern.emptyStateLabel.getStartedLink": "サンプルデータで始めましょう。", - "indexPatternManagement.createIndexPattern.emptyStateLabel.learnHowLink": "方法を学習", - "indexPatternManagement.createIndexPattern.emptyStateLabel.needToIndexLabel": "インデックスパターンを作成する前に、Elasticsearchへのデータのインデックスが必要です。", "indexPatternManagement.createIndexPattern.includeSystemIndicesToggleSwitchLabel": "システムと非表示のインデックスを含める", "indexPatternManagement.createIndexPattern.loadClustersFailMsg": "リモートクラスターの読み込みに失敗", "indexPatternManagement.createIndexPattern.loadIndicesFailMsg": "インデックスの読み込みに失敗", @@ -2403,7 +2398,6 @@ "indexPatternManagement.createIndexPattern.step.indexPatternPlaceholder": "index-name-*", "indexPatternManagement.createIndexPattern.step.invalidCharactersErrorMessage": "{indexPatternName}にはスペースや{characterList}は使えません。", "indexPatternManagement.createIndexPattern.step.loadingHeader": "一致するインデックスを検索中…", - "indexPatternManagement.createIndexPattern.step.loadingLabel": "お待ちください…", "indexPatternManagement.createIndexPattern.step.nextStepButton": "次のステップ", "indexPatternManagement.createIndexPattern.step.pagingLabel": "ページごとの行数: {perPage}", "indexPatternManagement.createIndexPattern.step.status.matchAnyLabel.matchAnyDetail": "インデックスパターンは、{sourceCount, plural, one {個のソース} other {個のソース} }と一致します。", @@ -2553,15 +2547,6 @@ "indexPatternManagement.indexPattern.sectionsHeader": "インデックスパターン", "indexPatternManagement.indexPattern.titleExistsLabel": "「{title}」というタイトルのインデックスパターンがすでに存在します。", "indexPatternManagement.indexPatternList.createButton.betaLabel": "ベータ", - "indexPatternManagement.indexPatternPrompt.exampleOne": "チャートを作成したりコンテンツを素早くクエリできるように log-west-001 という名前の単一のデータソースをインデックスします。", - "indexPatternManagement.indexPatternPrompt.exampleOneTitle": "単一のデータソース", - "indexPatternManagement.indexPatternPrompt.examplesTitle": "インデックスパターンの例", - "indexPatternManagement.indexPatternPrompt.exampleThree": "比較目的に履歴の動向を集約できるよう、これらのログのアーカイブされた月々のロールアップメトリックスを指定どおりに別々のインデックスパターンにグループ分けします。", - "indexPatternManagement.indexPatternPrompt.exampleThreeTitle": "カスタムグルーピング", - "indexPatternManagement.indexPatternPrompt.exampleTwo": "すべての西海岸のサーバーログに対してクエリを実行できるように、頭に「log-west」の付いたすべての受信データソースをグループ化します。", - "indexPatternManagement.indexPatternPrompt.exampleTwoTitle": "複数データソース", - "indexPatternManagement.indexPatternPrompt.subtitle": "インデックスパターンは、Kibanaで共有フィールドにクエリを実行できるよう、種類の異なるデータソースをバケットにまとめることができます。", - "indexPatternManagement.indexPatternPrompt.title": "インデックスパターンについて", "indexPatternManagement.indexPatterns.badge.readOnly.text": "読み取り専用", "indexPatternManagement.indexPatterns.badge.readOnly.tooltip": "インデックスパターンを保存できません", "indexPatternManagement.indexPatterns.createBreadcrumb": "インデックスパターンを作成", @@ -14062,10 +14047,6 @@ "xpack.observability.home.sectionsubtitle": "ログ、メトリック、トレースを大規模に、1つのスタックにまとめて、環境内のあらゆる場所で生じるイベントの監視、分析、対応を行います。", "xpack.observability.home.sectionTitle": "エコシステム全体の一元的な可視性", "xpack.observability.home.title": "オブザーバビリティ", - "xpack.observability.ingestManafer.beta": "ベータ", - "xpack.observability.ingestManafer.button": "Ingest Managerベータを試す", - "xpack.observability.ingestManafer.text": "Elasticエージェントでは、シンプルかつ統合された方法で、ログ、メトリック、他の種類のデータの監視をホストに追加することができます。複数のBeatsと他のエージェントをインストールする必要はありません。このため、インフラストラクチャ全体での構成のデプロイが簡単で高速になりました。", - "xpack.observability.ingestManafer.title": "新しいIngest Managerをご覧になりましたか?", "xpack.observability.landing.breadcrumb": "はじめて使う", "xpack.observability.news.readFullStory": "詳細なストーリーを読む", "xpack.observability.news.title": "新機能", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b273f6cc81baf61..af5e68b7e44d710 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2389,11 +2389,6 @@ "indexPatternManagement.createIndexPattern.description": "索引模式可以匹配单个源,例如 {single} 或 {multiple} 个数据源、{star}。", "indexPatternManagement.createIndexPattern.documentation": "阅读文档", "indexPatternManagement.createIndexPattern.emptyState.checkDataButton": "检查新数据", - "indexPatternManagement.createIndexPattern.emptyStateHeader": "找不到任何 Elasticsearch 数据", - "indexPatternManagement.createIndexPattern.emptyStateLabel.emptyStateDetail": "{needToIndex}{learnHowLink}或{getStartedLink}", - "indexPatternManagement.createIndexPattern.emptyStateLabel.getStartedLink": "开始使用一些样例数据集。", - "indexPatternManagement.createIndexPattern.emptyStateLabel.learnHowLink": "了解操作方法", - "indexPatternManagement.createIndexPattern.emptyStateLabel.needToIndexLabel": "您需要在 Elasticsearch 中索引一些数据后,才能创建索引模式。", "indexPatternManagement.createIndexPattern.includeSystemIndicesToggleSwitchLabel": "包括系统和隐藏索引", "indexPatternManagement.createIndexPattern.loadClustersFailMsg": "无法加载远程集群", "indexPatternManagement.createIndexPattern.loadIndicesFailMsg": "无法加载索引", @@ -2404,7 +2399,6 @@ "indexPatternManagement.createIndexPattern.step.indexPatternPlaceholder": "index-name-*", "indexPatternManagement.createIndexPattern.step.invalidCharactersErrorMessage": "{indexPatternName} 不能包含空格或字符:{characterList}", "indexPatternManagement.createIndexPattern.step.loadingHeader": "正在寻找匹配的索引......", - "indexPatternManagement.createIndexPattern.step.loadingLabel": "请稍候......", "indexPatternManagement.createIndexPattern.step.nextStepButton": "下一步", "indexPatternManagement.createIndexPattern.step.pagingLabel": "每页行数:{perPage}", "indexPatternManagement.createIndexPattern.step.status.matchAnyLabel.matchAnyDetail": "您的索引模式可以匹配{sourceCount, plural, one {您的 # 个源} other {您的 # 个源中的任何一个} }。", @@ -2554,15 +2548,6 @@ "indexPatternManagement.indexPattern.sectionsHeader": "索引模式", "indexPatternManagement.indexPattern.titleExistsLabel": "具有标题“{title}”的索引模式已存在。", "indexPatternManagement.indexPatternList.createButton.betaLabel": "公测版", - "indexPatternManagement.indexPatternPrompt.exampleOne": "索引单个称作 log-west-001 的数据源,以便可以快速地构建图表或查询其内容。", - "indexPatternManagement.indexPatternPrompt.exampleOneTitle": "单数据源", - "indexPatternManagement.indexPatternPrompt.examplesTitle": "索引模式示例", - "indexPatternManagement.indexPatternPrompt.exampleThree": "具体而言,将这些日志每月存档的汇总/打包指标分组成不同的索引模式,从而可以聚合历史趋势以进行比较。", - "indexPatternManagement.indexPatternPrompt.exampleThreeTitle": "定制分组", - "indexPatternManagement.indexPatternPrompt.exampleTwo": "分组以 log-west* 开头的所有传入数据源,以便可以查询所有西海岸服务器日志。", - "indexPatternManagement.indexPatternPrompt.exampleTwoTitle": "多数据源", - "indexPatternManagement.indexPatternPrompt.subtitle": "索引模式允许您将异类的数据源一起装入存储桶,从而可以在 Kibana 中查询它们共享的字段。", - "indexPatternManagement.indexPatternPrompt.title": "关于索引模式", "indexPatternManagement.indexPatterns.badge.readOnly.text": "只读", "indexPatternManagement.indexPatterns.badge.readOnly.tooltip": "无法保存索引模式", "indexPatternManagement.indexPatterns.createBreadcrumb": "创建索引模式", @@ -14067,10 +14052,6 @@ "xpack.observability.home.sectionsubtitle": "通过根据需要将日志、指标和跟踪都置于单个堆栈上,来监测、分析和响应环境中任何位置发生的事件。", "xpack.observability.home.sectionTitle": "整个生态系统的统一可见性", "xpack.observability.home.title": "可观测性", - "xpack.observability.ingestManafer.beta": "公测版", - "xpack.observability.ingestManafer.button": "试用采集管理器公测版", - "xpack.observability.ingestManafer.text": "通过 Elastic 代理,您能够以简单统一的方式将日志、指标和其他类型数据的监测添加到主机。不再需要安装多个 Beats 和其他代理,这样在整个基础设施中部署配置会更轻松更快速。", - "xpack.observability.ingestManafer.title": "是否了解我们全新的采集管理器?", "xpack.observability.landing.breadcrumb": "入门", "xpack.observability.news.readFullStory": "详细了解", "xpack.observability.news.title": "最近的新闻", diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/slack.ts index 90660cc99507d30..91511b508aca67a 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/slack.ts @@ -4,26 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ +import http from 'http'; +import getPort from 'get-port'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { - getExternalServiceSimulatorPath, - ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { getSlackServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function slackTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const kibanaServer = getService('kibanaServer'); describe('slack action', () => { - let slackSimulatorURL: string = ''; + let slackSimulatorURL: string = ''; + let slackServer: http.Server; - // need to wait for kibanaServer to settle ... - before(() => { - slackSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SLACK) - ); + before(async () => { + slackServer = await getSlackServer(); + const availablePort = await getPort({ port: 9000 }); + slackServer.listen(availablePort); + slackSimulatorURL = `http://localhost:${availablePort}`; }); it('should return 403 when creating a slack action', async () => { @@ -44,5 +43,9 @@ export default function slackTest({ getService }: FtrProviderContext) { 'Action type .slack is disabled because your basic license does not support it. Please upgrade your license.', }); }); + + after(() => { + slackServer.close(); + }); }); } diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/webhook.ts index af1d413ff3c462b..039f1d4dd3275d9 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/webhook.ts @@ -4,25 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ +import http from 'http'; +import getPort from 'get-port'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { - getExternalServiceSimulatorPath, - ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { getWebhookServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function webhookTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const kibanaServer = getService('kibanaServer'); describe('webhook action', () => { - let webhookSimulatorURL: string = ''; - + let webhookSimulatorURL: string = ''; + let webhookServer: http.Server; // need to wait for kibanaServer to settle ... - before(() => { - webhookSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK) - ); + before(async () => { + webhookServer = await getWebhookServer(); + const availablePort = await getPort({ port: 9000 }); + webhookServer.listen(availablePort); + webhookSimulatorURL = `http://localhost:${availablePort}`; }); it('should return 403 when creating a webhook action', async () => { @@ -47,5 +46,9 @@ export default function webhookTest({ getService }: FtrProviderContext) { 'Action type .webhook is disabled because your basic license does not support it. Please upgrade your license.', }); }); + + after(() => { + webhookServer.close(); + }); }); } diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts index cb1271494c294eb..0f7acf5ead1a18b 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import http from 'http'; import { Plugin, CoreSetup, IRouter } from 'kibana/server'; import { EncryptedSavedObjectsPluginStart } from '../../../../../../../plugins/encrypted_saved_objects/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../../plugins/features/server'; @@ -13,6 +14,8 @@ import { initPlugin as initPagerduty } from './pagerduty_simulation'; import { initPlugin as initServiceNow } from './servicenow_simulation'; import { initPlugin as initJira } from './jira_simulation'; import { initPlugin as initResilient } from './resilient_simulation'; +import { initPlugin as initSlack } from './slack_simulation'; +import { initPlugin as initWebhook } from './webhook_simulation'; export const NAME = 'actions-FTS-external-service-simulators'; @@ -39,6 +42,14 @@ export function getAllExternalServiceSimulatorPaths(): string[] { return allPaths; } +export async function getWebhookServer(): Promise { + return await initWebhook(); +} + +export async function getSlackServer(): Promise { + return await initSlack(); +} + interface FixtureSetupDeps { actions: ActionsPluginSetupContract; features: FeaturesPluginSetup; diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/slack_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/slack_simulation.ts new file mode 100644 index 000000000000000..5032112e702e25d --- /dev/null +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/slack_simulation.ts @@ -0,0 +1,69 @@ +/* + * 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 http from 'http'; + +export async function initPlugin() { + return http.createServer((request, response) => { + if (request.method === 'POST') { + let data = ''; + request.on('data', (chunk) => { + data += chunk; + }); + request.on('end', () => { + const body = JSON.parse(data); + const text = body && body.text; + + if (text == null) { + response.statusCode = 400; + response.end('bad request to slack simulator'); + return; + } + + switch (text) { + case 'success': { + response.statusCode = 200; + response.end('ok'); + return; + } + case 'no_text': + response.statusCode = 400; + response.end('no_text'); + return; + + case 'invalid_payload': + response.statusCode = 400; + response.end('invalid_payload'); + return; + + case 'invalid_token': + response.statusCode = 403; + response.end('invalid_token'); + return; + + case 'status_500': + response.statusCode = 500; + response.end('simulated slack 500 response'); + return; + + case 'rate_limit': + const res = { + retry_after: 1, + ok: false, + error: 'rate_limited', + }; + + response.writeHead(429, { 'Content-Type': 'application/json', 'Retry-After': '1' }); + response.write(JSON.stringify(res)); + response.end(); + return; + } + response.statusCode = 400; + response.end('unknown request to slack simulator'); + }); + } + }); +} diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/webhook_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/webhook_simulation.ts new file mode 100644 index 000000000000000..44d8ea0c2da20ca --- /dev/null +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/webhook_simulation.ts @@ -0,0 +1,88 @@ +/* + * 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 http from 'http'; +import { fromNullable, map, filter, getOrElse } from 'fp-ts/lib/Option'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { constant } from 'fp-ts/lib/function'; + +export async function initPlugin() { + return http.createServer((request, response) => { + const credentials = pipe( + fromNullable(request.headers.authorization), + map((authorization) => authorization.split(/\s+/)), + filter((parts) => parts.length > 1), + map((parts) => Buffer.from(parts[1], 'base64').toString()), + filter((credentialsPart) => credentialsPart.indexOf(':') !== -1), + map((credentialsPart) => { + const [username, password] = credentialsPart.split(':'); + return { username, password }; + }), + getOrElse(constant({ username: '', password: '' })) + ); + + if (request.method === 'POST' || request.method === 'PUT') { + let data = ''; + request.on('data', (chunk) => { + data += chunk; + }); + request.on('end', () => { + switch (data) { + case 'success': + response.statusCode = 200; + response.end('OK'); + return; + case 'authenticate': + return validateAuthentication(credentials, response); + case 'success_post_method': + return validateRequestUsesMethod(request.method ?? '', 'post', response); + case 'success_put_method': + return validateRequestUsesMethod(request.method ?? '', 'put', response); + case 'failure': + response.statusCode = 500; + response.end('Error'); + return; + } + response.statusCode = 400; + response.end( + `unknown request to webhook simulator [${data ? `content: ${data}` : `no content`}]` + ); + return; + }); + } else { + request.on('end', () => { + response.statusCode = 400; + response.end('unknown request to webhook simulator [no content]'); + return; + }); + } + }); +} + +function validateAuthentication(credentials: any, res: any) { + try { + expect(credentials).to.eql({ + username: 'elastic', + password: 'changeme', + }); + res.statusCode = 200; + res.end('OK'); + } catch (ex) { + res.statusCode = 403; + res.end(`the validateAuthentication operation failed. ${ex.message}`); + } +} + +function validateRequestUsesMethod(requestMethod: string, method: string, res: any) { + try { + expect(requestMethod.toLowerCase()).to.eql(method); + res.statusCode = 200; + res.end('OK'); + } catch (ex) { + res.statusCode = 403; + res.end(`the validateAuthentication operation failed. ${ex.message}`); + } +} diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators_legacy/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators_legacy/index.ts deleted file mode 100644 index 43e6a73673556c7..000000000000000 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators_legacy/index.ts +++ /dev/null @@ -1,26 +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 Hapi from 'hapi'; -import { - getExternalServiceSimulatorPath, - NAME, - ExternalServiceSimulator, -} from '../actions_simulators/server/plugin'; - -import { initPlugin as initWebhook } from './webhook_simulation'; -import { initPlugin as initSlack } from './slack_simulation'; - -// eslint-disable-next-line import/no-default-export -export default function (kibana: any) { - return new kibana.Plugin({ - require: ['xpack_main'], - name: `${NAME}-legacy`, - init: (server: Hapi.Server) => { - initWebhook(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK)); - initSlack(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.SLACK)); - }, - }); -} diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators_legacy/package.json b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators_legacy/package.json deleted file mode 100644 index 644cd77d3be7535..000000000000000 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators_legacy/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "actions-fixtures-legacy", - "version": "0.0.0", - "kibana": { - "version": "kibana" - } -} diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators_legacy/slack_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators_legacy/slack_simulation.ts deleted file mode 100644 index b914386b136ccc2..000000000000000 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators_legacy/slack_simulation.ts +++ /dev/null @@ -1,74 +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 Joi from 'joi'; -import Hapi from 'hapi'; - -interface SlackRequest extends Hapi.Request { - payload: { - text: string; - }; -} -export function initPlugin(server: Hapi.Server, path: string) { - server.route({ - method: 'POST', - path, - options: { - auth: false, - validate: { - options: { abortEarly: false }, - payload: Joi.object().keys({ - text: Joi.string(), - }), - }, - }, - handler: slackHandler as Hapi.Lifecycle.Method, - }); -} -// Slack simulator: create a slack action pointing here, and you can get -// different responses based on the message posted. See the README.md for -// more info. - -function slackHandler(request: SlackRequest, h: any) { - const body = request.payload; - const text = body && body.text; - - if (text == null) { - return htmlResponse(h, 400, 'bad request to slack simulator'); - } - - switch (text) { - case 'success': - return htmlResponse(h, 200, 'ok'); - - case 'no_text': - return htmlResponse(h, 400, 'no_text'); - - case 'invalid_payload': - return htmlResponse(h, 400, 'invalid_payload'); - - case 'invalid_token': - return htmlResponse(h, 403, 'invalid_token'); - - case 'status_500': - return htmlResponse(h, 500, 'simulated slack 500 response'); - - case 'rate_limit': - const response = { - retry_after: 1, - ok: false, - error: 'rate_limited', - }; - - return h.response(response).type('application/json').header('retry-after', '1').code(429); - } - - return htmlResponse(h, 400, 'unknown request to slack simulator'); -} - -function htmlResponse(h: any, code: number, text: string) { - return h.response(text).type('text/html').code(code); -} diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators_legacy/webhook_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators_legacy/webhook_simulation.ts deleted file mode 100644 index 44e1aff162c9222..000000000000000 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators_legacy/webhook_simulation.ts +++ /dev/null @@ -1,112 +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 expect from '@kbn/expect'; -import Joi from 'joi'; -import Hapi, { Util } from 'hapi'; -import { fromNullable, map, filter, getOrElse } from 'fp-ts/lib/Option'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { constant } from 'fp-ts/lib/function'; - -interface WebhookRequest extends Hapi.Request { - payload: string; -} - -export async function initPlugin(server: Hapi.Server, path: string) { - server.auth.scheme('identifyCredentialsIfPresent', function identifyCredentialsIfPresent( - s: Hapi.Server, - options?: Hapi.ServerAuthSchemeOptions - ) { - const scheme = { - async authenticate(request: Hapi.Request, h: Hapi.ResponseToolkit) { - const credentials = pipe( - fromNullable(request.headers.authorization), - map((authorization) => authorization.split(/\s+/)), - filter((parts) => parts.length > 1), - map((parts) => Buffer.from(parts[1], 'base64').toString()), - filter((credentialsPart) => credentialsPart.indexOf(':') !== -1), - map((credentialsPart) => { - const [username, password] = credentialsPart.split(':'); - return { username, password }; - }), - getOrElse(constant({ username: '', password: '' })) - ); - - return h.authenticated({ credentials }); - }, - }; - - return scheme; - }); - server.auth.strategy('simple', 'identifyCredentialsIfPresent'); - - server.route({ - method: ['POST', 'PUT'], - path, - options: { - auth: 'simple', - validate: { - options: { abortEarly: false }, - payload: Joi.string(), - }, - }, - handler: webhookHandler as Hapi.Lifecycle.Method, - }); -} - -function webhookHandler(request: WebhookRequest, h: any) { - const body = request.payload; - - switch (body) { - case 'success': - return htmlResponse(h, 200, `OK`); - case 'authenticate': - return validateAuthentication(request, h); - case 'success_post_method': - return validateRequestUsesMethod(request, h, 'post'); - case 'success_put_method': - return validateRequestUsesMethod(request, h, 'put'); - case 'failure': - return htmlResponse(h, 500, `Error`); - } - - return htmlResponse( - h, - 400, - `unknown request to webhook simulator [${body ? `content: ${body}` : `no content`}]` - ); -} - -function validateAuthentication(request: WebhookRequest, h: any) { - const { - auth: { credentials }, - } = request; - try { - expect(credentials).to.eql({ - username: 'elastic', - password: 'changeme', - }); - return htmlResponse(h, 200, `OK`); - } catch (ex) { - return htmlResponse(h, 403, `the validateAuthentication operation failed. ${ex.message}`); - } -} - -function validateRequestUsesMethod( - request: WebhookRequest, - h: any, - method: Util.HTTP_METHODS_PARTIAL -) { - try { - expect(request.method).to.eql(method); - return htmlResponse(h, 200, `OK`); - } catch (ex) { - return htmlResponse(h, 403, `the validateAuthentication operation failed. ${ex.message}`); - } -} - -function htmlResponse(h: any, code: number, text: string) { - return h.response(text).type('text/html').code(code); -} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts index f21bc8edeef1e40..c68bcaa0ad4e8d9 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts @@ -5,28 +5,27 @@ */ import expect from '@kbn/expect'; - +import http from 'http'; +import getPort from 'get-port'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { - getExternalServiceSimulatorPath, - ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { getSlackServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function slackTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const kibanaServer = getService('kibanaServer'); describe('slack action', () => { let simulatedActionId = ''; - let slackSimulatorURL: string = ''; + let slackSimulatorURL: string = ''; + let slackServer: http.Server; // need to wait for kibanaServer to settle ... - before(() => { - slackSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SLACK) - ); + before(async () => { + slackServer = await getSlackServer(); + const availablePort = await getPort({ port: 9000 }); + slackServer.listen(availablePort); + slackSimulatorURL = `http://localhost:${availablePort}`; }); it('should return 200 when creating a slack action successfully', async () => { @@ -220,5 +219,9 @@ export default function slackTest({ getService }: FtrProviderContext) { expect(result.message).to.match(/error posting a slack message, retry later/); expect(result.retry).to.equal(true); }); + + after(() => { + slackServer.close(); + }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts index 7eba753d7e98bae..8f17ab54184b5bf 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts @@ -4,12 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import http from 'http'; +import getPort from 'get-port'; import expect from '@kbn/expect'; import { URL, format as formatUrl } from 'url'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, + getWebhookServer, } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; const defaultValues: Record = { @@ -30,11 +33,13 @@ export default function webhookTest({ getService }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); async function createWebhookAction( - urlWithCreds: string, - config: Record> = {} + webhookSimulatorURL: string, + config: Record> = {}, + kibanaUrlWithCreds: string ): Promise { - const { url: fullUrl, user, password } = extractCredentialsFromUrl(urlWithCreds); - const url = config.url && typeof config.url === 'object' ? parsePort(config.url) : fullUrl; + const { user, password } = extractCredentialsFromUrl(kibanaUrlWithCreds); + const url = + config.url && typeof config.url === 'object' ? parsePort(config.url) : webhookSimulatorURL; const composedConfig = { headers: { 'Content-Type': 'text/plain', @@ -61,11 +66,17 @@ export default function webhookTest({ getService }: FtrProviderContext) { } describe('webhook action', () => { - let webhookSimulatorURL: string = ''; - + let webhookSimulatorURL: string = ''; + let webhookServer: http.Server; + let kibanaURL: string = ''; // need to wait for kibanaServer to settle ... - before(() => { - webhookSimulatorURL = kibanaServer.resolveUrl( + before(async () => { + webhookServer = await getWebhookServer(); + const availablePort = await getPort({ port: 9000 }); + webhookServer.listen(availablePort); + webhookSimulatorURL = `http://localhost:${availablePort}`; + + kibanaURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK) ); }); @@ -117,7 +128,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { }); it('should send authentication to the webhook target', async () => { - const webhookActionId = await createWebhookAction(webhookSimulatorURL); + const webhookActionId = await createWebhookAction(webhookSimulatorURL, {}, kibanaURL); const { body: result } = await supertest .post(`/api/actions/action/${webhookActionId}/_execute`) .set('kbn-xsrf', 'test') @@ -132,7 +143,11 @@ export default function webhookTest({ getService }: FtrProviderContext) { }); it('should support the POST method against webhook target', async () => { - const webhookActionId = await createWebhookAction(webhookSimulatorURL, { method: 'post' }); + const webhookActionId = await createWebhookAction( + webhookSimulatorURL, + { method: 'post' }, + kibanaURL + ); const { body: result } = await supertest .post(`/api/actions/action/${webhookActionId}/_execute`) .set('kbn-xsrf', 'test') @@ -147,7 +162,11 @@ export default function webhookTest({ getService }: FtrProviderContext) { }); it('should support the PUT method against webhook target', async () => { - const webhookActionId = await createWebhookAction(webhookSimulatorURL, { method: 'put' }); + const webhookActionId = await createWebhookAction( + webhookSimulatorURL, + { method: 'put' }, + kibanaURL + ); const { body: result } = await supertest .post(`/api/actions/action/${webhookActionId}/_execute`) .set('kbn-xsrf', 'test') @@ -183,7 +202,11 @@ export default function webhookTest({ getService }: FtrProviderContext) { }); it('should handle unreachable webhook targets', async () => { - const webhookActionId = await createWebhookAction('http://some.non.existent.com/endpoint'); + const webhookActionId = await createWebhookAction( + 'http://some.non.existent.com/endpoint', + {}, + kibanaURL + ); const { body: result } = await supertest .post(`/api/actions/action/${webhookActionId}/_execute`) .set('kbn-xsrf', 'test') @@ -199,7 +222,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { }); it('should handle failing webhook targets', async () => { - const webhookActionId = await createWebhookAction(webhookSimulatorURL); + const webhookActionId = await createWebhookAction(webhookSimulatorURL, {}, kibanaURL); const { body: result } = await supertest .post(`/api/actions/action/${webhookActionId}/_execute`) .set('kbn-xsrf', 'test') @@ -214,6 +237,10 @@ export default function webhookTest({ getService }: FtrProviderContext) { expect(result.message).to.match(/error calling webhook, retry later/); expect(result.serviceMessage).to.eql('[500] Internal Server Error'); }); + + after(() => { + webhookServer.close(); + }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts index cac6355409ac93e..ab3a92d0b3f706d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts @@ -31,8 +31,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .then((response: SupertestResponse) => response.body); } - // FLAKY: https://github.com/elastic/kibana/issues/72803 - describe.skip('update', () => { + describe('update', () => { const objectRemover = new ObjectRemover(supertest); after(() => objectRemover.removeAll()); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts index b3572978cee703d..acfbad007d72207 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts @@ -4,24 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ +import http from 'http'; +import getPort from 'get-port'; import expect from '@kbn/expect'; import { URL, format as formatUrl } from 'url'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { - getExternalServiceSimulatorPath, - ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { getWebhookServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function webhookTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const kibanaServer = getService('kibanaServer'); async function createWebhookAction( - urlWithCreds: string, + webhookSimulatorURL: string, config: Record> = {} ): Promise { - const url = formatUrl(new URL(urlWithCreds), { auth: false }); + const url = formatUrl(new URL(webhookSimulatorURL), { auth: false }); const composedConfig = { headers: { 'Content-Type': 'text/plain', @@ -45,13 +43,13 @@ export default function webhookTest({ getService }: FtrProviderContext) { } describe('webhook action', () => { - let webhookSimulatorURL: string = ''; - - // need to wait for kibanaServer to settle ... - before(() => { - webhookSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK) - ); + let webhookSimulatorURL: string = ''; + let webhookServer: http.Server; + before(async () => { + webhookServer = await getWebhookServer(); + const availablePort = await getPort({ port: 9000 }); + webhookServer.listen(availablePort); + webhookSimulatorURL = `http://localhost:${availablePort}`; }); it('webhook can be executed without username and password', async () => { @@ -68,5 +66,9 @@ export default function webhookTest({ getService }: FtrProviderContext) { expect(result.status).to.eql('ok'); }); + + after(() => { + webhookServer.close(); + }); }); } diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts index bc36f70df3641df..cedd96f147c2b2c 100644 --- a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts @@ -130,7 +130,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it(`index pattern listing doesn't show create button`, async () => { await PageObjects.settings.clickKibanaIndexPatterns(); - await testSubjects.existOrFail('indexPatternTable'); + await testSubjects.existOrFail('emptyIndexPatternPrompt'); await testSubjects.missingOrFail('createIndexPatternButton'); }); diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/create_exception_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/create_exception_list_items.ts new file mode 100644 index 000000000000000..6148dbcc7090e62 --- /dev/null +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/create_exception_list_items.ts @@ -0,0 +1,119 @@ +/* + * 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 { ExceptionListItemSchema } from '../../../../plugins/lists/common'; +import { getExceptionListItemResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_item_schema.mock'; +import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; +import { + getCreateExceptionListItemMinimalSchemaMock, + getCreateExceptionListItemMinimalSchemaMockWithoutId, +} from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock'; +import { + EXCEPTION_LIST_ITEM_URL, + EXCEPTION_LIST_URL, +} from '../../../../plugins/lists/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +import { + removeListItemServerGeneratedProperties, + removeExceptionListItemServerGeneratedProperties, +} from '../../utils'; + +import { deleteAllExceptions } from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('create_exception_list_items', () => { + describe('validation errors', () => { + it('should give a 404 error that the exception list must exist first before being able to add a list item to the exception list', async () => { + const { body } = await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMock()) + .expect(404); + + expect(body).to.eql({ + message: 'exception list id: "some-list-id" does not exist', + status_code: 404, + }); + }); + }); + + describe('creating exception list items', () => { + afterEach(async () => { + await deleteAllExceptions(es); + }); + + it('should create a simple exception list item with a list item id', async () => { + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + const { body } = await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMock()) + .expect(200); + + const bodyToCompare = removeExceptionListItemServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getExceptionListItemResponseMockWithoutAutoGeneratedValues()); + }); + + it('should create a simple exception list item without an id', async () => { + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + const { body } = await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMockWithoutId()) + .expect(200); + + const bodyToCompare = removeListItemServerGeneratedProperties(body); + const outputList: Partial = { + ...getExceptionListItemResponseMockWithoutAutoGeneratedValues(), + item_id: body.item_id, + }; + expect(bodyToCompare).to.eql(outputList); + }); + + it('should cause a 409 conflict if we attempt to create the same exception list item twice', async () => { + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMock()) + .expect(200); + + const { body } = await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMock()) + .expect(409); + + expect(body).to.eql({ + message: 'exception list item id: "some-list-item-id" already exists', + status_code: 409, + }); + }); + }); + }); +}; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/create_exception_lists.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/create_exception_lists.ts new file mode 100644 index 000000000000000..2b654c72ae282e2 --- /dev/null +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/create_exception_lists.ts @@ -0,0 +1,77 @@ +/* + * 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 { ExceptionListSchema } from '../../../../plugins/lists/common'; +import { EXCEPTION_LIST_URL } from '../../../../plugins/lists/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { getExceptionResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_schema.mock'; +import { + getCreateExceptionListMinimalSchemaMock, + getCreateExceptionListMinimalSchemaMockWithoutId, +} from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; + +import { deleteAllExceptions, removeExceptionListServerGeneratedProperties } from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('create_exception_lists', () => { + describe('creating exception lists', () => { + afterEach(async () => { + await deleteAllExceptions(es); + }); + + it('should create a simple exception list', async () => { + const { body } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getExceptionResponseMockWithoutAutoGeneratedValues()); + }); + + it('should create a simple exception list without a list_id', async () => { + const { body } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMockWithoutId()) + .expect(200); + + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + const outputtedList: Partial = { + ...getExceptionResponseMockWithoutAutoGeneratedValues(), + list_id: bodyToCompare.list_id, + }; + expect(bodyToCompare).to.eql(outputtedList); + }); + + it('should cause a 409 conflict if we attempt to create the same list_id twice', async () => { + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + const { body } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(409); + + expect(body).to.eql({ + message: 'exception list id: "some-list-id" already exists', + status_code: 409, + }); + }); + }); + }); +}; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_exception_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_exception_list_items.ts new file mode 100644 index 000000000000000..16bdd264dc54646 --- /dev/null +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_exception_list_items.ts @@ -0,0 +1,119 @@ +/* + * 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 { ExceptionListItemSchema } from '../../../../plugins/lists/common'; +import { getExceptionListItemResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_item_schema.mock'; +import { + getCreateExceptionListItemMinimalSchemaMock, + getCreateExceptionListItemMinimalSchemaMockWithoutId, +} from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock'; +import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + EXCEPTION_LIST_URL, + EXCEPTION_LIST_ITEM_URL, +} from '../../../../plugins/lists/common/constants'; + +import { deleteAllExceptions, removeExceptionListItemServerGeneratedProperties } from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('delete_exception_list_items', () => { + describe('delete exception list items', () => { + afterEach(async () => { + await deleteAllExceptions(es); + }); + + it('should delete a single exception list item by its item_id', async () => { + // create an exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // create an exception list item + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMock()) + .expect(200); + + // delete the exception list item by its item_id + const { body } = await supertest + .delete( + `${EXCEPTION_LIST_ITEM_URL}?item_id=${ + getCreateExceptionListItemMinimalSchemaMock().item_id + }` + ) + .set('kbn-xsrf', 'true') + .expect(200); + + const bodyToCompare = removeExceptionListItemServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getExceptionListItemResponseMockWithoutAutoGeneratedValues()); + }); + + it('should delete a single exception list item using an auto generated id', async () => { + // create an exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // create an exception list item + const { body: bodyWithCreatedList } = await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMockWithoutId()) + .expect(200); + + // delete that exception list item by its auto-generated id + const { body } = await supertest + .delete(`${EXCEPTION_LIST_ITEM_URL}?id=${bodyWithCreatedList.id}`) + .set('kbn-xsrf', 'true') + .expect(200); + const outputtedList: Partial = { + ...getExceptionListItemResponseMockWithoutAutoGeneratedValues(), + item_id: body.item_id, + }; + + const bodyToCompare = removeExceptionListItemServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputtedList); + }); + + it('should return an error if the id does not exist when trying to delete it', async () => { + const { body } = await supertest + .delete(`${EXCEPTION_LIST_ITEM_URL}?id=c1e1b359-7ac1-4e96-bc81-c683c092436f`) + .set('kbn-xsrf', 'true') + .expect(404); + + expect(body).to.eql({ + message: 'exception list item id: "c1e1b359-7ac1-4e96-bc81-c683c092436f" does not exist', + status_code: 404, + }); + }); + + it('should return an error if the item_id does not exist when trying to delete it', async () => { + const { body } = await supertest + .delete(`${EXCEPTION_LIST_ITEM_URL}?item_id=c1e1b359-7ac1-4e96-bc81-c683c092436f`) + .set('kbn-xsrf', 'true') + .expect(404); + + expect(body).to.eql({ + message: + 'exception list item item_id: "c1e1b359-7ac1-4e96-bc81-c683c092436f" does not exist', + status_code: 404, + }); + }); + }); + }); +}; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_exception_lists.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_exception_lists.ts new file mode 100644 index 000000000000000..56e4bcd9641cfea --- /dev/null +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_exception_lists.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { ExceptionListSchema } from '../../../../plugins/lists/common'; +import { getExceptionResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_schema.mock'; +import { + getCreateExceptionListMinimalSchemaMock, + getCreateExceptionListMinimalSchemaMockWithoutId, +} from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { EXCEPTION_LIST_URL } from '../../../../plugins/lists/common/constants'; + +import { deleteAllExceptions, removeExceptionListServerGeneratedProperties } from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('delete_exception_lists', () => { + describe('delete exception lists', () => { + afterEach(async () => { + await deleteAllExceptions(es); + }); + + it('should delete a single exception list by its list_id', async () => { + // create an exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // delete the exception list by its list id + const { body } = await supertest + .delete( + `${EXCEPTION_LIST_URL}?list_id=${getCreateExceptionListMinimalSchemaMock().list_id}` + ) + .set('kbn-xsrf', 'true') + .expect(200); + + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getExceptionResponseMockWithoutAutoGeneratedValues()); + }); + + it('should delete a single exception list using an auto generated id', async () => { + // create an exception list + const { body: bodyWithCreatedList } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMockWithoutId()) + .expect(200); + + // delete that list by its auto-generated id + const { body } = await supertest + .delete(`${EXCEPTION_LIST_URL}?id=${bodyWithCreatedList.id}`) + .set('kbn-xsrf', 'true') + .expect(200); + + const outputtedList: Partial = { + ...getExceptionResponseMockWithoutAutoGeneratedValues(), + list_id: body.list_id, + }; + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputtedList); + }); + + it('should return an error if the id does not exist when trying to delete it', async () => { + const { body } = await supertest + .delete(`${EXCEPTION_LIST_URL}?id=c1e1b359-7ac1-4e96-bc81-c683c092436f`) + .set('kbn-xsrf', 'true') + .expect(404); + + expect(body).to.eql({ + message: 'exception list id: "c1e1b359-7ac1-4e96-bc81-c683c092436f" does not exist', + status_code: 404, + }); + }); + + it('should return an error if the list_id does not exist when trying to delete it', async () => { + const { body } = await supertest + .delete(`${EXCEPTION_LIST_URL}?list_id=c1e1b359-7ac1-4e96-bc81-c683c092436f`) + .set('kbn-xsrf', 'true') + .expect(404); + + expect(body).to.eql({ + message: 'exception list list_id: "c1e1b359-7ac1-4e96-bc81-c683c092436f" does not exist', + status_code: 404, + }); + }); + }); + }); +}; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/export_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/export_list_items.ts index 6fe783fc497f2c0..74c28f5abdfc1dd 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/export_list_items.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/export_list_items.ts @@ -96,8 +96,9 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .expect(200) .parse(binaryToString); - - expect(body.toString()).to.eql('127.0.0.2\n127.0.0.1\n'); + const bodyString = body.toString(); + expect(bodyString.includes('127.0.0.1')).to.be(true); + expect(bodyString.includes('127.0.0.2')).to.be(true); }); }); }); diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_list_items.ts new file mode 100644 index 000000000000000..a65e9f344986fae --- /dev/null +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_list_items.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { getExceptionListItemResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_item_schema.mock'; +import { getCreateExceptionListItemMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock'; +import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + EXCEPTION_LIST_URL, + EXCEPTION_LIST_ITEM_URL, +} from '../../../../plugins/lists/common/constants'; + +import { deleteAllExceptions, removeExceptionListItemServerGeneratedProperties } from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('find_exception_list_items', () => { + describe('find exception list items', () => { + afterEach(async () => { + await deleteAllExceptions(es); + }); + + it('should return an empty find body correctly if no exception list items are loaded', async () => { + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + const { body } = await supertest + .get( + `${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${ + getCreateExceptionListMinimalSchemaMock().list_id + }` + ) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).to.eql({ + data: [], + page: 1, + per_page: 20, + total: 0, + }); + }); + + it('should return 404 if given a list_id that does not exist', async () => { + const { body } = await supertest + .get(`${EXCEPTION_LIST_ITEM_URL}/_find?list_id=non_exist`) + .set('kbn-xsrf', 'true') + .send() + .expect(404); + + expect(body).to.eql({ + message: 'exception list id: "non_exist" does not exist', + status_code: 404, + }); + }); + + it('should return a single exception list item when a single exception list item is loaded from a find with defaults added', async () => { + // add the exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // add a single exception list item + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMock()) + .expect(200); + + // query the single exception list from _find + const { body } = await supertest + .get( + `${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${ + getCreateExceptionListMinimalSchemaMock().list_id + }` + ) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + body.data = [removeExceptionListItemServerGeneratedProperties(body.data[0])]; + expect(body).to.eql({ + data: [getExceptionListItemResponseMockWithoutAutoGeneratedValues()], + page: 1, + per_page: 20, + total: 1, + }); + }); + }); + }); +}; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_lists.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_lists.ts new file mode 100644 index 000000000000000..c2328a7d112f430 --- /dev/null +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_lists.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { getExceptionResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_schema.mock'; +import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { EXCEPTION_LIST_URL } from '../../../../plugins/lists/common/constants'; + +import { deleteAllExceptions, removeExceptionListServerGeneratedProperties } from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('find_exception_lists', () => { + describe('find exception lists', () => { + afterEach(async () => { + await deleteAllExceptions(es); + }); + + it('should return an empty find body correctly if no exception lists are loaded', async () => { + const { body } = await supertest + .get(`${EXCEPTION_LIST_URL}/_find`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).to.eql({ + data: [], + page: 1, + per_page: 20, + total: 0, + }); + }); + + it('should return a single exception list when a single exception list is loaded from a find with defaults added', async () => { + // add a single exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // query the single exception list from _find + const { body } = await supertest + .get(`${EXCEPTION_LIST_URL}/_find`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + body.data = [removeExceptionListServerGeneratedProperties(body.data[0])]; + expect(body).to.eql({ + data: [getExceptionResponseMockWithoutAutoGeneratedValues()], + page: 1, + per_page: 20, + total: 1, + }); + }); + }); + }); +}; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/import_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/import_list_items.ts index 4befb6bbaf05004..7b7a6173fb40813 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/import_list_items.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/import_list_items.ts @@ -45,7 +45,7 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - describe('importing rules with an index', () => { + describe('importing lists with an index', () => { beforeEach(async () => { await createListsIndex(supertest); }); diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/index.ts index 302877a680aa666..5458b4a9a7db259 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/index.ts @@ -23,5 +23,15 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./find_list_items')); loadTestFile(require.resolve('./import_list_items')); loadTestFile(require.resolve('./export_list_items')); + loadTestFile(require.resolve('./create_exception_lists')); + loadTestFile(require.resolve('./create_exception_list_items')); + loadTestFile(require.resolve('./read_exception_lists')); + loadTestFile(require.resolve('./read_exception_list_items')); + loadTestFile(require.resolve('./update_exception_lists')); + loadTestFile(require.resolve('./update_exception_list_items')); + loadTestFile(require.resolve('./delete_exception_lists')); + loadTestFile(require.resolve('./delete_exception_list_items')); + loadTestFile(require.resolve('./find_exception_lists')); + loadTestFile(require.resolve('./find_exception_list_items')); }); }; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/read_exception_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/read_exception_list_items.ts new file mode 100644 index 000000000000000..26b969e940a2b3b --- /dev/null +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/read_exception_list_items.ts @@ -0,0 +1,159 @@ +/* + * 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 { getExceptionListItemResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_item_schema.mock'; +import { + getCreateExceptionListItemMinimalSchemaMock, + getCreateExceptionListItemMinimalSchemaMockWithoutId, +} from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock'; +import { ExceptionListItemSchema } from '../../../../plugins/lists/common'; +import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + EXCEPTION_LIST_URL, + EXCEPTION_LIST_ITEM_URL, +} from '../../../../plugins/lists/common/constants'; + +import { deleteAllExceptions, removeExceptionListItemServerGeneratedProperties } from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('read_exception_list_items', () => { + describe('reading exception list items', () => { + afterEach(async () => { + await deleteAllExceptions(es); + }); + + it('should be able to read a single exception list items using item_id', async () => { + // create a simple exception list to read + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + const { body } = await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMock()) + .expect(200); + + const bodyToCompare = removeExceptionListItemServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getExceptionListItemResponseMockWithoutAutoGeneratedValues()); + }); + + it('should be able to read a single exception list item using id', async () => { + // create a simple exception list to read + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // create a simple exception list item to read + const { body: createListBody } = await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMock()) + .expect(200); + + const { body } = await supertest + .get(`${EXCEPTION_LIST_ITEM_URL}?id=${createListBody.id}`) + .set('kbn-xsrf', 'true') + .expect(200); + + const bodyToCompare = removeExceptionListItemServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getExceptionListItemResponseMockWithoutAutoGeneratedValues()); + }); + + it('should be able to read a single list item with an auto-generated id', async () => { + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // create a simple exception list item to read + const { body: createListBody } = await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMockWithoutId()) + .expect(200); + + const { body } = await supertest + .get(`${EXCEPTION_LIST_ITEM_URL}?id=${createListBody.id}`) + .set('kbn-xsrf', 'true') + .expect(200); + + const outputtedList: Partial = { + ...getExceptionListItemResponseMockWithoutAutoGeneratedValues(), + item_id: body.item_id, + }; + + const bodyToCompare = removeExceptionListItemServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputtedList); + }); + + it('should be able to read a single list item with an auto-generated item_id', async () => { + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // create a simple exception list item to read + const { body: createListBody } = await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMockWithoutId()) + .expect(200); + + const { body } = await supertest + .get(`${EXCEPTION_LIST_ITEM_URL}?item_id=${createListBody.item_id}`) + .set('kbn-xsrf', 'true') + .expect(200); + + const outputtedList: Partial = { + ...getExceptionListItemResponseMockWithoutAutoGeneratedValues(), + item_id: body.item_id, + }; + + const bodyToCompare = removeExceptionListItemServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputtedList); + }); + + it('should return 404 if given a fake id', async () => { + const { body } = await supertest + .get(`${EXCEPTION_LIST_ITEM_URL}?id=c1e1b359-7ac1-4e96-bc81-c683c092436f`) + .set('kbn-xsrf', 'true') + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'exception list item id: "c1e1b359-7ac1-4e96-bc81-c683c092436f" does not exist', + }); + }); + + it('should return 404 if given a fake list_id', async () => { + const { body } = await supertest + .get(`${EXCEPTION_LIST_ITEM_URL}?item_id=c1e1b359-7ac1-4e96-bc81-c683c092436f`) + .set('kbn-xsrf', 'true') + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: + 'exception list item item_id: "c1e1b359-7ac1-4e96-bc81-c683c092436f" does not exist', + }); + }); + }); + }); +}; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/read_exception_lists.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/read_exception_lists.ts new file mode 100644 index 000000000000000..ee6bef3200f5c78 --- /dev/null +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/read_exception_lists.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 expect from '@kbn/expect'; + +import { ExceptionListSchema } from '../../../../plugins/lists/common'; +import { getExceptionResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_schema.mock'; +import { + getCreateExceptionListMinimalSchemaMock, + getCreateExceptionListMinimalSchemaMockWithoutId, +} from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { EXCEPTION_LIST_URL } from '../../../../plugins/lists/common/constants'; + +import { deleteAllExceptions, removeExceptionListServerGeneratedProperties } from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('read_exception_lists', () => { + describe('reading exception lists', () => { + afterEach(async () => { + await deleteAllExceptions(es); + }); + + it('should be able to read a single exception list using list_id', async () => { + // create a simple exception list to read + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + const { body } = await supertest + .get(`${EXCEPTION_LIST_URL}?list_id=${getCreateExceptionListMinimalSchemaMock().list_id}`) + .set('kbn-xsrf', 'true') + .expect(200); + + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getExceptionResponseMockWithoutAutoGeneratedValues()); + }); + + it('should be able to read a single exception list using id', async () => { + // create a simple exception list to read + const { body: createListBody } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + const { body } = await supertest + .get(`${EXCEPTION_LIST_URL}?id=${createListBody.id}`) + .set('kbn-xsrf', 'true') + .expect(200); + + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getExceptionResponseMockWithoutAutoGeneratedValues()); + }); + + it('should be able to read a single list with an auto-generated list_id', async () => { + // create a simple exception list to read + const { body: createListBody } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMockWithoutId()) + .expect(200); + + const { body } = await supertest + .get(`${EXCEPTION_LIST_URL}?list_id=${createListBody.list_id}`) + .set('kbn-xsrf', 'true') + .expect(200); + + const outputtedList: Partial = { + ...getExceptionResponseMockWithoutAutoGeneratedValues(), + list_id: body.list_id, + }; + + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputtedList); + }); + + it('should return 404 if given a fake id', async () => { + const { body } = await supertest + .get(`${EXCEPTION_LIST_URL}?id=c1e1b359-7ac1-4e96-bc81-c683c092436f`) + .set('kbn-xsrf', 'true') + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'exception list id: "c1e1b359-7ac1-4e96-bc81-c683c092436f" does not exist', + }); + }); + + it('should return 404 if given a fake list_id', async () => { + const { body } = await supertest + .get(`${EXCEPTION_LIST_URL}?list_id=c1e1b359-7ac1-4e96-bc81-c683c092436f`) + .set('kbn-xsrf', 'true') + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'exception list list_id: "c1e1b359-7ac1-4e96-bc81-c683c092436f" does not exist', + }); + }); + }); + }); +}; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/update_exception_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/update_exception_list_items.ts new file mode 100644 index 000000000000000..40fb705620a1968 --- /dev/null +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/update_exception_list_items.ts @@ -0,0 +1,168 @@ +/* + * 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 { getExceptionListItemResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_item_schema.mock'; +import { getCreateExceptionListItemMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock'; +import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + EXCEPTION_LIST_URL, + EXCEPTION_LIST_ITEM_URL, +} from '../../../../plugins/lists/common/constants'; + +import { deleteAllExceptions, removeExceptionListServerGeneratedProperties } from '../../utils'; +import { + UpdateExceptionListItemSchema, + ExceptionListItemSchema, +} from '../../../../plugins/lists/common/schemas'; + +import { getUpdateMinimalExceptionListItemSchemaMock } from '../../../../plugins/lists/common/schemas/request/update_exception_list_item_schema.mock'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('update_exception_list_items', () => { + describe('update exception list items', () => { + afterEach(async () => { + await deleteAllExceptions(es); + }); + + it('should update a single exception list item property of name using an id', async () => { + // create a simple exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // create a simple exception list item + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMock()) + .expect(200); + + // update a exception list item's name + const updatedList: UpdateExceptionListItemSchema = { + ...getUpdateMinimalExceptionListItemSchemaMock(), + name: 'some other name', + }; + + const { body } = await supertest + .put(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(updatedList) + .expect(200); + + const outputList: Partial = { + ...getExceptionListItemResponseMockWithoutAutoGeneratedValues(), + name: 'some other name', + }; + + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputList); + }); + + it('should update a single exception list item property of name using an auto-generated item_id', async () => { + // create a simple exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // eslint-disable-next-line @typescript-eslint/naming-convention + const { item_id, ...itemNoId } = getCreateExceptionListItemMinimalSchemaMock(); + + // create a simple exception list item + const { body: createListBody } = await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(itemNoId) + .expect(200); + + // update a exception list item's name + const updatedList: UpdateExceptionListItemSchema = { + ...getUpdateMinimalExceptionListItemSchemaMock(), + item_id: createListBody.item_id, + name: 'some other name', + }; + + const { body } = await supertest + .put(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(updatedList) + .expect(200); + + const outputList: Partial = { + ...getExceptionListItemResponseMockWithoutAutoGeneratedValues(), + name: 'some other name', + item_id: body.item_id, + }; + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputList); + }); + + it('should give a 404 if it is given a fake exception list item id', async () => { + const updatedList: UpdateExceptionListItemSchema = { + ...getUpdateMinimalExceptionListItemSchemaMock(), + id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d', + }; + delete updatedList.item_id; + + const { body } = await supertest + .put(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(updatedList) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'exception list item id: "5096dec6-b6b9-4d8d-8f93-6c2602079d9d" does not exist', + }); + }); + + it('should give a 404 if it is given a fake item_id', async () => { + const updatedList: UpdateExceptionListItemSchema = { + ...getUpdateMinimalExceptionListItemSchemaMock(), + item_id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d', + }; + + const { body } = await supertest + .put(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(updatedList) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: + 'exception list item item_id: "5096dec6-b6b9-4d8d-8f93-6c2602079d9d" does not exist', + }); + }); + + it('should give a 404 if both id and list_id is null', async () => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { item_id, ...listNoId } = getUpdateMinimalExceptionListItemSchemaMock(); + + const { body } = await supertest + .put(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(listNoId) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'either id or item_id need to be defined', + }); + }); + }); + }); +}; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/update_exception_lists.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/update_exception_lists.ts new file mode 100644 index 000000000000000..bd30dd87963eda2 --- /dev/null +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/update_exception_lists.ts @@ -0,0 +1,182 @@ +/* + * 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 { getExceptionResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_schema.mock'; +import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { EXCEPTION_LIST_URL } from '../../../../plugins/lists/common/constants'; + +import { deleteAllExceptions, removeExceptionListServerGeneratedProperties } from '../../utils'; +import { + UpdateExceptionListSchema, + ExceptionListSchema, +} from '../../../../plugins/lists/common/schemas'; + +import { getUpdateMinimalExceptionListSchemaMock } from '../../../../plugins/lists/common/schemas/request/update_exception_list_schema.mock'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('update_exception_lists', () => { + describe('update exception lists', () => { + afterEach(async () => { + await deleteAllExceptions(es); + }); + + it('should update a single exception list property of name using an id', async () => { + // create a simple exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // update a exception list's name + const updatedList: UpdateExceptionListSchema = { + ...getUpdateMinimalExceptionListSchemaMock(), + name: 'some other name', + }; + + const { body } = await supertest + .put(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(updatedList) + .expect(200); + + const outputList: Partial = { + ...getExceptionResponseMockWithoutAutoGeneratedValues(), + name: 'some other name', + version: 2, + }; + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputList); + }); + + it('should update a single exception list property of name using an auto-generated list_id', async () => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { list_id, ...listNoId } = getCreateExceptionListMinimalSchemaMock(); + + // create a simple exception list + const { body: createListBody } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(listNoId) + .expect(200); + + // update a exception list's name + const updatedList: UpdateExceptionListSchema = { + ...getUpdateMinimalExceptionListSchemaMock(), + id: createListBody.id, + name: 'some other name', + }; + + const { body } = await supertest + .put(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(updatedList) + .expect(200); + + const outputList: Partial = { + ...getExceptionResponseMockWithoutAutoGeneratedValues(), + name: 'some other name', + list_id: body.list_id, + version: 2, + }; + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputList); + }); + + it('should change the version of a list when it updates two properties', async () => { + // create a simple exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // update a simple exception list property of name and description + // update a exception list's name + const updatedList: UpdateExceptionListSchema = { + ...getUpdateMinimalExceptionListSchemaMock(), + name: 'some other name', + description: 'some other description', + }; + + const { body } = await supertest + .put(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(updatedList) + .expect(200); + + const outputList: Partial = { + ...getExceptionResponseMockWithoutAutoGeneratedValues(), + name: 'some other name', + description: 'some other description', + version: 2, + }; + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputList); + }); + + it('should give a 404 if it is given a fake id', async () => { + const updatedList: UpdateExceptionListSchema = { + ...getUpdateMinimalExceptionListSchemaMock(), + id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d', + }; + delete updatedList.list_id; + + const { body } = await supertest + .put(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(updatedList) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'exception list id: "5096dec6-b6b9-4d8d-8f93-6c2602079d9d" does not exist', + }); + }); + + it('should give a 404 if it is given a fake list_id', async () => { + const updatedList: UpdateExceptionListSchema = { + ...getUpdateMinimalExceptionListSchemaMock(), + list_id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d', + }; + + const { body } = await supertest + .put(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(updatedList) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'exception list list_id: "5096dec6-b6b9-4d8d-8f93-6c2602079d9d" does not exist', + }); + }); + + it('should give a 404 if both id and list_id is null', async () => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { list_id, ...listNoId } = getUpdateMinimalExceptionListSchemaMock(); + + const { body } = await supertest + .put(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(listNoId) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'either id or list_id need to be defined', + }); + }); + }); + }); +}; diff --git a/x-pack/test/lists_api_integration/utils.ts b/x-pack/test/lists_api_integration/utils.ts index 272768fdf50b300..54a13fc027c99d8 100644 --- a/x-pack/test/lists_api_integration/utils.ts +++ b/x-pack/test/lists_api_integration/utils.ts @@ -6,8 +6,13 @@ import { SuperTest } from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; +import { Client } from '@elastic/elasticsearch'; -import { ListItemSchema } from '../../plugins/lists/common/schemas'; +import { + ListItemSchema, + ExceptionListSchema, + ExceptionListItemSchema, +} from '../../plugins/lists/common/schemas'; import { ListSchema } from '../../plugins/lists/common'; import { LIST_INDEX } from '../../plugins/lists/common/constants'; @@ -83,6 +88,30 @@ export const removeListItemServerGeneratedProperties = ( return removedProperties; }; +/** + * This will remove server generated properties such as date times, etc... + * @param list List to pass in to remove typical server generated properties + */ +export const removeExceptionListItemServerGeneratedProperties = ( + list: Partial +): Partial => { + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + const { created_at, updated_at, id, tie_breaker_id, _version, ...removedProperties } = list; + return removedProperties; +}; + +/** + * This will remove server generated properties such as date times, etc... + * @param list List to pass in to remove typical server generated properties + */ +export const removeExceptionListServerGeneratedProperties = ( + list: Partial +): Partial => { + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + const { created_at, updated_at, id, tie_breaker_id, _version, ...removedProperties } = list; + return removedProperties; +}; + // Similar to ReactJs's waitFor from here: https://testing-library.com/docs/dom-testing-library/api-async#waitfor export const waitFor = async ( functionToTest: () => Promise, @@ -124,3 +153,32 @@ export const binaryToString = (res: any, callback: any): void => { callback(null, Buffer.from(res.data)); }); }; + +/** + * Remove all exceptions from the .kibana index + * This will retry 20 times before giving up and hopefully still not interfere with other tests + * @param es The ElasticSearch handle + */ +export const deleteAllExceptions = async (es: Client, retryCount = 20): Promise => { + if (retryCount > 0) { + try { + await es.deleteByQuery({ + index: '.kibana', + q: 'type:exception-list or type:exception-list-agnostic', + wait_for_completion: true, + refresh: true, + body: {}, + }); + } catch (err) { + // eslint-disable-next-line no-console + console.log( + `Failure trying to deleteAllExceptions, retries left are: ${retryCount - 1}`, + err + ); + await deleteAllExceptions(es, retryCount - 1); + } + } else { + // eslint-disable-next-line no-console + console.log('Could not deleteAllExceptions, no retries are left'); + } +}; diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js index ea95eb42dd6ff5b..c87a5039360b8f2 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js @@ -28,8 +28,7 @@ export default function ({ getService }) { const testHistoryIndex = '.kibana_task_manager_test_result'; const supertest = supertestAsPromised(url.format(config.get('servers.kibana'))); - // FLAKY: https://github.com/elastic/kibana/issues/71390 - describe.skip('scheduling and running tasks', () => { + describe('scheduling and running tasks', () => { beforeEach( async () => await supertest.delete('/api/sample_tasks').set('kbn-xsrf', 'xxx').expect(200) ); diff --git a/yarn.lock b/yarn.lock index 7731d2f7a8ea19a..42c4b800e6b0c0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25,14 +25,7 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" - integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g== - dependencies: - "@babel/highlight" "^7.8.3" - -"@babel/code-frame@^7.10.4": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== @@ -48,38 +41,7 @@ invariant "^2.2.4" semver "^5.5.0" -"@babel/compat-data@^7.8.6", "@babel/compat-data@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.9.0.tgz#04815556fc90b0c174abd2c0c1bb966faa036a6c" - integrity sha512-zeFQrr+284Ekvd9e7KAX954LkapWiOmQtsfHirhxqfdlX6MEC32iRE+pqUGlYIBchdevaCwvzxWGSy/YBNI85g== - dependencies: - browserslist "^4.9.1" - invariant "^2.2.4" - semver "^5.5.0" - -"@babel/core@^7.0.1", "@babel/core@^7.1.0", "@babel/core@^7.4.3", "@babel/core@^7.7.5": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.9.0.tgz#ac977b538b77e132ff706f3b8a4dbad09c03c56e" - integrity sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w== - dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.9.0" - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helpers" "^7.9.0" - "@babel/parser" "^7.9.0" - "@babel/template" "^7.8.6" - "@babel/traverse" "^7.9.0" - "@babel/types" "^7.9.0" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.1" - json5 "^2.1.2" - lodash "^4.17.13" - resolve "^1.3.2" - semver "^5.4.1" - source-map "^0.5.0" - -"@babel/core@^7.11.1": +"@babel/core@^7.0.1", "@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.4.3", "@babel/core@^7.7.5": version "7.11.1" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.1.tgz#2c55b604e73a40dc21b0e52650b11c65cf276643" integrity sha512-XqF7F6FWQdKGGWAzGELL+aCO1p+lRY5Tj5/tbT3St1G8NaH70jhhDIKknIZaDans0OQBG5wRAldROLHSt44BgQ== @@ -101,17 +63,7 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.0.0", "@babel/generator@^7.9.0": - version "7.9.4" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.4.tgz#12441e90c3b3c4159cdecf312075bf1a8ce2dbce" - integrity sha512-rjP8ahaDy/ouhrvCoU1E5mqaitWrxwuNGU+dy1EpaoK48jZay4MdkskKGIMHLZNewg8sAsqpGSREJwP0zH3YQA== - dependencies: - "@babel/types" "^7.9.0" - jsesc "^2.5.1" - lodash "^4.17.13" - source-map "^0.5.0" - -"@babel/generator@^7.11.0": +"@babel/generator@^7.0.0", "@babel/generator@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.0.tgz#4b90c78d8c12825024568cbe83ee6c9af193585c" integrity sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ== @@ -120,24 +72,7 @@ jsesc "^2.5.1" source-map "^0.5.0" -"@babel/generator@^7.9.5": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.5.tgz#27f0917741acc41e6eaaced6d68f96c3fa9afaf9" - integrity sha512-GbNIxVB3ZJe3tLeDm1HSn2AhuD/mVcyLDpgtLXa5tplmWrJdF/elxB56XNqCuD6szyNkDi6wuoKXln3QeBmCHQ== - dependencies: - "@babel/types" "^7.9.5" - jsesc "^2.5.1" - lodash "^4.17.13" - source-map "^0.5.0" - -"@babel/helper-annotate-as-pure@^7.0.0", "@babel/helper-annotate-as-pure@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz#60bc0bc657f63a0924ff9a4b4a0b24a13cf4deee" - integrity sha512-6o+mJrZBxOoEX77Ezv9zwW7WV8DdluouRKNY/IR5u/YTMuKHgugHOzYWlYvYLpLA9nPsQCAAASpCIbjI9Mv+Uw== - dependencies: - "@babel/types" "^7.8.3" - -"@babel/helper-annotate-as-pure@^7.10.4": +"@babel/helper-annotate-as-pure@^7.0.0", "@babel/helper-annotate-as-pure@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3" integrity sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA== @@ -152,14 +87,6 @@ "@babel/helper-explode-assignable-expression" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-builder-binary-assignment-operator-visitor@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.8.3.tgz#c84097a427a061ac56a1c30ebf54b7b22d241503" - integrity sha512-5eFOm2SyFPK4Rh3XMMRDjN7lBH0orh3ss0g3rTYZnBQ+r6YPj7lgDyCvPphynHvUrobJmeMignBr6Acw9mAPlw== - dependencies: - "@babel/helper-explode-assignable-expression" "^7.8.3" - "@babel/types" "^7.8.3" - "@babel/helper-builder-react-jsx-experimental@^7.10.4": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.10.5.tgz#f35e956a19955ff08c1258e44a515a6d6248646b" @@ -169,15 +96,6 @@ "@babel/helper-module-imports" "^7.10.4" "@babel/types" "^7.10.5" -"@babel/helper-builder-react-jsx-experimental@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.9.0.tgz#066d80262ade488f9c1b1823ce5db88a4cedaa43" - integrity sha512-3xJEiyuYU4Q/Ar9BsHisgdxZsRlsShMe90URZ0e6przL26CCs8NJbDoxH94kKT17PcxlMhsCAwZd90evCo26VQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.8.3" - "@babel/helper-module-imports" "^7.8.3" - "@babel/types" "^7.9.0" - "@babel/helper-builder-react-jsx@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.10.4.tgz#8095cddbff858e6fa9c326daee54a2f2732c1d5d" @@ -186,14 +104,6 @@ "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-builder-react-jsx@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.9.0.tgz#16bf391990b57732700a3278d4d9a81231ea8d32" - integrity sha512-weiIo4gaoGgnhff54GQ3P5wsUQmnSwpkvU0r6ZHq6TzoSzKy4JxHEgnxNytaKbov2a9z/CVNyzliuCOUPEX3Jw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.8.3" - "@babel/types" "^7.9.0" - "@babel/helper-compilation-targets@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz#804ae8e3f04376607cc791b9d47d540276332bd2" @@ -205,17 +115,6 @@ levenary "^1.1.1" semver "^5.5.0" -"@babel/helper-compilation-targets@^7.8.7": - version "7.8.7" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.8.7.tgz#dac1eea159c0e4bd46e309b5a1b04a66b53c1dde" - integrity sha512-4mWm8DCK2LugIS+p1yArqvG1Pf162upsIsjE7cNBjez+NjliQpVhj20obE520nao0o14DaTnFJv+Fw5a0JpoUw== - dependencies: - "@babel/compat-data" "^7.8.6" - browserslist "^4.9.1" - invariant "^2.2.4" - levenary "^1.1.1" - semver "^5.5.0" - "@babel/helper-create-class-features-plugin@^7.10.4", "@babel/helper-create-class-features-plugin@^7.10.5": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz#9f61446ba80e8240b0a5c85c6fdac8459d6f259d" @@ -228,18 +127,6 @@ "@babel/helper-replace-supers" "^7.10.4" "@babel/helper-split-export-declaration" "^7.10.4" -"@babel/helper-create-class-features-plugin@^7.8.3": - version "7.8.6" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.8.6.tgz#243a5b46e2f8f0f674dc1387631eb6b28b851de0" - integrity sha512-klTBDdsr+VFFqaDHm5rR69OpEQtO2Qv8ECxHS1mNhJJvaHArR6a1xTf5K/eZW7eZpJbhCx3NW1Yt/sKsLXLblg== - dependencies: - "@babel/helper-function-name" "^7.8.3" - "@babel/helper-member-expression-to-functions" "^7.8.3" - "@babel/helper-optimise-call-expression" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-replace-supers" "^7.8.6" - "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/helper-create-regexp-features-plugin@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz#fdd60d88524659a0b6959c0579925e425714f3b8" @@ -249,15 +136,6 @@ "@babel/helper-regex" "^7.10.4" regexpu-core "^4.7.0" -"@babel/helper-create-regexp-features-plugin@^7.8.3", "@babel/helper-create-regexp-features-plugin@^7.8.8": - version "7.8.8" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.8.tgz#5d84180b588f560b7864efaeea89243e58312087" - integrity sha512-LYVPdwkrQEiX9+1R29Ld/wTrmQu1SSKYnuOk3g0CkcZMA1p0gsNxJFj/3gBdaJ7Cg0Fnek5z0DsMULePP7Lrqg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.8.3" - "@babel/helper-regex" "^7.8.3" - regexpu-core "^4.7.0" - "@babel/helper-define-map@^7.10.4": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz#b53c10db78a640800152692b13393147acb9bb30" @@ -267,15 +145,6 @@ "@babel/types" "^7.10.5" lodash "^4.17.19" -"@babel/helper-define-map@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.8.3.tgz#a0655cad5451c3760b726eba875f1cd8faa02c15" - integrity sha512-PoeBYtxoZGtct3md6xZOCWPcKuMuk3IHhgxsRRNtnNShebf4C8YonTSblsK4tvDbm+eJAw2HAPOfCr+Q/YRG/g== - dependencies: - "@babel/helper-function-name" "^7.8.3" - "@babel/types" "^7.8.3" - lodash "^4.17.13" - "@babel/helper-explode-assignable-expression@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.10.4.tgz#40a1cd917bff1288f699a94a75b37a1a2dbd8c7c" @@ -284,14 +153,6 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-explode-assignable-expression@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.8.3.tgz#a728dc5b4e89e30fc2dfc7d04fa28a930653f982" - integrity sha512-N+8eW86/Kj147bO9G2uclsg5pwfs/fqqY5rwgIL7eTBklgXjcOJ3btzS5iM6AitJcftnY7pm2lGsrJVYLGjzIw== - dependencies: - "@babel/traverse" "^7.8.3" - "@babel/types" "^7.8.3" - "@babel/helper-function-name@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" @@ -301,24 +162,6 @@ "@babel/template" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-function-name@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz#eeeb665a01b1f11068e9fb86ad56a1cb1a824cca" - integrity sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA== - dependencies: - "@babel/helper-get-function-arity" "^7.8.3" - "@babel/template" "^7.8.3" - "@babel/types" "^7.8.3" - -"@babel/helper-function-name@^7.9.5": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz#2b53820d35275120e1874a82e5aabe1376920a5c" - integrity sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw== - dependencies: - "@babel/helper-get-function-arity" "^7.8.3" - "@babel/template" "^7.8.3" - "@babel/types" "^7.9.5" - "@babel/helper-get-function-arity@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" @@ -326,13 +169,6 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-get-function-arity@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5" - integrity sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA== - dependencies: - "@babel/types" "^7.8.3" - "@babel/helper-hoist-variables@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz#d49b001d1d5a68ca5e6604dda01a6297f7c9381e" @@ -340,13 +176,6 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-hoist-variables@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.8.3.tgz#1dbe9b6b55d78c9b4183fc8cdc6e30ceb83b7134" - integrity sha512-ky1JLOjcDUtSc+xkt0xhYff7Z6ILTAHKmZLHPxAhOP0Nd77O+3nCsd6uSVYur6nJnCI029CrNbYlc0LoPfAPQg== - dependencies: - "@babel/types" "^7.8.3" - "@babel/helper-member-expression-to-functions@^7.10.4", "@babel/helper-member-expression-to-functions@^7.10.5": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz#ae69c83d84ee82f4b42f96e2a09410935a8f26df" @@ -354,21 +183,7 @@ dependencies: "@babel/types" "^7.11.0" -"@babel/helper-member-expression-to-functions@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz#659b710498ea6c1d9907e0c73f206eee7dadc24c" - integrity sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA== - dependencies: - "@babel/types" "^7.8.3" - -"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz#7fe39589b39c016331b6b8c3f441e8f0b1419498" - integrity sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg== - dependencies: - "@babel/types" "^7.8.3" - -"@babel/helper-module-imports@^7.10.4": +"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620" integrity sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw== @@ -388,19 +203,6 @@ "@babel/types" "^7.11.0" lodash "^4.17.19" -"@babel/helper-module-transforms@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz#43b34dfe15961918707d247327431388e9fe96e5" - integrity sha512-0FvKyu0gpPfIQ8EkxlrAydOWROdHpBmiCiRwLkUiBGhCUPRRbVD2/tm3sFr/c/GWFrQ/ffutGUAnx7V0FzT2wA== - dependencies: - "@babel/helper-module-imports" "^7.8.3" - "@babel/helper-replace-supers" "^7.8.6" - "@babel/helper-simple-access" "^7.8.3" - "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/template" "^7.8.6" - "@babel/types" "^7.9.0" - lodash "^4.17.13" - "@babel/helper-optimise-call-expression@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" @@ -408,19 +210,7 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-optimise-call-expression@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz#7ed071813d09c75298ef4f208956006b6111ecb9" - integrity sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ== - dependencies: - "@babel/types" "^7.8.3" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670" - integrity sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ== - -"@babel/helper-plugin-utils@^7.10.4": +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== @@ -432,13 +222,6 @@ dependencies: lodash "^4.17.19" -"@babel/helper-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.8.3.tgz#139772607d51b93f23effe72105b319d2a4c6965" - integrity sha512-BWt0QtYv/cg/NecOAZMdcn/waj/5P26DR4mVLXfFtDokSR6fyuG0Pj+e2FqtSME+MqED1khnSMulkmGl8qWiUQ== - dependencies: - lodash "^4.17.13" - "@babel/helper-remap-async-to-generator@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.10.4.tgz#fce8bea4e9690bbe923056ded21e54b4e8b68ed5" @@ -450,17 +233,6 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-remap-async-to-generator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.8.3.tgz#273c600d8b9bf5006142c1e35887d555c12edd86" - integrity sha512-kgwDmw4fCg7AVgS4DukQR/roGp+jP+XluJE5hsRZwxCYGg+Rv9wSGErDWhlI90FODdYfd4xG4AQRiMDjjN0GzA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.8.3" - "@babel/helper-wrap-function" "^7.8.3" - "@babel/template" "^7.8.3" - "@babel/traverse" "^7.8.3" - "@babel/types" "^7.8.3" - "@babel/helper-replace-supers@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf" @@ -471,16 +243,6 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-replace-supers@^7.8.3", "@babel/helper-replace-supers@^7.8.6": - version "7.8.6" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz#5ada744fd5ad73203bf1d67459a27dcba67effc8" - integrity sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.8.3" - "@babel/helper-optimise-call-expression" "^7.8.3" - "@babel/traverse" "^7.8.6" - "@babel/types" "^7.8.6" - "@babel/helper-simple-access@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461" @@ -489,14 +251,6 @@ "@babel/template" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-simple-access@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz#7f8109928b4dab4654076986af575231deb639ae" - integrity sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw== - dependencies: - "@babel/template" "^7.8.3" - "@babel/types" "^7.8.3" - "@babel/helper-skip-transparent-expression-wrappers@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz#eec162f112c2f58d3af0af125e3bb57665146729" @@ -511,28 +265,11 @@ dependencies: "@babel/types" "^7.11.0" -"@babel/helper-split-export-declaration@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz#31a9f30070f91368a7182cf05f831781065fc7a9" - integrity sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA== - dependencies: - "@babel/types" "^7.8.3" - "@babel/helper-validator-identifier@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== -"@babel/helper-validator-identifier@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz#ad53562a7fc29b3b9a91bbf7d10397fd146346ed" - integrity sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw== - -"@babel/helper-validator-identifier@^7.9.5": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz#90977a8e6fbf6b431a7dc31752eee233bf052d80" - integrity sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g== - "@babel/helper-wrap-function@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz#8a6f701eab0ff39f765b5a1cfef409990e624b87" @@ -543,16 +280,6 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-wrap-function@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz#9dbdb2bb55ef14aaa01fe8c99b629bd5352d8610" - integrity sha512-LACJrbUET9cQDzb6kG7EeD7+7doC3JNvUgTEQOx2qaO1fKlzE/Bf05qs9w1oXQMmXlPO65lC3Tq9S6gZpTErEQ== - dependencies: - "@babel/helper-function-name" "^7.8.3" - "@babel/template" "^7.8.3" - "@babel/traverse" "^7.8.3" - "@babel/types" "^7.8.3" - "@babel/helpers@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.4.tgz#2abeb0d721aff7c0a97376b9e1f6f65d7a475044" @@ -562,25 +289,7 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helpers@^7.9.0": - version "7.9.2" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.9.2.tgz#b42a81a811f1e7313b88cba8adc66b3d9ae6c09f" - integrity sha512-JwLvzlXVPjO8eU9c/wF9/zOIN7X6h8DYf7mG4CiFRZRvZNKEF5dQ3H3V+ASkHoIB3mWhatgl5ONhyqHRI6MppA== - dependencies: - "@babel/template" "^7.8.3" - "@babel/traverse" "^7.9.0" - "@babel/types" "^7.9.0" - -"@babel/highlight@^7.0.0", "@babel/highlight@^7.8.3": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.9.0.tgz#4e9b45ccb82b79607271b2979ad82c7b68163079" - integrity sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ== - dependencies: - "@babel/helper-validator-identifier" "^7.9.0" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/highlight@^7.10.4": +"@babel/highlight@^7.0.0", "@babel/highlight@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== @@ -589,15 +298,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.2.0", "@babel/parser@^7.7.5", "@babel/parser@^7.8.6", "@babel/parser@^7.9.0": - version "7.9.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8" - integrity sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA== - -"@babel/parser@^7.10.4", "@babel/parser@^7.11.0", "@babel/parser@^7.11.1", "@babel/parser@^7.11.2": - version "7.11.2" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.2.tgz#0882ab8a455df3065ea2dcb4c753b2460a24bead" - integrity sha512-Vuj/+7vLo6l1Vi7uuO+1ngCDNeVmNbTngcJFKCR/oEtz8tKz0CJxZEGmPt9KcIloZhOZ3Zit6xbpXT2MDlS9Vw== +"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.11.0", "@babel/parser@^7.11.1", "@babel/parser@^7.11.2", "@babel/parser@^7.2.0", "@babel/parser@^7.7.5": + version "7.11.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.3.tgz#9e1eae46738bcd08e23e867bab43e7b95299a8f9" + integrity sha512-REo8xv7+sDxkKvoxEywIdsNFiZLybwdI7hcT5uEPyQrSMB4YQ973BfC9OOrD/81MaIjh6UxdulIQXkjmiH3PcA== "@babel/plugin-proposal-async-generator-functions@^7.10.4": version "7.10.5" @@ -608,16 +312,7 @@ "@babel/helper-remap-async-to-generator" "^7.10.4" "@babel/plugin-syntax-async-generators" "^7.8.0" -"@babel/plugin-proposal-async-generator-functions@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.8.3.tgz#bad329c670b382589721b27540c7d288601c6e6f" - integrity sha512-NZ9zLv848JsV3hs8ryEh7Uaz/0KsmPLqv0+PdkDJL1cJy0K4kOCFa8zc1E3mp+RHPQcpdfb/6GovEsW4VDrOMw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-remap-async-to-generator" "^7.8.3" - "@babel/plugin-syntax-async-generators" "^7.8.0" - -"@babel/plugin-proposal-class-properties@^7.10.4": +"@babel/plugin-proposal-class-properties@^7.10.4", "@babel/plugin-proposal-class-properties@^7.7.0": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz#a33bf632da390a59c7a8c570045d1115cd778807" integrity sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg== @@ -625,14 +320,6 @@ "@babel/helper-create-class-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-proposal-class-properties@^7.7.0": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.8.3.tgz#5e06654af5cd04b608915aada9b2a6788004464e" - integrity sha512-EqFhbo7IosdgPgZggHaNObkmO1kNUe3slaKu54d5OWvy+p9QIKOzK1GAEpAIsZtWVtPXUHSMcT4smvDrCfY4AA== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-proposal-dynamic-import@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz#ba57a26cb98b37741e9d5bca1b8b0ddf8291f17e" @@ -641,14 +328,6 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-dynamic-import" "^7.8.0" -"@babel/plugin-proposal-dynamic-import@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.8.3.tgz#38c4fe555744826e97e2ae930b0fb4cc07e66054" - integrity sha512-NyaBbyLFXFLT9FP+zk0kYlUlA8XtCUbehs67F0nnEg7KICgMc2mNkIeu9TYhKzyXMkrapZFwAhXLdnt4IYHy1w== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-dynamic-import" "^7.8.0" - "@babel/plugin-proposal-export-namespace-from@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.4.tgz#570d883b91031637b3e2958eea3c438e62c05f54" @@ -665,14 +344,6 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-json-strings" "^7.8.0" -"@babel/plugin-proposal-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.8.3.tgz#da5216b238a98b58a1e05d6852104b10f9a70d6b" - integrity sha512-KGhQNZ3TVCQG/MjRbAUwuH+14y9q0tpxs1nWWs3pbSleRdDro9SAMMDyye8HhY1gqZ7/NqIc8SKhya0wRDgP1Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-json-strings" "^7.8.0" - "@babel/plugin-proposal-logical-assignment-operators@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.11.0.tgz#9f80e482c03083c87125dee10026b58527ea20c8" @@ -689,14 +360,6 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" -"@babel/plugin-proposal-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.8.3.tgz#e4572253fdeed65cddeecfdab3f928afeb2fd5d2" - integrity sha512-TS9MlfzXpXKt6YYomudb/KU7nQI6/xnapG6in1uZxoxDghuSMZsPb6D2fyUwNYSAp4l1iR7QtFOjkqcRYcUsfw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" - "@babel/plugin-proposal-numeric-separator@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz#ce1590ff0a65ad12970a609d78855e9a4c1aef06" @@ -705,15 +368,7 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-numeric-separator" "^7.10.4" -"@babel/plugin-proposal-numeric-separator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.8.3.tgz#5d6769409699ec9b3b68684cd8116cedff93bad8" - integrity sha512-jWioO1s6R/R+wEHizfaScNsAx+xKgwTLNXSh7tTC4Usj3ItsPEhYkEpU4h+lpnBwq7NBVOJXfO6cRFYcX69JUQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.8.3" - -"@babel/plugin-proposal-object-rest-spread@^7.11.0": +"@babel/plugin-proposal-object-rest-spread@^7.11.0", "@babel/plugin-proposal-object-rest-spread@^7.6.2": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz#bd81f95a1f746760ea43b6c2d3d62b11790ad0af" integrity sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA== @@ -722,14 +377,6 @@ "@babel/plugin-syntax-object-rest-spread" "^7.8.0" "@babel/plugin-transform-parameters" "^7.10.4" -"@babel/plugin-proposal-object-rest-spread@^7.6.2", "@babel/plugin-proposal-object-rest-spread@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.9.0.tgz#a28993699fc13df165995362693962ba6b061d6f" - integrity sha512-UgqBv6bjq4fDb8uku9f+wcm1J7YxJ5nT7WO/jBr0cl0PLKb7t1O6RNR1kZbjgx2LQtsDI9hwoQVmn0yhXeQyow== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-object-rest-spread" "^7.8.0" - "@babel/plugin-proposal-optional-catch-binding@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz#31c938309d24a78a49d68fdabffaa863758554dd" @@ -738,14 +385,6 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" -"@babel/plugin-proposal-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.8.3.tgz#9dee96ab1650eed88646ae9734ca167ac4a9c5c9" - integrity sha512-0gkX7J7E+AtAw9fcwlVQj8peP61qhdg/89D5swOkjYbkboA2CVckn3kiyum1DE0wskGb7KJJxBdyEBApDLLVdw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" - "@babel/plugin-proposal-optional-chaining@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz#de5866d0646f6afdaab8a566382fe3a221755076" @@ -755,14 +394,6 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" "@babel/plugin-syntax-optional-chaining" "^7.8.0" -"@babel/plugin-proposal-optional-chaining@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.9.0.tgz#31db16b154c39d6b8a645292472b98394c292a58" - integrity sha512-NDn5tu3tcv4W30jNhmc2hyD5c56G6cXx4TesJubhxrJeCvuuMpttxr0OnNCqbZGhFjLrg+NIhxxC+BK5F6yS3w== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.0" - "@babel/plugin-proposal-private-methods@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.4.tgz#b160d972b8fdba5c7d111a145fc8c421fc2a6909" @@ -771,7 +402,7 @@ "@babel/helper-create-class-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-proposal-unicode-property-regex@^7.10.4": +"@babel/plugin-proposal-unicode-property-regex@^7.10.4", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz#4483cda53041ce3413b7fe2f00022665ddfaa75d" integrity sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA== @@ -779,14 +410,6 @@ "@babel/helper-create-regexp-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-proposal-unicode-property-regex@^7.4.4", "@babel/plugin-proposal-unicode-property-regex@^7.8.3": - version "7.8.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.8.tgz#ee3a95e90cdc04fe8cd92ec3279fa017d68a0d1d" - integrity sha512-EVhjVsMpbhLw9ZfHWSx2iy13Q8Z/eg8e8ccVWt23sWQK5l1UdkoLJPN5w69UA4uITGBnEZD2JOe4QOHycYKv8A== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.8.8" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-async-generators@^7.8.0", "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" @@ -801,20 +424,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-class-properties@^7.10.4": +"@babel/plugin-syntax-class-properties@^7.10.4", "@babel/plugin-syntax-class-properties@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz#6644e6a0baa55a61f9e3231f6c9eeb6ee46c124c" integrity sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-class-properties@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.8.3.tgz#6cb933a8872c8d359bfde69bbeaae5162fd1e8f7" - integrity sha512-UcAyQWg2bAN647Q+O811tG9MrJ38Z10jjhQdKNAL8fsyPzE3cCN/uT+f55cFVY4aGO4jqJAvmqsuY3GQDwAoXg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-dynamic-import@^7.2.0", "@babel/plugin-syntax-dynamic-import@^7.8.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" @@ -829,12 +445,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-syntax-flow@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.8.3.tgz#f2c883bd61a6316f2c89380ae5122f923ba4527f" - integrity sha512-innAx3bUbA0KSYj2E2MNFSn9hiCeowOFLxlsuhXzw8hMQnzkDomUr9QCD7E9VF60NmnG1sNTuuv6Qf4f8INYsg== +"@babel/plugin-syntax-flow@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.10.4.tgz#53351dd7ae01995e567d04ce42af1a6e0ba846a6" + integrity sha512-yxQsX1dJixF4qEEdzVbst3SZQ58Nrooz8NV9Z9GL4byTE25BvJgl5lf0RECUf0fh28rZBb/RYTWn/eeKwCMrZQ== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-json-strings@^7.8.0", "@babel/plugin-syntax-json-strings@^7.8.3": version "7.8.3" @@ -850,27 +466,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-jsx@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.8.3.tgz#521b06c83c40480f1e58b4fd33b92eceb1d6ea94" - integrity sha512-WxdW9xyLgBdefoo0Ynn3MRSkhe5tFVxxKNVdnZSh318WrG2e2jH+E9wd/++JsqcLJZPfz87njQJ8j2Upjm0M0A== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-logical-assignment-operators@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.8.3.tgz#3995d7d7ffff432f6ddc742b47e730c054599897" - integrity sha512-Zpg2Sgc++37kuFl6ppq2Q7Awc6E6AIW671x5PY8E/f7MCIyPPGK/EoeZXvvY3P42exZ3Q4/t3YOzP/HiN79jDg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0", "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" @@ -878,20 +480,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-numeric-separator@^7.10.4": +"@babel/plugin-syntax-numeric-separator@^7.10.4", "@babel/plugin-syntax-numeric-separator@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-numeric-separator@^7.8.0", "@babel/plugin-syntax-numeric-separator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.8.3.tgz#0e3fb63e09bea1b11e96467271c8308007e7c41f" - integrity sha512-H7dCMAdN83PcCmqmkHB5dtp+Xa9a6LKSvA2hiFBC/5alSHxM5VgWZXFqDi0YFe8XNGT6iCa+z4V4zSt/PdZ7Dw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-object-rest-spread@^7.8.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" @@ -920,13 +515,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-top-level-await@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.8.3.tgz#3acdece695e6b13aaf57fc291d1a800950c71391" - integrity sha512-kwj1j9lL/6Wd0hROD3b/OZZ7MSrZLqqn9RAZ5+cYYsflQ9HZBIKCUkr3+uL1MEJ1NePiUbf98jjiMQSv0NMR9g== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-typescript@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.10.4.tgz#2f55e770d3501e83af217d782cb7517d7bb34d25" @@ -941,13 +529,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-arrow-functions@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.8.3.tgz#82776c2ed0cd9e1a49956daeb896024c9473b8b6" - integrity sha512-0MRF+KC8EqH4dbuITCWwPSzsyO3HIWWlm30v8BbbpOrS1B++isGxPnnuq/IZvOX5J2D/p7DQalQm+/2PnlKGxg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-async-to-generator@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz#41a5017e49eb6f3cda9392a51eef29405b245a37" @@ -957,15 +538,6 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-remap-async-to-generator" "^7.10.4" -"@babel/plugin-transform-async-to-generator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.8.3.tgz#4308fad0d9409d71eafb9b1a6ee35f9d64b64086" - integrity sha512-imt9tFLD9ogt56Dd5CI/6XgpukMwd/fLGSrix2httihVe7LOGVPhyhMh1BU5kDM7iHD08i8uUtmV2sWaBFlHVQ== - dependencies: - "@babel/helper-module-imports" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-remap-async-to-generator" "^7.8.3" - "@babel/plugin-transform-block-scoped-functions@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz#1afa595744f75e43a91af73b0d998ecfe4ebc2e8" @@ -973,13 +545,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-block-scoped-functions@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.8.3.tgz#437eec5b799b5852072084b3ae5ef66e8349e8a3" - integrity sha512-vo4F2OewqjbB1+yaJ7k2EJFHlTP3jR634Z9Cj9itpqNjuLXvhlVxgnjsHsdRgASR8xYDrx6onw4vW5H6We0Jmg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-block-scoping@^7.10.4": version "7.11.1" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.11.1.tgz#5b7efe98852bef8d652c0b28144cd93a9e4b5215" @@ -987,14 +552,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-block-scoping@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.8.3.tgz#97d35dab66857a437c166358b91d09050c868f3a" - integrity sha512-pGnYfm7RNRgYRi7bids5bHluENHqJhrV4bCZRwc5GamaWIIs07N4rZECcmJL6ZClwjDz1GbdMZFtPs27hTB06w== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - lodash "^4.17.13" - "@babel/plugin-transform-classes@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz#405136af2b3e218bc4a1926228bc917ab1a0adc7" @@ -1009,20 +566,6 @@ "@babel/helper-split-export-declaration" "^7.10.4" globals "^11.1.0" -"@babel/plugin-transform-classes@^7.9.0": - version "7.9.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.9.2.tgz#8603fc3cc449e31fdbdbc257f67717536a11af8d" - integrity sha512-TC2p3bPzsfvSsqBZo0kJnuelnoK9O3welkUpqSqBQuBF6R5MN2rysopri8kNvtlGIb2jmUO7i15IooAZJjZuMQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.8.3" - "@babel/helper-define-map" "^7.8.3" - "@babel/helper-function-name" "^7.8.3" - "@babel/helper-optimise-call-expression" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-replace-supers" "^7.8.6" - "@babel/helper-split-export-declaration" "^7.8.3" - globals "^11.1.0" - "@babel/plugin-transform-computed-properties@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz#9ded83a816e82ded28d52d4b4ecbdd810cdfc0eb" @@ -1030,13 +573,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-computed-properties@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.8.3.tgz#96d0d28b7f7ce4eb5b120bb2e0e943343c86f81b" - integrity sha512-O5hiIpSyOGdrQZRQ2ccwtTVkgUDBBiCuK//4RJ6UfePllUTCENOzKxfh6ulckXKc0DixTFLCfb2HVkNA7aDpzA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-destructuring@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz#70ddd2b3d1bea83d01509e9bb25ddb3a74fc85e5" @@ -1044,14 +580,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-destructuring@^7.8.3": - version "7.8.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.8.8.tgz#fadb2bc8e90ccaf5658de6f8d4d22ff6272a2f4b" - integrity sha512-eRJu4Vs2rmttFCdhPUM3bV0Yo/xPSdPw6ML9KHs/bjB4bLA5HXlbvYXPOD5yASodGod+krjYx21xm1QmL8dCJQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-transform-dotall-regex@^7.10.4": +"@babel/plugin-transform-dotall-regex@^7.10.4", "@babel/plugin-transform-dotall-regex@^7.4.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz#469c2062105c1eb6a040eaf4fac4b488078395ee" integrity sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA== @@ -1059,14 +588,6 @@ "@babel/helper-create-regexp-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-dotall-regex@^7.4.4", "@babel/plugin-transform-dotall-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.8.3.tgz#c3c6ec5ee6125c6993c5cbca20dc8621a9ea7a6e" - integrity sha512-kLs1j9Nn4MQoBYdRXH6AeaXMbEJFaFu/v1nQkvib6QzTj8MZI5OQzqmD83/2jEM1z0DLilra5aWO5YpyC0ALIw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-duplicate-keys@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz#697e50c9fee14380fe843d1f306b295617431e47" @@ -1074,13 +595,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-duplicate-keys@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.8.3.tgz#8d12df309aa537f272899c565ea1768e286e21f1" - integrity sha512-s8dHiBUbcbSgipS4SMFuWGqCvyge5V2ZeAWzR6INTVC3Ltjig/Vw1G2Gztv0vU/hRG9X8IvKvYdoksnUfgXOEQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-exponentiation-operator@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz#5ae338c57f8cf4001bdb35607ae66b92d665af2e" @@ -1089,21 +603,13 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-exponentiation-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.8.3.tgz#581a6d7f56970e06bf51560cd64f5e947b70d7b7" - integrity sha512-zwIpuIymb3ACcInbksHaNcR12S++0MDLKkiqXHl3AzpgdKlFNhog+z/K0+TGW+b0w5pgTq4H6IwV/WhxbGYSjQ== - dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-transform-flow-strip-types@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.9.0.tgz#8a3538aa40434e000b8f44a3c5c9ac7229bd2392" - integrity sha512-7Qfg0lKQhEHs93FChxVLAvhBshOPQDtJUTVHr/ZwQNRccCm4O9D79r9tVSoV8iNwjP1YgfD+e/fgHcPkN1qEQg== +"@babel/plugin-transform-flow-strip-types@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.10.4.tgz#c497957f09e86e3df7296271e9eb642876bf7788" + integrity sha512-XTadyuqNst88UWBTdLjM+wEY7BFnY2sYtPyAidfC7M/QaZnSuIZpMvLxqGT7phAcnGyWh/XQFLKcGf04CnvxSQ== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-flow" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-flow" "^7.10.4" "@babel/plugin-transform-for-of@^7.10.4": version "7.10.4" @@ -1112,13 +618,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-for-of@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.9.0.tgz#0f260e27d3e29cd1bb3128da5e76c761aa6c108e" - integrity sha512-lTAnWOpMwOXpyDx06N+ywmF3jNbafZEqZ96CGYabxHrxNX8l5ny7dt4bK/rGwAh9utyP2b2Hv7PlZh1AAS54FQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-function-name@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz#6a467880e0fc9638514ba369111811ddbe2644b7" @@ -1127,14 +626,6 @@ "@babel/helper-function-name" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-function-name@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.8.3.tgz#279373cb27322aaad67c2683e776dfc47196ed8b" - integrity sha512-rO/OnDS78Eifbjn5Py9v8y0aR+aSYhDhqAwVfsTl0ERuMZyr05L1aFSCJnbv2mmsLkit/4ReeQ9N2BgLnOcPCQ== - dependencies: - "@babel/helper-function-name" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-literals@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz#9f42ba0841100a135f22712d0e391c462f571f3c" @@ -1142,13 +633,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-literals@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.8.3.tgz#aef239823d91994ec7b68e55193525d76dbd5dc1" - integrity sha512-3Tqf8JJ/qB7TeldGl+TT55+uQei9JfYaregDcEAyBZ7akutriFrt6C/wLYIer6OYhleVQvH/ntEhjE/xMmy10A== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-member-expression-literals@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz#b1ec44fcf195afcb8db2c62cd8e551c881baf8b7" @@ -1156,13 +640,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-member-expression-literals@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.8.3.tgz#963fed4b620ac7cbf6029c755424029fa3a40410" - integrity sha512-3Wk2EXhnw+rP+IDkK6BdtPKsUE5IeZ6QOGrPYvw52NwBStw9V1ZVzxgK6fSKSxqUvH9eQPR3tm3cOq79HlsKYA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-modules-amd@^7.10.4": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz#1b9cddaf05d9e88b3aad339cb3e445c4f020a9b1" @@ -1172,15 +649,6 @@ "@babel/helper-plugin-utils" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-amd@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.9.0.tgz#19755ee721912cf5bb04c07d50280af3484efef4" - integrity sha512-vZgDDF003B14O8zJy0XXLnPH4sg+9X5hFBBGN1V+B2rgrB+J2xIypSN6Rk9imB2hSTHQi5OHLrFWsZab1GMk+Q== - dependencies: - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helper-plugin-utils" "^7.8.3" - babel-plugin-dynamic-import-node "^2.3.0" - "@babel/plugin-transform-modules-commonjs@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz#66667c3eeda1ebf7896d41f1f16b17105a2fbca0" @@ -1191,16 +659,6 @@ "@babel/helper-simple-access" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.9.0.tgz#e3e72f4cbc9b4a260e30be0ea59bdf5a39748940" - integrity sha512-qzlCrLnKqio4SlgJ6FMMLBe4bySNis8DFn1VkGmOcxG9gqEyPIOzeQrA//u0HAKrWpJlpZbZMPB1n/OPa4+n8g== - dependencies: - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-simple-access" "^7.8.3" - babel-plugin-dynamic-import-node "^2.3.0" - "@babel/plugin-transform-modules-systemjs@^7.10.4": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz#6270099c854066681bae9e05f87e1b9cadbe8c85" @@ -1211,16 +669,6 @@ "@babel/helper-plugin-utils" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-systemjs@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.9.0.tgz#e9fd46a296fc91e009b64e07ddaa86d6f0edeb90" - integrity sha512-FsiAv/nao/ud2ZWy4wFacoLOm5uxl0ExSQ7ErvP7jpoihLR6Cq90ilOFyX9UXct3rbtKsAiZ9kFt5XGfPe/5SQ== - dependencies: - "@babel/helper-hoist-variables" "^7.8.3" - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helper-plugin-utils" "^7.8.3" - babel-plugin-dynamic-import-node "^2.3.0" - "@babel/plugin-transform-modules-umd@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz#9a8481fe81b824654b3a0b65da3df89f3d21839e" @@ -1229,14 +677,6 @@ "@babel/helper-module-transforms" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-modules-umd@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.9.0.tgz#e909acae276fec280f9b821a5f38e1f08b480697" - integrity sha512-uTWkXkIVtg/JGRSIABdBoMsoIeoHQHPTL0Y2E7xf5Oj7sLqwVsNXOkNk0VJc7vF0IMBsPeikHxFjGe+qmwPtTQ== - dependencies: - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-named-capturing-groups-regex@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz#78b4d978810b6f3bcf03f9e318f2fc0ed41aecb6" @@ -1244,13 +684,6 @@ dependencies: "@babel/helper-create-regexp-features-plugin" "^7.10.4" -"@babel/plugin-transform-named-capturing-groups-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.8.3.tgz#a2a72bffa202ac0e2d0506afd0939c5ecbc48c6c" - integrity sha512-f+tF/8UVPU86TrCb06JoPWIdDpTNSGGcAtaD9mLP0aYGA0OS0j7j7DHJR0GTFrUZPUU6loZhbsVZgTh0N+Qdnw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.8.3" - "@babel/plugin-transform-new-target@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz#9097d753cb7b024cb7381a3b2e52e9513a9c6888" @@ -1258,13 +691,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-new-target@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.8.3.tgz#60cc2ae66d85c95ab540eb34babb6434d4c70c43" - integrity sha512-QuSGysibQpyxexRyui2vca+Cmbljo8bcRckgzYV4kRIsHpVeyeC3JDO63pY+xFZ6bWOBn7pfKZTqV4o/ix9sFw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-object-super@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz#d7146c4d139433e7a6526f888c667e314a093894" @@ -1273,14 +699,6 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-replace-supers" "^7.10.4" -"@babel/plugin-transform-object-super@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.8.3.tgz#ebb6a1e7a86ffa96858bd6ac0102d65944261725" - integrity sha512-57FXk+gItG/GejofIyLIgBKTas4+pEU47IXKDBWFTxdPd7F80H8zybyAY7UoblVfBhBGs2EKM+bJUu2+iUYPDQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-replace-supers" "^7.8.3" - "@babel/plugin-transform-parameters@^7.10.4": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz#59d339d58d0b1950435f4043e74e2510005e2c4a" @@ -1289,14 +707,6 @@ "@babel/helper-get-function-arity" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-parameters@^7.8.7": - version "7.9.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.9.3.tgz#3028d0cc20ddc733166c6e9c8534559cee09f54a" - integrity sha512-fzrQFQhp7mIhOzmOtPiKffvCYQSK10NR8t6BBz2yPbeUHb9OLW8RZGtgDRBn8z2hGcwvKDL3vC7ojPTLNxmqEg== - dependencies: - "@babel/helper-get-function-arity" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-property-literals@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.4.tgz#f6fe54b6590352298785b83edd815d214c42e3c0" @@ -1304,19 +714,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-property-literals@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.8.3.tgz#33194300d8539c1ed28c62ad5087ba3807b98263" - integrity sha512-uGiiXAZMqEoQhRWMK17VospMZh5sXWg+dlh2soffpkAl96KAm+WZuJfa6lcELotSRmooLqg0MWdH6UUq85nmmg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-react-constant-elements@^7.0.0", "@babel/plugin-transform-react-constant-elements@^7.2.0", "@babel/plugin-transform-react-constant-elements@^7.6.3": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.9.0.tgz#a75abc936a3819edec42d3386d9f1c93f28d9d9e" - integrity sha512-wXMXsToAUOxJuBBEHajqKLFWcCkOSLshTI2ChCFFj1zDd7od4IOxiwLCOObNUvOpkxLpjIuaIdBMmNt6ocCPAw== + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.10.4.tgz#0f485260bf1c29012bb973e7e404749eaac12c9e" + integrity sha512-cYmQBW1pXrqBte1raMkAulXmi7rjg3VI6ZLg9QIic8Hq7BtYXaWuZSxsr2siOMI6SWwpxjWfnwhTUrd7JlAV7g== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-react-display-name@^7.10.4": version "7.10.4" @@ -1325,13 +728,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-react-display-name@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.8.3.tgz#70ded987c91609f78353dd76d2fb2a0bb991e8e5" - integrity sha512-3Jy/PCw8Fe6uBKtEgz3M82ljt+lTg+xJaM4og+eyu83qLT87ZUSckn0wy7r31jflURWLO83TW6Ylf7lyXj3m5A== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-react-jsx-development@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.10.4.tgz#6ec90f244394604623880e15ebc3c34c356258ba" @@ -1341,15 +737,6 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-jsx" "^7.10.4" -"@babel/plugin-transform-react-jsx-development@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.9.0.tgz#3c2a130727caf00c2a293f0aed24520825dbf754" - integrity sha512-tK8hWKrQncVvrhvtOiPpKrQjfNX3DtkNLSX4ObuGcpS9p0QrGetKmlySIGR07y48Zft8WVgPakqd/bk46JrMSw== - dependencies: - "@babel/helper-builder-react-jsx-experimental" "^7.9.0" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-jsx" "^7.8.3" - "@babel/plugin-transform-react-jsx-self@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.10.4.tgz#cd301a5fed8988c182ed0b9d55e9bd6db0bd9369" @@ -1358,14 +745,6 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-jsx" "^7.10.4" -"@babel/plugin-transform-react-jsx-self@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.9.0.tgz#f4f26a325820205239bb915bad8e06fcadabb49b" - integrity sha512-K2ObbWPKT7KUTAoyjCsFilOkEgMvFG+y0FqOl6Lezd0/13kMkkjHskVsZvblRPj1PHA44PrToaZANrryppzTvQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-jsx" "^7.8.3" - "@babel/plugin-transform-react-jsx-source@^7.10.4": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.10.5.tgz#34f1779117520a779c054f2cdd9680435b9222b4" @@ -1374,14 +753,6 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-jsx" "^7.10.4" -"@babel/plugin-transform-react-jsx-source@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.9.0.tgz#89ef93025240dd5d17d3122294a093e5e0183de0" - integrity sha512-K6m3LlSnTSfRkM6FcRk8saNEeaeyG5k7AVkBU2bZK3+1zdkSED3qNdsWrUgQBeTVD2Tp3VMmerxVO2yM5iITmw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-jsx" "^7.8.3" - "@babel/plugin-transform-react-jsx@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.10.4.tgz#673c9f913948764a4421683b2bef2936968fddf2" @@ -1392,16 +763,6 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-jsx" "^7.10.4" -"@babel/plugin-transform-react-jsx@^7.9.4": - version "7.9.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.9.4.tgz#86f576c8540bd06d0e95e0b61ea76d55f6cbd03f" - integrity sha512-Mjqf3pZBNLt854CK0C/kRuXAnE6H/bo7xYojP+WGtX8glDGSibcwnsWwhwoSuRg0+EBnxPC1ouVnuetUIlPSAw== - dependencies: - "@babel/helper-builder-react-jsx" "^7.9.0" - "@babel/helper-builder-react-jsx-experimental" "^7.9.0" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-jsx" "^7.8.3" - "@babel/plugin-transform-react-pure-annotations@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.10.4.tgz#3eefbb73db94afbc075f097523e445354a1c6501" @@ -1417,13 +778,6 @@ dependencies: regenerator-transform "^0.14.2" -"@babel/plugin-transform-regenerator@^7.8.7": - version "7.8.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.8.7.tgz#5e46a0dca2bee1ad8285eb0527e6abc9c37672f8" - integrity sha512-TIg+gAl4Z0a3WmD3mbYSk+J9ZUH6n/Yc57rtKRnlA/7rcCvpekHXe0CMZHP1gYp7/KLe9GHTuIba0vXmls6drA== - dependencies: - regenerator-transform "^0.14.2" - "@babel/plugin-transform-reserved-words@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz#8f2682bcdcef9ed327e1b0861585d7013f8a54dd" @@ -1431,13 +785,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-reserved-words@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.8.3.tgz#9a0635ac4e665d29b162837dd3cc50745dfdf1f5" - integrity sha512-mwMxcycN3omKFDjDQUl+8zyMsBfjRFr0Zn/64I41pmjv4NJuqcYlEtezwYtw9TFd9WR1vN5kiM+O0gMZzO6L0A== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-runtime@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.11.0.tgz#e27f78eb36f19448636e05c33c90fd9ad9b8bccf" @@ -1455,13 +802,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-shorthand-properties@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.8.3.tgz#28545216e023a832d4d3a1185ed492bcfeac08c8" - integrity sha512-I9DI6Odg0JJwxCHzbzW08ggMdCezoWcuQRz3ptdudgwaHxTjxw5HgdFJmZIkIMlRymL6YiZcped4TTCB0JcC8w== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-spread@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.11.0.tgz#fa84d300f5e4f57752fe41a6d1b3c554f13f17cc" @@ -1470,13 +810,6 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" -"@babel/plugin-transform-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.8.3.tgz#9c8ffe8170fdfb88b114ecb920b82fb6e95fe5e8" - integrity sha512-CkuTU9mbmAoFOI1tklFWYYbzX5qCIZVXPVy0jpXgGwkplCndQAa58s2jr66fTeQnA64bDox0HL4U56CFYoyC7g== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-sticky-regex@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz#8f3889ee8657581130a29d9cc91d7c73b7c4a28d" @@ -1485,14 +818,6 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-regex" "^7.10.4" -"@babel/plugin-transform-sticky-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.8.3.tgz#be7a1290f81dae767475452199e1f76d6175b100" - integrity sha512-9Spq0vGCD5Bb4Z/ZXXSK5wbbLFMG085qd2vhL1JYu1WcQ5bXqZBAYRzU1d+p79GcHs2szYv5pVQCX13QgldaWw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-regex" "^7.8.3" - "@babel/plugin-transform-template-literals@^7.10.4": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz#78bc5d626a6642db3312d9d0f001f5e7639fde8c" @@ -1501,14 +826,6 @@ "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-template-literals@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.8.3.tgz#7bfa4732b455ea6a43130adc0ba767ec0e402a80" - integrity sha512-820QBtykIQOLFT8NZOcTRJ1UNuztIELe4p9DCgvj4NK+PwluSJ49we7s9FB1HIGNIYT7wFUJ0ar2QpCDj0escQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-typeof-symbol@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz#9509f1a7eec31c4edbffe137c16cc33ff0bc5bfc" @@ -1516,13 +833,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-typeof-symbol@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.8.4.tgz#ede4062315ce0aaf8a657a920858f1a2f35fc412" - integrity sha512-2QKyfjGdvuNfHsb7qnBBlKclbD4CfshH2KvDabiijLMGXPHJXGxtDzwIF7bQP+T0ysw8fYTtxPafgfs/c1Lrqg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-typescript@^7.10.4": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.11.0.tgz#2b4879676af37342ebb278216dd090ac67f13abb" @@ -1547,81 +857,7 @@ "@babel/helper-create-regexp-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-unicode-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.8.3.tgz#0cef36e3ba73e5c57273effb182f46b91a1ecaad" - integrity sha512-+ufgJjYdmWfSQ+6NS9VGUR2ns8cjJjYbrbi11mZBTaWm+Fui/ncTLFF28Ei1okavY+xkojGr1eJxNsWYeA5aZw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/preset-env@^7.0.0", "@babel/preset-env@^7.4.3", "@babel/preset-env@^7.4.5": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.9.0.tgz#a5fc42480e950ae8f5d9f8f2bbc03f52722df3a8" - integrity sha512-712DeRXT6dyKAM/FMbQTV/FvRCms2hPCx+3weRjZ8iQVQWZejWWk1wwG6ViWMyqb/ouBbGOl5b6aCk0+j1NmsQ== - dependencies: - "@babel/compat-data" "^7.9.0" - "@babel/helper-compilation-targets" "^7.8.7" - "@babel/helper-module-imports" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-proposal-async-generator-functions" "^7.8.3" - "@babel/plugin-proposal-dynamic-import" "^7.8.3" - "@babel/plugin-proposal-json-strings" "^7.8.3" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-proposal-numeric-separator" "^7.8.3" - "@babel/plugin-proposal-object-rest-spread" "^7.9.0" - "@babel/plugin-proposal-optional-catch-binding" "^7.8.3" - "@babel/plugin-proposal-optional-chaining" "^7.9.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.8.3" - "@babel/plugin-syntax-async-generators" "^7.8.0" - "@babel/plugin-syntax-dynamic-import" "^7.8.0" - "@babel/plugin-syntax-json-strings" "^7.8.0" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" - "@babel/plugin-syntax-numeric-separator" "^7.8.0" - "@babel/plugin-syntax-object-rest-spread" "^7.8.0" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" - "@babel/plugin-syntax-optional-chaining" "^7.8.0" - "@babel/plugin-syntax-top-level-await" "^7.8.3" - "@babel/plugin-transform-arrow-functions" "^7.8.3" - "@babel/plugin-transform-async-to-generator" "^7.8.3" - "@babel/plugin-transform-block-scoped-functions" "^7.8.3" - "@babel/plugin-transform-block-scoping" "^7.8.3" - "@babel/plugin-transform-classes" "^7.9.0" - "@babel/plugin-transform-computed-properties" "^7.8.3" - "@babel/plugin-transform-destructuring" "^7.8.3" - "@babel/plugin-transform-dotall-regex" "^7.8.3" - "@babel/plugin-transform-duplicate-keys" "^7.8.3" - "@babel/plugin-transform-exponentiation-operator" "^7.8.3" - "@babel/plugin-transform-for-of" "^7.9.0" - "@babel/plugin-transform-function-name" "^7.8.3" - "@babel/plugin-transform-literals" "^7.8.3" - "@babel/plugin-transform-member-expression-literals" "^7.8.3" - "@babel/plugin-transform-modules-amd" "^7.9.0" - "@babel/plugin-transform-modules-commonjs" "^7.9.0" - "@babel/plugin-transform-modules-systemjs" "^7.9.0" - "@babel/plugin-transform-modules-umd" "^7.9.0" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.8.3" - "@babel/plugin-transform-new-target" "^7.8.3" - "@babel/plugin-transform-object-super" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.8.7" - "@babel/plugin-transform-property-literals" "^7.8.3" - "@babel/plugin-transform-regenerator" "^7.8.7" - "@babel/plugin-transform-reserved-words" "^7.8.3" - "@babel/plugin-transform-shorthand-properties" "^7.8.3" - "@babel/plugin-transform-spread" "^7.8.3" - "@babel/plugin-transform-sticky-regex" "^7.8.3" - "@babel/plugin-transform-template-literals" "^7.8.3" - "@babel/plugin-transform-typeof-symbol" "^7.8.4" - "@babel/plugin-transform-unicode-regex" "^7.8.3" - "@babel/preset-modules" "^0.1.3" - "@babel/types" "^7.9.0" - browserslist "^4.9.1" - core-js-compat "^3.6.2" - invariant "^2.2.2" - levenary "^1.1.1" - semver "^5.5.0" - -"@babel/preset-env@^7.11.0": +"@babel/preset-env@^7.0.0", "@babel/preset-env@^7.11.0", "@babel/preset-env@^7.4.3", "@babel/preset-env@^7.4.5": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.11.0.tgz#860ee38f2ce17ad60480c2021ba9689393efb796" integrity sha512-2u1/k7rG/gTh02dylX2kL3S0IJNF+J6bfDSp4DI2Ma8QN6Y9x9pmAax59fsCk6QUQG0yqH47yJWA+u1I1LccAg== @@ -1696,12 +932,12 @@ semver "^5.5.0" "@babel/preset-flow@^7.0.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.9.0.tgz#fee847c3e090b0b2d9227c1949e4da1d1379280d" - integrity sha512-88uSmlshIrlmPkNkEcx3UpSZ6b8n0UGBq0/0ZMZCF/uxAW0XIAUuDHBhIOAh0pvweafH4RxOwi/H3rWhtqOYPA== + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.10.4.tgz#e0d9c72f8cb02d1633f6a5b7b16763aa2edf659f" + integrity sha512-XI6l1CptQCOBv+ZKYwynyswhtOKwpZZp5n0LG1QKCo8erRhqjoQV6nvx61Eg30JHpysWQSBwA2AWRU3pBbSY5g== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-flow-strip-types" "^7.9.0" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-flow-strip-types" "^7.10.4" "@babel/preset-modules@^0.1.3": version "0.1.3" @@ -1714,19 +950,7 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" -"@babel/preset-react@^7.0.0": - version "7.9.4" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.9.4.tgz#c6c97693ac65b6b9c0b4f25b948a8f665463014d" - integrity sha512-AxylVB3FXeOTQXNXyiuAQJSvss62FEotbX2Pzx3K/7c+MKJMdSg6Ose6QYllkdCFA8EInCJVw7M/o5QbLuA4ZQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-react-display-name" "^7.8.3" - "@babel/plugin-transform-react-jsx" "^7.9.4" - "@babel/plugin-transform-react-jsx-development" "^7.9.0" - "@babel/plugin-transform-react-jsx-self" "^7.9.0" - "@babel/plugin-transform-react-jsx-source" "^7.9.0" - -"@babel/preset-react@^7.10.4": +"@babel/preset-react@^7.0.0", "@babel/preset-react@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.10.4.tgz#92e8a66d816f9911d11d4cc935be67adfc82dbcf" integrity sha512-BrHp4TgOIy4M19JAfO1LhycVXOPWdDbTRep7eVyatf174Hff+6Uk53sDyajqZPu8W1qXRBiYOfIamek6jA7YVw== @@ -1759,9 +983,9 @@ source-map-support "^0.5.16" "@babel/runtime-corejs2@^7.2.0", "@babel/runtime-corejs2@^7.6.3": - version "7.9.2" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.9.2.tgz#f11d074ff99b9b4319b5ecf0501f12202bf2bf4d" - integrity sha512-ayjSOxuK2GaSDJFCtLgHnYjuMyIpViNujWrZo8GUpN60/n7juzJKK5yOo6RFVb0zdU9ACJFK+MsZrUnj3OmXMw== + version "7.11.2" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.11.2.tgz#700a03945ebad0d31ba6690fc8a6bcc9040faa47" + integrity sha512-AC/ciV28adSSpEkBglONBWq4/Lvm6GAZuxIoyVtsnUpZMl0bxLtoChEnYAkP+47KyOCayZanojtflUEUJtR/6Q== dependencies: core-js "^2.6.5" regenerator-runtime "^0.13.4" @@ -1773,42 +997,19 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": - version "7.9.2" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06" - integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/runtime@^7.11.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.11.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.3.1", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.7": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.5.tgz#303d8bd440ecd5a491eae6117fd3367698674c5c" - integrity sha512-otddXKhdNn7d0ptoFRHtMLa8LqDxLYwTjB4nYgM1yy5N6gU/MUf8zqyyLltCH3yAVitBzmwK4us+DD0l/MauAg== - dependencies: - regenerator-runtime "^0.13.4" - "@babel/standalone@^7.4.5": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.10.5.tgz#4ee38dc79fda10a2a0da0897f09e270310151314" - integrity sha512-PERGHqhQ7H3TrdglvSW4pEHULywMJEdytnzaR0VPF1HN45aS+3FcE62efb90XPKS9TlgrEUkYDvYMt+0m6G0YA== - -"@babel/template@^7.0.0", "@babel/template@^7.3.3", "@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6": - version "7.8.6" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" - integrity sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg== - dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/parser" "^7.8.6" - "@babel/types" "^7.8.6" + version "7.11.3" + resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.11.3.tgz#043e6ff3b12226e41ed2013418b9a4c055d9c21e" + integrity sha512-rcoT32Hw0faYhmCDR0P84ODKL5kpEdhYPgdzlTKs7+v9oJaVLsGvq0xlkmLRj01F6LrItH3tY9eEoRsPLie4RQ== -"@babel/template@^7.10.4": +"@babel/template@^7.0.0", "@babel/template@^7.10.4", "@babel/template@^7.3.3", "@babel/template@^7.7.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== @@ -1817,22 +1018,7 @@ "@babel/parser" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.6", "@babel/traverse@^7.4.5", "@babel/traverse@^7.8.3", "@babel/traverse@^7.8.6", "@babel/traverse@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.0.tgz#d3882c2830e513f4fe4cec9fe76ea1cc78747892" - integrity sha512-jAZQj0+kn4WTHO5dUZkZKhbFrqZE7K5LAQ5JysMnmvGij+wOdr+8lWqPeW0BcF4wFwrEXXtdGO7wcV6YPJcf3w== - dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.9.0" - "@babel/helper-function-name" "^7.8.3" - "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/parser" "^7.9.0" - "@babel/types" "^7.9.0" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.13" - -"@babel/traverse@^7.10.4", "@babel/traverse@^7.11.0": +"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.6", "@babel/traverse@^7.10.4", "@babel/traverse@^7.11.0", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.4": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.0.tgz#9b996ce1b98f53f7c3e4175115605d56ed07dd24" integrity sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg== @@ -1847,31 +1033,7 @@ globals "^11.1.0" lodash "^4.17.19" -"@babel/traverse@^7.7.4": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.5.tgz#6e7c56b44e2ac7011a948c21e283ddd9d9db97a2" - integrity sha512-c4gH3jsvSuGUezlP6rzSJ6jf8fYjLj3hsMZRx/nX0h+fmHN0w+ekubRrHPqnMec0meycA2nwCsJ7dC8IPem2FQ== - dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.9.5" - "@babel/helper-function-name" "^7.9.5" - "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/parser" "^7.9.0" - "@babel/types" "^7.9.5" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.13" - -"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.0.tgz#00b064c3df83ad32b2dbf5ff07312b15c7f1efb5" - integrity sha512-BS9JKfXkzzJl8RluW4JGknzpiUV7ZrvTayM6yfqLTVBEnFtyowVIOu6rqxRd5cVO6yGoWf4T8u8dgK9oB+GCng== - dependencies: - "@babel/helper-validator-identifier" "^7.9.0" - lodash "^4.17.13" - to-fast-properties "^2.0.0" - -"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0": +"@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4", "@babel/types@^7.4.0", "@babel/types@^7.4.4": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.0.tgz#2ae6bf1ba9ae8c3c43824e5861269871b206e90d" integrity sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA== @@ -1880,24 +1042,6 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" -"@babel/types@^7.3.3": - version "7.9.6" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.6.tgz#2c5502b427251e9de1bd2dff95add646d95cc9f7" - integrity sha512-qxXzvBO//jO9ZnoasKF1uJzHd2+M6Q2ZPIVfnFps8JJvXy0ZBbwbNOmE6SGIY5XOY6d1Bo5lb9d9RJ8nv3WSeA== - dependencies: - "@babel/helper-validator-identifier" "^7.9.5" - lodash "^4.17.13" - to-fast-properties "^2.0.0" - -"@babel/types@^7.4", "@babel/types@^7.9.5": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.5.tgz#89231f82915a8a566a703b3b20133f73da6b9444" - integrity sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg== - dependencies: - "@babel/helper-validator-identifier" "^7.9.5" - lodash "^4.17.13" - to-fast-properties "^2.0.0" - "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -7695,13 +6839,6 @@ babel-plugin-add-react-displayname@^0.0.5: resolved "https://registry.yarnpkg.com/babel-plugin-add-react-displayname/-/babel-plugin-add-react-displayname-0.0.5.tgz#339d4cddb7b65fd62d1df9db9fe04de134122bd5" integrity sha1-M51M3be2X9YtHfnbn+BN4TQSK9U= -babel-plugin-dynamic-import-node@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f" - integrity sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ== - dependencies: - object.assign "^4.1.0" - babel-plugin-dynamic-import-node@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" @@ -8653,16 +7790,6 @@ browserslist@^4.8.3: electron-to-chromium "^1.3.338" node-releases "^1.1.46" -browserslist@^4.9.1: - version "4.11.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.11.0.tgz#aef4357b10a8abda00f97aac7cd587b2082ba1ad" - integrity sha512-WqEC7Yr5wUH5sg6ruR++v2SGOQYpyUdYYd4tZoAq1F7y+QXoLoYGXVbxhtaIqWmAJjtNTRjVD3HuJc1OXTel2A== - dependencies: - caniuse-lite "^1.0.30001035" - electron-to-chromium "^1.3.380" - node-releases "^1.1.52" - pkg-up "^3.1.0" - bser@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719" @@ -8751,11 +7878,6 @@ builtin-status-codes@^3.0.0: resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= -builtins@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/builtins/-/builtins-0.0.7.tgz#355219cd6cf18dbe7c01cc7fd2dce765cfdc549a" - integrity sha1-NVIZzWzxjb58Acx/0tznZc/cVJo= - bytes@1: version "1.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-1.0.0.tgz#3569ede8ba34315fab99c3e92cb04c7220de1fa8" @@ -9089,7 +8211,7 @@ can-use-dom@^0.1.0: resolved "https://registry.yarnpkg.com/can-use-dom/-/can-use-dom-0.1.0.tgz#22cc4a34a0abc43950f42c6411024a3f6366b45a" integrity sha1-IsxKNKCrxDlQ9CxkEQJKP2NmtFo= -caniuse-lite@^1.0.30000984, caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001022, caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001043: +caniuse-lite@^1.0.30000984, caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001022, caniuse-lite@^1.0.30001043: version "1.0.30001094" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001094.tgz#0b11d02e1cdc201348dbd8e3e57bd9b6ce82b175" integrity sha512-ufHZNtMaDEuRBpTbqD93tIQnngmJ+oBknjvr0IbFympSdtFpAUFmNv4mVKbb53qltxFx0nK3iy32S9AqkLzUNA== @@ -12470,11 +11592,6 @@ electron-to-chromium@^1.3.191, electron-to-chromium@^1.3.338: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.340.tgz#5d4fe78e984d4211194cf5a52e08069543da146f" integrity sha512-hRFBAglhcj5iVYH+o8QU0+XId1WGoc0VGowJB1cuJAt3exHGrivZvWeAO5BRgBZqwZtwxjm8a5MQeGoT/Su3ww== -electron-to-chromium@^1.3.380: - version "1.3.382" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.382.tgz#cad02da655c33f7a3d6ca7525bd35c17e90f3a8f" - integrity sha512-gJfxOcgnBlXhfnUUObsq3n3ReU8CT6S8je97HndYRkKsNZMJJ38zO/pI5aqO7L3Myfq+E3pqPyKK/ynyLEQfBA== - electron-to-chromium@^1.3.413: version "1.3.465" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.465.tgz#d692e5c383317570c2bd82092a24a0308c6ccf29" @@ -14977,7 +14094,7 @@ get-own-enumerable-property-symbols@^3.0.0: resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.0.tgz#b877b49a5c16aefac3655f2ed2ea5b684df8d203" integrity sha512-CIJYJC4GGF06TakLg8z4GQKvDsx9EMspVxOYih7LerEL/WosUnFIww45CGfxfeKHqlg3twgUrYRT1O3WQqjGCg== -get-port@4.2.0: +get-port@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/get-port/-/get-port-4.2.0.tgz#e37368b1e863b7629c43c5a323625f95cf24b119" integrity sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw== @@ -22121,13 +21238,6 @@ node-releases@^1.1.25, node-releases@^1.1.46: dependencies: semver "^6.3.0" -node-releases@^1.1.52: - version "1.1.52" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.52.tgz#bcffee3e0a758e92e44ecfaecd0a47554b0bcba9" - integrity sha512-snSiT1UypkgGt2wxPqS6ImEUICbNCMb31yaxWrOLXjhlt2z2/IBpaOxzONExqSm4y5oLnAqjjRWu+wsDzK5yNQ== - dependencies: - semver "^6.3.0" - node-releases@^1.1.53: version "1.1.58" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.58.tgz#8ee20eef30fa60e52755fcc0942def5a734fe935" @@ -30844,13 +29954,6 @@ validate-npm-package-license@^3.0.1: spdx-correct "~1.0.0" spdx-expression-parse "~1.0.0" -validate-npm-package-name@2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-2.2.2.tgz#f65695b22f7324442019a3c7fa39a6e7fd299085" - integrity sha1-9laVsi9zJEQgGaPH+jmm5/0pkIU= - dependencies: - builtins "0.0.7" - validator@^10.11.0: version "10.11.0" resolved "https://registry.yarnpkg.com/validator/-/validator-10.11.0.tgz#003108ea6e9a9874d31ccc9e5006856ccd76b228"