diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 53270e4517192..dccfe48d8f528 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -57,6 +57,13 @@ /x-pack/test/functional/services/transform_ui/ @elastic/ml-ui /x-pack/test/functional/services/transform.ts @elastic/ml-ui +# Maps +/x-pack/legacy/plugins/maps/ @elastic/kibana-gis +/x-pack/test/api_integration/apis/maps/ @elastic/kibana-gis +/x-pack/test/functional/apps/maps/ @elastic/kibana-gis +/x-pack/test/functional/es_archives/maps/ @elastic/kibana-gis +/x-pack/test/visual_regression/tests/maps/index.js @elastic/kibana-gis + # Operations /src/dev/ @elastic/kibana-operations /src/setup_node_env/ @elastic/kibana-operations diff --git a/.github/workflows/pr-project-assigner.yml b/.github/workflows/pr-project-assigner.yml index aea8a9cad6b1f..59123731dce66 100644 --- a/.github/workflows/pr-project-assigner.yml +++ b/.github/workflows/pr-project-assigner.yml @@ -11,5 +11,11 @@ jobs: uses: elastic/github-actions/project-assigner@v1.0.0 id: project_assigner with: - issue-mappings: '[{"label": "Team:AppAch", "projectName": "kibana-app-arch", "columnId": 6173897}]' - ghToken: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + issue-mappings: | + [ + { "label": "Team:AppArch", "projectName": "kibana-app-arch", "columnId": 6173897 }, + { "label": "Feature:Lens", "projectName": "Lens", "columnId": 6219362 }, + { "label": "Team:Platform", "projectName": "kibana-platform", "columnId": 5514360 }, + {"label": "Team:Canvas", "projectName": "canvas", "columnId": 6187580} + ] + ghToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/project-assigner.yml b/.github/workflows/project-assigner.yml index c7f17993249eb..aec3bf88f0ee2 100644 --- a/.github/workflows/project-assigner.yml +++ b/.github/workflows/project-assigner.yml @@ -11,7 +11,7 @@ jobs: uses: elastic/github-actions/project-assigner@v1.0.0 id: project_assigner with: - issue-mappings: '[{"label": "Team:AppArch", "projectName": "kibana-app-arch", "columnId": 6173895}]' + issue-mappings: '[{"label": "Team:AppArch", "projectName": "kibana-app-arch", "columnId": 6173895}, {"label": "Feature:Lens", "projectName": "Lens", "columnId": 6219363}, {"label": "Team:Canvas", "projectName": "canvas", "columnId": 6187593}]' ghToken: ${{ secrets.GITHUB_TOKEN }} - + diff --git a/docs/apm/advanced-queries.asciidoc b/docs/apm/advanced-queries.asciidoc index 1f064c1cad3fd..942882f8c4dfb 100644 --- a/docs/apm/advanced-queries.asciidoc +++ b/docs/apm/advanced-queries.asciidoc @@ -1,9 +1,7 @@ [[advanced-queries]] === Advanced queries -When querying, you're simply searching and selecting data from fields in Elasticsearch documents. -It may be helpful to view some of your documents in {kibana-ref}/discover.html[Discover] to better understand how APM data is stored in Elasticsearch. - +When querying in the APM app, you're simply searching and selecting data from fields in Elasticsearch documents. Queries entered into the query bar are also added as parameters to the URL, so it's easy to share a specific query or view with others. @@ -13,11 +11,48 @@ In the screenshot below, you can begin to see some of the transaction fields ava image::apm/images/apm-query-bar.png[Example of the Kibana Query bar in APM app in Kibana] [float] -==== Example queries +==== Example APM app queries * Exclude response times slower than 2000 ms: `transaction.duration.us > 2000000` * Filter by response status code: `context.response.status_code >= 400` * Filter by single user ID: `context.user.id : 12` -* View _all_ transactions for an endpoint, instead of just a sample - `processor.event: "transaction" AND transaction.name: ""` TIP: Read the {kibana-ref}/kuery-query.html[Kibana Query Language Enhancements] documentation to learn more about the capabilities of the {kib} query language. + +[float] +[[discover-advanced-queries]] +=== Querying in the Discover app + +It may also be helpful to view your APM data in the {kibana-ref}/discover.html[Discover app]. +Querying documents in Discover works the same way as querying in the APM app, +and all of the example queries listed above can also be used in the Discover app. + +[float] +==== Example Discover app query + +One example where you may want to make use of the Discover app, +is for viewing _all_ transactions for an endpoint, instead of just a sample. + +TIP: Starting in v7.6, you can view 10 samples per bucket in the APM app, instead of just one. + +Use the APM app to find a transaction name and time bucket that you're interested in learning more about. +Then, switch to the Discover app and make a search: + +["source","sh"] +----- +processor.event: "transaction" AND transaction.name: "" and transaction.duration.us > 13000 and transaction.duration.us < 14000` +----- + +In this example, we're interested in viewing all of the `APIRestController#customers` transactions +that took between 13 and 14 milliseconds. Here's what Discover returns: + +[role="screenshot"] +image::apm/images/advanced-discover.png[View all transactions in bucket] + +You can now explore the data until you find a specific transaction that you're interested in. +Copy that transaction's `transaction.id`, and paste it into the APM app to view the data in the context of the APM app: + +[role="screenshot"] +image::apm/images/specific-transaction-search.png[View specific transaction in apm app] +[role="screenshot"] +image::apm/images/specific-transaction.png[View specific transaction in apm app] diff --git a/docs/apm/images/advanced-discover.png b/docs/apm/images/advanced-discover.png new file mode 100644 index 0000000000000..56ba58b2c1d41 Binary files /dev/null and b/docs/apm/images/advanced-discover.png differ diff --git a/docs/apm/images/specific-transaction-search.png b/docs/apm/images/specific-transaction-search.png new file mode 100644 index 0000000000000..4ed548f015713 Binary files /dev/null and b/docs/apm/images/specific-transaction-search.png differ diff --git a/docs/apm/images/specific-transaction.png b/docs/apm/images/specific-transaction.png new file mode 100644 index 0000000000000..9911dbd879f41 Binary files /dev/null and b/docs/apm/images/specific-transaction.png differ diff --git a/docs/canvas/canvas-elements.asciidoc b/docs/canvas/canvas-elements.asciidoc index c5c6f116ee34e..dc605a47de383 100644 --- a/docs/canvas/canvas-elements.asciidoc +++ b/docs/canvas/canvas-elements.asciidoc @@ -20,24 +20,24 @@ When you add elements to your workpad, you can: [[add-canvas-element]] === Add elements to your workpad -Choose the elements to display on your workpad, then familiarize yourself with the element using the preconfigured demo data. +Choose the elements to display on your workpad, then familiarize yourself with the element using the preconfigured demo data. By default, every element you add to a workpad uses demo data until you change the data source. The demo data includes a small sample data set that you can use to experiment with your element. . Click *Add element*. -. In the *Elements* window, select the element you want to use. +. In the *Elements* window, select the element you want to use. + [role="screenshot"] image::images/canvas-element-select.gif[Canvas elements] -. Play around with the default settings and see what the element can do. +. Play around with the default settings and see what the element can do. -TIP: Want to use a different element? You can delete the element by selecting it, clicking the *Element options* icon in the top right corner, then selecting *Delete*. +TIP: Want to use a different element? You can delete the element by selecting it, clicking the *Element options* icon in the top right, then selecting *Delete*. [float] [[connect-element-data]] === Connect the element to your data -When you are ready to move on from the demo data, connect the element to your own data. +When you have finished using the demo data, connect the element to a data source. . Make sure that the element is selected, then select *Data*. @@ -45,55 +45,51 @@ When you are ready to move on from the demo data, connect the element to your ow [float] [[elasticsearch-sql-data-source]] -==== Connect to Elasticsearch SQL +==== Connect to {es} SQL -Access your data in Elasticsearch using the Elasticsearch SQL syntax. +Access your data in {es} using SQL syntax. For information about SQL syntax, refer to {ref}/sql-spec.html[SQL language]. -Unfamiliar with writing Elasticsearch SQL queries? For more information, refer to {ref}/sql-spec.html[SQL language]. +. Click *{es} SQL*. -. Click *Elasticsearch SQL*. +. In the *{es} SQL query* box, enter your query, then *Preview* it. -. In the *Elasticearch SQL query* box, enter your query, then *Preview* it. - -. If everything looks correct, *Save* it. +. If everything looks correct, *Save* it. [float] [[elasticsearch-raw-doc-data-source]] -==== Connect to Elasticsearch raw data +==== Connect to {es} raw data -Use the Lucene query syntax to use your raw data in Elasticsearch. +Access your raw data in {es} without the use of aggregations. Use {es} raw data when you have low volume datasets, or to plot exact, non-aggregated values. -For for more information about the Lucene query string sytax, refer to <>. +To use targeted queries, you can enter a query using the <>. -. Click *Elasticsearch raw documents*. +. Click *{es} raw documents*. -. In the *Index* field, enter the index pattern that you want to display. +. In the *Index* field, enter the index pattern that you want to display. . From the *Fields* dropdown, select the associated fields you want to display. . To sort the data, select an option from the *Sort Field* and *Sort Order* dropdowns. -. For more targeted queries, enter a *Query* using the Lucene query string syntax. +. For more targeted queries, enter a *Query* using the Lucene query string syntax. -. *Preview* the query. +. *Preview* the query. -. If your query looks correct, *Save* it. +. If your query looks correct, *Save* it. [float] [[timelion-data-source]] ==== Connect to Timelion -Use <> queries to use your time series data. +Access your time series data using <> queries. To use Timelion queries, you can enter a query using the <>. . Click *Timelion*. -. Enter a *Query* using the Lucene query string syntax. -+ -For for more information about the Lucene query string syntax, refer to <>. +. Enter a *Query* using the Lucene query string syntax. . Enter the *Interval*, then *Preview* the query. -. If your query looks correct, *Save* it. +. If your query looks correct, *Save* it. [float] [[configure-display-options]] @@ -109,7 +105,7 @@ When you connect your element to a data source, the element often appears as a w . Click *Display* -. Change the display options for the element. +. Change the display options for the element. [float] [[element-display-container]] @@ -122,7 +118,7 @@ Further define the appearance of the element container and border. . Expand *Container style*. . Change the *Appearance* and *Border* options. - + [float] [[apply-element-styles]] ==== Apply a set of styles @@ -155,7 +151,7 @@ Increase or decrease how often your data refreshes on your workpad. [role="screenshot"] image::images/canvas-refresh-interval.png[Element data refresh interval] -TIP: To manually refresh the data, click the *Refresh data* icon. +TIP: To manually refresh the data, click the *Refresh data* icon. [float] [[organize-element]] @@ -223,7 +219,7 @@ Change the order of how the elements are displayed on your workpad. . Select an element. -. In the top right corder, click the *Element options* icon. +. In the top right corder, click the *Element options* icon. . Select *Order*, then select the order that you want the element to appear. @@ -262,7 +258,7 @@ When you have run out of room on your workpad page, add more pages. . Click *Page 1*, then click *+*. -. On the *Page* editor panel on the right, select the page transition from the *Transition* dropdown. +. On the *Page* editor panel on the right, select the page transition from the *Transition* dropdown. + [role="screenshot"] image::images/canvas-add-pages.gif[Add pages] diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.md b/docs/development/core/public/kibana-plugin-public.httpservicebase.md deleted file mode 100644 index 9ea77c95b343e..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.md +++ /dev/null @@ -1,37 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) - -## HttpServiceBase interface - - -Signature: - -```typescript -export interface HttpServiceBase -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [anonymousPaths](./kibana-plugin-public.httpservicebase.anonymouspaths.md) | IAnonymousPaths | APIs for denoting certain paths for not requiring authentication | -| [basePath](./kibana-plugin-public.httpservicebase.basepath.md) | IBasePath | APIs for manipulating the basePath on URL segments. | -| [delete](./kibana-plugin-public.httpservicebase.delete.md) | HttpHandler | Makes an HTTP request with the DELETE method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | -| [fetch](./kibana-plugin-public.httpservicebase.fetch.md) | HttpHandler | Makes an HTTP request. Defaults to a GET request unless overriden. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | -| [get](./kibana-plugin-public.httpservicebase.get.md) | HttpHandler | Makes an HTTP request with the GET method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | -| [head](./kibana-plugin-public.httpservicebase.head.md) | HttpHandler | Makes an HTTP request with the HEAD method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | -| [options](./kibana-plugin-public.httpservicebase.options.md) | HttpHandler | Makes an HTTP request with the OPTIONS method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | -| [patch](./kibana-plugin-public.httpservicebase.patch.md) | HttpHandler | Makes an HTTP request with the PATCH method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | -| [post](./kibana-plugin-public.httpservicebase.post.md) | HttpHandler | Makes an HTTP request with the POST method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | -| [put](./kibana-plugin-public.httpservicebase.put.md) | HttpHandler | Makes an HTTP request with the PUT method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | - -## Methods - -| Method | Description | -| --- | --- | -| [addLoadingCount(countSource$)](./kibana-plugin-public.httpservicebase.addloadingcount.md) | Adds a new source of loading counts. Used to show the global loading indicator when sum of all observed counts are more than 0. | -| [getLoadingCount$()](./kibana-plugin-public.httpservicebase.getloadingcount_.md) | Get the sum of all loading count sources as a single Observable. | -| [intercept(interceptor)](./kibana-plugin-public.httpservicebase.intercept.md) | Adds a new [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) to the global HTTP client. | -| [removeAllInterceptors()](./kibana-plugin-public.httpservicebase.removeallinterceptors.md) | Removes all configured interceptors. | - diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.removeallinterceptors.md b/docs/development/core/public/kibana-plugin-public.httpservicebase.removeallinterceptors.md deleted file mode 100644 index 0432ec29a22b6..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.removeallinterceptors.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [removeAllInterceptors](./kibana-plugin-public.httpservicebase.removeallinterceptors.md) - -## HttpServiceBase.removeAllInterceptors() method - -Removes all configured interceptors. - -Signature: - -```typescript -removeAllInterceptors(): void; -``` -Returns: - -`void` - diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.addloadingcount.md b/docs/development/core/public/kibana-plugin-public.httpsetup.addloadingcountsource.md similarity index 62% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.addloadingcount.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.addloadingcountsource.md index e984fea48625d..a2fe66bb55c77 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.addloadingcount.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.addloadingcountsource.md @@ -1,15 +1,15 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [addLoadingCount](./kibana-plugin-public.httpservicebase.addloadingcount.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [addLoadingCountSource](./kibana-plugin-public.httpsetup.addloadingcountsource.md) -## HttpServiceBase.addLoadingCount() method +## HttpSetup.addLoadingCountSource() method Adds a new source of loading counts. Used to show the global loading indicator when sum of all observed counts are more than 0. Signature: ```typescript -addLoadingCount(countSource$: Observable): void; +addLoadingCountSource(countSource$: Observable): void; ``` ## Parameters diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.anonymouspaths.md b/docs/development/core/public/kibana-plugin-public.httpsetup.anonymouspaths.md similarity index 57% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.anonymouspaths.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.anonymouspaths.md index e94757c5eb031..a9268ca1d8ed6 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.anonymouspaths.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.anonymouspaths.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [anonymousPaths](./kibana-plugin-public.httpservicebase.anonymouspaths.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [anonymousPaths](./kibana-plugin-public.httpsetup.anonymouspaths.md) -## HttpServiceBase.anonymousPaths property +## HttpSetup.anonymousPaths property APIs for denoting certain paths for not requiring authentication diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.basepath.md b/docs/development/core/public/kibana-plugin-public.httpsetup.basepath.md similarity index 57% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.basepath.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.basepath.md index 6c5f690a5c607..6b0726dc8ef2b 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.basepath.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.basepath.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [basePath](./kibana-plugin-public.httpservicebase.basepath.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [basePath](./kibana-plugin-public.httpsetup.basepath.md) -## HttpServiceBase.basePath property +## HttpSetup.basePath property APIs for manipulating the basePath on URL segments. diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.delete.md b/docs/development/core/public/kibana-plugin-public.httpsetup.delete.md similarity index 63% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.delete.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.delete.md index 73022ef4f2946..565f0eb336d4f 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.delete.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.delete.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [delete](./kibana-plugin-public.httpservicebase.delete.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [delete](./kibana-plugin-public.httpsetup.delete.md) -## HttpServiceBase.delete property +## HttpSetup.delete property Makes an HTTP request with the DELETE method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.fetch.md b/docs/development/core/public/kibana-plugin-public.httpsetup.fetch.md similarity index 64% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.fetch.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.fetch.md index 3a1ae4892a3dd..2d6447363fa9b 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.fetch.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.fetch.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [fetch](./kibana-plugin-public.httpservicebase.fetch.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [fetch](./kibana-plugin-public.httpsetup.fetch.md) -## HttpServiceBase.fetch property +## HttpSetup.fetch property Makes an HTTP request. Defaults to a GET request unless overriden. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.get.md b/docs/development/core/public/kibana-plugin-public.httpsetup.get.md similarity index 63% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.get.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.get.md index a61b3dd140e50..0c484e33e9b58 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.get.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.get.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [get](./kibana-plugin-public.httpservicebase.get.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [get](./kibana-plugin-public.httpsetup.get.md) -## HttpServiceBase.get property +## HttpSetup.get property Makes an HTTP request with the GET method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.getloadingcount_.md b/docs/development/core/public/kibana-plugin-public.httpsetup.getloadingcount_.md similarity index 59% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.getloadingcount_.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.getloadingcount_.md index 0b2129330cd01..628b62b2ffc27 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.getloadingcount_.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.getloadingcount_.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [getLoadingCount$](./kibana-plugin-public.httpservicebase.getloadingcount_.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [getLoadingCount$](./kibana-plugin-public.httpsetup.getloadingcount_.md) -## HttpServiceBase.getLoadingCount$() method +## HttpSetup.getLoadingCount$() method Get the sum of all loading count sources as a single Observable. diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.head.md b/docs/development/core/public/kibana-plugin-public.httpsetup.head.md similarity index 63% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.head.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.head.md index 4624d95f03bf3..e4d49c843e572 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.head.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.head.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [head](./kibana-plugin-public.httpservicebase.head.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [head](./kibana-plugin-public.httpsetup.head.md) -## HttpServiceBase.head property +## HttpSetup.head property Makes an HTTP request with the HEAD method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.intercept.md b/docs/development/core/public/kibana-plugin-public.httpsetup.intercept.md similarity index 72% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.intercept.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.intercept.md index 8cf5bf813df09..1bda0c6166e65 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.intercept.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.intercept.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [intercept](./kibana-plugin-public.httpservicebase.intercept.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [intercept](./kibana-plugin-public.httpsetup.intercept.md) -## HttpServiceBase.intercept() method +## HttpSetup.intercept() method Adds a new [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) to the global HTTP client. diff --git a/docs/development/core/public/kibana-plugin-public.httpsetup.md b/docs/development/core/public/kibana-plugin-public.httpsetup.md index 7ef037ea7abd1..8a14d26c57ca3 100644 --- a/docs/development/core/public/kibana-plugin-public.httpsetup.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.md @@ -2,12 +2,35 @@ [Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) -## HttpSetup type +## HttpSetup interface -See [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) Signature: ```typescript -export declare type HttpSetup = HttpServiceBase; +export interface HttpSetup ``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [anonymousPaths](./kibana-plugin-public.httpsetup.anonymouspaths.md) | IAnonymousPaths | APIs for denoting certain paths for not requiring authentication | +| [basePath](./kibana-plugin-public.httpsetup.basepath.md) | IBasePath | APIs for manipulating the basePath on URL segments. | +| [delete](./kibana-plugin-public.httpsetup.delete.md) | HttpHandler | Makes an HTTP request with the DELETE method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | +| [fetch](./kibana-plugin-public.httpsetup.fetch.md) | HttpHandler | Makes an HTTP request. Defaults to a GET request unless overriden. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | +| [get](./kibana-plugin-public.httpsetup.get.md) | HttpHandler | Makes an HTTP request with the GET method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | +| [head](./kibana-plugin-public.httpsetup.head.md) | HttpHandler | Makes an HTTP request with the HEAD method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | +| [options](./kibana-plugin-public.httpsetup.options.md) | HttpHandler | Makes an HTTP request with the OPTIONS method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | +| [patch](./kibana-plugin-public.httpsetup.patch.md) | HttpHandler | Makes an HTTP request with the PATCH method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | +| [post](./kibana-plugin-public.httpsetup.post.md) | HttpHandler | Makes an HTTP request with the POST method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | +| [put](./kibana-plugin-public.httpsetup.put.md) | HttpHandler | Makes an HTTP request with the PUT method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. | + +## Methods + +| Method | Description | +| --- | --- | +| [addLoadingCountSource(countSource$)](./kibana-plugin-public.httpsetup.addloadingcountsource.md) | Adds a new source of loading counts. Used to show the global loading indicator when sum of all observed counts are more than 0. | +| [getLoadingCount$()](./kibana-plugin-public.httpsetup.getloadingcount_.md) | Get the sum of all loading count sources as a single Observable. | +| [intercept(interceptor)](./kibana-plugin-public.httpsetup.intercept.md) | Adds a new [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) to the global HTTP client. | + diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.options.md b/docs/development/core/public/kibana-plugin-public.httpsetup.options.md similarity index 62% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.options.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.options.md index 0820beb2752f2..4ea5be8826bff 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.options.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.options.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [options](./kibana-plugin-public.httpservicebase.options.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [options](./kibana-plugin-public.httpsetup.options.md) -## HttpServiceBase.options property +## HttpSetup.options property Makes an HTTP request with the OPTIONS method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.patch.md b/docs/development/core/public/kibana-plugin-public.httpsetup.patch.md similarity index 63% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.patch.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.patch.md index 00e1ffc0e16bf..ef1d50005b012 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.patch.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.patch.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [patch](./kibana-plugin-public.httpservicebase.patch.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [patch](./kibana-plugin-public.httpsetup.patch.md) -## HttpServiceBase.patch property +## HttpSetup.patch property Makes an HTTP request with the PATCH method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.post.md b/docs/development/core/public/kibana-plugin-public.httpsetup.post.md similarity index 63% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.post.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.post.md index 3771a7c910895..1c19c35ac3038 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.post.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.post.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [post](./kibana-plugin-public.httpservicebase.post.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [post](./kibana-plugin-public.httpsetup.post.md) -## HttpServiceBase.post property +## HttpSetup.post property Makes an HTTP request with the POST method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. diff --git a/docs/development/core/public/kibana-plugin-public.httpservicebase.put.md b/docs/development/core/public/kibana-plugin-public.httpsetup.put.md similarity index 63% rename from docs/development/core/public/kibana-plugin-public.httpservicebase.put.md rename to docs/development/core/public/kibana-plugin-public.httpsetup.put.md index 6e43aafa916bc..e5243d8c80dae 100644 --- a/docs/development/core/public/kibana-plugin-public.httpservicebase.put.md +++ b/docs/development/core/public/kibana-plugin-public.httpsetup.put.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) > [put](./kibana-plugin-public.httpservicebase.put.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpSetup](./kibana-plugin-public.httpsetup.md) > [put](./kibana-plugin-public.httpsetup.put.md) -## HttpServiceBase.put property +## HttpSetup.put property Makes an HTTP request with the PUT method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. diff --git a/docs/development/core/public/kibana-plugin-public.httpstart.md b/docs/development/core/public/kibana-plugin-public.httpstart.md index bb9247c63897a..9abf319acf00d 100644 --- a/docs/development/core/public/kibana-plugin-public.httpstart.md +++ b/docs/development/core/public/kibana-plugin-public.httpstart.md @@ -4,10 +4,10 @@ ## HttpStart type -See [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) +See [HttpSetup](./kibana-plugin-public.httpsetup.md) Signature: ```typescript -export declare type HttpStart = HttpServiceBase; +export declare type HttpStart = HttpSetup; ``` diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index 2c43f36ede09e..e2c2866b57b6b 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -56,7 +56,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HttpHeadersInit](./kibana-plugin-public.httpheadersinit.md) | | | [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) | An object that may define global interceptor functions for different parts of the request and response lifecycle. See [IHttpInterceptController](./kibana-plugin-public.ihttpinterceptcontroller.md). | | [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) | Fetch API options available to [HttpHandler](./kibana-plugin-public.httphandler.md)s. | -| [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) | | +| [HttpSetup](./kibana-plugin-public.httpsetup.md) | | | [I18nStart](./kibana-plugin-public.i18nstart.md) | I18nStart.Context is required by any localizable React component from @kbn/i18n and @elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. | | [IAnonymousPaths](./kibana-plugin-public.ianonymouspaths.md) | APIs for denoting paths as not requiring authentication | | [IBasePath](./kibana-plugin-public.ibasepath.md) | APIs for manipulating the basePath on URL segments. | @@ -118,8 +118,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HandlerContextType](./kibana-plugin-public.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md) to represent the type of the context. | | [HandlerFunction](./kibana-plugin-public.handlerfunction.md) | A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-public.icontextcontainer.md) | | [HandlerParameters](./kibana-plugin-public.handlerparameters.md) | Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md), excluding the [HandlerContextType](./kibana-plugin-public.handlercontexttype.md). | -| [HttpSetup](./kibana-plugin-public.httpsetup.md) | See [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) | -| [HttpStart](./kibana-plugin-public.httpstart.md) | See [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) | +| [HttpStart](./kibana-plugin-public.httpstart.md) | See [HttpSetup](./kibana-plugin-public.httpsetup.md) | | [IContextProvider](./kibana-plugin-public.icontextprovider.md) | A function that returns a context value for a specific key of given context type. | | [IToasts](./kibana-plugin-public.itoasts.md) | Methods for adding and removing global toast messages. See [ToastsApi](./kibana-plugin-public.toastsapi.md). | | [MountPoint](./kibana-plugin-public.mountpoint.md) | A function that should mount DOM content inside the provided container element and return a handler to unmount it. | diff --git a/docs/development/core/server/kibana-plugin-server.basepath.get.md b/docs/development/core/server/kibana-plugin-server.basepath.get.md index 6ef7022f10e62..a20bc1a4e3174 100644 --- a/docs/development/core/server/kibana-plugin-server.basepath.get.md +++ b/docs/development/core/server/kibana-plugin-server.basepath.get.md @@ -9,5 +9,5 @@ returns `basePath` value, specific for an incoming request. Signature: ```typescript -get: (request: KibanaRequest | LegacyRequest) => string; +get: (request: LegacyRequest | KibanaRequest) => string; ``` diff --git a/docs/development/core/server/kibana-plugin-server.basepath.md b/docs/development/core/server/kibana-plugin-server.basepath.md index 77f50abc60369..bf91619ac230b 100644 --- a/docs/development/core/server/kibana-plugin-server.basepath.md +++ b/docs/development/core/server/kibana-plugin-server.basepath.md @@ -16,11 +16,11 @@ export declare class BasePath | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| [get](./kibana-plugin-server.basepath.get.md) | | (request: KibanaRequest<unknown, unknown, unknown, any> | LegacyRequest) => string | returns basePath value, specific for an incoming request. | +| [get](./kibana-plugin-server.basepath.get.md) | | (request: LegacyRequest | KibanaRequest<unknown, unknown, unknown, any>) => string | returns basePath value, specific for an incoming request. | | [prepend](./kibana-plugin-server.basepath.prepend.md) | | (path: string) => string | Prepends path with the basePath. | | [remove](./kibana-plugin-server.basepath.remove.md) | | (path: string) => string | Removes the prepended basePath from the path. | | [serverBasePath](./kibana-plugin-server.basepath.serverbasepath.md) | | string | returns the server's basePathSee [BasePath.get](./kibana-plugin-server.basepath.get.md) for getting the basePath value for a specific request | -| [set](./kibana-plugin-server.basepath.set.md) | | (request: KibanaRequest<unknown, unknown, unknown, any> | LegacyRequest, requestSpecificBasePath: string) => void | sets basePath value, specific for an incoming request. | +| [set](./kibana-plugin-server.basepath.set.md) | | (request: LegacyRequest | KibanaRequest<unknown, unknown, unknown, any>, requestSpecificBasePath: string) => void | sets basePath value, specific for an incoming request. | ## Remarks diff --git a/docs/development/core/server/kibana-plugin-server.basepath.set.md b/docs/development/core/server/kibana-plugin-server.basepath.set.md index 56a7f644d34cc..ac08baa0bb99e 100644 --- a/docs/development/core/server/kibana-plugin-server.basepath.set.md +++ b/docs/development/core/server/kibana-plugin-server.basepath.set.md @@ -9,5 +9,5 @@ sets `basePath` value, specific for an incoming request. Signature: ```typescript -set: (request: KibanaRequest | LegacyRequest, requestSpecificBasePath: string) => void; +set: (request: LegacyRequest | KibanaRequest, requestSpecificBasePath: string) => void; ``` diff --git a/docs/development/core/server/kibana-plugin-server.corestart.md b/docs/development/core/server/kibana-plugin-server.corestart.md index e523717a37ac8..167c69d5fe329 100644 --- a/docs/development/core/server/kibana-plugin-server.corestart.md +++ b/docs/development/core/server/kibana-plugin-server.corestart.md @@ -18,4 +18,5 @@ export interface CoreStart | --- | --- | --- | | [capabilities](./kibana-plugin-server.corestart.capabilities.md) | CapabilitiesStart | [CapabilitiesStart](./kibana-plugin-server.capabilitiesstart.md) | | [savedObjects](./kibana-plugin-server.corestart.savedobjects.md) | SavedObjectsServiceStart | [SavedObjectsServiceStart](./kibana-plugin-server.savedobjectsservicestart.md) | +| [uiSettings](./kibana-plugin-server.corestart.uisettings.md) | UiSettingsServiceStart | [UiSettingsServiceStart](./kibana-plugin-server.uisettingsservicestart.md) | diff --git a/docs/development/core/server/kibana-plugin-server.corestart.uisettings.md b/docs/development/core/server/kibana-plugin-server.corestart.uisettings.md new file mode 100644 index 0000000000000..323e929f2918e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.corestart.uisettings.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreStart](./kibana-plugin-server.corestart.md) > [uiSettings](./kibana-plugin-server.corestart.uisettings.md) + +## CoreStart.uiSettings property + +[UiSettingsServiceStart](./kibana-plugin-server.uisettingsservicestart.md) + +Signature: + +```typescript +uiSettings: UiSettingsServiceStart; +``` diff --git a/docs/development/core/server/kibana-plugin-server.logger.get.md b/docs/development/core/server/kibana-plugin-server.logger.get.md new file mode 100644 index 0000000000000..b4a2d8a124260 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.logger.get.md @@ -0,0 +1,33 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Logger](./kibana-plugin-server.logger.md) > [get](./kibana-plugin-server.logger.get.md) + +## Logger.get() method + +Returns a new [Logger](./kibana-plugin-server.logger.md) instance extending the current logger context. + +Signature: + +```typescript +get(...childContextPaths: string[]): Logger; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| childContextPaths | string[] | | + +Returns: + +`Logger` + +## Example + + +```typescript +const logger = loggerFactory.get('plugin', 'service'); // 'plugin.service' context +const subLogger = logger.get('feature'); // 'plugin.service.feature' context + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index ea5ca6502b076..a39c27a758f9d 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -90,7 +90,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginManifest](./kibana-plugin-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. | | [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) | | | [PluginsServiceStart](./kibana-plugin-server.pluginsservicestart.md) | | -| [RequestHandlerContext](./kibana-plugin-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients: - [savedObjects.client](./kibana-plugin-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [elasticsearch.dataClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request | +| [RequestHandlerContext](./kibana-plugin-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients: - [savedObjects.client](./kibana-plugin-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [elasticsearch.dataClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request | | [RouteConfig](./kibana-plugin-server.routeconfig.md) | Route specific configuration. | | [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) | Additional route options. | | [RouteConfigOptionsBody](./kibana-plugin-server.routeconfigoptionsbody.md) | Additional body options for a route | @@ -137,6 +137,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) | SessionStorage factory to bind one to an incoming request | | [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) | UiSettings parameters defined by the plugins. | | [UiSettingsServiceSetup](./kibana-plugin-server.uisettingsservicesetup.md) | | +| [UiSettingsServiceStart](./kibana-plugin-server.uisettingsservicestart.md) | | | [UserProvidedValues](./kibana-plugin-server.userprovidedvalues.md) | Describes the values explicitly set by user. | | [UuidServiceSetup](./kibana-plugin-server.uuidservicesetup.md) | APIs to access the application's instance uuid. | diff --git a/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md b/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md index c9fc80596efa9..d9b781e1e550e 100644 --- a/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md +++ b/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md @@ -6,7 +6,7 @@ Plugin specific context passed to a route handler. -Provides the following clients: - [savedObjects.client](./kibana-plugin-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [elasticsearch.dataClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request +Provides the following clients: - [savedObjects.client](./kibana-plugin-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [elasticsearch.dataClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request Signature: diff --git a/docs/development/core/server/kibana-plugin-server.uisettingsservicesetup.register.md b/docs/development/core/server/kibana-plugin-server.uisettingsservicesetup.register.md index 8091a7cec44aa..0047b5275408e 100644 --- a/docs/development/core/server/kibana-plugin-server.uisettingsservicesetup.register.md +++ b/docs/development/core/server/kibana-plugin-server.uisettingsservicesetup.register.md @@ -24,5 +24,17 @@ register(settings: Record): void; ## Example -setup(core: CoreSetup){ core.uiSettings.register(\[{ foo: { name: i18n.translate('my foo settings'), value: true, description: 'add some awesomeness', }, }\]); } + +```ts +setup(core: CoreSetup){ + core.uiSettings.register([{ + foo: { + name: i18n.translate('my foo settings'), + value: true, + description: 'add some awesomeness', + }, + }]); +} + +``` diff --git a/docs/development/core/server/kibana-plugin-server.uisettingsservicestart.asscopedtoclient.md b/docs/development/core/server/kibana-plugin-server.uisettingsservicestart.asscopedtoclient.md new file mode 100644 index 0000000000000..072dd39faa084 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.uisettingsservicestart.asscopedtoclient.md @@ -0,0 +1,37 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [UiSettingsServiceStart](./kibana-plugin-server.uisettingsservicestart.md) > [asScopedToClient](./kibana-plugin-server.uisettingsservicestart.asscopedtoclient.md) + +## UiSettingsServiceStart.asScopedToClient() method + +Creates a [IUiSettingsClient](./kibana-plugin-server.iuisettingsclient.md) with provided \*scoped\* saved objects client. + +This should only be used in the specific case where the client needs to be accessed from outside of the scope of a [RequestHandler](./kibana-plugin-server.requesthandler.md). + +Signature: + +```typescript +asScopedToClient(savedObjectsClient: SavedObjectsClientContract): IUiSettingsClient; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| savedObjectsClient | SavedObjectsClientContract | | + +Returns: + +`IUiSettingsClient` + +## Example + + +```ts +start(core: CoreStart) { + const soClient = core.savedObjects.getScopedClient(arbitraryRequest); + const uiSettingsClient = core.uiSettings.asScopedToClient(soClient); +} + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.uisettingsservicestart.md b/docs/development/core/server/kibana-plugin-server.uisettingsservicestart.md new file mode 100644 index 0000000000000..ee3563552275a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.uisettingsservicestart.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [UiSettingsServiceStart](./kibana-plugin-server.uisettingsservicestart.md) + +## UiSettingsServiceStart interface + + +Signature: + +```typescript +export interface UiSettingsServiceStart +``` + +## Methods + +| Method | Description | +| --- | --- | +| [asScopedToClient(savedObjectsClient)](./kibana-plugin-server.uisettingsservicestart.asscopedtoclient.md) | Creates a [IUiSettingsClient](./kibana-plugin-server.iuisettingsclient.md) with provided \*scoped\* saved objects client.This should only be used in the specific case where the client needs to be accessed from outside of the scope of a [RequestHandler](./kibana-plugin-server.requesthandler.md). | + diff --git a/docs/maps/vector-style-properties.asciidoc b/docs/maps/vector-style-properties.asciidoc index f51632218add1..5656a7f04d0e3 100644 --- a/docs/maps/vector-style-properties.asciidoc +++ b/docs/maps/vector-style-properties.asciidoc @@ -8,32 +8,52 @@ Point, polygon, and line features support different styling properties. [[point-style-properties]] ==== Point style properties +You can add text labels to your Point features by configuring label style properties. + +[cols="2*"] +|=== +|*Label* +|Specifies label content. +|*Label color* +|The text color. +|*Label size* +|The size of the text font, in pixels. +|=== + You can symbolize Point features as *Circle markers* or *Icons*. Use *Circle marker* to symbolize Points as circles. -*Fill color*:: The fill color of the point features. - -*Border color*:: The border color of the point features. - -*Border width*:: The border width of the point features. - -*Symbol size*:: The radius of the symbol size, in pixels. +[cols="2*"] +|=== +|*Border color* +|The border color of the point features. +|*Border width* +|The border width of the point features. +|*Fill color* +|The fill color of the point features. +|*Symbol size* +|The radius of the symbol size, in pixels. +|=== Use *Icon* to symbolize Points as icons. -*Fill color*:: The fill color of the point features. - -*Border color*:: The border color of the point features. - -*Border width*:: The border width of the point features. +[cols="2*"] +|=== +|*Border color* +|The border color of the point features. +|*Border width* +|The border width of the point features. +|*Fill color* +|The fill color of the point features. +|*Symbol orientation* +|The symbol orientation rotating the icon clockwise. +|*Symbol size* +|The radius of the symbol size, in pixels. +|=== -*Symbol orientation*:: The symbol orientation rotating the icon clockwise. - -*Symbol size*:: The radius of the symbol size, in pixels. -+ Available icons -+ + [role="screenshot"] image::maps/images/maki-icons.png[] @@ -42,17 +62,25 @@ image::maps/images/maki-icons.png[] [[polygon-style-properties]] ==== Polygon style properties -*Fill color*:: The fill color of the polygon features. - -*Border color*:: The border color of the polygon features. - -*Border width*:: The border width of the polygon features. +[cols="2*"] +|=== +|*Border color* +|The border color of the polygon features. +|*Border width* +|The border width of the polygon features. +|*Fill color* +|The fill color of the polygon features. +|=== [float] [[line-style-properties]] ==== Line style properties -*Border color*:: The color of the line features. - -*Border width*:: The width of the line features. +[cols="2*"] +|=== +|*Border color* +|The color of the line features. +|*Border width* +|The width of the line features. +|=== diff --git a/docs/user/reporting/reporting-troubleshooting.asciidoc b/docs/user/reporting/reporting-troubleshooting.asciidoc index 92464c24b45ea..ca7fa6abcc9d9 100644 --- a/docs/user/reporting/reporting-troubleshooting.asciidoc +++ b/docs/user/reporting/reporting-troubleshooting.asciidoc @@ -17,6 +17,7 @@ dependencies for Chromium. Make sure Kibana server OS has the appropriate packages installed for the distribution. If you are using CentOS/RHEL systems, install the following packages: + * `ipa-gothic-fonts` * `xorg-x11-fonts-100dpi` * `xorg-x11-fonts-75dpi` @@ -28,6 +29,7 @@ If you are using CentOS/RHEL systems, install the following packages: * `freetype` If you are using Ubuntu/Debian systems, install the following packages: + * `fonts-liberation` * `libfontconfig1` @@ -105,9 +107,10 @@ has its own command-line method to generate its own debug logs, which can someti caused by Kibana or Chromium. See more at https://github.com/GoogleChrome/puppeteer/blob/v1.19.0/README.md#debugging-tips Using Puppeteer's debug method when launching Kibana would look like: -> Enable verbose logging - internal DevTools protocol traffic will be logged via the debug module under the puppeteer namespace. -> ``` -> env DEBUG="puppeteer:*" ./bin/kibana -> ``` +``` +env DEBUG="puppeteer:*" ./bin/kibana +``` +The internal DevTools protocol traffic will be logged via the `debug` module under the `puppeteer` namespace. + The Puppeteer logs are very verbose and could possibly contain sensitive information. Handle the generated output with care. diff --git a/docs/user/reporting/watch-example.asciidoc b/docs/user/reporting/watch-example.asciidoc index 4c769c85975c4..627e31017230c 100644 --- a/docs/user/reporting/watch-example.asciidoc +++ b/docs/user/reporting/watch-example.asciidoc @@ -56,7 +56,16 @@ report from the Kibana UI. //For more information, see <>. //<>. -NOTE: Reporting is integrated with Watcher only as an email attachment type. +[NOTE] +==== +Reporting is integrated with Watcher only as an email attachment type. + +The report Generation URL might contain date-math expressions +that cause the watch to fail with a `parse_exception`. +Remove curly braces `{` `}` from date-math expressions and +URL-encode characters to avoid this. +For example: `...(range:(%27@timestamp%27:(gte:now-15m%2Fd,lte:now%2Fd))))...` For more information about configuring watches, see {ref}/how-watcher-works.html[How Watcher works]. +==== diff --git a/package.json b/package.json index 985fbfd1b6a2f..1cec7aaacbbc9 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ "@elastic/charts": "^14.0.0", "@elastic/datemath": "5.0.2", "@elastic/ems-client": "1.0.5", - "@elastic/eui": "17.0.0", + "@elastic/eui": "17.1.2", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "2.3.3", @@ -365,8 +365,8 @@ "@types/uuid": "^3.4.4", "@types/vinyl-fs": "^2.4.11", "@types/zen-observable": "^0.8.0", - "@typescript-eslint/eslint-plugin": "^2.10.0", - "@typescript-eslint/parser": "^2.10.0", + "@typescript-eslint/eslint-plugin": "^2.12.0", + "@typescript-eslint/parser": "^2.12.0", "angular-mocks": "^1.7.8", "archiver": "^3.1.1", "axe-core": "^3.3.2", diff --git a/packages/eslint-config-kibana/package.json b/packages/eslint-config-kibana/package.json index 7917297883b03..04602d196a7f3 100644 --- a/packages/eslint-config-kibana/package.json +++ b/packages/eslint-config-kibana/package.json @@ -15,8 +15,8 @@ }, "homepage": "https://github.com/elastic/eslint-config-kibana#readme", "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^2.10.0", - "@typescript-eslint/parser": "^2.10.0", + "@typescript-eslint/eslint-plugin": "^2.12.0", + "@typescript-eslint/parser": "^2.12.0", "babel-eslint": "^10.0.3", "eslint": "^6.5.1", "eslint-plugin-babel": "^5.3.0", diff --git a/packages/kbn-analytics/scripts/build.js b/packages/kbn-analytics/scripts/build.js index b7fbe629246ec..bb28c1460c9c2 100644 --- a/packages/kbn-analytics/scripts/build.js +++ b/packages/kbn-analytics/scripts/build.js @@ -55,7 +55,9 @@ run( '--extensions', '.ts,.js,.tsx', ...(flags.watch ? ['--watch'] : ['--quiet']), - ...(flags['source-maps'] ? ['--source-maps', 'inline'] : []), + ...(!flags['source-maps'] || !!process.env.CODE_COVERAGE + ? [] + : ['--source-maps', 'inline']), ], wait: true, env: { diff --git a/packages/kbn-i18n/scripts/build.js b/packages/kbn-i18n/scripts/build.js index ccdddc87dbc18..0764451c74575 100644 --- a/packages/kbn-i18n/scripts/build.js +++ b/packages/kbn-i18n/scripts/build.js @@ -55,7 +55,9 @@ run( '--extensions', '.ts,.js,.tsx', ...(flags.watch ? ['--watch'] : ['--quiet']), - ...(flags['source-maps'] ? ['--source-maps', 'inline'] : []), + ...(!flags['source-maps'] || !!process.env.CODE_COVERAGE + ? [] + : ['--source-maps', 'inline']), ], wait: true, env: { diff --git a/packages/kbn-utility-types/index.ts b/packages/kbn-utility-types/index.ts index 495b5fb374b43..36bbc8cc82873 100644 --- a/packages/kbn-utility-types/index.ts +++ b/packages/kbn-utility-types/index.ts @@ -18,6 +18,7 @@ */ import { PromiseType } from 'utility-types'; +export { $Values, Required, Optional, Class } from 'utility-types'; /** * Returns wrapped type of a promise. diff --git a/packages/kbn-utility-types/package.json b/packages/kbn-utility-types/package.json index a79d08677020b..a999eb41eb781 100644 --- a/packages/kbn-utility-types/package.json +++ b/packages/kbn-utility-types/package.json @@ -13,7 +13,7 @@ "clean": "del target" }, "dependencies": { - "utility-types": "^3.7.0" + "utility-types": "^3.10.0" }, "devDependencies": { "del-cli": "^3.0.0", diff --git a/rfcs/text/0007_lifecycle_unblocked.md b/rfcs/text/0007_lifecycle_unblocked.md new file mode 100644 index 0000000000000..cb978d3dcd7ba --- /dev/null +++ b/rfcs/text/0007_lifecycle_unblocked.md @@ -0,0 +1,374 @@ +- Start Date: 2019-09-11 +- RFC PR: (leave this empty) +- Kibana Issue: (leave this empty) + +## Table of contents +- [Summary](#summary) +- [Motivation](#motivation) +- [Detailed design](#detailed-design) + - [
  1. Synchronous lifecycle methods
](#ollisynchronous-lifecycle-methodsliol) + - [
  1. Synchronous Context Provider functions
](#ol-start2lisynchronous-context-provider-functionsliol) + - [
  1. Core should not expose API's as observables
](#ol-start3licore-should-not-expose-apis-as-observablesliol) + - [
  1. Complete example code
](#ol-start4licomplete-example-codeliol) + - [
  1. Core should expose a status signal for Core services & plugins
](#ol-start5licore-should-expose-a-status-signal-for-core-services-amp-pluginsliol) +- [Drawbacks](#drawbacks) +- [Alternatives](#alternatives) + - [
  1. Introduce a lifecycle/context provider timeout
](#olliintroduce-a-lifecyclecontext-provider-timeoutliol) + - [
  1. Treat anything that blocks Kibana from starting up as a bug
](#ol-start2litreat-anything-that-blocks-kibana-from-starting-up-as-a-bugliol) +- [Adoption strategy](#adoption-strategy) +- [How we teach this](#how-we-teach-this) +- [Unresolved questions](#unresolved-questions) +- [Footnotes](#footnotes) + +# Summary + +Prevent plugin lifecycle methods from blocking Kibana startup by making the +following changes: +1. Synchronous lifecycle methods +2. Synchronous context provider functions +3. Core should not expose API's as observables + +# Motivation +Plugin lifecycle methods and context provider functions are async +(promise-returning) functions. Core runs these functions in series and waits +for each plugin's lifecycle/context provider function to resolve before +calling the next. This allows plugins to depend on the API's returned from +other plugins. + +With the current design, a single lifecycle method that blocks will block all +of Kibana from starting up. Similarly, a blocking context provider will block +all the handlers that depend on that context. Plugins (including legacy +plugins) rely heavily on this blocking behaviour to ensure that all conditions +required for their plugin's operation are met before their plugin is started +and exposes it's API's. This means a single plugin with a network error that +isn't retried or a dependency on an external host that is down, could block +all of Kibana from starting up. + +We should make it impossible for a single plugin lifecycle function to stall +all of kibana. + +# Detailed design + +### 1. Synchronous lifecycle methods +Lifecycle methods are synchronous functions, they can perform async operations +but Core doesn't wait for these to complete. This guarantees that no plugin +lifecycle function can block other plugins or core from starting up [1]. + +Core will still expose special API's that are able block the setup lifecycle +such as registering Saved Object migrations, but this will be limited to +operations where the risk of blocking all of kibana starting up is limited. + +### 2. Synchronous Context Provider functions +Making context provider functions synchronous guarantees that a context +handler will never be blocked by registered context providers. They can expose +async API's which could potentially have blocking behaviour. + +```ts +export type IContextProvider< + THandler extends HandlerFunction, + TContextName extends keyof HandlerContextType +> = ( + context: Partial>, + ...rest: HandlerParameters +) => + | HandlerContextType[TContextName]; +``` + +### 3. Core should not expose API's as observables +All Core API's should be reactive: when internal state changes, their behaviour +should change accordingly. But, exposing these internal state changes as part +of the API contract leaks internal implementation details consumers can't do +anything useful with and don't care about. + +For example: Core currently exposes `core.elasticsearch.adminClient$`, an +Observable which emits a pre-configured elasticsearch client every time there's +a configuration change. This includes changes to the logging configuration and +might in the future include updating the authentication headers sent to +elasticsearch https://github.com/elastic/kibana/issues/19829. As a plugin +author who wants to make search requests against elasticsearch I shouldn't +have to care about, react to, or keep track of, how many times the underlying +configuration has changed. I want to use the `callAsInternalUser` method and I +expect Core to use the most up to date configuration to send this request. + +> Note: It would not be desirable for Core to dynamically load all +> configuration changes. Changing the Elasticsearch `hosts` could mean Kibana +> is pointing to a completely new Elasticsearch cluster. Since this is a risky +> change to make and would likely require core and almost all plugins to +> completely re-initialize, it's safer to require a complete Kibana restart. + +This does not mean we should remove all observables from Core's API's. When an +API consumer is interested in the *state changes itself* it absolutely makes +sense to expose this as an Observable. Good examples of this is exposing +plugin config as this is state that changes over time to which a plugin should +directly react to. + +This is important in the context of synchronous lifecycle methods and context +handlers since exposing convenient API's become very ugly: + +*(3.1): exposing Observable-based API's through the route handler context:* +```ts +// Before: Using an async context provider +coreSetup.http.registerRouteHandlerContext(coreId, 'core', async (context, req) => { + const adminClient = await coreSetup.elasticsearch.adminClient$.pipe(take(1)).toPromise(); + const dataClient = await coreSetup.elasticsearch.dataClient$.pipe(take(1)).toPromise(); + return { + elasticsearch: { + adminClient: adminClient.asScoped(req), + dataClient: dataClient.asScoped(req), + }, + }; +}); + +// After: Using a synchronous context provider +coreSetup.http.registerRouteHandlerContext(coreId, 'core', async (context, req) => { + return { + elasticsearch: { + // (3.1.1) We can expose a convenient API by doing a lot of work + adminClient: () => { + callAsInternalUser: async (...args) => { + const adminClient = await coreSetup.elasticsearch.adminClient$.pipe(take(1)).toPromise(); + return adminClient.asScoped(req).callAsinternalUser(args); + }, + callAsCurrentUser: async (...args) => { + adminClient = await coreSetup.elasticsearch.adminClient$.pipe(take(1)).toPromise(); + return adminClient.asScoped(req).callAsCurrentUser(args); + } + }, + // (3.1.2) Or a lazy approach which perpetuates the problem to consumers: + dataClient: async () => { + const dataClient = await coreSetup.elasticsearch.dataClient$.pipe(take(1)).toPromise(); + return dataClient.asScoped(req); + }, + }, + }; +}); +``` + +### 4. Complete example code +*(4.1) Doing async operations in a plugin's setup lifecycle* +```ts +export class Plugin { + public setup(core: CoreSetup) { + // Async setup is possible and any operations involving async API's + // will still block until these API's are ready, (savedObjects find only + // resolves once the elasticsearch client has established a connection to + // the cluster). The difference is that these details are now internal to + // the API. + (async () => { + const docs = await core.savedObjects.client.find({...}); + ... + await core.savedObjects.client.update(...); + })(); + } +} +``` + +*(4.2) Exposing an API from a plugin's setup lifecycle* +```ts +export class Plugin { + constructor(private readonly initializerContext: PluginInitializerContext) {} + private async initSavedConfig(core: CoreSetup) { + // Note: pulling a config value here means our code isn't reactive to + // changes, but this is equivalent to doing it in an async setup lifecycle. + const config = await this.initializerContext.config + .create>() + .pipe(first()) + .toPromise(); + try { + const savedConfig = await core.savedObjects.internalRepository.get({...}); + return Object.assign({}, config, savedConfig); + } catch (e) { + if (SavedObjectErrorHelpers.isNotFoundError(e)) { + return await core.savedObjects.internalRepository.create(config, {...}); + } + } + } + public setup(core: CoreSetup) { + // savedConfigPromise resolves with the same kind of "setup state" that a + // plugin would have constructed in an async setup lifecycle. + const savedConfigPromise = initSavedConfig(core); + return { + ping: async () => { + const savedConfig = await savedConfigPromise; + if (config.allowPing === false || savedConfig.allowPing === false) { + throw new Error('ping() has been disabled'); + } + // Note: the elasticsearch client no longer exposes an adminClient$ + // observable, improving the ergonomics of consuming the API. + return await core.elasticsearch.adminClient.callAsInternalUser('ping', ...); + } + }; + } +} +``` + +*(4.3) Exposing an observable free Elasticsearch API from the route context* +```ts +coreSetup.http.registerRouteHandlerContext(coreId, 'core', async (context, req) => { + return { + elasticsearch: { + adminClient: coreSetup.elasticsearch.adminClient.asScoped(req), + dataClient: coreSetup.elasticsearch.adminClient.asScoped(req), + }, + }; +}); +``` + +### 5. Core should expose a status signal for Core services & plugins +Core should expose a global mechanism for core services and plugins to signal +their status. This is equivalent to the legacy status API +`kibana.Plugin.status` which allowed plugins to set their status to e.g. 'red' +or 'green'. The exact design of this API is outside of the scope of this RFC. + +What is important, is that there is a global mechanism to signal status +changes which Core then makes visible to system administrators in the Kibana +logs and the `/status` HTTP API. Plugins should be able to inspect and +subscribe to status changes from any of their dependencies. + +This will provide an obvious mechanism for plugins to signal that the +conditions which are required for this plugin to operate are not currently +present and manual intervention might be required. Status changes can happen +in both setup and start lifecycles e.g.: + - [setup] a required remote host is down + - [start] a remote host which was up during setup, started returning + connection timeout errors. + +# Drawbacks +Not being able to block on a lifecycle method means plugins can no longer be +certain that all setup is "complete" before they expose their API's or reach +the start lifecycle. + +A plugin might want to poll an external host to ensure that the host is up in +its setup lifecycle before making network requests to this host in it's start +lifecycle. + +Even if Kibana was using a valid, but incorrect configuration for the remote +host, with synchronous lifecycles Kibana would still start up. Although the +status API and logs would indicate a problem, these might not be monitored +leading to the error only being discovered once someone tries to use it's +functionality. This is an acceptable drawback because it buys us isolation. +Some problems might go unnoticed, but no single plugin should affect the +availability of all other plugins. + +In effect, the plugin is polling the world to construct a snapshot +of state which drives future behaviour. Modeling this with lifecycle functions +is insufficient since it assumes that any state constructed in the setup +lifecycle is static and won't and can't be changed in the future. + +For example: a plugin's setup lifecycle might poll for the existence of a +custom Elasticsearch index and if it doesn't exist, create it. Should there be +an Elasticsearch restore which deletes the index, the plugin wouldn't be able +to gracefully recover by simply running it's setup lifecycle a second time. + +The once-off nature of lifecycle methods are incompatible with the real-world +dynamic conditions under which plugins run. Not being able to block a +lifecycle method is, therefore, only a drawback when plugins are authored under +the false illusion of stability. + +# Alternatives +## 1. Introduce a lifecycle/context provider timeout +Lifecycle methods and context providers would timeout after X seconds and any +API's they expose would not be available if the timeout had been reached. + +Drawbacks: +1. A blocking setup lifecycle makes it easy for plugin authors to fall into + the trap of assuming that their plugin's behaviour can continue to operate + based on the snapshot of conditions present during setup. + +2. For lifecycle methods: there would be no way to recover from a timeout, + once a timeout had been reached the API will remain unavailable. + + Context providers have the benefit of being re-created for each handler + call, so a single timeout would not permanently disable the API. + +3. Plugins have less control over their behaviour. When an upstream server + becomes unavailable, a plugin might prefer to keep retrying the request + indefinitely or only timeout after more than X seconds. It also isn't able + to expose detailed error information to downstream consumers such as + specifying which host or service is unavailable. + +4. (minor) Introduces an additional failure condition that needs to be handled. + Consumers should handle the API not being available in setup, as well as, + error responses from the API itself. Since remote hosts like Elasticsearch + could go down even after a successful setup, this effectively means API + consumers have to handle the same error condition in two places. + +## 2. Treat anything that blocks Kibana from starting up as a bug +Keep the existing New Platform blocking behaviour, but through strong +conventions and developer awareness minimize the risk of plugins blocking +Kibana's startup indefinetely. By logging detailed diagnostic info on any +plugins that appear to be blocking startup, we can aid system administrators +to recover a blocked Kibana. + +A parallel can be drawn between Kibana's async plugin initialization and the TC39 +proposal for [top-level await](https://github.com/tc39/proposal-top-level-await). +> enables modules to act as big async functions: With top-level await, +> ECMAScript Modules (ESM) can await resources, causing other modules who +> import them to wait before they start evaluating their body + +They believe the benefits outweigh the risk of modules blocking loading since: + - [developer education should result in correct usage](https://github.com/tc39/proposal-top-level-await#will-top-level-await-cause-developers-to-make-their-code-block-longer-than-it-should) + - [there are existing unavoidable ways in which modules could block loading such as infinite loops or recursion](https://github.com/tc39/proposal-top-level-await#does-top-level-await-increase-the-risk-of-deadlocks) + + +Drawbacks: +1. A blocking setup lifecycle makes it easy for plugin authors to fall into + the trap of assuming that their plugin's behaviour can continue to operate + based on the snapshot of conditions present during setup. +2. This opens up the potential for a bug in Elastic or third-party plugins to + effectively "break" kibana. Instead of a single plugin being disabled all + of kibana would be down requiring manual intervention by a system + administrator. + +# Adoption strategy +Although the eventual goal is to have sync-only lifecycles / providers, we +will start by deprecating async behaviour and implementing a 30s timeout as +per alternative (1). This will immediately lower the impact of plugin bugs +while at the same time enabling a more incremental rollout and the flexibility +to discover use cases that would require adopting Core API's to support sync +lifecycles / providers. + +Adoption and implementation should be handled as follows: + - Adopt Core API’s to make sync lifecycles easier (3) + - Update migration guide and other documentation examples. + - Deprecate async lifecycles / context providers with a warning. Add a + timeout of 30s after which a plugin and it's dependencies will be disabled. + - Refactor existing plugin lifecycles which are easily converted to sync + - Future: remove async timeout lifecycles / context providers + +The following New Platform plugins or shims currently rely on async lifecycle +functions and will be impacted: +1. [region_map](https://github.com/elastic/kibana/blob/6039709929caf0090a4130b8235f3a53bd04ed84/src/legacy/core_plugins/region_map/public/plugin.ts#L68) +2. [tile_map](https://github.com/elastic/kibana/blob/6039709929caf0090a4130b8235f3a53bd04ed84/src/legacy/core_plugins/tile_map/public/plugin.ts#L62) +3. [vis_type_table](https://github.com/elastic/kibana/blob/6039709929caf0090a4130b8235f3a53bd04ed84/src/legacy/core_plugins/vis_type_table/public/plugin.ts#L61) +4. [vis_type_vega](https://github.com/elastic/kibana/blob/6039709929caf0090a4130b8235f3a53bd04ed84/src/legacy/core_plugins/vis_type_vega/public/plugin.ts#L59) +5. [timelion](https://github.com/elastic/kibana/blob/9d69b72a5f200e58220231035b19da852fc6b0a5/src/plugins/timelion/server/plugin.ts#L40) +6. [code](https://github.com/elastic/kibana/blob/5049b460b47d4ae3432e1d9219263bb4be441392/x-pack/legacy/plugins/code/server/plugin.ts#L129-L149) +7. [spaces](https://github.com/elastic/kibana/blob/096c7ee51136327f778845c636d7c4f1188e5db2/x-pack/legacy/plugins/spaces/server/new_platform/plugin.ts#L95) +8. [licensing](https://github.com/elastic/kibana/blob/4667c46caef26f8f47714504879197708debae32/x-pack/plugins/licensing/server/plugin.ts) +9. [security](https://github.com/elastic/kibana/blob/0f2324e44566ce2cf083d89082841e57d2db6ef6/x-pack/plugins/security/server/plugin.ts#L96) + +# How we teach this + +Async Plugin lifecycle methods and async context provider functions have been +deprecated. In the future all lifecycle methods will by sync only. Plugins +should treat the setup lifecycle as a place in time to register functionality +with core or other plugins' API's and not as a mechanism to kick off and wait +for any initialization that's required for the plugin to be able to run. + +# Unresolved questions +1. ~~Are the drawbacks worth the benefits or can we live with Kibana potentially +being blocked for the sake of convenient async lifecycle stages?~~ + +2. Should core provide conventions or patterns for plugins to construct a + snapshot of state and reactively updating this state and the behaviour it + drives as the state of the world changes? + +3. Do plugins ever need to read config values and pass these as parameters to + Core API’s? If so we would have to expose synchronous config values to + support sync lifecycles. + +# Footnotes +[1] Synchronous lifecycles can still be blocked by e.g. an infine for loop, +but this would always be unintentional behaviour in contrast to intentional +async behaviour like blocking until an external service becomes available. diff --git a/src/cli/cluster/__mocks__/cluster.js b/src/cli/cluster/cluster.mock.ts similarity index 85% rename from src/cli/cluster/__mocks__/cluster.js rename to src/cli/cluster/cluster.mock.ts index d653771136ae6..332f8aad53ba1 100644 --- a/src/cli/cluster/__mocks__/cluster.js +++ b/src/cli/cluster/cluster.mock.ts @@ -18,12 +18,15 @@ */ /* eslint-env jest */ +// eslint-disable-next-line max-classes-per-file import EventEmitter from 'events'; import { assign, random } from 'lodash'; import { delay } from 'bluebird'; class MockClusterFork extends EventEmitter { - constructor(cluster) { + public exitCode = 0; + + constructor(cluster: MockCluster) { super(); let dead = true; @@ -49,9 +52,9 @@ class MockClusterFork extends EventEmitter { send: jest.fn(), }); - jest.spyOn(this, 'on'); - jest.spyOn(this, 'off'); - jest.spyOn(this, 'emit'); + jest.spyOn(this as EventEmitter, 'on'); + jest.spyOn(this as EventEmitter, 'off'); + jest.spyOn(this as EventEmitter, 'emit'); (async () => { await wait(); @@ -61,11 +64,7 @@ class MockClusterFork extends EventEmitter { } } -class MockCluster extends EventEmitter { +export class MockCluster extends EventEmitter { fork = jest.fn(() => new MockClusterFork(this)); setupMaster = jest.fn(); } - -export function mockCluster() { - return new MockCluster(); -} diff --git a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualization_register.js b/src/cli/cluster/cluster_manager.test.mocks.ts similarity index 78% rename from src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualization_register.js rename to src/cli/cluster/cluster_manager.test.mocks.ts index c50cda56c7151..53984fd12cbf1 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualization_register.js +++ b/src/cli/cluster/cluster_manager.test.mocks.ts @@ -17,9 +17,6 @@ * under the License. */ -import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; -import './saved_visualizations'; - -SavedObjectRegistryProvider.register(savedVisualizations => { - return savedVisualizations; -}); +import { MockCluster } from './cluster.mock'; +export const mockCluster = new MockCluster(); +jest.mock('cluster', () => mockCluster); diff --git a/src/cli/cluster/cluster_manager.test.js b/src/cli/cluster/cluster_manager.test.ts similarity index 84% rename from src/cli/cluster/cluster_manager.test.js rename to src/cli/cluster/cluster_manager.test.ts index be8a096db9a66..bd37e854e1691 100644 --- a/src/cli/cluster/cluster_manager.test.js +++ b/src/cli/cluster/cluster_manager.test.ts @@ -17,8 +17,7 @@ * under the License. */ -import { mockCluster } from './__mocks__/cluster'; -jest.mock('cluster', () => mockCluster()); +import { mockCluster } from './cluster_manager.test.mocks'; jest.mock('readline', () => ({ createInterface: jest.fn(() => ({ on: jest.fn(), @@ -27,15 +26,14 @@ jest.mock('readline', () => ({ })), })); -import cluster from 'cluster'; import { sample } from 'lodash'; -import ClusterManager from './cluster_manager'; -import Worker from './worker'; +import { ClusterManager } from './cluster_manager'; +import { Worker } from './worker'; describe('CLI cluster manager', () => { beforeEach(() => { - cluster.fork.mockImplementation(() => { + mockCluster.fork.mockImplementation(() => { return { process: { kill: jest.fn(), @@ -44,16 +42,16 @@ describe('CLI cluster manager', () => { off: jest.fn(), on: jest.fn(), send: jest.fn(), - }; + } as any; }); }); afterEach(() => { - cluster.fork.mockReset(); + mockCluster.fork.mockReset(); }); test('has two workers', () => { - const manager = ClusterManager.create({}); + const manager = new ClusterManager({}, {} as any); expect(manager.workers).toHaveLength(2); for (const worker of manager.workers) expect(worker).toBeInstanceOf(Worker); @@ -63,7 +61,7 @@ describe('CLI cluster manager', () => { }); test('delivers broadcast messages to other workers', () => { - const manager = ClusterManager.create({}); + const manager = new ClusterManager({}, {} as any); for (const worker of manager.workers) { Worker.prototype.start.call(worker); // bypass the debounced start method @@ -76,10 +74,10 @@ describe('CLI cluster manager', () => { messenger.emit('broadcast', football); for (const worker of manager.workers) { if (worker === messenger) { - expect(worker.fork.send).not.toHaveBeenCalled(); + expect(worker.fork!.send).not.toHaveBeenCalled(); } else { - expect(worker.fork.send).toHaveBeenCalledTimes(1); - expect(worker.fork.send).toHaveBeenCalledWith(football); + expect(worker.fork!.send).toHaveBeenCalledTimes(1); + expect(worker.fork!.send).toHaveBeenCalledWith(football); } } }); @@ -88,7 +86,7 @@ describe('CLI cluster manager', () => { test('correctly configures `BasePathProxy`.', async () => { const basePathProxyMock = { start: jest.fn() }; - ClusterManager.create({}, {}, basePathProxyMock); + new ClusterManager({}, {} as any, basePathProxyMock as any); expect(basePathProxyMock.start).toHaveBeenCalledWith({ shouldRedirectFromOldBasePath: expect.any(Function), @@ -97,13 +95,13 @@ describe('CLI cluster manager', () => { }); describe('proxy is configured with the correct `shouldRedirectFromOldBasePath` and `blockUntil` functions.', () => { - let clusterManager; - let shouldRedirectFromOldBasePath; - let blockUntil; + let clusterManager: ClusterManager; + let shouldRedirectFromOldBasePath: (path: string) => boolean; + let blockUntil: () => Promise; beforeEach(async () => { const basePathProxyMock = { start: jest.fn() }; - clusterManager = ClusterManager.create({}, {}, basePathProxyMock); + clusterManager = new ClusterManager({}, {} as any, basePathProxyMock as any); jest.spyOn(clusterManager.server, 'on'); jest.spyOn(clusterManager.server, 'off'); @@ -146,7 +144,7 @@ describe('CLI cluster manager', () => { expect(clusterManager.server.on).toHaveBeenCalledTimes(2); expect(clusterManager.server.on).toHaveBeenCalledWith('crashed', expect.any(Function)); - const [, [eventName, onCrashed]] = clusterManager.server.on.mock.calls; + const [, [eventName, onCrashed]] = (clusterManager.server.on as jest.Mock).mock.calls; // Check event name to make sure we call the right callback, // in Jest 23 we could use `toHaveBeenNthCalledWith` instead. expect(eventName).toBe('crashed'); @@ -164,7 +162,7 @@ describe('CLI cluster manager', () => { expect(clusterManager.server.on).toHaveBeenCalledTimes(2); expect(clusterManager.server.on).toHaveBeenCalledWith('listening', expect.any(Function)); - const [[eventName, onListening]] = clusterManager.server.on.mock.calls; + const [[eventName, onListening]] = (clusterManager.server.on as jest.Mock).mock.calls; // Check event name to make sure we call the right callback, // in Jest 23 we could use `toHaveBeenNthCalledWith` instead. expect(eventName).toBe('listening'); diff --git a/src/cli/cluster/cluster_manager.js b/src/cli/cluster/cluster_manager.ts similarity index 83% rename from src/cli/cluster/cluster_manager.js rename to src/cli/cluster/cluster_manager.ts index cd1b3a0dadfc6..d97f7485fb4d2 100644 --- a/src/cli/cluster/cluster_manager.js +++ b/src/cli/cluster/cluster_manager.ts @@ -20,26 +20,38 @@ import { resolve } from 'path'; import { format as formatUrl } from 'url'; import opn from 'opn'; - import { debounce, invoke, bindAll, once, uniq } from 'lodash'; import * as Rx from 'rxjs'; import { first, mapTo, filter, map, take } from 'rxjs/operators'; import { REPO_ROOT } from '@kbn/dev-utils'; +import { FSWatcher } from 'chokidar'; + +import { LegacyConfig } from '../../core/server/legacy/config'; +import { BasePathProxyServer } from '../../core/server/http'; +// @ts-ignore import Log from '../log'; -import Worker from './worker'; -import { Config } from '../../legacy/server/config/config'; +import { Worker } from './worker'; process.env.kbnWorkerType = 'managr'; -export default class ClusterManager { - static create(opts, settings = {}, basePathProxy) { - return new ClusterManager(opts, Config.withDefaultSchema(settings), basePathProxy); - } - - constructor(opts, config, basePathProxy) { +export class ClusterManager { + public optimizer: Worker; + public server: Worker; + public workers: Worker[]; + + private watcher: FSWatcher | null = null; + private basePathProxy: BasePathProxyServer | undefined; + private log: any; + private addedCount = 0; + private inReplMode: boolean; + + constructor( + opts: Record, + config: LegacyConfig, + basePathProxy?: BasePathProxyServer + ) { this.log = new Log(opts.quiet, opts.silent); - this.addedCount = 0; this.inReplMode = !!opts.repl; this.basePathProxy = basePathProxy; @@ -79,7 +91,7 @@ export default class ClusterManager { worker.on('broadcast', msg => { this.workers.forEach(to => { if (to !== worker && to.online) { - to.fork.send(msg); + to.fork!.send(msg); } }); }); @@ -90,10 +102,10 @@ export default class ClusterManager { // and all workers. This is only used by LogRotator service // when the cluster mode is enabled this.server.on('reloadLoggingConfigFromServerWorker', () => { - process.emit('message', { reloadLoggingConfig: true }); + process.emit('message' as any, { reloadLoggingConfig: true } as any); this.workers.forEach(worker => { - worker.fork.send({ reloadLoggingConfig: true }); + worker.fork!.send({ reloadLoggingConfig: true }); }); }); @@ -111,9 +123,9 @@ export default class ClusterManager { } if (opts.watch) { - const pluginPaths = config.get('plugins.paths'); + const pluginPaths = config.get('plugins.paths'); const scanDirs = [ - ...config.get('plugins.scanDirs'), + ...config.get('plugins.scanDirs'), resolve(REPO_ROOT, 'src/plugins'), resolve(REPO_ROOT, 'x-pack/plugins'), ]; @@ -131,7 +143,7 @@ export default class ClusterManager { resolve(path, 'scripts'), resolve(path, 'docs') ), - [] + [] as string[] ); this.setupWatching(extraPaths, pluginInternalDirsIgnore); @@ -149,7 +161,7 @@ export default class ClusterManager { } } - setupOpen(openUrl) { + setupOpen(openUrl: string) { const serverListening$ = Rx.merge( Rx.fromEvent(this.server, 'listening').pipe(mapTo(true)), Rx.fromEvent(this.server, 'fork:exit').pipe(mapTo(false)), @@ -157,7 +169,7 @@ export default class ClusterManager { ); const optimizeSuccess$ = Rx.fromEvent(this.optimizer, 'optimizeStatus').pipe( - map(msg => !!msg.success) + map((msg: any) => !!msg.success) ); Rx.combineLatest(serverListening$, optimizeSuccess$) @@ -169,8 +181,10 @@ export default class ClusterManager { .then(() => opn(openUrl)); } - setupWatching(extraPaths, pluginInternalDirsIgnore) { + setupWatching(extraPaths: string[], pluginInternalDirsIgnore: string[]) { + // eslint-disable-next-line @typescript-eslint/no-var-requires const chokidar = require('chokidar'); + // eslint-disable-next-line @typescript-eslint/no-var-requires const { fromRoot } = require('../../core/server/utils'); const watchPaths = [ @@ -204,7 +218,7 @@ export default class ClusterManager { ...ignorePaths, 'plugins/java_languageserver', ], - }); + }) as FSWatcher; this.watcher.on('add', this.onWatcherAdd); this.watcher.on('error', this.onWatcherError); @@ -213,8 +227,8 @@ export default class ClusterManager { 'ready', once(() => { // start sending changes to workers - this.watcher.removeListener('add', this.onWatcherAdd); - this.watcher.on('all', this.onWatcherChange); + this.watcher!.removeListener('add', this.onWatcherAdd); + this.watcher!.on('all', this.onWatcherChange); this.log.good('watching for changes', `(${this.addedCount} files)`); this.startCluster(); @@ -229,6 +243,7 @@ export default class ClusterManager { if (this.inReplMode) { return; } + // eslint-disable-next-line @typescript-eslint/no-var-requires const readline = require('readline'); const rl = readline.createInterface(process.stdin, process.stdout); @@ -263,16 +278,16 @@ export default class ClusterManager { this.addedCount += 1; } - onWatcherChange(e, path) { + onWatcherChange(e: any, path: string) { invoke(this.workers, 'onChange', path); } - onWatcherError(err) { + onWatcherError(err: any) { this.log.bad('failed to watch files!\n', err.stack); process.exit(1); // eslint-disable-line no-process-exit } - shouldRedirectFromOldBasePath(path) { + shouldRedirectFromOldBasePath(path: string) { // strip `s/{id}` prefix when checking for need to redirect if (path.startsWith('s/')) { path = path diff --git a/src/cli/cluster/worker.test.js b/src/cli/cluster/worker.test.ts similarity index 80% rename from src/cli/cluster/worker.test.js rename to src/cli/cluster/worker.test.ts index b43cc123abcbb..4f9337681e083 100644 --- a/src/cli/cluster/worker.test.js +++ b/src/cli/cluster/worker.test.ts @@ -17,22 +17,20 @@ * under the License. */ -import { mockCluster } from './__mocks__/cluster'; -jest.mock('cluster', () => mockCluster()); +import { mockCluster } from './cluster_manager.test.mocks'; -import cluster from 'cluster'; - -import Worker from './worker'; +import { Worker, ClusterWorker } from './worker'; +// @ts-ignore import Log from '../log'; -const workersToShutdown = []; +const workersToShutdown: Worker[] = []; -function assertListenerAdded(emitter, event) { +function assertListenerAdded(emitter: NodeJS.EventEmitter, event: any) { expect(emitter.on).toHaveBeenCalledWith(event, expect.any(Function)); } -function assertListenerRemoved(emitter, event) { - const [, onEventListener] = emitter.on.mock.calls.find(([eventName]) => { +function assertListenerRemoved(emitter: NodeJS.EventEmitter, event: any) { + const [, onEventListener] = (emitter.on as jest.Mock).mock.calls.find(([eventName]) => { return eventName === event; }); @@ -44,6 +42,7 @@ function setup(opts = {}) { log: new Log(false, true), ...opts, baseArgv: [], + type: 'test', }); workersToShutdown.push(worker); @@ -53,7 +52,7 @@ function setup(opts = {}) { describe('CLI cluster manager', () => { afterEach(async () => { while (workersToShutdown.length > 0) { - const worker = workersToShutdown.pop(); + const worker = workersToShutdown.pop() as Worker; // If `fork` exists we should set `exitCode` to the non-zero value to // prevent worker from auto restart. if (worker.fork) { @@ -63,14 +62,14 @@ describe('CLI cluster manager', () => { await worker.shutdown(); } - cluster.fork.mockClear(); + mockCluster.fork.mockClear(); }); describe('#onChange', () => { describe('opts.watch = true', () => { test('restarts the fork', () => { const worker = setup({ watch: true }); - jest.spyOn(worker, 'start').mockImplementation(() => {}); + jest.spyOn(worker, 'start').mockResolvedValue(); worker.onChange('/some/path'); expect(worker.changes).toEqual(['/some/path']); expect(worker.start).toHaveBeenCalledTimes(1); @@ -80,7 +79,7 @@ describe('CLI cluster manager', () => { describe('opts.watch = false', () => { test('does not restart the fork', () => { const worker = setup({ watch: false }); - jest.spyOn(worker, 'start').mockImplementation(() => {}); + jest.spyOn(worker, 'start').mockResolvedValue(); worker.onChange('/some/path'); expect(worker.changes).toEqual([]); expect(worker.start).not.toHaveBeenCalled(); @@ -94,13 +93,13 @@ describe('CLI cluster manager', () => { const worker = setup(); await worker.start(); expect(worker).toHaveProperty('online', true); - const fork = worker.fork; - expect(fork.process.kill).not.toHaveBeenCalled(); + const fork = worker.fork as ClusterWorker; + expect(fork!.process.kill).not.toHaveBeenCalled(); assertListenerAdded(fork, 'message'); assertListenerAdded(fork, 'online'); assertListenerAdded(fork, 'disconnect'); await worker.shutdown(); - expect(fork.process.kill).toHaveBeenCalledTimes(1); + expect(fork!.process.kill).toHaveBeenCalledTimes(1); assertListenerRemoved(fork, 'message'); assertListenerRemoved(fork, 'online'); assertListenerRemoved(fork, 'disconnect'); @@ -120,7 +119,7 @@ describe('CLI cluster manager', () => { test(`is bound to fork's message event`, async () => { const worker = setup(); await worker.start(); - expect(worker.fork.on).toHaveBeenCalledWith('message', expect.any(Function)); + expect(worker.fork!.on).toHaveBeenCalledWith('message', expect.any(Function)); }); }); @@ -138,8 +137,8 @@ describe('CLI cluster manager', () => { test('calls #onMessage with message parts', () => { const worker = setup(); jest.spyOn(worker, 'onMessage').mockImplementation(() => {}); - worker.parseIncomingMessage([10, 100, 1000, 10000]); - expect(worker.onMessage).toHaveBeenCalledWith(10, 100, 1000, 10000); + worker.parseIncomingMessage(['event', 'some-data']); + expect(worker.onMessage).toHaveBeenCalledWith('event', 'some-data'); }); }); }); @@ -149,7 +148,7 @@ describe('CLI cluster manager', () => { test('emits the data to be broadcasted', () => { const worker = setup(); const data = {}; - jest.spyOn(worker, 'emit').mockImplementation(() => {}); + jest.spyOn(worker, 'emit').mockImplementation(() => true); worker.onMessage('WORKER_BROADCAST', data); expect(worker.emit).toHaveBeenCalledWith('broadcast', data); }); @@ -158,7 +157,7 @@ describe('CLI cluster manager', () => { describe('when sent WORKER_LISTENING message', () => { test('sets the listening flag and emits the listening event', () => { const worker = setup(); - jest.spyOn(worker, 'emit').mockImplementation(() => {}); + jest.spyOn(worker, 'emit').mockImplementation(() => true); expect(worker).toHaveProperty('listening', false); worker.onMessage('WORKER_LISTENING'); expect(worker).toHaveProperty('listening', true); @@ -170,8 +169,6 @@ describe('CLI cluster manager', () => { test('does nothing', () => { const worker = setup(); worker.onMessage('asdlfkajsdfahsdfiohuasdofihsdoif'); - worker.onMessage({}); - worker.onMessage(23049283094); }); }); }); @@ -185,7 +182,7 @@ describe('CLI cluster manager', () => { await worker.start(); - expect(cluster.fork).toHaveBeenCalledTimes(1); + expect(mockCluster.fork).toHaveBeenCalledTimes(1); expect(worker.on).toHaveBeenCalledWith('fork:online', expect.any(Function)); }); @@ -193,12 +190,12 @@ describe('CLI cluster manager', () => { const worker = setup(); jest.spyOn(process, 'on'); - jest.spyOn(cluster, 'on'); + jest.spyOn(mockCluster, 'on'); await worker.start(); - expect(cluster.on).toHaveBeenCalledTimes(1); - expect(cluster.on).toHaveBeenCalledWith('exit', expect.any(Function)); + expect(mockCluster.on).toHaveBeenCalledTimes(1); + expect(mockCluster.on).toHaveBeenCalledWith('exit', expect.any(Function)); expect(process.on).toHaveBeenCalledTimes(1); expect(process.on).toHaveBeenCalledWith('exit', expect.any(Function)); }); diff --git a/src/cli/cluster/worker.js b/src/cli/cluster/worker.ts similarity index 75% rename from src/cli/cluster/worker.js rename to src/cli/cluster/worker.ts index 2250075f20a60..fb87f1a87654c 100644 --- a/src/cli/cluster/worker.js +++ b/src/cli/cluster/worker.ts @@ -21,25 +21,57 @@ import _ from 'lodash'; import cluster from 'cluster'; import { EventEmitter } from 'events'; -import { BinderFor } from '../../legacy/utils'; +import { BinderFor } from '../../legacy/utils/binder_for'; import { fromRoot } from '../../core/server/utils'; const cliPath = fromRoot('src/cli'); const baseArgs = _.difference(process.argv.slice(2), ['--no-watch']); const baseArgv = [process.execPath, cliPath].concat(baseArgs); +export type ClusterWorker = cluster.Worker & { + killed: boolean; + exitCode?: number; +}; + cluster.setupMaster({ exec: cliPath, silent: false, }); -const dead = fork => { +const dead = (fork: ClusterWorker) => { return fork.isDead() || fork.killed; }; -export default class Worker extends EventEmitter { - constructor(opts) { - opts = opts || {}; +interface WorkerOptions { + type: string; + log: any; // src/cli/log.js + argv?: string[]; + title?: string; + watch?: boolean; + baseArgv?: string[]; +} + +export class Worker extends EventEmitter { + private readonly clusterBinder: BinderFor; + private readonly processBinder: BinderFor; + + private type: string; + private title: string; + private log: any; + private forkBinder: BinderFor | null = null; + private startCount: number; + private watch: boolean; + private env: Record; + + public fork: ClusterWorker | null = null; + public changes: string[]; + + // status flags + public online = false; // the fork can accept messages + public listening = false; // the fork is listening for connections + public crashed = false; // the fork crashed + + constructor(opts: WorkerOptions) { super(); this.log = opts.log; @@ -48,15 +80,9 @@ export default class Worker extends EventEmitter { this.watch = opts.watch !== false; this.startCount = 0; - // status flags - this.online = false; // the fork can accept messages - this.listening = false; // the fork is listening for connections - this.crashed = false; // the fork crashed - this.changes = []; - this.forkBinder = null; // defined when the fork is - this.clusterBinder = new BinderFor(cluster); + this.clusterBinder = new BinderFor(cluster as any); // lack the 'off' method this.processBinder = new BinderFor(process); this.env = { @@ -66,7 +92,7 @@ export default class Worker extends EventEmitter { }; } - onExit(fork, code) { + onExit(fork: ClusterWorker, code: number) { if (this.fork !== fork) return; // we have our fork's exit, so stop listening for others @@ -91,7 +117,7 @@ export default class Worker extends EventEmitter { } } - onChange(path) { + onChange(path: string) { if (!this.watch) return; this.changes.push(path); this.start(); @@ -104,7 +130,7 @@ export default class Worker extends EventEmitter { this.fork.killed = true; // stop listening to the fork, it's just going to die - this.forkBinder.destroy(); + this.forkBinder!.destroy(); // we don't need to react to process.exit anymore this.processBinder.destroy(); @@ -114,12 +140,14 @@ export default class Worker extends EventEmitter { } } - parseIncomingMessage(msg) { - if (!Array.isArray(msg)) return; - this.onMessage(...msg); + parseIncomingMessage(msg: any) { + if (!Array.isArray(msg)) { + return; + } + this.onMessage(msg[0], msg[1]); } - onMessage(type, data) { + onMessage(type: string, data?: any) { switch (type) { case 'WORKER_BROADCAST': this.emit('broadcast', data); @@ -170,16 +198,16 @@ export default class Worker extends EventEmitter { this.log.warn(`restarting ${this.title}...`); } - this.fork = cluster.fork(this.env); + this.fork = cluster.fork(this.env) as ClusterWorker; this.forkBinder = new BinderFor(this.fork); // when the fork sends a message, comes online, or loses its connection, then react - this.forkBinder.on('message', msg => this.parseIncomingMessage(msg)); + this.forkBinder.on('message', (msg: any) => this.parseIncomingMessage(msg)); this.forkBinder.on('online', () => this.onOnline()); this.forkBinder.on('disconnect', () => this.onDisconnect()); // when the cluster says a fork has exited, check if it is ours - this.clusterBinder.on('exit', (fork, code) => this.onExit(fork, code)); + this.clusterBinder.on('exit', (fork: ClusterWorker, code: number) => this.onExit(fork, code)); // when the process exits, make sure we kill our workers this.processBinder.on('exit', () => this.shutdown()); diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 5bb22579d123e..1c78de966c46f 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -46,6 +46,8 @@ - [How to](#how-to) - [Configure plugin](#configure-plugin) - [Handle plugin configuration deprecations](#handle-plugin-config-deprecations) + - [Use scoped services](#use-scoped-services) + - [Declare a custom scoped service](#declare-a-custom-scoped-service) - [Mock new platform services in tests](#mock-new-platform-services-in-tests) - [Writing mocks for your plugin](#writing-mocks-for-your-plugin) - [Using mocks in your tests](#using-mocks-in-your-tests) @@ -1190,22 +1192,23 @@ In server code, `core` can be accessed from either `server.newPlatform` or `kbnS | `server.config()` | [`initializerContext.config.create()`](/docs/development/core/server/kibana-plugin-server.plugininitializercontext.config.md) | Must also define schema. See _[how to configure plugin](#configure-plugin)_ | | `server.route` | [`core.http.createRouter`](/docs/development/core/server/kibana-plugin-server.httpservicesetup.createrouter.md) | [Examples](./MIGRATION_EXAMPLES.md#route-registration) | | `request.getBasePath()` | [`core.http.basePath.get`](/docs/development/core/server/kibana-plugin-server.httpservicesetup.basepath.md) | | -| `server.plugins.elasticsearch.getCluster('data')` | [`core.elasticsearch.dataClient$`](/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient_.md) | Handlers will also include a pre-configured client | -| `server.plugins.elasticsearch.getCluster('admin')` | [`core.elasticsearch.adminClient$`](/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient_.md) | Handlers will also include a pre-configured client | -| `xpackMainPlugin.info.feature(pluginID).registerLicenseCheckResultsGenerator` | [`x-pack licensing plugin`](/x-pack/plugins/licensing/README.md) | | +| `server.plugins.elasticsearch.getCluster('data')` | [`context.elasticsearch.dataClient`](/docs/development/core/server/kibana-plugin-server.iscopedclusterclient.md) | | +| `server.plugins.elasticsearch.getCluster('admin')` | [`context.elasticsearch.adminClient`](/docs/development/core/server/kibana-plugin-server.iscopedclusterclient.md) | | | `server.savedObjects.setScopedSavedObjectsClientFactory` | [`core.savedObjects.setClientFactory`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.setclientfactory.md) | | | `server.savedObjects.addScopedSavedObjectsClientWrapperFactory` | [`core.savedObjects.addClientWrapper`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.addclientwrapper.md) | | | `server.savedObjects.getSavedObjectsRepository` | [`core.savedObjects.createInternalRepository`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.createinternalrepository.md) [`core.savedObjects.createScopedRepository`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.createscopedrepository.md) | | | `server.savedObjects.getScopedSavedObjectsClient` | [`core.savedObjects.getScopedClient`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicestart.getscopedclient.md) | | | `request.getSavedObjectsClient` | [`context.core.savedObjects.client`](/docs/development/core/server/kibana-plugin-server.requesthandlercontext.core.md) | | +| `request.getUiSettingsService` | [`context.uiSettings.client`](/docs/development/core/server/kibana-plugin-server.iuisettingsclient.md) | | | `kibana.Plugin.deprecations` | [Handle plugin configuration deprecations](#handle-plugin-config-deprecations) and [`PluginConfigDescriptor.deprecations`](docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md) | Deprecations from New Platform are not applied to legacy configuration | _See also: [Server's CoreSetup API Docs](/docs/development/core/server/kibana-plugin-server.coresetup.md)_ ##### Plugin services -| Legacy Platform | New Platform | Notes | -| ------------------------------------------- | ------------------------------------------------------------------------------ | ----- | -| `server.plugins.xpack_main.registerFeature` | [`plugins.features.registerFeature`](x-pack/plugins/features/server/plugin.ts) | | +| Legacy Platform | New Platform | Notes | +| ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ----- | +| `server.plugins.xpack_main.registerFeature` | [`plugins.features.registerFeature`](x-pack/plugins/features/server/plugin.ts) | | +| `server.plugins.xpack_main.feature(pluginID).registerLicenseCheckResultsGenerator` | [`x-pack licensing plugin`](/x-pack/plugins/licensing/README.md) | | #### UI Exports @@ -1399,7 +1402,7 @@ export const config: PluginConfigDescriptor = { deprecations: ({ rename, unused }) => [ rename('oldProperty', 'newProperty'), unused('someUnusedProperty'), - ] + ] }; ``` @@ -1413,7 +1416,7 @@ export const config: PluginConfigDescriptor = { deprecations: ({ renameFromRoot, unusedFromRoot }) => [ renameFromRoot('oldplugin.property', 'myplugin.property'), unusedFromRoot('oldplugin.deprecated'), - ] + ] }; ``` @@ -1421,6 +1424,68 @@ Note that deprecations registered in new platform's plugins are not applied to t During migration, if you still need the deprecations to be effective in the legacy plugin, you need to declare them in both plugin definitions. +### Use scoped services +Whenever Kibana needs to get access to data saved in elasticsearch, it should perform a check whether an end-user has access to the data. +In the legacy platform, Kibana requires to bind elasticsearch related API with an incoming request to access elasticsearch service on behalf of a user. +```js + async function handler(req, res) { + const dataCluster = server.plugins.elasticsearch.getCluster('data'); + const data = await dataCluster.callWithRequest(req, 'ping'); + } +``` + +The new platform introduced [a handler interface](/rfcs/text/0003_handler_interface.md) on the server-side to perform that association internally. Core services, that require impersonation with an incoming request, are +exposed via `context` argument of [the request handler interface.](/docs/development/core/server/kibana-plugin-server.requesthandler.md) +The above example looks in the new platform as +```js + async function handler(context, req, res) { + const data = await context.core.elasticsearch.adminClient.callAsInternalUser('ping') + } +``` + +The [request handler context](/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md) exposed the next scoped **core** services: +| Legacy Platform | New Platform | +| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------| +| `request.getSavedObjectsClient` | [`context.savedObjects.client`](/docs/development/core/server/kibana-plugin-server.savedobjectsclient.md) | +| `server.plugins.elasticsearch.getCluster('admin')` | [`context.elasticsearch.adminClient`](/docs/development/core/server/kibana-plugin-server.iscopedclusterclient.md) | +| `server.plugins.elasticsearch.getCluster('data')` | [`context.elasticsearch.dataClient`](/docs/development/core/server/kibana-plugin-server.iscopedclusterclient.md) | +| `request.getUiSettingsService` | [`context.uiSettings.client`](/docs/development/core/server/kibana-plugin-server.iuisettingsclient.md) | + +#### Declare a custom scoped service +Plugins can extend the handler context with custom API that will be available to the plugin itself and all dependent plugins. +For example, the plugin creates a custom elasticsearch client and want to use it via the request handler context: + +```ts +import { CoreSetup, IScopedClusterClient } from 'kibana/server'; + +export interface MyPluginContext { + client: IScopedClusterClient; +} + +// extend RequestHandlerContext when a dependent plugin imports MyPluginContext from the file +declare module 'src/core/server' { + interface RequestHandlerContext { + myPlugin?: MyPluginContext; + } +} + +class Plugin { + setup(core: CoreSetup) { + const client = core.elasticsearch.createClient('myClient'); + core.http.registerRouteHandlerContext('myPlugin', (context, req, res) => { + return { client: client.asScoped(req) }; + }); + + router.get( + { path: '/api/my-plugin/', validate }, + async (context, req, res) => { + const data = await context.myPlugin.client.callAsCurrentUser('endpoint'); + ... + } + ); + } +``` + ### Mock new platform services in tests #### Writing mocks for your plugin diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index abc4c144356e8..2a9dca96062dc 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -211,7 +211,7 @@ export class CoreSystem { const injectedMetadata = await this.injectedMetadata.start(); const uiSettings = await this.uiSettings.start(); const docLinks = await this.docLinks.start({ injectedMetadata }); - const http = await this.http.start({ injectedMetadata, fatalErrors: this.fatalErrorsSetup }); + const http = await this.http.start({ injectedMetadata, fatalErrors: this.fatalErrorsSetup! }); const savedObjects = await this.savedObjects.start({ http }); const i18n = await this.i18n.start(); const application = await this.application.start({ http, injectedMetadata }); diff --git a/src/core/public/http/__snapshots__/http_service.test.ts.snap b/src/core/public/http/__snapshots__/http_service.test.ts.snap deleted file mode 100644 index 3d0309476365d..0000000000000 --- a/src/core/public/http/__snapshots__/http_service.test.ts.snap +++ /dev/null @@ -1,38 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`addLoadingCount() adds a fatal error if source observable emits a negative number 1`] = ` -Array [ - Array [ - [Error: Observables passed to loadingCount.add() must only emit positive numbers], - ], -] -`; - -exports[`addLoadingCount() adds a fatal error if source observables emit an error 1`] = ` -Array [ - Array [ - [Error: foo bar], - ], -] -`; - -exports[`getLoadingCount$() emits 0 initially, the right count when sources emit their own count, and ends with zero 1`] = ` -Array [ - 0, - 100, - 110, - 111, - 11, - 21, - 20, - 0, -] -`; - -exports[`getLoadingCount$() only emits when loading count changes 1`] = ` -Array [ - 0, - 1, - 0, -] -`; diff --git a/src/core/public/http/anonymous_paths.test.ts b/src/core/public/http/anonymous_paths.test.ts deleted file mode 100644 index bf9212f625f1e..0000000000000 --- a/src/core/public/http/anonymous_paths.test.ts +++ /dev/null @@ -1,107 +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 { AnonymousPaths } from './anonymous_paths'; -import { BasePath } from './base_path_service'; - -describe('#register', () => { - it(`allows paths that don't start with /`, () => { - const basePath = new BasePath('/foo'); - const anonymousPaths = new AnonymousPaths(basePath); - anonymousPaths.register('bar'); - }); - - it(`allows paths that end with '/'`, () => { - const basePath = new BasePath('/foo'); - const anonymousPaths = new AnonymousPaths(basePath); - anonymousPaths.register('/bar/'); - }); -}); - -describe('#isAnonymous', () => { - it('returns true for registered paths', () => { - const basePath = new BasePath('/foo'); - const anonymousPaths = new AnonymousPaths(basePath); - anonymousPaths.register('/bar'); - expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true); - }); - - it('returns true for paths registered with a trailing slash, but call "isAnonymous" with no trailing slash', () => { - const basePath = new BasePath('/foo'); - const anonymousPaths = new AnonymousPaths(basePath); - anonymousPaths.register('/bar/'); - expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true); - }); - - it('returns true for paths registered without a trailing slash, but call "isAnonymous" with a trailing slash', () => { - const basePath = new BasePath('/foo'); - const anonymousPaths = new AnonymousPaths(basePath); - anonymousPaths.register('/bar'); - expect(anonymousPaths.isAnonymous('/foo/bar/')).toBe(true); - }); - - it('returns true for paths registered without a starting slash', () => { - const basePath = new BasePath('/foo'); - const anonymousPaths = new AnonymousPaths(basePath); - anonymousPaths.register('bar'); - expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true); - }); - - it('returns true for paths registered with a starting slash', () => { - const basePath = new BasePath('/foo'); - const anonymousPaths = new AnonymousPaths(basePath); - anonymousPaths.register('/bar'); - expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true); - }); - - it('when there is no basePath and calling "isAnonymous" without a starting slash, returns true for paths registered with a starting slash', () => { - const basePath = new BasePath('/'); - const anonymousPaths = new AnonymousPaths(basePath); - anonymousPaths.register('/bar'); - expect(anonymousPaths.isAnonymous('bar')).toBe(true); - }); - - it('when there is no basePath and calling "isAnonymous" with a starting slash, returns true for paths registered with a starting slash', () => { - const basePath = new BasePath('/'); - const anonymousPaths = new AnonymousPaths(basePath); - anonymousPaths.register('/bar'); - expect(anonymousPaths.isAnonymous('/bar')).toBe(true); - }); - - it('returns true for paths whose capitalization is different', () => { - const basePath = new BasePath('/foo'); - const anonymousPaths = new AnonymousPaths(basePath); - anonymousPaths.register('/BAR'); - expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true); - }); - - it('returns false for other paths', () => { - const basePath = new BasePath('/foo'); - const anonymousPaths = new AnonymousPaths(basePath); - anonymousPaths.register('/bar'); - expect(anonymousPaths.isAnonymous('/foo/foo')).toBe(false); - }); - - it('returns false for sub-paths of registered paths', () => { - const basePath = new BasePath('/foo'); - const anonymousPaths = new AnonymousPaths(basePath); - anonymousPaths.register('/bar'); - expect(anonymousPaths.isAnonymous('/foo/bar/baz')).toBe(false); - }); -}); diff --git a/src/core/public/http/anonymous_paths.ts b/src/core/public/http/anonymous_paths.ts deleted file mode 100644 index 300c4d64df353..0000000000000 --- a/src/core/public/http/anonymous_paths.ts +++ /dev/null @@ -1,53 +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 { IAnonymousPaths, IBasePath } from 'src/core/public'; - -export class AnonymousPaths implements IAnonymousPaths { - private readonly paths = new Set(); - - constructor(private basePath: IBasePath) {} - - public isAnonymous(path: string): boolean { - const pathWithoutBasePath = this.basePath.remove(path); - return this.paths.has(this.normalizePath(pathWithoutBasePath)); - } - - public register(path: string) { - this.paths.add(this.normalizePath(path)); - } - - private normalizePath(path: string) { - // always lower-case it - let normalized = path.toLowerCase(); - - // remove the slash from the end - if (normalized.endsWith('/')) { - normalized = normalized.slice(0, normalized.length - 1); - } - - // put a slash at the start - if (!normalized.startsWith('/')) { - normalized = `/${normalized}`; - } - - // it's normalized!!! - return normalized; - } -} diff --git a/src/core/public/http/anonymous_paths_service.test.ts b/src/core/public/http/anonymous_paths_service.test.ts new file mode 100644 index 0000000000000..515715d9a613d --- /dev/null +++ b/src/core/public/http/anonymous_paths_service.test.ts @@ -0,0 +1,109 @@ +/* + * 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 { AnonymousPathsService } from './anonymous_paths_service'; +import { BasePath } from './base_path'; + +describe('#setup()', () => { + describe('#register', () => { + it(`allows paths that don't start with /`, () => { + const basePath = new BasePath('/foo'); + const anonymousPaths = new AnonymousPathsService().setup({ basePath }); + anonymousPaths.register('bar'); + }); + + it(`allows paths that end with '/'`, () => { + const basePath = new BasePath('/foo'); + const anonymousPaths = new AnonymousPathsService().setup({ basePath }); + anonymousPaths.register('/bar/'); + }); + }); + + describe('#isAnonymous', () => { + it('returns true for registered paths', () => { + const basePath = new BasePath('/foo'); + const anonymousPaths = new AnonymousPathsService().setup({ basePath }); + anonymousPaths.register('/bar'); + expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true); + }); + + it('returns true for paths registered with a trailing slash, but call "isAnonymous" with no trailing slash', () => { + const basePath = new BasePath('/foo'); + const anonymousPaths = new AnonymousPathsService().setup({ basePath }); + anonymousPaths.register('/bar/'); + expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true); + }); + + it('returns true for paths registered without a trailing slash, but call "isAnonymous" with a trailing slash', () => { + const basePath = new BasePath('/foo'); + const anonymousPaths = new AnonymousPathsService().setup({ basePath }); + anonymousPaths.register('/bar'); + expect(anonymousPaths.isAnonymous('/foo/bar/')).toBe(true); + }); + + it('returns true for paths registered without a starting slash', () => { + const basePath = new BasePath('/foo'); + const anonymousPaths = new AnonymousPathsService().setup({ basePath }); + anonymousPaths.register('bar'); + expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true); + }); + + it('returns true for paths registered with a starting slash', () => { + const basePath = new BasePath('/foo'); + const anonymousPaths = new AnonymousPathsService().setup({ basePath }); + anonymousPaths.register('/bar'); + expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true); + }); + + it('when there is no basePath and calling "isAnonymous" without a starting slash, returns true for paths registered with a starting slash', () => { + const basePath = new BasePath('/'); + const anonymousPaths = new AnonymousPathsService().setup({ basePath }); + anonymousPaths.register('/bar'); + expect(anonymousPaths.isAnonymous('bar')).toBe(true); + }); + + it('when there is no basePath and calling "isAnonymous" with a starting slash, returns true for paths registered with a starting slash', () => { + const basePath = new BasePath('/'); + const anonymousPaths = new AnonymousPathsService().setup({ basePath }); + anonymousPaths.register('/bar'); + expect(anonymousPaths.isAnonymous('/bar')).toBe(true); + }); + + it('returns true for paths whose capitalization is different', () => { + const basePath = new BasePath('/foo'); + const anonymousPaths = new AnonymousPathsService().setup({ basePath }); + anonymousPaths.register('/BAR'); + expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true); + }); + + it('returns false for other paths', () => { + const basePath = new BasePath('/foo'); + const anonymousPaths = new AnonymousPathsService().setup({ basePath }); + anonymousPaths.register('/bar'); + expect(anonymousPaths.isAnonymous('/foo/foo')).toBe(false); + }); + + it('returns false for sub-paths of registered paths', () => { + const basePath = new BasePath('/foo'); + const anonymousPaths = new AnonymousPathsService().setup({ basePath }); + anonymousPaths.register('/bar'); + expect(anonymousPaths.isAnonymous('/foo/bar/baz')).toBe(false); + }); + }); +}); diff --git a/src/core/public/http/anonymous_paths_service.ts b/src/core/public/http/anonymous_paths_service.ts new file mode 100644 index 0000000000000..ee9b3578c0270 --- /dev/null +++ b/src/core/public/http/anonymous_paths_service.ts @@ -0,0 +1,68 @@ +/* + * 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 { IAnonymousPaths, IBasePath } from 'src/core/public'; +import { CoreService } from '../../types'; + +interface Deps { + basePath: IBasePath; +} + +export class AnonymousPathsService implements CoreService { + private readonly paths = new Set(); + + public setup({ basePath }: Deps) { + return { + isAnonymous: (path: string): boolean => { + const pathWithoutBasePath = basePath.remove(path); + return this.paths.has(normalizePath(pathWithoutBasePath)); + }, + + register: (path: string) => { + this.paths.add(normalizePath(path)); + }, + + normalizePath, + }; + } + + public start(deps: Deps) { + return this.setup(deps); + } + + public stop() {} +} + +const normalizePath = (path: string) => { + // always lower-case it + let normalized = path.toLowerCase(); + + // remove the slash from the end + if (normalized.endsWith('/')) { + normalized = normalized.slice(0, normalized.length - 1); + } + + // put a slash at the start + if (!normalized.startsWith('/')) { + normalized = `/${normalized}`; + } + + // it's normalized!!! + return normalized; +}; diff --git a/src/core/public/http/base_path_service.test.ts b/src/core/public/http/base_path.test.ts similarity index 98% rename from src/core/public/http/base_path_service.test.ts rename to src/core/public/http/base_path.test.ts index 65403c906e614..63b7fa61cee84 100644 --- a/src/core/public/http/base_path_service.test.ts +++ b/src/core/public/http/base_path.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { BasePath } from './base_path_service'; +import { BasePath } from './base_path'; describe('BasePath', () => { describe('#get()', () => { diff --git a/src/core/public/http/base_path_service.ts b/src/core/public/http/base_path.ts similarity index 100% rename from src/core/public/http/base_path_service.ts rename to src/core/public/http/base_path.ts diff --git a/src/core/public/http/fetch.test.ts b/src/core/public/http/fetch.test.ts new file mode 100644 index 0000000000000..adb3d696a962f --- /dev/null +++ b/src/core/public/http/fetch.test.ts @@ -0,0 +1,569 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// @ts-ignore +import fetchMock from 'fetch-mock/es5/client'; +import { readFileSync } from 'fs'; +import { join } from 'path'; + +import { Fetch } from './fetch'; +import { BasePath } from './base_path'; +import { IHttpResponse } from './types'; + +function delay(duration: number) { + return new Promise(r => setTimeout(r, duration)); +} + +describe('Fetch', () => { + const fetchInstance = new Fetch({ + basePath: new BasePath('http://localhost/myBase'), + kibanaVersion: 'VERSION', + }); + afterEach(() => { + fetchMock.restore(); + }); + + describe('http requests', () => { + it('should use supplied request method', async () => { + fetchMock.post('*', {}); + await fetchInstance.fetch('/my/path', { method: 'POST' }); + + expect(fetchMock.lastOptions()!.method).toBe('POST'); + }); + + it('should use supplied Content-Type', async () => { + fetchMock.get('*', {}); + await fetchInstance.fetch('/my/path', { headers: { 'Content-Type': 'CustomContentType' } }); + + expect(fetchMock.lastOptions()!.headers).toMatchObject({ + 'content-type': 'CustomContentType', + }); + }); + + it('should use supplied pathname and querystring', async () => { + fetchMock.get('*', {}); + await fetchInstance.fetch('/my/path', { query: { a: 'b' } }); + + expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path?a=b'); + }); + + it('should use supplied headers', async () => { + fetchMock.get('*', {}); + await fetchInstance.fetch('/my/path', { + headers: { myHeader: 'foo' }, + }); + + expect(fetchMock.lastOptions()!.headers).toEqual({ + 'content-type': 'application/json', + 'kbn-version': 'VERSION', + myheader: 'foo', + }); + }); + + it('should return response', async () => { + fetchMock.get('*', { foo: 'bar' }); + const json = await fetchInstance.fetch('/my/path'); + expect(json).toEqual({ foo: 'bar' }); + }); + + it('should prepend url with basepath by default', async () => { + fetchMock.get('*', {}); + await fetchInstance.fetch('/my/path'); + expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path'); + }); + + it('should not prepend url with basepath when disabled', async () => { + fetchMock.get('*', {}); + await fetchInstance.fetch('my/path', { prependBasePath: false }); + expect(fetchMock.lastUrl()).toBe('/my/path'); + }); + + it('should not include undefined query params', async () => { + fetchMock.get('*', {}); + await fetchInstance.fetch('/my/path', { query: { a: undefined } }); + expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path'); + }); + + it('should make request with defaults', async () => { + fetchMock.get('*', {}); + await fetchInstance.fetch('/my/path'); + + const lastCall = fetchMock.lastCall(); + + expect(lastCall!.request.credentials).toBe('same-origin'); + expect(lastCall![1]).toMatchObject({ + method: 'GET', + headers: { + 'content-type': 'application/json', + 'kbn-version': 'VERSION', + }, + }); + }); + + it('should expose detailed response object when asResponse = true', async () => { + fetchMock.get('*', { foo: 'bar' }); + + const response = await fetchInstance.fetch('/my/path', { asResponse: true }); + + expect(response.request).toBeInstanceOf(Request); + expect(response.response).toBeInstanceOf(Response); + expect(response.body).toEqual({ foo: 'bar' }); + }); + + it('should reject on network error', async () => { + expect.assertions(1); + fetchMock.get('*', { status: 500 }); + + await expect(fetchInstance.fetch('/my/path')).rejects.toThrow(/Internal Server Error/); + }); + + it('should contain error message when throwing response', async () => { + fetchMock.get('*', { status: 404, body: { foo: 'bar' } }); + + await expect(fetchInstance.fetch('/my/path')).rejects.toMatchObject({ + message: 'Not Found', + body: { + foo: 'bar', + }, + response: { + status: 404, + url: 'http://localhost/myBase/my/path', + }, + }); + }); + + it('should support get() helper', async () => { + fetchMock.get('*', {}); + await fetchInstance.get('/my/path', { method: 'POST' }); + + expect(fetchMock.lastOptions()!.method).toBe('GET'); + }); + + it('should support head() helper', async () => { + fetchMock.head('*', {}); + await fetchInstance.head('/my/path', { method: 'GET' }); + + expect(fetchMock.lastOptions()!.method).toBe('HEAD'); + }); + + it('should support post() helper', async () => { + fetchMock.post('*', {}); + await fetchInstance.post('/my/path', { method: 'GET', body: '{}' }); + + expect(fetchMock.lastOptions()!.method).toBe('POST'); + }); + + it('should support put() helper', async () => { + fetchMock.put('*', {}); + await fetchInstance.put('/my/path', { method: 'GET', body: '{}' }); + + expect(fetchMock.lastOptions()!.method).toBe('PUT'); + }); + + it('should support patch() helper', async () => { + fetchMock.patch('*', {}); + await fetchInstance.patch('/my/path', { method: 'GET', body: '{}' }); + + expect(fetchMock.lastOptions()!.method).toBe('PATCH'); + }); + + it('should support delete() helper', async () => { + fetchMock.delete('*', {}); + await fetchInstance.delete('/my/path', { method: 'GET' }); + + expect(fetchMock.lastOptions()!.method).toBe('DELETE'); + }); + + it('should support options() helper', async () => { + fetchMock.mock('*', { method: 'OPTIONS' }); + await fetchInstance.options('/my/path', { method: 'GET' }); + + expect(fetchMock.lastOptions()!.method).toBe('OPTIONS'); + }); + + it('should make requests for NDJSON content', async () => { + const content = readFileSync(join(__dirname, '_import_objects.ndjson'), { + encoding: 'utf-8', + }); + const body = new FormData(); + + body.append('file', content); + fetchMock.post('*', { + body: content, + headers: { 'Content-Type': 'application/ndjson' }, + }); + + const data = await fetchInstance.post('/my/path', { + body, + headers: { + 'Content-Type': undefined, + }, + }); + + expect(data).toBeInstanceOf(Blob); + + const ndjson = await new Response(data).text(); + + expect(ndjson).toEqual(content); + }); + }); + + describe('interception', () => { + beforeEach(async () => { + fetchMock.get('*', { foo: 'bar' }); + }); + + afterEach(() => { + fetchMock.restore(); + fetchInstance.removeAllInterceptors(); + }); + + it('should make request and receive response', async () => { + fetchInstance.intercept({}); + + const body = await fetchInstance.fetch('/my/path'); + + expect(fetchMock.called()).toBe(true); + expect(body).toEqual({ foo: 'bar' }); + }); + + it('should be able to manipulate request instance', async () => { + fetchInstance.intercept({ + request(request) { + request.headers.set('Content-Type', 'CustomContentType'); + }, + }); + fetchInstance.intercept({ + request(request) { + return new Request('/my/route', request); + }, + }); + + const body = await fetchInstance.fetch('/my/path'); + + expect(fetchMock.called()).toBe(true); + expect(body).toEqual({ foo: 'bar' }); + expect(fetchMock.lastOptions()!.headers).toMatchObject({ + 'content-type': 'CustomContentType', + }); + expect(fetchMock.lastUrl()).toBe('/my/route'); + }); + + it('should call interceptors in correct order', async () => { + const order: string[] = []; + + fetchInstance.intercept({ + request() { + order.push('Request 1'); + }, + response() { + order.push('Response 1'); + }, + }); + fetchInstance.intercept({ + request() { + order.push('Request 2'); + }, + response() { + order.push('Response 2'); + }, + }); + fetchInstance.intercept({ + request() { + order.push('Request 3'); + }, + response() { + order.push('Response 3'); + }, + }); + + const body = await fetchInstance.fetch('/my/path'); + + expect(fetchMock.called()).toBe(true); + expect(body).toEqual({ foo: 'bar' }); + expect(order).toEqual([ + 'Request 3', + 'Request 2', + 'Request 1', + 'Response 1', + 'Response 2', + 'Response 3', + ]); + }); + + it('should skip remaining interceptors when controller halts during request', async () => { + const usedSpy = jest.fn(); + const unusedSpy = jest.fn(); + + fetchInstance.intercept({ request: unusedSpy, response: unusedSpy }); + fetchInstance.intercept({ + request(request, controller) { + controller.halt(); + }, + response: unusedSpy, + }); + fetchInstance.intercept({ + request: usedSpy, + response: unusedSpy, + }); + + fetchInstance.fetch('/my/path').then(unusedSpy, unusedSpy); + await delay(1000); + + expect(unusedSpy).toHaveBeenCalledTimes(0); + expect(usedSpy).toHaveBeenCalledTimes(1); + expect(fetchMock.called()).toBe(false); + }); + + it('should skip remaining interceptors when controller halts during response', async () => { + const usedSpy = jest.fn(); + const unusedSpy = jest.fn(); + + fetchInstance.intercept({ + request: usedSpy, + response(response, controller) { + controller.halt(); + }, + }); + fetchInstance.intercept({ request: usedSpy, response: unusedSpy }); + fetchInstance.intercept({ request: usedSpy, response: unusedSpy }); + + fetchInstance.fetch('/my/path').then(unusedSpy, unusedSpy); + await delay(1000); + + expect(fetchMock.called()).toBe(true); + expect(usedSpy).toHaveBeenCalledTimes(3); + expect(unusedSpy).toHaveBeenCalledTimes(0); + }); + + it('should skip remaining interceptors when controller halts during responseError', async () => { + fetchMock.post('*', 401); + + const unusedSpy = jest.fn(); + + fetchInstance.intercept({ + responseError(response, controller) { + controller.halt(); + }, + }); + fetchInstance.intercept({ response: unusedSpy, responseError: unusedSpy }); + + fetchInstance.post('/my/path').then(unusedSpy, unusedSpy); + await delay(1000); + + expect(fetchMock.called()).toBe(true); + expect(unusedSpy).toHaveBeenCalledTimes(0); + }); + + it('should not fetch if exception occurs during request interception', async () => { + const usedSpy = jest.fn(); + const unusedSpy = jest.fn(); + + fetchInstance.intercept({ + request: unusedSpy, + requestError: usedSpy, + response: unusedSpy, + responseError: unusedSpy, + }); + fetchInstance.intercept({ + request() { + throw new Error('Interception Error'); + }, + response: unusedSpy, + responseError: unusedSpy, + }); + fetchInstance.intercept({ request: usedSpy, response: unusedSpy, responseError: unusedSpy }); + + await expect(fetchInstance.fetch('/my/path')).rejects.toThrow(/Interception Error/); + expect(fetchMock.called()).toBe(false); + expect(unusedSpy).toHaveBeenCalledTimes(0); + expect(usedSpy).toHaveBeenCalledTimes(2); + }); + + it('should succeed if request throws but caught by interceptor', async () => { + const usedSpy = jest.fn(); + const unusedSpy = jest.fn(); + + fetchInstance.intercept({ + request: unusedSpy, + requestError({ request }) { + return new Request('/my/route', request); + }, + response: usedSpy, + }); + fetchInstance.intercept({ + request() { + throw new Error('Interception Error'); + }, + response: usedSpy, + }); + fetchInstance.intercept({ request: usedSpy, response: usedSpy }); + + await expect(fetchInstance.fetch('/my/route')).resolves.toEqual({ foo: 'bar' }); + expect(fetchMock.called()).toBe(true); + expect(unusedSpy).toHaveBeenCalledTimes(0); + expect(usedSpy).toHaveBeenCalledTimes(4); + }); + + it('should accumulate request information', async () => { + const routes = ['alpha', 'beta', 'gamma']; + const createRequest = jest.fn( + (request: Request) => new Request(`/api/${routes.shift()}`, request) + ); + + fetchInstance.intercept({ + request: createRequest, + }); + fetchInstance.intercept({ + requestError(httpErrorRequest) { + return httpErrorRequest.request; + }, + }); + fetchInstance.intercept({ + request(request) { + throw new Error('Invalid'); + }, + }); + fetchInstance.intercept({ + request: createRequest, + }); + fetchInstance.intercept({ + request: createRequest, + }); + + await expect(fetchInstance.fetch('/my/route')).resolves.toEqual({ foo: 'bar' }); + expect(fetchMock.called()).toBe(true); + expect(routes.length).toBe(0); + expect(createRequest.mock.calls[0][0].url).toContain('/my/route'); + expect(createRequest.mock.calls[1][0].url).toContain('/api/alpha'); + expect(createRequest.mock.calls[2][0].url).toContain('/api/beta'); + expect(fetchMock.lastCall()!.request.url).toContain('/api/gamma'); + }); + + it('should accumulate response information', async () => { + const bodies = ['alpha', 'beta', 'gamma']; + const createResponse = jest.fn((httpResponse: IHttpResponse) => ({ + body: bodies.shift(), + })); + + fetchInstance.intercept({ + response: createResponse, + }); + fetchInstance.intercept({ + response: createResponse, + }); + fetchInstance.intercept({ + response(httpResponse) { + throw new Error('Invalid'); + }, + }); + fetchInstance.intercept({ + responseError({ error, ...httpResponse }) { + return httpResponse; + }, + }); + fetchInstance.intercept({ + response: createResponse, + }); + + await expect(fetchInstance.fetch('/my/route')).resolves.toEqual('gamma'); + expect(fetchMock.called()).toBe(true); + expect(bodies.length).toBe(0); + expect(createResponse.mock.calls[0][0].body).toEqual({ foo: 'bar' }); + expect(createResponse.mock.calls[1][0].body).toBe('alpha'); + expect(createResponse.mock.calls[2][0].body).toBe('beta'); + }); + + describe('request availability during interception', () => { + it('should be available to responseError when response throws', async () => { + let spiedRequest: Request | undefined; + + fetchInstance.intercept({ + response() { + throw new Error('Internal Server Error'); + }, + }); + fetchInstance.intercept({ + responseError({ request }) { + spiedRequest = request; + }, + }); + + await expect(fetchInstance.fetch('/my/path')).rejects.toThrow(); + expect(fetchMock.called()).toBe(true); + expect(spiedRequest).toBeDefined(); + }); + }); + + describe('response availability during interception', () => { + it('should be available to responseError when network request fails', async () => { + fetchMock.restore(); + fetchMock.get('*', { status: 500 }); + + let spiedResponse: Response | undefined; + + fetchInstance.intercept({ + responseError({ response }) { + spiedResponse = response; + }, + }); + + await expect(fetchInstance.fetch('/my/path')).rejects.toThrow(); + expect(spiedResponse).toBeDefined(); + }); + }); + + it('should actually halt request interceptors in reverse order', async () => { + const unusedSpy = jest.fn(); + + fetchInstance.intercept({ request: unusedSpy }); + fetchInstance.intercept({ + request(request, controller) { + controller.halt(); + }, + }); + + fetchInstance.fetch('/my/path'); + await delay(500); + + expect(unusedSpy).toHaveBeenCalledTimes(0); + }); + + it('should recover from failing request interception via request error interceptor', async () => { + const usedSpy = jest.fn(); + + fetchInstance.intercept({ + requestError(httpErrorRequest) { + return httpErrorRequest.request; + }, + response: usedSpy, + }); + + fetchInstance.intercept({ + request(request, controller) { + throw new Error('Request Error'); + }, + response: usedSpy, + }); + + await expect(fetchInstance.fetch('/my/path')).resolves.toEqual({ foo: 'bar' }); + expect(usedSpy).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/src/core/public/http/fetch.ts b/src/core/public/http/fetch.ts index 472b617cacd7f..b86f1f5c08029 100644 --- a/src/core/public/http/fetch.ts +++ b/src/core/public/http/fetch.ts @@ -35,20 +35,30 @@ interface Params { const JSON_CONTENT = /^(application\/(json|x-javascript)|text\/(x-)?javascript|x-json)(;.*)?$/; const NDJSON_CONTENT = /^(application\/ndjson)(;.*)?$/; -export class FetchService { +export class Fetch { private readonly interceptors = new Set(); constructor(private readonly params: Params) {} public intercept(interceptor: HttpInterceptor) { this.interceptors.add(interceptor); - return () => this.interceptors.delete(interceptor); + return () => { + this.interceptors.delete(interceptor); + }; } public removeAllInterceptors() { this.interceptors.clear(); } + public readonly delete = this.shorthand('DELETE'); + public readonly get = this.shorthand('GET'); + public readonly head = this.shorthand('HEAD'); + public readonly options = this.shorthand('options'); + public readonly patch = this.shorthand('PATCH'); + public readonly post = this.shorthand('POST'); + public readonly put = this.shorthand('PUT'); + public fetch: HttpHandler = async ( path: string, options: HttpFetchOptions = {} @@ -152,4 +162,9 @@ export class FetchService { return new HttpResponse({ request, response, body }); } + + private shorthand(method: string) { + return (path: string, options: HttpFetchOptions = {}) => + this.fetch(path, { ...options, method }); + } } diff --git a/src/core/public/http/http_service.mock.ts b/src/core/public/http/http_service.mock.ts index 5887e7b3e96d0..1111fd39ec78e 100644 --- a/src/core/public/http/http_service.mock.ts +++ b/src/core/public/http/http_service.mock.ts @@ -20,7 +20,7 @@ import { HttpService } from './http_service'; import { HttpSetup } from './types'; import { BehaviorSubject } from 'rxjs'; -import { BasePath } from './base_path_service'; +import { BasePath } from './base_path'; export type HttpSetupMock = jest.Mocked & { basePath: BasePath; @@ -41,15 +41,13 @@ const createServiceMock = ({ basePath = '' } = {}): HttpSetupMock => ({ register: jest.fn(), isAnonymous: jest.fn(), }, - addLoadingCount: jest.fn(), + addLoadingCountSource: jest.fn(), getLoadingCount$: jest.fn().mockReturnValue(new BehaviorSubject(0)), - stop: jest.fn(), intercept: jest.fn(), - removeAllInterceptors: jest.fn(), }); const createMock = ({ basePath = '' } = {}) => { - const mocked: jest.Mocked> = { + const mocked: jest.Mocked> = { setup: jest.fn(), start: jest.fn(), stop: jest.fn(), diff --git a/src/legacy/core_plugins/input_control_vis/index.js b/src/core/public/http/http_service.test.mocks.ts similarity index 71% rename from src/legacy/core_plugins/input_control_vis/index.js rename to src/core/public/http/http_service.test.mocks.ts index a21b79e28fb7f..e60dad0509699 100644 --- a/src/legacy/core_plugins/input_control_vis/index.js +++ b/src/core/public/http/http_service.test.mocks.ts @@ -17,14 +17,9 @@ * under the License. */ -import { resolve } from 'path'; +import { loadingCountServiceMock } from './loading_count_service.mock'; -export default function(kibana) { - return new kibana.Plugin({ - uiExports: { - visTypes: ['plugins/input_control_vis/register_vis'], - interpreter: ['plugins/input_control_vis/input_control_fn'], - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }, - }); -} +export const loadingServiceMock = loadingCountServiceMock.create(); +jest.doMock('./loading_count_service', () => ({ + LoadingCountService: jest.fn(() => loadingServiceMock), +})); diff --git a/src/core/public/http/http_service.test.ts b/src/core/public/http/http_service.test.ts index 09f3cca419e4d..f95d25d116976 100644 --- a/src/core/public/http/http_service.test.ts +++ b/src/core/public/http/http_service.test.ts @@ -17,692 +17,22 @@ * under the License. */ -import * as Rx from 'rxjs'; -import { toArray } from 'rxjs/operators'; // @ts-ignore import fetchMock from 'fetch-mock/es5/client'; -import { readFileSync } from 'fs'; -import { join } from 'path'; -import { setup, SetupTap } from '../../../test_utils/public/http_test_setup'; -import { IHttpResponse } from './types'; - -function delay(duration: number) { - return new Promise(r => setTimeout(r, duration)); -} - -const setupFakeBasePath: SetupTap = injectedMetadata => { - injectedMetadata.getBasePath.mockReturnValue('/foo/bar'); -}; - -describe('basePath.get()', () => { - it('returns an empty string if no basePath is injected', () => { - const { http } = setup(injectedMetadata => { - injectedMetadata.getBasePath.mockReturnValue(undefined as any); - }); - - expect(http.basePath.get()).toBe(''); - }); - - it('returns the injected basePath', () => { - const { http } = setup(setupFakeBasePath); - - expect(http.basePath.get()).toBe('/foo/bar'); - }); -}); - -describe('http requests', () => { - afterEach(() => { - fetchMock.restore(); - }); - - it('should use supplied request method', async () => { - const { http } = setup(); - - fetchMock.post('*', {}); - await http.fetch('/my/path', { method: 'POST' }); - - expect(fetchMock.lastOptions()!.method).toBe('POST'); - }); - - it('should use supplied Content-Type', async () => { - const { http } = setup(); - - fetchMock.get('*', {}); - await http.fetch('/my/path', { headers: { 'Content-Type': 'CustomContentType' } }); - - expect(fetchMock.lastOptions()!.headers).toMatchObject({ - 'content-type': 'CustomContentType', - }); - }); - - it('should use supplied pathname and querystring', async () => { - const { http } = setup(); - - fetchMock.get('*', {}); - await http.fetch('/my/path', { query: { a: 'b' } }); - - expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path?a=b'); - }); - - it('should use supplied headers', async () => { - const { http } = setup(); - - fetchMock.get('*', {}); - await http.fetch('/my/path', { - headers: { myHeader: 'foo' }, - }); - - expect(fetchMock.lastOptions()!.headers).toEqual({ - 'content-type': 'application/json', - 'kbn-version': 'kibanaVersion', - myheader: 'foo', - }); - }); - - it('should return response', async () => { - const { http } = setup(); - fetchMock.get('*', { foo: 'bar' }); - const json = await http.fetch('/my/path'); - expect(json).toEqual({ foo: 'bar' }); - }); - - it('should prepend url with basepath by default', async () => { - const { http } = setup(); - fetchMock.get('*', {}); - await http.fetch('/my/path'); - expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path'); - }); - - it('should not prepend url with basepath when disabled', async () => { - const { http } = setup(); - fetchMock.get('*', {}); - await http.fetch('my/path', { prependBasePath: false }); - expect(fetchMock.lastUrl()).toBe('/my/path'); - }); - - it('should not include undefined query params', async () => { - const { http } = setup(); - fetchMock.get('*', {}); - await http.fetch('/my/path', { query: { a: undefined } }); - expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path'); - }); - - it('should make request with defaults', async () => { - const { http } = setup(); - - fetchMock.get('*', {}); - await http.fetch('/my/path'); - - const lastCall = fetchMock.lastCall(); - - expect(lastCall!.request.credentials).toBe('same-origin'); - expect(lastCall![1]).toMatchObject({ - method: 'GET', - headers: { - 'content-type': 'application/json', - 'kbn-version': 'kibanaVersion', - }, - }); - }); - - it('should expose detailed response object when asResponse = true', async () => { - const { http } = setup(); - - fetchMock.get('*', { foo: 'bar' }); - - const response = await http.fetch('/my/path', { asResponse: true }); - - expect(response.request).toBeInstanceOf(Request); - expect(response.response).toBeInstanceOf(Response); - expect(response.body).toEqual({ foo: 'bar' }); - }); - - it('should reject on network error', async () => { - const { http } = setup(); - - expect.assertions(1); - fetchMock.get('*', { status: 500 }); - - await expect(http.fetch('/my/path')).rejects.toThrow(/Internal Server Error/); - }); - - it('should contain error message when throwing response', async () => { - const { http } = setup(); - - fetchMock.get('*', { status: 404, body: { foo: 'bar' } }); - - await expect(http.fetch('/my/path')).rejects.toMatchObject({ - message: 'Not Found', - body: { - foo: 'bar', - }, - response: { - status: 404, - url: 'http://localhost/myBase/my/path', - }, - }); - }); - - it('should support get() helper', async () => { - const { http } = setup(); - - fetchMock.get('*', {}); - await http.get('/my/path', { method: 'POST' }); - - expect(fetchMock.lastOptions()!.method).toBe('GET'); - }); - - it('should support head() helper', async () => { - const { http } = setup(); - - fetchMock.head('*', {}); - await http.head('/my/path', { method: 'GET' }); - - expect(fetchMock.lastOptions()!.method).toBe('HEAD'); - }); - - it('should support post() helper', async () => { - const { http } = setup(); - - fetchMock.post('*', {}); - await http.post('/my/path', { method: 'GET', body: '{}' }); - - expect(fetchMock.lastOptions()!.method).toBe('POST'); - }); - - it('should support put() helper', async () => { - const { http } = setup(); - - fetchMock.put('*', {}); - await http.put('/my/path', { method: 'GET', body: '{}' }); - - expect(fetchMock.lastOptions()!.method).toBe('PUT'); - }); - - it('should support patch() helper', async () => { - const { http } = setup(); - - fetchMock.patch('*', {}); - await http.patch('/my/path', { method: 'GET', body: '{}' }); - - expect(fetchMock.lastOptions()!.method).toBe('PATCH'); - }); - - it('should support delete() helper', async () => { - const { http } = setup(); - - fetchMock.delete('*', {}); - await http.delete('/my/path', { method: 'GET' }); - - expect(fetchMock.lastOptions()!.method).toBe('DELETE'); - }); - - it('should support options() helper', async () => { - const { http } = setup(); - - fetchMock.mock('*', { method: 'OPTIONS' }); - await http.options('/my/path', { method: 'GET' }); - - expect(fetchMock.lastOptions()!.method).toBe('OPTIONS'); - }); - - it('should make requests for NDJSON content', async () => { - const { http } = setup(); - const content = readFileSync(join(__dirname, '_import_objects.ndjson'), { encoding: 'utf-8' }); - const body = new FormData(); - - body.append('file', content); - fetchMock.post('*', { - body: content, - headers: { 'Content-Type': 'application/ndjson' }, - }); - - const data = await http.post('/my/path', { - body, - headers: { - 'Content-Type': undefined, - }, - }); - - expect(data).toBeInstanceOf(Blob); - - const ndjson = await new Response(data).text(); - - expect(ndjson).toEqual(content); - }); -}); - -describe('interception', () => { - const { http } = setup(); - - beforeEach(() => { - fetchMock.get('*', { foo: 'bar' }); - }); - - afterEach(() => { - fetchMock.restore(); - http.removeAllInterceptors(); - }); - - it('should make request and receive response', async () => { - http.intercept({}); - - const body = await http.fetch('/my/path'); - - expect(fetchMock.called()).toBe(true); - expect(body).toEqual({ foo: 'bar' }); - }); - - it('should be able to manipulate request instance', async () => { - http.intercept({ - request(request) { - request.headers.set('Content-Type', 'CustomContentType'); - }, - }); - http.intercept({ - request(request) { - return new Request('/my/route', request); - }, - }); - - const body = await http.fetch('/my/path'); - - expect(fetchMock.called()).toBe(true); - expect(body).toEqual({ foo: 'bar' }); - expect(fetchMock.lastOptions()!.headers).toMatchObject({ - 'content-type': 'CustomContentType', - }); - expect(fetchMock.lastUrl()).toBe('/my/route'); - }); - - it('should call interceptors in correct order', async () => { - const order: string[] = []; - - http.intercept({ - request() { - order.push('Request 1'); - }, - response() { - order.push('Response 1'); - }, - }); - http.intercept({ - request() { - order.push('Request 2'); - }, - response() { - order.push('Response 2'); - }, - }); - http.intercept({ - request() { - order.push('Request 3'); - }, - response() { - order.push('Response 3'); - }, - }); - - const body = await http.fetch('/my/path'); - - expect(fetchMock.called()).toBe(true); - expect(body).toEqual({ foo: 'bar' }); - expect(order).toEqual([ - 'Request 3', - 'Request 2', - 'Request 1', - 'Response 1', - 'Response 2', - 'Response 3', - ]); - }); - - it('should skip remaining interceptors when controller halts during request', async () => { - const usedSpy = jest.fn(); - const unusedSpy = jest.fn(); - - http.intercept({ request: unusedSpy, response: unusedSpy }); - http.intercept({ - request(request, controller) { - controller.halt(); - }, - response: unusedSpy, - }); - http.intercept({ - request: usedSpy, - response: unusedSpy, - }); - - http.fetch('/my/path').then(unusedSpy, unusedSpy); - await delay(1000); - - expect(unusedSpy).toHaveBeenCalledTimes(0); - expect(usedSpy).toHaveBeenCalledTimes(1); - expect(fetchMock.called()).toBe(false); - }); - - it('should skip remaining interceptors when controller halts during response', async () => { - const usedSpy = jest.fn(); - const unusedSpy = jest.fn(); - - http.intercept({ - request: usedSpy, - response(response, controller) { - controller.halt(); - }, - }); - http.intercept({ request: usedSpy, response: unusedSpy }); - http.intercept({ request: usedSpy, response: unusedSpy }); - - http.fetch('/my/path').then(unusedSpy, unusedSpy); - await delay(1000); - - expect(fetchMock.called()).toBe(true); - expect(usedSpy).toHaveBeenCalledTimes(3); - expect(unusedSpy).toHaveBeenCalledTimes(0); - }); - - it('should skip remaining interceptors when controller halts during responseError', async () => { - fetchMock.post('*', 401); - - const unusedSpy = jest.fn(); - - http.intercept({ - responseError(response, controller) { - controller.halt(); - }, - }); - http.intercept({ response: unusedSpy, responseError: unusedSpy }); - - http.post('/my/path').then(unusedSpy, unusedSpy); - await delay(1000); - - expect(fetchMock.called()).toBe(true); - expect(unusedSpy).toHaveBeenCalledTimes(0); - }); - - it('should not fetch if exception occurs during request interception', async () => { - const usedSpy = jest.fn(); - const unusedSpy = jest.fn(); - - http.intercept({ - request: unusedSpy, - requestError: usedSpy, - response: unusedSpy, - responseError: unusedSpy, - }); - http.intercept({ - request() { - throw new Error('Interception Error'); - }, - response: unusedSpy, - responseError: unusedSpy, - }); - http.intercept({ request: usedSpy, response: unusedSpy, responseError: unusedSpy }); - - await expect(http.fetch('/my/path')).rejects.toThrow(/Interception Error/); - expect(fetchMock.called()).toBe(false); - expect(unusedSpy).toHaveBeenCalledTimes(0); - expect(usedSpy).toHaveBeenCalledTimes(2); - }); - - it('should succeed if request throws but caught by interceptor', async () => { - const usedSpy = jest.fn(); - const unusedSpy = jest.fn(); - - http.intercept({ - request: unusedSpy, - requestError({ request }) { - return new Request('/my/route', request); - }, - response: usedSpy, - }); - http.intercept({ - request() { - throw new Error('Interception Error'); - }, - response: usedSpy, - }); - http.intercept({ request: usedSpy, response: usedSpy }); - - await expect(http.fetch('/my/route')).resolves.toEqual({ foo: 'bar' }); - expect(fetchMock.called()).toBe(true); - expect(unusedSpy).toHaveBeenCalledTimes(0); - expect(usedSpy).toHaveBeenCalledTimes(4); - }); - - it('should accumulate request information', async () => { - const routes = ['alpha', 'beta', 'gamma']; - const createRequest = jest.fn( - (request: Request) => new Request(`/api/${routes.shift()}`, request) - ); - - http.intercept({ - request: createRequest, - }); - http.intercept({ - requestError(httpErrorRequest) { - return httpErrorRequest.request; - }, - }); - http.intercept({ - request(request) { - throw new Error('Invalid'); - }, - }); - http.intercept({ - request: createRequest, - }); - http.intercept({ - request: createRequest, - }); - - await expect(http.fetch('/my/route')).resolves.toEqual({ foo: 'bar' }); - expect(fetchMock.called()).toBe(true); - expect(routes.length).toBe(0); - expect(createRequest.mock.calls[0][0].url).toContain('/my/route'); - expect(createRequest.mock.calls[1][0].url).toContain('/api/alpha'); - expect(createRequest.mock.calls[2][0].url).toContain('/api/beta'); - expect(fetchMock.lastCall()!.request.url).toContain('/api/gamma'); - }); - - it('should accumulate response information', async () => { - const bodies = ['alpha', 'beta', 'gamma']; - const createResponse = jest.fn((httpResponse: IHttpResponse) => ({ - body: bodies.shift(), - })); - - http.intercept({ - response: createResponse, - }); - http.intercept({ - response: createResponse, - }); - http.intercept({ - response(httpResponse) { - throw new Error('Invalid'); - }, - }); - http.intercept({ - responseError({ error, ...httpResponse }) { - return httpResponse; - }, - }); - http.intercept({ - response: createResponse, - }); - - await expect(http.fetch('/my/route')).resolves.toEqual('gamma'); - expect(fetchMock.called()).toBe(true); - expect(bodies.length).toBe(0); - expect(createResponse.mock.calls[0][0].body).toEqual({ foo: 'bar' }); - expect(createResponse.mock.calls[1][0].body).toBe('alpha'); - expect(createResponse.mock.calls[2][0].body).toBe('beta'); - }); - - describe('request availability during interception', () => { - it('should be available to responseError when response throws', async () => { - let spiedRequest: Request | undefined; - - http.intercept({ - response() { - throw new Error('Internal Server Error'); - }, - }); - http.intercept({ - responseError({ request }) { - spiedRequest = request; - }, - }); - - await expect(http.fetch('/my/path')).rejects.toThrow(); - expect(fetchMock.called()).toBe(true); - expect(spiedRequest).toBeDefined(); - }); - }); - - describe('response availability during interception', () => { - it('should be available to responseError when network request fails', async () => { - fetchMock.restore(); - fetchMock.get('*', { status: 500 }); - - let spiedResponse: Response | undefined; - - http.intercept({ - responseError({ response }) { - spiedResponse = response; - }, - }); - - await expect(http.fetch('/my/path')).rejects.toThrow(); - expect(spiedResponse).toBeDefined(); - }); - }); - - it('should actually halt request interceptors in reverse order', async () => { - const unusedSpy = jest.fn(); - - http.intercept({ request: unusedSpy }); - http.intercept({ - request(request, controller) { - controller.halt(); - }, - }); - - http.fetch('/my/path'); - await delay(500); - - expect(unusedSpy).toHaveBeenCalledTimes(0); - }); - - it('should recover from failing request interception via request error interceptor', async () => { - const usedSpy = jest.fn(); - - http.intercept({ - requestError(httpErrorRequest) { - return httpErrorRequest.request; - }, - response: usedSpy, - }); - - http.intercept({ - request(request, controller) { - throw new Error('Request Error'); - }, - response: usedSpy, - }); - - await expect(http.fetch('/my/path')).resolves.toEqual({ foo: 'bar' }); - expect(usedSpy).toHaveBeenCalledTimes(2); - }); -}); - -describe('addLoadingCount()', () => { - it('subscribes to passed in sources, unsubscribes on stop', () => { - const { httpService, http } = setup(); - - const unsubA = jest.fn(); - const subA = jest.fn().mockReturnValue(unsubA); - http.addLoadingCount(new Rx.Observable(subA)); - expect(subA).toHaveBeenCalledTimes(1); - expect(unsubA).not.toHaveBeenCalled(); - - const unsubB = jest.fn(); - const subB = jest.fn().mockReturnValue(unsubB); - http.addLoadingCount(new Rx.Observable(subB)); - expect(subB).toHaveBeenCalledTimes(1); - expect(unsubB).not.toHaveBeenCalled(); - +import { loadingServiceMock } from './http_service.test.mocks'; + +import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock'; +import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; +import { HttpService } from './http_service'; + +describe('#stop()', () => { + it('calls loadingCount.stop()', () => { + const injectedMetadata = injectedMetadataServiceMock.createSetupContract(); + const fatalErrors = fatalErrorsServiceMock.createSetupContract(); + const httpService = new HttpService(); + httpService.setup({ fatalErrors, injectedMetadata }); + httpService.start({ fatalErrors, injectedMetadata }); httpService.stop(); - - expect(subA).toHaveBeenCalledTimes(1); - expect(unsubA).toHaveBeenCalledTimes(1); - expect(subB).toHaveBeenCalledTimes(1); - expect(unsubB).toHaveBeenCalledTimes(1); - }); - - it('adds a fatal error if source observables emit an error', async () => { - const { http, fatalErrors } = setup(); - - http.addLoadingCount(Rx.throwError(new Error('foo bar'))); - expect(fatalErrors.add.mock.calls).toMatchSnapshot(); - }); - - it('adds a fatal error if source observable emits a negative number', async () => { - const { http, fatalErrors } = setup(); - - http.addLoadingCount(Rx.of(1, 2, 3, 4, -9)); - expect(fatalErrors.add.mock.calls).toMatchSnapshot(); - }); -}); - -describe('getLoadingCount$()', () => { - it('emits 0 initially, the right count when sources emit their own count, and ends with zero', async () => { - const { httpService, http } = setup(); - - const countA$ = new Rx.Subject(); - const countB$ = new Rx.Subject(); - const countC$ = new Rx.Subject(); - const promise = http - .getLoadingCount$() - .pipe(toArray()) - .toPromise(); - - http.addLoadingCount(countA$); - http.addLoadingCount(countB$); - http.addLoadingCount(countC$); - - countA$.next(100); - countB$.next(10); - countC$.next(1); - countA$.complete(); - countB$.next(20); - countC$.complete(); - countB$.next(0); - - httpService.stop(); - expect(await promise).toMatchSnapshot(); - }); - - it('only emits when loading count changes', async () => { - const { httpService, http } = setup(); - - const count$ = new Rx.Subject(); - const promise = http - .getLoadingCount$() - .pipe(toArray()) - .toPromise(); - - http.addLoadingCount(count$); - count$.next(0); - count$.next(0); - count$.next(0); - count$.next(0); - count$.next(0); - count$.next(1); - count$.next(1); - httpService.stop(); - - expect(await promise).toMatchSnapshot(); + expect(loadingServiceMock.stop).toHaveBeenCalled(); }); }); diff --git a/src/core/public/http/http_service.ts b/src/core/public/http/http_service.ts index 477bcd6152d44..567cdd310cbdf 100644 --- a/src/core/public/http/http_service.ts +++ b/src/core/public/http/http_service.ts @@ -17,32 +17,52 @@ * under the License. */ -import { HttpSetup, HttpStart, HttpServiceBase } from './types'; -import { setup } from './http_setup'; +import { HttpSetup, HttpStart } from './types'; import { InjectedMetadataSetup } from '../injected_metadata'; import { FatalErrorsSetup } from '../fatal_errors'; +import { BasePath } from './base_path'; +import { AnonymousPathsService } from './anonymous_paths_service'; +import { LoadingCountService } from './loading_count_service'; +import { Fetch } from './fetch'; +import { CoreService } from '../../types'; interface HttpDeps { injectedMetadata: InjectedMetadataSetup; - fatalErrors: FatalErrorsSetup | null; + fatalErrors: FatalErrorsSetup; } /** @internal */ -export class HttpService { - private service!: HttpServiceBase; +export class HttpService implements CoreService { + private readonly anonymousPaths = new AnonymousPathsService(); + private readonly loadingCount = new LoadingCountService(); - public setup(deps: HttpDeps): HttpSetup { - this.service = setup(deps.injectedMetadata, deps.fatalErrors); - return this.service; + public setup({ injectedMetadata, fatalErrors }: HttpDeps): HttpSetup { + const kibanaVersion = injectedMetadata.getKibanaVersion(); + const basePath = new BasePath(injectedMetadata.getBasePath()); + const fetchService = new Fetch({ basePath, kibanaVersion }); + const loadingCount = this.loadingCount.setup({ fatalErrors }); + + return { + basePath, + anonymousPaths: this.anonymousPaths.setup({ basePath }), + intercept: fetchService.intercept.bind(fetchService), + fetch: fetchService.fetch.bind(fetchService), + delete: fetchService.delete.bind(fetchService), + get: fetchService.get.bind(fetchService), + head: fetchService.head.bind(fetchService), + options: fetchService.options.bind(fetchService), + patch: fetchService.patch.bind(fetchService), + post: fetchService.post.bind(fetchService), + put: fetchService.put.bind(fetchService), + ...loadingCount, + }; } - public start(deps: HttpDeps): HttpStart { - return this.service || this.setup(deps); + public start(deps: HttpDeps) { + return this.setup(deps); } public stop() { - if (this.service) { - this.service.stop(); - } + this.loadingCount.stop(); } } diff --git a/src/core/public/http/http_setup.ts b/src/core/public/http/http_setup.ts deleted file mode 100644 index c63750849f13a..0000000000000 --- a/src/core/public/http/http_setup.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { BehaviorSubject, Observable, Subject } from 'rxjs'; -import { - distinctUntilChanged, - endWith, - map, - pairwise, - startWith, - takeUntil, - tap, -} from 'rxjs/operators'; -import { InjectedMetadataSetup } from '../injected_metadata'; -import { FatalErrorsSetup } from '../fatal_errors'; -import { HttpFetchOptions, HttpServiceBase } from './types'; -import { HttpInterceptController } from './http_intercept_controller'; -import { HttpInterceptHaltError } from './http_intercept_halt_error'; -import { BasePath } from './base_path_service'; -import { AnonymousPaths } from './anonymous_paths'; -import { FetchService } from './fetch'; - -export function checkHalt(controller: HttpInterceptController, error?: Error) { - if (error instanceof HttpInterceptHaltError) { - throw error; - } else if (controller.halted) { - throw new HttpInterceptHaltError(); - } -} - -export const setup = ( - injectedMetadata: InjectedMetadataSetup, - fatalErrors: FatalErrorsSetup | null -): HttpServiceBase => { - const loadingCount$ = new BehaviorSubject(0); - const stop$ = new Subject(); - const kibanaVersion = injectedMetadata.getKibanaVersion(); - const basePath = new BasePath(injectedMetadata.getBasePath()); - const anonymousPaths = new AnonymousPaths(basePath); - - const fetchService = new FetchService({ basePath, kibanaVersion }); - - function shorthand(method: string) { - return (path: string, options: HttpFetchOptions = {}) => - fetchService.fetch(path, { ...options, method }); - } - - function stop() { - stop$.next(); - loadingCount$.complete(); - } - - function addLoadingCount(count$: Observable) { - count$ - .pipe( - distinctUntilChanged(), - - tap(count => { - if (count < 0) { - throw new Error( - 'Observables passed to loadingCount.add() must only emit positive numbers' - ); - } - }), - - // use takeUntil() so that we can finish each stream on stop() the same way we do when they complete, - // by removing the previous count from the total - takeUntil(stop$), - endWith(0), - startWith(0), - pairwise(), - map(([prev, next]) => next - prev) - ) - .subscribe({ - next: delta => { - loadingCount$.next(loadingCount$.getValue() + delta); - }, - error: error => { - if (fatalErrors) { - fatalErrors.add(error); - } - }, - }); - } - - function getLoadingCount$() { - return loadingCount$.pipe(distinctUntilChanged()); - } - - return { - stop, - basePath, - anonymousPaths, - intercept: fetchService.intercept.bind(fetchService), - removeAllInterceptors: fetchService.removeAllInterceptors.bind(fetchService), - fetch: fetchService.fetch.bind(fetchService), - delete: shorthand('DELETE'), - get: shorthand('GET'), - head: shorthand('HEAD'), - options: shorthand('OPTIONS'), - patch: shorthand('PATCH'), - post: shorthand('POST'), - put: shorthand('PUT'), - addLoadingCount, - getLoadingCount$, - }; -}; diff --git a/src/core/public/http/loading_count_service.mock.ts b/src/core/public/http/loading_count_service.mock.ts new file mode 100644 index 0000000000000..79928aa4b160d --- /dev/null +++ b/src/core/public/http/loading_count_service.mock.ts @@ -0,0 +1,50 @@ +/* + * 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 { LoadingCountSetup, LoadingCountService } from './loading_count_service'; +import { BehaviorSubject } from 'rxjs'; + +const createSetupContractMock = () => { + const setupContract: jest.Mocked = { + addLoadingCountSource: jest.fn(), + getLoadingCount$: jest.fn(), + }; + setupContract.getLoadingCount$.mockReturnValue(new BehaviorSubject(0)); + return setupContract; +}; + +type LoadingCountServiceContract = PublicMethodsOf; +const createServiceMock = () => { + const mocked: jest.Mocked = { + setup: jest.fn(), + start: jest.fn(), + stop: jest.fn(), + }; + + mocked.setup.mockReturnValue(createSetupContractMock()); + mocked.start.mockReturnValue(createSetupContractMock()); + + return mocked; +}; + +export const loadingCountServiceMock = { + create: createServiceMock, + createSetupContract: createSetupContractMock, + createStartContract: createSetupContractMock, +}; diff --git a/src/core/public/http/loading_count_service.test.ts b/src/core/public/http/loading_count_service.test.ts new file mode 100644 index 0000000000000..3ba4d315178cc --- /dev/null +++ b/src/core/public/http/loading_count_service.test.ts @@ -0,0 +1,152 @@ +/* + * 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 { Observable, throwError, of, Subject } from 'rxjs'; +import { toArray } from 'rxjs/operators'; + +import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock'; +import { LoadingCountService } from './loading_count_service'; + +describe('LoadingCountService', () => { + const setup = () => { + const fatalErrors = fatalErrorsServiceMock.createSetupContract(); + const service = new LoadingCountService(); + const loadingCount = service.setup({ fatalErrors }); + return { fatalErrors, loadingCount, service }; + }; + + describe('addLoadingCountSource()', () => { + it('subscribes to passed in sources, unsubscribes on stop', () => { + const { service, loadingCount } = setup(); + + const unsubA = jest.fn(); + const subA = jest.fn().mockReturnValue(unsubA); + loadingCount.addLoadingCountSource(new Observable(subA)); + expect(subA).toHaveBeenCalledTimes(1); + expect(unsubA).not.toHaveBeenCalled(); + + const unsubB = jest.fn(); + const subB = jest.fn().mockReturnValue(unsubB); + loadingCount.addLoadingCountSource(new Observable(subB)); + expect(subB).toHaveBeenCalledTimes(1); + expect(unsubB).not.toHaveBeenCalled(); + + service.stop(); + + expect(subA).toHaveBeenCalledTimes(1); + expect(unsubA).toHaveBeenCalledTimes(1); + expect(subB).toHaveBeenCalledTimes(1); + expect(unsubB).toHaveBeenCalledTimes(1); + }); + + it('adds a fatal error if source observables emit an error', () => { + const { loadingCount, fatalErrors } = setup(); + + loadingCount.addLoadingCountSource(throwError(new Error('foo bar'))); + expect(fatalErrors.add.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: foo bar], + ], + ] + `); + }); + + it('adds a fatal error if source observable emits a negative number', () => { + const { loadingCount, fatalErrors } = setup(); + + loadingCount.addLoadingCountSource(of(1, 2, 3, 4, -9)); + expect(fatalErrors.add.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: Observables passed to loadingCount.add() must only emit positive numbers], + ], + ] + `); + }); + }); + + describe('getLoadingCount$()', () => { + it('emits 0 initially, the right count when sources emit their own count, and ends with zero', async () => { + const { service, loadingCount } = setup(); + + const countA$ = new Subject(); + const countB$ = new Subject(); + const countC$ = new Subject(); + const promise = loadingCount + .getLoadingCount$() + .pipe(toArray()) + .toPromise(); + + loadingCount.addLoadingCountSource(countA$); + loadingCount.addLoadingCountSource(countB$); + loadingCount.addLoadingCountSource(countC$); + + countA$.next(100); + countB$.next(10); + countC$.next(1); + countA$.complete(); + countB$.next(20); + countC$.complete(); + countB$.next(0); + + service.stop(); + expect(await promise).toMatchInlineSnapshot(` + Array [ + 0, + 100, + 110, + 111, + 11, + 21, + 20, + 0, + ] + `); + }); + + it('only emits when loading count changes', async () => { + const { service, loadingCount } = setup(); + + const count$ = new Subject(); + const promise = loadingCount + .getLoadingCount$() + .pipe(toArray()) + .toPromise(); + + loadingCount.addLoadingCountSource(count$); + count$.next(0); + count$.next(0); + count$.next(0); + count$.next(0); + count$.next(0); + count$.next(1); + count$.next(1); + service.stop(); + + expect(await promise).toMatchInlineSnapshot(` + Array [ + 0, + 1, + 0, + ] + `); + }); + }); +}); diff --git a/src/core/public/http/loading_count_service.ts b/src/core/public/http/loading_count_service.ts new file mode 100644 index 0000000000000..14b945e0801ca --- /dev/null +++ b/src/core/public/http/loading_count_service.ts @@ -0,0 +1,93 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BehaviorSubject, Observable, Subject } from 'rxjs'; +import { + distinctUntilChanged, + endWith, + map, + pairwise, + startWith, + takeUntil, + tap, +} from 'rxjs/operators'; +import { FatalErrorsSetup } from '../fatal_errors'; +import { CoreService } from '../../types'; + +/** @public */ +export interface LoadingCountSetup { + addLoadingCountSource(countSource$: Observable): void; + + getLoadingCount$(): Observable; +} + +/** + * See {@link LoadingCountSetup}. + * @public + */ +export type LoadingCountStart = LoadingCountSetup; + +/** @internal */ +export class LoadingCountService implements CoreService { + private readonly stop$ = new Subject(); + private readonly loadingCount$ = new BehaviorSubject(0); + + public setup({ fatalErrors }: { fatalErrors: FatalErrorsSetup }) { + return { + getLoadingCount$: () => this.loadingCount$.pipe(distinctUntilChanged()), + addLoadingCountSource: (count$: Observable) => { + count$ + .pipe( + distinctUntilChanged(), + + tap(count => { + if (count < 0) { + throw new Error( + 'Observables passed to loadingCount.add() must only emit positive numbers' + ); + } + }), + + // use takeUntil() so that we can finish each stream on stop() the same way we do when they complete, + // by removing the previous count from the total + takeUntil(this.stop$), + endWith(0), + startWith(0), + pairwise(), + map(([prev, next]) => next - prev) + ) + .subscribe({ + next: delta => { + this.loadingCount$.next(this.loadingCount$.getValue() + delta); + }, + error: error => fatalErrors.add(error), + }); + }, + }; + } + + public start({ fatalErrors }: { fatalErrors: FatalErrorsSetup }) { + return this.setup({ fatalErrors }); + } + + public stop() { + this.stop$.next(); + this.loadingCount$.complete(); + } +} diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts index 48385a72325db..27ffddc79cf65 100644 --- a/src/core/public/http/types.ts +++ b/src/core/public/http/types.ts @@ -20,10 +20,7 @@ import { Observable } from 'rxjs'; /** @public */ -export interface HttpServiceBase { - /** @internal */ - stop(): void; - +export interface HttpSetup { /** * APIs for manipulating the basePath on URL segments. */ @@ -41,11 +38,6 @@ export interface HttpServiceBase { */ intercept(interceptor: HttpInterceptor): () => void; - /** - * Removes all configured interceptors. - */ - removeAllInterceptors(): void; - /** Makes an HTTP request. Defaults to a GET request unless overriden. See {@link HttpHandler} for options. */ fetch: HttpHandler; /** Makes an HTTP request with the DELETE method. See {@link HttpHandler} for options. */ @@ -68,7 +60,7 @@ export interface HttpServiceBase { * more than 0. * @param countSource$ an Observable to subscribe to for loading count updates. */ - addLoadingCount(countSource$: Observable): void; + addLoadingCountSource(countSource$: Observable): void; /** * Get the sum of all loading count sources as a single Observable. @@ -76,6 +68,12 @@ export interface HttpServiceBase { getLoadingCount$(): Observable; } +/** + * See {@link HttpSetup} + * @public + */ +export type HttpStart = HttpSetup; + /** * APIs for manipulating the basePath on URL segments. * @public @@ -112,18 +110,6 @@ export interface IAnonymousPaths { register(path: string): void; } -/** - * See {@link HttpServiceBase} - * @public - */ -export type HttpSetup = HttpServiceBase; - -/** - * See {@link HttpServiceBase} - * @public - */ -export type HttpStart = HttpServiceBase; - /** @public */ export interface HttpHeadersInit { [name: string]: any; diff --git a/src/core/public/index.ts b/src/core/public/index.ts index f83ca2564de8e..7488f9b973b71 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -121,7 +121,6 @@ export { } from './saved_objects'; export { - HttpServiceBase, HttpHeadersInit, HttpRequestInit, HttpFetchOptions, diff --git a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap index 626c91b6a9668..9bd686776138f 100644 --- a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap +++ b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap @@ -31,7 +31,7 @@ Array [ ] `; -exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"
Flyout content
"`; +exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"
Flyout content
"`; exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 1`] = ` Array [ @@ -74,4 +74,4 @@ Array [ ] `; -exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
Flyout content 2
"`; +exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
Flyout content 2
"`; diff --git a/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap b/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap index 3928c54f90179..131ec836f5252 100644 --- a/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap +++ b/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap @@ -29,7 +29,7 @@ Array [ ] `; -exports[`ModalService openModal() renders a modal to the DOM 2`] = `"
Modal content
"`; +exports[`ModalService openModal() renders a modal to the DOM 2`] = `"
Modal content
"`; exports[`ModalService openModal() with a currently active modal replaces the current modal with a new one 1`] = ` Array [ diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 83b4e67c1cb15..dfbb6b4a6fbf5 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -544,8 +544,8 @@ export interface HttpRequestInit { } // @public (undocumented) -export interface HttpServiceBase { - addLoadingCount(countSource$: Observable): void; +export interface HttpSetup { + addLoadingCountSource(countSource$: Observable): void; anonymousPaths: IAnonymousPaths; basePath: IBasePath; delete: HttpHandler; @@ -558,16 +558,10 @@ export interface HttpServiceBase { patch: HttpHandler; post: HttpHandler; put: HttpHandler; - removeAllInterceptors(): void; - // @internal (undocumented) - stop(): void; } // @public -export type HttpSetup = HttpServiceBase; - -// @public -export type HttpStart = HttpServiceBase; +export type HttpStart = HttpSetup; // @public export interface I18nStart { @@ -877,7 +871,7 @@ export interface SavedObjectsBulkUpdateOptions { // @public export class SavedObjectsClient { // @internal - constructor(http: HttpServiceBase); + constructor(http: HttpSetup); bulkCreate: (objects?: SavedObjectsBulkCreateObject[], options?: SavedObjectsBulkCreateOptions) => Promise>; bulkGet: (objects?: { id: string; diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index c71fe51956c28..dab98ee66cdb1 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -36,7 +36,7 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../legacy/ui/public/error_auto_create_index/error_auto_create_index'; import { SimpleSavedObject } from './simple_saved_object'; -import { HttpFetchOptions, HttpServiceBase } from '../http'; +import { HttpFetchOptions, HttpSetup } from '../http'; type SavedObjectsFindOptions = Omit; @@ -158,7 +158,7 @@ export type SavedObjectsClientContract = PublicMethodsOf; * @public */ export class SavedObjectsClient { - private http: HttpServiceBase; + private http: HttpSetup; private batchQueue: BatchQueueEntry[]; /** @@ -194,7 +194,7 @@ export class SavedObjectsClient { ); /** @internal */ - constructor(http: HttpServiceBase) { + constructor(http: HttpSetup) { this.http = http; this.batchQueue = []; } diff --git a/src/core/public/ui_settings/ui_settings_service.test.ts b/src/core/public/ui_settings/ui_settings_service.test.ts index afb68c4844901..2747a78d93fa6 100644 --- a/src/core/public/ui_settings/ui_settings_service.test.ts +++ b/src/core/public/ui_settings/ui_settings_service.test.ts @@ -38,7 +38,7 @@ describe('#stop', () => { it('stops the uiSettingsClient and uiSettingsApi', async () => { const service = new UiSettingsService(); let loadingCount$: Rx.Observable; - defaultDeps.http.addLoadingCount.mockImplementation(obs$ => (loadingCount$ = obs$)); + defaultDeps.http.addLoadingCountSource.mockImplementation(obs$ => (loadingCount$ = obs$)); const client = service.setup(defaultDeps); service.stop(); diff --git a/src/core/public/ui_settings/ui_settings_service.ts b/src/core/public/ui_settings/ui_settings_service.ts index 5a03cd1cfeedc..1e01d15fa337b 100644 --- a/src/core/public/ui_settings/ui_settings_service.ts +++ b/src/core/public/ui_settings/ui_settings_service.ts @@ -38,7 +38,7 @@ export class UiSettingsService { public setup({ http, injectedMetadata }: UiSettingsServiceDeps): IUiSettingsClient { this.uiSettingsApi = new UiSettingsApi(http); - http.addLoadingCount(this.uiSettingsApi.getLoadingCount$()); + http.addLoadingCountSource(this.uiSettingsApi.getLoadingCount$()); // TODO: Migrate away from legacyMetadata https://github.com/elastic/kibana/issues/22779 const legacyMetadata = injectedMetadata.getLegacyMetadata(); diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index 3bed8fe4186ac..5c52e71cd54bb 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -313,6 +313,7 @@ type RequestHandlerEnhanced< /** * A function executed when route path matched requested resource path. * Request handler is expected to return a result of one of {@link KibanaResponseFactory} functions. + * @param context {@link RequestHandlerContext} - the core context exposed for this request. * @param request {@link KibanaRequest} - object containing information about requested resource, * such as path, method, headers, parameters, query, body, etc. * @param response {@link KibanaResponseFactory} - a set of helper functions used to respond to a request. diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 2aaa8306e871f..060265120b865 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -43,7 +43,7 @@ import { ElasticsearchServiceSetup, IScopedClusterClient } from './elasticsearch import { HttpServiceSetup } from './http'; import { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId } from './plugins'; import { ContextSetup } from './context'; -import { IUiSettingsClient, UiSettingsServiceSetup } from './ui_settings'; +import { IUiSettingsClient, UiSettingsServiceSetup, UiSettingsServiceStart } from './ui_settings'; import { SavedObjectsClientContract } from './saved_objects/types'; import { SavedObjectsServiceSetup, SavedObjectsServiceStart } from './saved_objects'; import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; @@ -204,6 +204,7 @@ export { UiSettingsParams, UiSettingsType, UiSettingsServiceSetup, + UiSettingsServiceStart, UserProvidedValues, } from './ui_settings'; @@ -234,6 +235,8 @@ export { LegacyServiceSetupDeps, LegacyServiceStartDeps } from './legacy'; * data client which uses the credentials of the incoming request * - {@link ScopedClusterClient | elasticsearch.adminClient} - Elasticsearch * admin client which uses the credentials of the incoming request + * - {@link IUiSettingsClient | uiSettings.client} - uiSettings client + * which uses the credentials of the incoming request * * @public */ @@ -284,6 +287,8 @@ export interface CoreStart { capabilities: CapabilitiesStart; /** {@link SavedObjectsServiceStart} */ savedObjects: SavedObjectsServiceStart; + /** {@link UiSettingsServiceStart} */ + uiSettings: UiSettingsServiceStart; } export { diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts index 06cf848bff25a..52adaaccab4b7 100644 --- a/src/core/server/internal_types.ts +++ b/src/core/server/internal_types.ts @@ -19,7 +19,7 @@ import { InternalElasticsearchServiceSetup } from './elasticsearch'; import { InternalHttpServiceSetup } from './http'; -import { InternalUiSettingsServiceSetup } from './ui_settings'; +import { InternalUiSettingsServiceSetup, InternalUiSettingsServiceStart } from './ui_settings'; import { ContextSetup } from './context'; import { InternalSavedObjectsServiceStart, @@ -45,4 +45,5 @@ export interface InternalCoreSetup { export interface InternalCoreStart { capabilities: CapabilitiesStart; savedObjects: InternalSavedObjectsServiceStart; + uiSettings: InternalUiSettingsServiceStart; } diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index 17ec1e9756432..c652bb1c94887 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -27,7 +27,7 @@ import { findLegacyPluginSpecsMock } from './legacy_service.test.mocks'; import { BehaviorSubject, throwError } from 'rxjs'; import { LegacyService, LegacyServiceSetupDeps, LegacyServiceStartDeps } from '.'; // @ts-ignore: implicit any for JS file -import MockClusterManager from '../../../cli/cluster/cluster_manager'; +import { ClusterManager as MockClusterManager } from '../../../cli/cluster/cluster_manager'; import KbnServer from '../../../legacy/server/kbn_server'; import { Config, Env, ObjectToConfigAdapter } from '../config'; import { getEnvOptions } from '../config/__mocks__/env'; @@ -98,6 +98,7 @@ beforeEach(() => { core: { capabilities: capabilitiesServiceMock.createStartContract(), savedObjects: savedObjectsServiceMock.createStartContract(), + uiSettings: uiSettingsServiceMock.createStartContract(), plugins: { contracts: new Map() }, }, plugins: {}, @@ -354,9 +355,15 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => { await devClusterLegacyService.setup(setupDeps); await devClusterLegacyService.start(startDeps); - const [[cliArgs, , basePathProxy]] = MockClusterManager.create.mock.calls; - expect(cliArgs.basePath).toBe(false); - expect(basePathProxy).not.toBeDefined(); + expect(MockClusterManager).toHaveBeenCalledTimes(1); + expect(MockClusterManager).toHaveBeenCalledWith( + expect.objectContaining({ silent: true, basePath: false }), + expect.objectContaining({ + get: expect.any(Function), + set: expect.any(Function), + }), + undefined + ); }); test('creates ClusterManager with base path proxy.', async () => { @@ -376,11 +383,15 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => { await devClusterLegacyService.setup(setupDeps); await devClusterLegacyService.start(startDeps); - expect(MockClusterManager.create).toBeCalledTimes(1); - - const [[cliArgs, , basePathProxy]] = MockClusterManager.create.mock.calls; - expect(cliArgs.basePath).toEqual(true); - expect(basePathProxy).toBeInstanceOf(BasePathProxyServer); + expect(MockClusterManager).toHaveBeenCalledTimes(1); + expect(MockClusterManager).toHaveBeenCalledWith( + expect.objectContaining({ quiet: true, basePath: true }), + expect.objectContaining({ + get: expect.any(Function), + set: expect.any(Function), + }), + expect.any(BasePathProxyServer) + ); }); }); diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 1bba38433d7f4..2e8a467eff995 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -244,7 +244,7 @@ export class LegacyService implements CoreService { private async createClusterManager(config: LegacyConfig) { const basePathProxy$ = this.coreContext.env.cliArgs.basePath - ? combineLatest(this.devConfig$, this.httpConfig$).pipe( + ? combineLatest([this.devConfig$, this.httpConfig$]).pipe( first(), map( ([dev, http]) => @@ -253,7 +253,9 @@ export class LegacyService implements CoreService { ) : EMPTY; - require('../../../cli/cluster/cluster_manager').create( + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { ClusterManager } = require('../../../cli/cluster/cluster_manager'); + return new ClusterManager( this.coreContext.env.cliArgs, config, await basePathProxy$.toPromise() @@ -310,6 +312,7 @@ export class LegacyService implements CoreService { const coreStart: CoreStart = { capabilities: startDeps.core.capabilities, savedObjects: { getScopedClient: startDeps.core.savedObjects.getScopedClient }, + uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient }, }; // eslint-disable-next-line @typescript-eslint/no-var-requires diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 3a68b18409b0a..53849b040c413 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -121,6 +121,7 @@ function createCoreStartMock() { const mock: MockedKeys = { capabilities: capabilitiesServiceMock.createStartContract(), savedObjects: savedObjectsServiceMock.createStartContract(), + uiSettings: uiSettingsServiceMock.createStartContract(), }; return mock; @@ -143,6 +144,7 @@ function createInternalCoreStartMock() { const startDeps: InternalCoreStart = { capabilities: capabilitiesServiceMock.createStartContract(), savedObjects: savedObjectsServiceMock.createStartContract(), + uiSettings: uiSettingsServiceMock.createStartContract(), }; return startDeps; } diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 04a7547fd3747..6e9a7967e9eca 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -200,6 +200,11 @@ export function createPluginStartContext( capabilities: { resolveCapabilities: deps.capabilities.resolveCapabilities, }, - savedObjects: { getScopedClient: deps.savedObjects.getScopedClient }, + savedObjects: { + getScopedClient: deps.savedObjects.getScopedClient, + }, + uiSettings: { + asScopedToClient: deps.uiSettings.asScopedToClient, + }, }; } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 4e6493a17aea1..85c0af8131ccb 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -449,11 +449,11 @@ export interface AuthToolkit { export class BasePath { // @internal constructor(serverBasePath?: string); - get: (request: KibanaRequest | LegacyRequest) => string; + get: (request: LegacyRequest | KibanaRequest) => string; prepend: (path: string) => string; remove: (path: string) => string; readonly serverBasePath: string; - set: (request: KibanaRequest | LegacyRequest, requestSpecificBasePath: string) => void; + set: (request: LegacyRequest | KibanaRequest, requestSpecificBasePath: string) => void; } // Warning: (ae-forgotten-export) The symbol "BootstrapArgs" needs to be exported by the entry point index.d.ts @@ -574,6 +574,8 @@ export interface CoreStart { capabilities: CapabilitiesStart; // (undocumented) savedObjects: SavedObjectsServiceStart; + // (undocumented) + uiSettings: UiSettingsServiceStart; } // @public @@ -1822,6 +1824,11 @@ export interface UiSettingsServiceSetup { register(settings: Record): void; } +// @public (undocumented) +export interface UiSettingsServiceStart { + asScopedToClient(savedObjectsClient: SavedObjectsClientContract): IUiSettingsClient; +} + // @public export type UiSettingsType = 'json' | 'markdown' | 'number' | 'select' | 'boolean' | 'string'; diff --git a/src/core/server/server.test.mocks.ts b/src/core/server/server.test.mocks.ts index 59925f46543e7..67a878be86742 100644 --- a/src/core/server/server.test.mocks.ts +++ b/src/core/server/server.test.mocks.ts @@ -65,6 +65,12 @@ jest.doMock('./context/context_service', () => ({ ContextService: jest.fn(() => mockContextService), })); +import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; +export const mockUiSettingsService = uiSettingsServiceMock.create(); +jest.doMock('./ui_settings/ui_settings_service', () => ({ + UiSettingsService: jest.fn(() => mockUiSettingsService), +})); + export const mockEnsureValidConfiguration = jest.fn(); jest.doMock('./legacy/config/ensure_valid_configuration', () => ({ ensureValidConfiguration: mockEnsureValidConfiguration, diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index d593a6275fa4c..27dff1e208aaf 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -26,6 +26,7 @@ import { mockSavedObjectsService, mockContextService, mockEnsureValidConfiguration, + mockUiSettingsService, } from './server.test.mocks'; import { BehaviorSubject } from 'rxjs'; @@ -57,6 +58,7 @@ test('sets up services on "setup"', async () => { expect(mockPluginsService.setup).not.toHaveBeenCalled(); expect(mockLegacyService.setup).not.toHaveBeenCalled(); expect(mockSavedObjectsService.setup).not.toHaveBeenCalled(); + expect(mockUiSettingsService.setup).not.toHaveBeenCalled(); await server.setup(); @@ -65,6 +67,7 @@ test('sets up services on "setup"', async () => { expect(mockPluginsService.setup).toHaveBeenCalledTimes(1); expect(mockLegacyService.setup).toHaveBeenCalledTimes(1); expect(mockSavedObjectsService.setup).toHaveBeenCalledTimes(1); + expect(mockUiSettingsService.setup).toHaveBeenCalledTimes(1); }); test('injects legacy dependency to context#setup()', async () => { @@ -100,11 +103,14 @@ test('runs services on "start"', async () => { expect(mockHttpService.start).not.toHaveBeenCalled(); expect(mockLegacyService.start).not.toHaveBeenCalled(); expect(mockSavedObjectsService.start).not.toHaveBeenCalled(); + expect(mockUiSettingsService.start).not.toHaveBeenCalled(); + await server.start(); expect(mockHttpService.start).toHaveBeenCalledTimes(1); expect(mockLegacyService.start).toHaveBeenCalledTimes(1); expect(mockSavedObjectsService.start).toHaveBeenCalledTimes(1); + expect(mockUiSettingsService.start).toHaveBeenCalledTimes(1); }); test('does not fail on "setup" if there are unused paths detected', async () => { @@ -125,6 +131,7 @@ test('stops services on "stop"', async () => { expect(mockPluginsService.stop).not.toHaveBeenCalled(); expect(mockLegacyService.stop).not.toHaveBeenCalled(); expect(mockSavedObjectsService.stop).not.toHaveBeenCalled(); + expect(mockUiSettingsService.stop).not.toHaveBeenCalled(); await server.stop(); @@ -133,6 +140,7 @@ test('stops services on "stop"', async () => { expect(mockPluginsService.stop).toHaveBeenCalledTimes(1); expect(mockLegacyService.stop).toHaveBeenCalledTimes(1); expect(mockSavedObjectsService.stop).toHaveBeenCalledTimes(1); + expect(mockUiSettingsService.stop).toHaveBeenCalledTimes(1); }); test(`doesn't setup core services if config validation fails`, async () => { @@ -146,6 +154,7 @@ test(`doesn't setup core services if config validation fails`, async () => { expect(mockElasticsearchService.setup).not.toHaveBeenCalled(); expect(mockPluginsService.setup).not.toHaveBeenCalled(); expect(mockLegacyService.setup).not.toHaveBeenCalled(); + expect(mockUiSettingsService.setup).not.toHaveBeenCalled(); }); test(`doesn't setup core services if legacy config validation fails`, async () => { @@ -164,4 +173,5 @@ test(`doesn't setup core services if legacy config validation fails`, async () = expect(mockPluginsService.setup).not.toHaveBeenCalled(); expect(mockLegacyService.setup).not.toHaveBeenCalled(); expect(mockSavedObjectsService.stop).not.toHaveBeenCalled(); + expect(mockUiSettingsService.setup).not.toHaveBeenCalled(); }); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 89d99d6c4a4ec..5ca3800f3fb8f 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -158,14 +158,18 @@ export class Server { this.log.debug('starting server'); const savedObjectsStart = await this.savedObjects.start({}); const capabilitiesStart = this.capabilities.start(); + const uiSettingsStart = await this.uiSettings.start(); + const pluginsStart = await this.plugins.start({ capabilities: capabilitiesStart, savedObjects: savedObjectsStart, + uiSettings: uiSettingsStart, }); const coreStart = { capabilities: capabilitiesStart, savedObjects: savedObjectsStart, + uiSettings: uiSettingsStart, plugins: pluginsStart, }; await this.legacy.start({ @@ -186,6 +190,7 @@ export class Server { await this.savedObjects.stop(); await this.elasticsearch.stop(); await this.http.stop(); + await this.uiSettings.stop(); } private registerDefaultRoute(httpSetup: InternalHttpServiceSetup) { diff --git a/src/core/server/ui_settings/index.ts b/src/core/server/ui_settings/index.ts index fd0a21bed4e12..f1185474c2160 100644 --- a/src/core/server/ui_settings/index.ts +++ b/src/core/server/ui_settings/index.ts @@ -24,9 +24,11 @@ export { UiSettingsService } from './ui_settings_service'; export { UiSettingsServiceSetup, + UiSettingsServiceStart, IUiSettingsClient, UiSettingsParams, InternalUiSettingsServiceSetup, + InternalUiSettingsServiceStart, UiSettingsType, UserProvidedValues, } from './types'; diff --git a/src/core/server/ui_settings/types.ts b/src/core/server/ui_settings/types.ts index 49d3d3b33392f..5e3f0a4fbb6bd 100644 --- a/src/core/server/ui_settings/types.ts +++ b/src/core/server/ui_settings/types.ts @@ -125,6 +125,7 @@ export interface UiSettingsServiceSetup { * @param settings * * @example + * ```ts * setup(core: CoreSetup){ * core.uiSettings.register([{ * foo: { @@ -134,6 +135,29 @@ export interface UiSettingsServiceSetup { * }, * }]); * } + * ``` */ register(settings: Record): void; } + +/** @public */ +export interface UiSettingsServiceStart { + /** + * Creates a {@link IUiSettingsClient} with provided *scoped* saved objects client. + * + * This should only be used in the specific case where the client needs to be accessed + * from outside of the scope of a {@link RequestHandler}. + * + * @example + * ```ts + * start(core: CoreStart) { + * const soClient = core.savedObjects.getScopedClient(arbitraryRequest); + * const uiSettingsClient = core.uiSettings.asScopedToClient(soClient); + * } + * ``` + */ + asScopedToClient(savedObjectsClient: SavedObjectsClientContract): IUiSettingsClient; +} + +/** @internal */ +export type InternalUiSettingsServiceStart = UiSettingsServiceStart; diff --git a/src/core/server/ui_settings/ui_settings_service.mock.ts b/src/core/server/ui_settings/ui_settings_service.mock.ts index bb21109a2f967..b850963a0bc1b 100644 --- a/src/core/server/ui_settings/ui_settings_service.mock.ts +++ b/src/core/server/ui_settings/ui_settings_service.mock.ts @@ -17,7 +17,12 @@ * under the License. */ -import { IUiSettingsClient, InternalUiSettingsServiceSetup } from './types'; +import { + IUiSettingsClient, + InternalUiSettingsServiceSetup, + InternalUiSettingsServiceStart, +} from './types'; +import { UiSettingsService } from './ui_settings_service'; const createClientMock = () => { const mocked: jest.Mocked = { @@ -46,7 +51,31 @@ const createSetupMock = () => { return mocked; }; +const createStartMock = () => { + const mocked: jest.Mocked = { + asScopedToClient: jest.fn(), + }; + + mocked.asScopedToClient.mockReturnValue(createClientMock()); + + return mocked; +}; + +type UiSettingsServiceContract = PublicMethodsOf; +const createMock = () => { + const mocked: jest.Mocked = { + setup: jest.fn(), + start: jest.fn(), + stop: jest.fn(), + }; + mocked.setup.mockResolvedValue(createSetupMock()); + mocked.start.mockResolvedValue(createStartMock()); + return mocked; +}; + export const uiSettingsServiceMock = { createSetupContract: createSetupMock, + createStartContract: createStartMock, createClient: createClientMock, + create: createMock, }; diff --git a/src/core/server/ui_settings/ui_settings_service.test.ts b/src/core/server/ui_settings/ui_settings_service.test.ts index d7a085a220190..d908a91a39c70 100644 --- a/src/core/server/ui_settings/ui_settings_service.test.ts +++ b/src/core/server/ui_settings/ui_settings_service.test.ts @@ -114,4 +114,40 @@ describe('uiSettings', () => { }); }); }); + + describe('#start', () => { + describe('#asScopedToClient', () => { + it('passes saved object type "config" to UiSettingsClient', async () => { + const service = new UiSettingsService(coreContext); + await service.setup(setupDeps); + const start = await service.start(); + start.asScopedToClient(savedObjectsClient); + + expect(MockUiSettingsClientConstructor).toBeCalledTimes(1); + expect(MockUiSettingsClientConstructor.mock.calls[0][0].type).toBe('config'); + }); + + it('passes overrides to UiSettingsClient', async () => { + const service = new UiSettingsService(coreContext); + await service.setup(setupDeps); + const start = await service.start(); + start.asScopedToClient(savedObjectsClient); + expect(MockUiSettingsClientConstructor).toBeCalledTimes(1); + expect(MockUiSettingsClientConstructor.mock.calls[0][0].overrides).toBe(overrides); + expect(MockUiSettingsClientConstructor.mock.calls[0][0].overrides).toEqual(overrides); + }); + + it('passes a copy of set defaults to UiSettingsClient', async () => { + const service = new UiSettingsService(coreContext); + const setup = await service.setup(setupDeps); + setup.register(defaults); + const start = await service.start(); + start.asScopedToClient(savedObjectsClient); + + expect(MockUiSettingsClientConstructor).toBeCalledTimes(1); + expect(MockUiSettingsClientConstructor.mock.calls[0][0].defaults).toEqual(defaults); + expect(MockUiSettingsClientConstructor.mock.calls[0][0].defaults).not.toBe(defaults); + }); + }); + }); }); diff --git a/src/core/server/ui_settings/ui_settings_service.ts b/src/core/server/ui_settings/ui_settings_service.ts index 8458a80de4952..db08c3cad85a2 100644 --- a/src/core/server/ui_settings/ui_settings_service.ts +++ b/src/core/server/ui_settings/ui_settings_service.ts @@ -25,9 +25,13 @@ import { Logger } from '../logging'; import { SavedObjectsClientContract } from '../saved_objects/types'; import { InternalHttpServiceSetup } from '../http'; -import { UiSettingsConfigType } from './ui_settings_config'; +import { UiSettingsConfigType, config as uiConfigDefinition } from './ui_settings_config'; import { UiSettingsClient } from './ui_settings_client'; -import { InternalUiSettingsServiceSetup, UiSettingsParams } from './types'; +import { + InternalUiSettingsServiceSetup, + InternalUiSettingsServiceStart, + UiSettingsParams, +} from './types'; import { mapToObject } from '../../utils/'; import { registerRoutes } from './routes'; @@ -37,42 +41,52 @@ interface SetupDeps { } /** @internal */ -export class UiSettingsService implements CoreService { +export class UiSettingsService + implements CoreService { private readonly log: Logger; private readonly config$: Observable; private readonly uiSettingsDefaults = new Map(); + private overrides: Record = {}; constructor(private readonly coreContext: CoreContext) { this.log = coreContext.logger.get('ui-settings-service'); - this.config$ = coreContext.configService.atPath('uiSettings'); + this.config$ = coreContext.configService.atPath(uiConfigDefinition.path); } public async setup(deps: SetupDeps): Promise { registerRoutes(deps.http.createRouter('')); this.log.debug('Setting up ui settings service'); - const overrides = await this.getOverrides(deps); - const { version, buildNum } = this.coreContext.env.packageInfo; - + this.overrides = await this.getOverrides(deps); return { register: this.register.bind(this), - asScopedToClient: (savedObjectsClient: SavedObjectsClientContract) => { - return new UiSettingsClient({ - type: 'config', - id: version, - buildNum, - savedObjectsClient, - defaults: mapToObject(this.uiSettingsDefaults), - overrides, - log: this.log, - }); - }, + asScopedToClient: this.getScopedClientFactory(), }; } - public async start() {} + public async start(): Promise { + return { + asScopedToClient: this.getScopedClientFactory(), + }; + } public async stop() {} + private getScopedClientFactory(): ( + savedObjectsClient: SavedObjectsClientContract + ) => UiSettingsClient { + const { version, buildNum } = this.coreContext.env.packageInfo; + return (savedObjectsClient: SavedObjectsClientContract) => + new UiSettingsClient({ + type: 'config', + id: version, + buildNum, + savedObjectsClient, + defaults: mapToObject(this.uiSettingsDefaults), + overrides: this.overrides, + log: this.log, + }); + } + private register(settings: Record = {}) { Object.entries(settings).forEach(([key, value]) => { if (this.uiSettingsDefaults.has(key)) { @@ -93,7 +107,6 @@ export class UiSettingsService implements CoreService { - setup(...params: any[]): Promise; - start(...params: any[]): Promise; - stop(): Promise; + setup(...params: any[]): TSetup | Promise; + start(...params: any[]): TStart | Promise; + stop(): void | Promise; } diff --git a/src/fixtures/stubbed_logstash_index_pattern.js b/src/fixtures/stubbed_logstash_index_pattern.js index 22fbf0ab5a5f8..e20d1b5cd7717 100644 --- a/src/fixtures/stubbed_logstash_index_pattern.js +++ b/src/fixtures/stubbed_logstash_index_pattern.js @@ -21,7 +21,7 @@ import StubIndexPattern from 'test_utils/stub_index_pattern'; import stubbedLogstashFields from 'fixtures/logstash_fields'; import { getKbnFieldType } from '../plugins/data/common'; -import { mockUiSettings } from '../legacy/ui/public/new_platform/new_platform.karma_mock'; +import { npSetup } from '../legacy/ui/public/new_platform/new_platform.karma_mock'; export default function stubbedLogstashIndexPatternService() { const mockLogstashFields = stubbedLogstashFields(); @@ -41,13 +41,8 @@ export default function stubbedLogstashIndexPatternService() { }; }); - const indexPattern = new StubIndexPattern( - 'logstash-*', - cfg => cfg, - 'time', - fields, - mockUiSettings - ); + const indexPattern = new StubIndexPattern('logstash-*', cfg => cfg, 'time', fields, npSetup.core); + indexPattern.id = 'logstash-*'; indexPattern.isTimeNanosBased = () => false; diff --git a/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/mode/script_highlight_rules.js b/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/mode/script_highlight_rules.js index 801580f4e158c..b3999c76493c0 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/mode/script_highlight_rules.js +++ b/src/legacy/core_plugins/console/public/np_ready/application/models/legacy_core_editor/mode/script_highlight_rules.js @@ -61,7 +61,6 @@ export function ScriptHighlightRules() { }, { token: 'script.keyword.operator', - regex: '\\?\\.|\\*\\.|=~|==~|!|%|&|\\*|\\-\\-|\\-|\\+\\+|\\+|~|===|==|=|!=|!==|<=|>=|<<=|>>=|>>>=|<>|<|>|->|!|&&|\\|\\||\\?\\:|\\*=|%=|\\+=|\\-=|&=|\\^=|\\b(?:in|instanceof|new|typeof|void)', }, diff --git a/src/legacy/core_plugins/input_control_vis/index.ts b/src/legacy/core_plugins/input_control_vis/index.ts new file mode 100644 index 0000000000000..8f6178e26126b --- /dev/null +++ b/src/legacy/core_plugins/input_control_vis/index.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 { resolve } from 'path'; +import { Legacy } from 'kibana'; + +import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; + +const inputControlVisPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => + new Plugin({ + id: 'input_control_vis', + require: ['kibana', 'elasticsearch', 'visualizations', 'interpreter', 'data'], + publicDir: resolve(__dirname, 'public'), + uiExports: { + styleSheetPaths: resolve(__dirname, 'public/index.scss'), + hacks: [resolve(__dirname, 'public/legacy')], + injectDefaultVars: server => ({}), + }, + init: (server: Legacy.Server) => ({}), + config(Joi: any) { + return Joi.object({ + enabled: Joi.boolean().default(true), + }).default(); + }, + } as Legacy.PluginSpecOptions); + +// eslint-disable-next-line import/no-default-export +export default inputControlVisPluginInitializer; diff --git a/src/legacy/core_plugins/input_control_vis/public/__snapshots__/input_control_fn.test.js.snap b/src/legacy/core_plugins/input_control_vis/public/__snapshots__/input_control_fn.test.ts.snap similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/__snapshots__/input_control_fn.test.js.snap rename to src/legacy/core_plugins/input_control_vis/public/__snapshots__/input_control_fn.test.ts.snap diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.js.snap b/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.tsx.snap similarity index 73% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.js.snap rename to src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.tsx.snap index 809214f756713..632fe63e9e148 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.js.snap +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/controls_tab.test.tsx.snap @@ -16,9 +16,33 @@ exports[`renders ControlsTab 1`] = ` "size": 5, "type": "terms", }, + "parent": "parent", "type": "list", } } + deps={ + Object { + "core": Object { + "getStartServices": [MockFunction], + "injectedMetadata": Object { + "getInjectedVar": [MockFunction], + }, + }, + "data": Object { + "query": Object { + "filterManager": Object { + "fieldName": "myField", + "getAppFilters": [MockFunction], + "getGlobalFilters": [MockFunction], + "getIndexPattern": [Function], + }, + "timefilter": Object { + "timefilter": Object {}, + }, + }, + }, + } + } getIndexPattern={[Function]} handleCheckboxOptionChange={[Function]} handleFieldNameChange={[Function]} @@ -49,9 +73,33 @@ exports[`renders ControlsTab 1`] = ` "options": Object { "step": 1, }, + "parent": "parent", "type": "range", } } + deps={ + Object { + "core": Object { + "getStartServices": [MockFunction], + "injectedMetadata": Object { + "getInjectedVar": [MockFunction], + }, + }, + "data": Object { + "query": Object { + "filterManager": Object { + "fieldName": "myField", + "getAppFilters": [MockFunction], + "getGlobalFilters": [MockFunction], + "getIndexPattern": [Function], + }, + "timefilter": Object { + "timefilter": Object {}, + }, + }, + }, + } + } getIndexPattern={[Function]} handleCheckboxOptionChange={[Function]} handleFieldNameChange={[Function]} diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/list_control_editor.test.js.snap b/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/list_control_editor.test.tsx.snap similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/list_control_editor.test.js.snap rename to src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/list_control_editor.test.tsx.snap index ff3d1ffc146e3..9bc8b1b9ac5cd 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/list_control_editor.test.js.snap +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/__snapshots__/list_control_editor.test.tsx.snap @@ -3,6 +3,7 @@ exports[`renders dynamic options should display disabled dynamic options with tooltip for non-string fields 1`] = ` { + return fields.find(({ name: n }) => n === name); +}; + +export const getDepsMock = (): InputControlVisDependencies => + ({ + core: { + getStartServices: jest.fn().mockReturnValue([ + null, + { + data: { + ui: { + IndexPatternSelect: () => (
) as any, + }, + indexPatterns: { + get: () => ({ + fields, + }), + }, + }, + }, + ]), + injectedMetadata: { + getInjectedVar: jest.fn().mockImplementation(key => { + switch (key) { + case 'autocompleteTimeout': + return 1000; + case 'autocompleteTerminateAfter': + return 100000; + default: + return ''; + } + }), + }, + }, + data: { + query: { + filterManager: { + fieldName: 'myField', + getIndexPattern: () => ({ + fields, + }), + getAppFilters: jest.fn().mockImplementation(() => []), + getGlobalFilters: jest.fn().mockImplementation(() => []), + }, + timefilter: { + timefilter: {}, + }, + }, + }, + } as any); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_index_pattern_mock.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_index_pattern_mock.ts similarity index 82% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_index_pattern_mock.js rename to src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_index_pattern_mock.ts index c693bf100e265..638dd7170cb8d 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_index_pattern_mock.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_index_pattern_mock.ts @@ -17,7 +17,12 @@ * under the License. */ -export const getIndexPatternMock = () => { +import { IIndexPattern } from '../../../../../../../plugins/data/public'; + +/** + * Returns forced **Partial** IndexPattern for use in tests + */ +export const getIndexPatternMock = (): Promise => { return Promise.resolve({ id: 'mockIndexPattern', title: 'mockIndexPattern', @@ -26,5 +31,5 @@ export const getIndexPatternMock = () => { { name: 'textField', type: 'string', aggregatable: false }, { name: 'numberField', type: 'number', aggregatable: true }, ], - }); + } as IIndexPattern); }; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_index_patterns_mock.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_index_patterns_mock.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_index_patterns_mock.js rename to src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_index_patterns_mock.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_search_service_mock.ts b/src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_search_service_mock.ts new file mode 100644 index 0000000000000..9da47bedcc784 --- /dev/null +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/get_search_service_mock.ts @@ -0,0 +1,46 @@ +/* + * 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 { SearchSource } from '../../../legacy_imports'; + +export const getSearchSourceMock = (esSearchResponse?: any): SearchSource => + jest.fn().mockImplementation(() => ({ + setParent: jest.fn(), + setField: jest.fn(), + fetch: jest.fn().mockResolvedValue( + esSearchResponse + ? esSearchResponse + : { + aggregations: { + termsAgg: { + buckets: [ + { + key: 'Zurich Airport', + doc_count: 691, + }, + { + key: 'Xi an Xianyang International Airport', + doc_count: 526, + }, + ], + }, + }, + } + ), + })); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/update_component.ts b/src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/update_component.ts new file mode 100644 index 0000000000000..881412a7c56fd --- /dev/null +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/__tests__/update_component.ts @@ -0,0 +1,31 @@ +/* + * 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 { ShallowWrapper, ReactWrapper } from 'enzyme'; + +export const updateComponent = async ( + component: + | ShallowWrapper, React.Component<{}, {}, any>> + | ReactWrapper, React.Component<{}, {}, any>> +) => { + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); +}; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/control_editor.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/control_editor.tsx similarity index 72% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/control_editor.js rename to src/legacy/core_plugins/input_control_vis/public/components/editor/control_editor.tsx index cc31b8d238dbe..dbac5d9d94371 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/control_editor.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/control_editor.tsx @@ -17,13 +17,10 @@ * under the License. */ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { RangeControlEditor } from './range_control_editor'; -import { ListControlEditor } from './list_control_editor'; -import { getTitle } from '../../editor_utils'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import React, { PureComponent, ChangeEvent } from 'react'; +import { InjectedIntlProps } from 'react-intl'; +import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { EuiAccordion, EuiButtonIcon, @@ -32,11 +29,45 @@ import { EuiFormRow, EuiPanel, EuiSpacer, + EuiSwitchEvent, } from '@elastic/eui'; -class ControlEditorUi extends Component { - changeLabel = evt => { - this.props.handleLabelChange(this.props.controlIndex, evt); +import { RangeControlEditor } from './range_control_editor'; +import { ListControlEditor } from './list_control_editor'; +import { getTitle, ControlParams, CONTROL_TYPES, ControlParamsOptions } from '../../editor_utils'; +import { IIndexPattern } from '../../../../../../plugins/data/public'; +import { InputControlVisDependencies } from '../../plugin'; + +interface ControlEditorUiProps { + controlIndex: number; + controlParams: ControlParams; + handleLabelChange: (controlIndex: number, event: ChangeEvent) => void; + moveControl: (controlIndex: number, direction: number) => void; + handleRemoveControl: (controlIndex: number) => void; + handleIndexPatternChange: (controlIndex: number, indexPatternId: string) => void; + handleFieldNameChange: (controlIndex: number, fieldName: string) => void; + getIndexPattern: (indexPatternId: string) => Promise; + handleCheckboxOptionChange: ( + controlIndex: number, + optionName: keyof ControlParamsOptions, + event: EuiSwitchEvent + ) => void; + handleNumberOptionChange: ( + controlIndex: number, + optionName: keyof ControlParamsOptions, + event: ChangeEvent + ) => void; + parentCandidates: Array<{ + value: string; + text: string; + }>; + handleParentChange: (controlIndex: number, event: ChangeEvent) => void; + deps: InputControlVisDependencies; +} + +class ControlEditorUi extends PureComponent { + changeLabel = (event: ChangeEvent) => { + this.props.handleLabelChange(this.props.controlIndex, event); }; removeControl = () => { @@ -51,18 +82,18 @@ class ControlEditorUi extends Component { this.props.moveControl(this.props.controlIndex, 1); }; - changeIndexPattern = evt => { - this.props.handleIndexPatternChange(this.props.controlIndex, evt); + changeIndexPattern = (indexPatternId: string) => { + this.props.handleIndexPatternChange(this.props.controlIndex, indexPatternId); }; - changeFieldName = evt => { - this.props.handleFieldNameChange(this.props.controlIndex, evt); + changeFieldName = (fieldName: string) => { + this.props.handleFieldNameChange(this.props.controlIndex, fieldName); }; renderEditor() { let controlEditor = null; switch (this.props.controlParams.type) { - case 'list': + case CONTROL_TYPES.LIST: controlEditor = ( ); break; - case 'range': + case CONTROL_TYPES.RANGE: controlEditor = ( ); break; @@ -167,24 +200,4 @@ class ControlEditorUi extends Component { } } -ControlEditorUi.propTypes = { - controlIndex: PropTypes.number.isRequired, - controlParams: PropTypes.object.isRequired, - handleLabelChange: PropTypes.func.isRequired, - moveControl: PropTypes.func.isRequired, - handleRemoveControl: PropTypes.func.isRequired, - handleIndexPatternChange: PropTypes.func.isRequired, - handleFieldNameChange: PropTypes.func.isRequired, - getIndexPattern: PropTypes.func.isRequired, - handleCheckboxOptionChange: PropTypes.func.isRequired, - handleNumberOptionChange: PropTypes.func.isRequired, - parentCandidates: PropTypes.arrayOf( - PropTypes.shape({ - value: PropTypes.string.isRequired, - text: PropTypes.string.isRequired, - }) - ).isRequired, - handleParentChange: PropTypes.func.isRequired, -}; - export const ControlEditor = injectI18n(ControlEditorUi); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.tsx similarity index 87% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js rename to src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.tsx index 28f435c27ea8f..4e7c9bafbf510 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.test.tsx @@ -17,45 +17,36 @@ * under the License. */ -jest.mock('../../../../../core_plugins/data/public/legacy', () => ({ - indexPatterns: { - indexPatterns: { - get: jest.fn(), - }, - }, -})); - -jest.mock('ui/new_platform', () => ({ - npStart: { - plugins: { - data: { - ui: { - IndexPatternSelect: () => { - return
; - }, - }, - }, - }, - }, -})); - import React from 'react'; import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; +// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; +import { getDepsMock } from './__tests__/get_deps_mock'; import { getIndexPatternMock } from './__tests__/get_index_pattern_mock'; -import { ControlsTab } from './controls_tab'; +import { ControlsTab, ControlsTabUiProps } from './controls_tab'; const indexPatternsMock = { get: getIndexPatternMock, }; -let props; +let props: ControlsTabUiProps; beforeEach(() => { props = { + deps: getDepsMock(), vis: { API: { indexPatterns: indexPatternsMock, }, + type: { + name: 'test', + title: 'test', + visualization: null, + requestHandler: 'test', + responseHandler: 'test', + stage: 'beta', + requiresSearch: false, + hidden: false, + }, }, stateParams: { controls: [ @@ -71,6 +62,7 @@ beforeEach(() => { size: 5, order: 'desc', }, + parent: 'parent', }, { id: '2', @@ -81,10 +73,12 @@ beforeEach(() => { options: { step: 1, }, + parent: 'parent', }, ], }, setValue: jest.fn(), + intl: null as any, }; }); @@ -105,7 +99,7 @@ describe('behavior', () => { 'controls', expect.arrayContaining(props.stateParams.controls) ); - expect(props.setValue.mock.calls[0][1].length).toEqual(3); + expect((props.setValue as jest.Mock).mock.calls[0][1].length).toEqual(3); }); test('remove control button', () => { @@ -120,6 +114,7 @@ describe('behavior', () => { fieldName: 'numberField', label: '', type: 'range', + parent: 'parent', options: { step: 1, }, @@ -142,6 +137,7 @@ describe('behavior', () => { fieldName: 'numberField', label: '', type: 'range', + parent: 'parent', options: { step: 1, }, @@ -152,6 +148,7 @@ describe('behavior', () => { fieldName: 'keywordField', label: 'custom label', type: 'list', + parent: 'parent', options: { type: 'terms', multiselect: true, @@ -177,6 +174,7 @@ describe('behavior', () => { fieldName: 'numberField', label: '', type: 'range', + parent: 'parent', options: { step: 1, }, @@ -187,6 +185,7 @@ describe('behavior', () => { fieldName: 'keywordField', label: 'custom label', type: 'list', + parent: 'parent', options: { type: 'terms', multiselect: true, diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.tsx similarity index 68% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.js rename to src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.tsx index 97036d7b0f5df..56381ef7d1570 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/controls_tab.tsx @@ -17,14 +17,10 @@ * under the License. */ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { ControlEditor } from './control_editor'; -import { addControl, moveControl, newControl, removeControl, setControl } from '../../editor_utils'; -import { getLineageMap, getParentCandidates } from '../../lineage'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; -import { npStart } from 'ui/new_platform'; +import React, { PureComponent, ChangeEvent } from 'react'; +import { InjectedIntlProps } from 'react-intl'; +import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiFlexGroup, @@ -32,55 +28,97 @@ import { EuiFormRow, EuiPanel, EuiSelect, + EuiSwitchEvent, } from '@elastic/eui'; -class ControlsTabUi extends Component { +import { ControlEditor } from './control_editor'; +import { + addControl, + moveControl, + newControl, + removeControl, + setControl, + ControlParams, + CONTROL_TYPES, + ControlParamsOptions, +} from '../../editor_utils'; +import { getLineageMap, getParentCandidates } from '../../lineage'; +import { IIndexPattern } from '../../../../../../plugins/data/public'; +import { VisOptionsProps } from '../../legacy_imports'; +import { InputControlVisDependencies } from '../../plugin'; + +interface ControlsTabUiState { + type: CONTROL_TYPES; +} + +interface ControlsTabUiParams { + controls: ControlParams[]; +} +type ControlsTabUiInjectedProps = InjectedIntlProps & + Pick, 'vis' | 'stateParams' | 'setValue'> & { + deps: InputControlVisDependencies; + }; + +export type ControlsTabUiProps = ControlsTabUiInjectedProps; + +class ControlsTabUi extends PureComponent { state = { - type: 'list', + type: CONTROL_TYPES.LIST, }; - getIndexPattern = async indexPatternId => { - return await npStart.plugins.data.indexPatterns.get(indexPatternId); + getIndexPattern = async (indexPatternId: string): Promise => { + const [, startDeps] = await this.props.deps.core.getStartServices(); + return await startDeps.data.indexPatterns.get(indexPatternId); }; - onChange = value => this.props.setValue('controls', value); + onChange = (value: ControlParams[]) => this.props.setValue('controls', value); - handleLabelChange = (controlIndex, evt) => { + handleLabelChange = (controlIndex: number, event: ChangeEvent) => { const updatedControl = this.props.stateParams.controls[controlIndex]; - updatedControl.label = evt.target.value; + updatedControl.label = event.target.value; this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl)); }; - handleIndexPatternChange = (controlIndex, indexPatternId) => { + handleIndexPatternChange = (controlIndex: number, indexPatternId: string) => { const updatedControl = this.props.stateParams.controls[controlIndex]; updatedControl.indexPattern = indexPatternId; updatedControl.fieldName = ''; this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl)); }; - handleFieldNameChange = (controlIndex, fieldName) => { + handleFieldNameChange = (controlIndex: number, fieldName: string) => { const updatedControl = this.props.stateParams.controls[controlIndex]; updatedControl.fieldName = fieldName; this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl)); }; - handleCheckboxOptionChange = (controlIndex, optionName, evt) => { + handleCheckboxOptionChange = ( + controlIndex: number, + optionName: keyof ControlParamsOptions, + event: EuiSwitchEvent + ) => { const updatedControl = this.props.stateParams.controls[controlIndex]; - updatedControl.options[optionName] = evt.target.checked; + // @ts-ignore + updatedControl.options[optionName] = event.target.checked; this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl)); }; - handleNumberOptionChange = (controlIndex, optionName, evt) => { + handleNumberOptionChange = ( + controlIndex: number, + optionName: keyof ControlParamsOptions, + event: ChangeEvent + ) => { const updatedControl = this.props.stateParams.controls[controlIndex]; - updatedControl.options[optionName] = parseFloat(evt.target.value); + // @ts-ignore + updatedControl.options[optionName] = parseFloat(event.target.value); this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl)); }; - handleRemoveControl = controlIndex => { + handleRemoveControl = (controlIndex: number) => { this.onChange(removeControl(this.props.stateParams.controls, controlIndex)); }; - moveControl = (controlIndex, direction) => { + moveControl = (controlIndex: number, direction: number) => { this.onChange(moveControl(this.props.stateParams.controls, controlIndex, direction)); }; @@ -88,9 +126,9 @@ class ControlsTabUi extends Component { this.onChange(addControl(this.props.stateParams.controls, newControl(this.state.type))); }; - handleParentChange = (controlIndex, evt) => { + handleParentChange = (controlIndex: number, event: ChangeEvent) => { const updatedControl = this.props.stateParams.controls[controlIndex]; - updatedControl.parent = evt.target.value; + updatedControl.parent = event.target.value; this.onChange(setControl(this.props.stateParams.controls, controlIndex, updatedControl)); }; @@ -117,6 +155,7 @@ class ControlsTabUi extends Component { handleNumberOptionChange={this.handleNumberOptionChange} parentCandidates={parentCandidates} handleParentChange={this.handleParentChange} + deps={this.props.deps} /> ); }); @@ -137,14 +176,14 @@ class ControlsTabUi extends Component { data-test-subj="selectControlType" options={[ { - value: 'range', + value: CONTROL_TYPES.RANGE, text: intl.formatMessage({ id: 'inputControl.editor.controlsTab.select.rangeDropDownOptionLabel', defaultMessage: 'Range slider', }), }, { - value: 'list', + value: CONTROL_TYPES.LIST, text: intl.formatMessage({ id: 'inputControl.editor.controlsTab.select.listDropDownOptionLabel', defaultMessage: 'Options list', @@ -152,7 +191,7 @@ class ControlsTabUi extends Component { }, ]} value={this.state.type} - onChange={evt => this.setState({ type: evt.target.value })} + onChange={event => this.setState({ type: event.target.value as CONTROL_TYPES })} aria-label={intl.formatMessage({ id: 'inputControl.editor.controlsTab.select.controlTypeAriaLabel', defaultMessage: 'Select control type', @@ -186,9 +225,8 @@ class ControlsTabUi extends Component { } } -ControlsTabUi.propTypes = { - vis: PropTypes.object.isRequired, - setValue: PropTypes.func.isRequired, -}; - export const ControlsTab = injectI18n(ControlsTabUi); + +export const getControlsTab = (deps: InputControlVisDependencies) => ( + props: Omit +) => ; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/field_select.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/field_select.tsx similarity index 70% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/field_select.js rename to src/legacy/core_plugins/input_control_vis/public/components/editor/field_select.tsx index 456ff17a316a1..bde2f09ab0a47 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/field_select.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/field_select.tsx @@ -18,43 +18,59 @@ */ import _ from 'lodash'; -import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { InjectedIntlProps } from 'react-intl'; + import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { EuiFormRow, EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui'; + +import { IIndexPattern, IFieldType } from '../../../../../../plugins/data/public'; + +interface FieldSelectUiState { + isLoading: boolean; + fields: Array>; + indexPatternId: string; +} + +export type FieldSelectUiProps = InjectedIntlProps & { + getIndexPattern: (indexPatternId: string) => Promise; + indexPatternId: string; + onChange: (value: any) => void; + fieldName?: string; + filterField?: (field: IFieldType) => boolean; + controlIndex: number; +}; -import { EuiFormRow, EuiComboBox } from '@elastic/eui'; +class FieldSelectUi extends Component { + private hasUnmounted: boolean; -class FieldSelectUi extends Component { - constructor(props) { + constructor(props: FieldSelectUiProps) { super(props); - this._hasUnmounted = false; + this.hasUnmounted = false; this.state = { isLoading: false, fields: [], indexPatternId: props.indexPatternId, }; - this.filterField = _.get(props, 'filterField', () => { - return true; - }); } componentWillUnmount() { - this._hasUnmounted = true; + this.hasUnmounted = true; } componentDidMount() { this.loadFields(this.state.indexPatternId); } - UNSAFE_componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps: FieldSelectUiProps) { if (this.props.indexPatternId !== nextProps.indexPatternId) { - this.loadFields(nextProps.indexPatternId); + this.loadFields(nextProps.indexPatternId ?? ''); } } - loadFields = indexPatternId => { + loadFields = (indexPatternId: string) => { this.setState( { isLoading: true, @@ -65,12 +81,12 @@ class FieldSelectUi extends Component { ); }; - debouncedLoad = _.debounce(async indexPatternId => { + debouncedLoad = _.debounce(async (indexPatternId: string) => { if (!indexPatternId || indexPatternId.length === 0) { return; } - let indexPattern; + let indexPattern: IIndexPattern; try { indexPattern = await this.props.getIndexPattern(indexPatternId); } catch (err) { @@ -78,7 +94,7 @@ class FieldSelectUi extends Component { return; } - if (this._hasUnmounted) { + if (this.hasUnmounted) { return; } @@ -88,17 +104,15 @@ class FieldSelectUi extends Component { return; } - const fieldsByTypeMap = new Map(); - const fields = []; - indexPattern.fields.filter(this.filterField).forEach(field => { - if (fieldsByTypeMap.has(field.type)) { - const fieldsList = fieldsByTypeMap.get(field.type); + const fieldsByTypeMap = new Map(); + const fields: Array> = []; + indexPattern.fields + .filter(this.props.filterField ?? (() => true)) + .forEach((field: IFieldType) => { + const fieldsList = fieldsByTypeMap.get(field.type) ?? []; fieldsList.push(field.name); fieldsByTypeMap.set(field.type, fieldsList); - } else { - fieldsByTypeMap.set(field.type, [field.name]); - } - }); + }); fieldsByTypeMap.forEach((fieldsList, fieldType) => { fields.push({ @@ -117,11 +131,11 @@ class FieldSelectUi extends Component { this.setState({ isLoading: false, - fields: fields, + fields, }); }, 300); - onChange = selectedOptions => { + onChange = (selectedOptions: Array>) => { this.props.onChange(_.get(selectedOptions, '0.value')); }; @@ -165,13 +179,4 @@ class FieldSelectUi extends Component { } } -FieldSelectUi.propTypes = { - getIndexPattern: PropTypes.func.isRequired, - indexPatternId: PropTypes.string, - onChange: PropTypes.func.isRequired, - fieldName: PropTypes.string, - filterField: PropTypes.func, - controlIndex: PropTypes.number.isRequired, -}; - export const FieldSelect = injectI18n(FieldSelectUi); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.tsx similarity index 73% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js rename to src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.tsx index 7d7fbc0539de0..66fdbca64f053 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.tsx @@ -17,15 +17,20 @@ * under the License. */ -import PropTypes from 'prop-types'; -import React from 'react'; +import React, { ComponentType } from 'react'; import { injectI18n } from '@kbn/i18n/react'; import { EuiFormRow } from '@elastic/eui'; +import { InjectedIntlProps } from 'react-intl'; +import { IndexPatternSelect } from 'src/plugins/data/public'; -import { npStart } from 'ui/new_platform'; -const { IndexPatternSelect } = npStart.plugins.data.ui; +export type IndexPatternSelectFormRowUiProps = InjectedIntlProps & { + onChange: (opt: any) => void; + indexPatternId: string; + controlIndex: number; + IndexPatternSelect: ComponentType; +}; -function IndexPatternSelectFormRowUi(props) { +function IndexPatternSelectFormRowUi(props: IndexPatternSelectFormRowUiProps) { const { controlIndex, indexPatternId, intl, onChange } = props; const selectId = `indexPatternSelect-${controlIndex}`; @@ -37,7 +42,7 @@ function IndexPatternSelectFormRowUi(props) { defaultMessage: 'Index Pattern', })} > - ); } -IndexPatternSelectFormRowUi.propTypes = { - onChange: PropTypes.func.isRequired, - indexPatternId: PropTypes.string, - controlIndex: PropTypes.number.isRequired, -}; - export const IndexPatternSelectFormRow = injectI18n(IndexPatternSelectFormRowUi); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.tsx similarity index 80% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js rename to src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.tsx index 24b14943c8bb3..de0187f87212f 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.tsx @@ -17,31 +17,21 @@ * under the License. */ -jest.mock('ui/new_platform', () => ({ - npStart: { - plugins: { - data: { - ui: { - IndexPatternSelect: () => { - return
; - }, - }, - }, - }, - }, -})); - import React from 'react'; import sinon from 'sinon'; import { shallow } from 'enzyme'; -import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; +// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; -import { getIndexPatternMock } from './__tests__/get_index_pattern_mock'; +import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { getDepsMock } from './__tests__/get_deps_mock'; +import { getIndexPatternMock } from './__tests__/get_index_pattern_mock'; import { ListControlEditor } from './list_control_editor'; +import { ControlParams } from '../../editor_utils'; +import { updateComponent } from './__tests__/update_component'; -const controlParams = { +const controlParamsBase: ControlParams = { id: '1', indexPattern: 'indexPattern1', fieldName: 'keywordField', @@ -53,11 +43,13 @@ const controlParams = { dynamicOptions: false, size: 10, }, + parent: '', }; -let handleFieldNameChange; -let handleIndexPatternChange; -let handleCheckboxOptionChange; -let handleNumberOptionChange; +const deps = getDepsMock(); +let handleFieldNameChange: sinon.SinonSpy; +let handleIndexPatternChange: sinon.SinonSpy; +let handleCheckboxOptionChange: sinon.SinonSpy; +let handleNumberOptionChange: sinon.SinonSpy; beforeEach(() => { handleFieldNameChange = sinon.spy(); @@ -68,8 +60,9 @@ beforeEach(() => { describe('renders', () => { test('should not display any options until field is selected', async () => { - const controlParams = { + const controlParams: ControlParams = { id: '1', + label: 'mock', indexPattern: 'mockIndexPattern', fieldName: '', type: 'list', @@ -79,9 +72,11 @@ describe('renders', () => { dynamicOptions: true, size: 5, }, + parent: '', }; const component = shallow( { /> ); - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); + await updateComponent(component); expect(component).toMatchSnapshot(); }); @@ -109,9 +101,10 @@ describe('renders', () => { ]; const component = shallow( { /> ); - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); + await updateComponent(component); expect(component).toMatchSnapshot(); }); describe('dynamic options', () => { test('should display dynamic options for string fields', async () => { - const controlParams = { + const controlParams: ControlParams = { id: '1', + label: 'mock', indexPattern: 'mockIndexPattern', fieldName: 'keywordField', type: 'list', @@ -142,9 +133,11 @@ describe('renders', () => { dynamicOptions: true, size: 5, }, + parent: '', }; const component = shallow( { /> ); - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); + await updateComponent(component); expect(component).toMatchSnapshot(); }); test('should display size field when dynamic options is disabled', async () => { - const controlParams = { + const controlParams: ControlParams = { id: '1', + label: 'mock', indexPattern: 'mockIndexPattern', fieldName: 'keywordField', type: 'list', @@ -177,9 +168,11 @@ describe('renders', () => { dynamicOptions: false, size: 5, }, + parent: '', }; const component = shallow( { /> ); - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); + await updateComponent(component); expect(component).toMatchSnapshot(); }); test('should display disabled dynamic options with tooltip for non-string fields', async () => { - const controlParams = { + const controlParams: ControlParams = { id: '1', + label: 'mock', indexPattern: 'mockIndexPattern', fieldName: 'numberField', type: 'list', @@ -212,9 +203,11 @@ describe('renders', () => { dynamicOptions: true, size: 5, }, + parent: '', }; const component = shallow( { /> ); - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); + await updateComponent(component); expect(component).toMatchSnapshot(); }); @@ -240,9 +230,10 @@ describe('renders', () => { test('handleCheckboxOptionChange - multiselect', async () => { const component = mountWithIntl( { /> ); - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); + await updateComponent(component); const checkbox = findTestSubject(component, 'listControlMultiselectInput'); checkbox.simulate('click'); @@ -268,10 +256,10 @@ test('handleCheckboxOptionChange - multiselect', async () => { handleCheckboxOptionChange, expectedControlIndex, expectedOptionName, - sinon.match(evt => { - // Synthetic `evt.target.checked` does not get altered by EuiSwitch, + sinon.match(event => { + // Synthetic `event.target.checked` does not get altered by EuiSwitch, // but its aria attribute is correctly updated - if (evt.target.getAttribute('aria-checked') === 'true') { + if (event.target.getAttribute('aria-checked') === 'true') { return true; } return false; @@ -282,9 +270,10 @@ test('handleCheckboxOptionChange - multiselect', async () => { test('handleNumberOptionChange - size', async () => { const component = mountWithIntl( { /> ); - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); + await updateComponent(component); const input = findTestSubject(component, 'listControlSizeInput'); input.simulate('change', { target: { value: 7 } }); @@ -310,8 +296,8 @@ test('handleNumberOptionChange - size', async () => { handleNumberOptionChange, expectedControlIndex, expectedOptionName, - sinon.match(evt => { - if (evt.target.value === 7) { + sinon.match(event => { + if (event.target.value === 7) { return true; } return false; @@ -322,9 +308,10 @@ test('handleNumberOptionChange - size', async () => { test('field name change', async () => { const component = shallowWithIntl( { /> ); - const update = async () => { - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - }; - // ensure that after async loading is complete the DynamicOptionsSwitch is not disabled expect( component.find('[data-test-subj="listControlDynamicOptionsSwitch"][disabled=false]') ).toHaveLength(0); - await update(); + await updateComponent(component); expect( component.find('[data-test-subj="listControlDynamicOptionsSwitch"][disabled=false]') ).toHaveLength(1); component.setProps({ controlParams: { - ...controlParams, + ...controlParamsBase, fieldName: 'numberField', }, }); @@ -361,20 +341,20 @@ test('field name change', async () => { expect( component.find('[data-test-subj="listControlDynamicOptionsSwitch"][disabled=true]') ).toHaveLength(0); - await update(); + await updateComponent(component); expect( component.find('[data-test-subj="listControlDynamicOptionsSwitch"][disabled=true]') ).toHaveLength(1); component.setProps({ - controlParams, + controlParams: controlParamsBase, }); // ensure that after async loading is complete the DynamicOptionsSwitch is not disabled again, because we switched to original "string" field expect( component.find('[data-test-subj="listControlDynamicOptionsSwitch"][disabled=false]') ).toHaveLength(0); - await update(); + await updateComponent(component); expect( component.find('[data-test-subj="listControlDynamicOptionsSwitch"][disabled=false]') ).toHaveLength(1); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.tsx similarity index 70% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.js rename to src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.tsx index 2ee225475b0fe..ff74d30a6e1a8 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.tsx @@ -17,35 +17,90 @@ * under the License. */ -import PropTypes from 'prop-types'; -import React, { Component, Fragment } from 'react'; +import React, { PureComponent, ChangeEvent, ComponentType } from 'react'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFormRow, + EuiFieldNumber, + EuiSwitch, + EuiSelect, + EuiSelectProps, + EuiSwitchEvent, +} from '@elastic/eui'; + import { IndexPatternSelectFormRow } from './index_pattern_select_form_row'; import { FieldSelect } from './field_select'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { ControlParams, ControlParamsOptions } from '../../editor_utils'; +import { + IIndexPattern, + IFieldType, + IndexPatternSelect, +} from '../../../../../../plugins/data/public'; +import { InputControlVisDependencies } from '../../plugin'; -import { EuiFormRow, EuiFieldNumber, EuiSwitch, EuiSelect } from '@elastic/eui'; +interface ListControlEditorState { + isLoadingFieldType: boolean; + isStringField: boolean; + prevFieldName: string; + IndexPatternSelect: ComponentType | null; +} -function filterField(field) { - return field.aggregatable && ['number', 'boolean', 'date', 'ip', 'string'].includes(field.type); +interface ListControlEditorProps { + getIndexPattern: (indexPatternId: string) => Promise; + controlIndex: number; + controlParams: ControlParams; + handleFieldNameChange: (fieldName: string) => void; + handleIndexPatternChange: (indexPatternId: string) => void; + handleCheckboxOptionChange: ( + controlIndex: number, + optionName: keyof ControlParamsOptions, + event: EuiSwitchEvent + ) => void; + handleNumberOptionChange: ( + controlIndex: number, + optionName: keyof ControlParamsOptions, + event: ChangeEvent + ) => void; + handleParentChange: (controlIndex: number, event: ChangeEvent) => void; + parentCandidates: EuiSelectProps['options']; + deps: InputControlVisDependencies; } -export class ListControlEditor extends Component { - state = { +function filterField(field: IFieldType) { + return ( + Boolean(field.aggregatable) && + ['number', 'boolean', 'date', 'ip', 'string'].includes(field.type) + ); +} + +export class ListControlEditor extends PureComponent< + ListControlEditorProps, + ListControlEditorState +> { + private isMounted: boolean = false; + + state: ListControlEditorState = { isLoadingFieldType: true, isStringField: false, prevFieldName: this.props.controlParams.fieldName, + IndexPatternSelect: null, }; componentDidMount() { - this._isMounted = true; + this.isMounted = true; this.loadIsStringField(); + this.getIndexPatternSelect(); } componentWillUnmount() { - this._isMounted = false; + this.isMounted = false; } - static getDerivedStateFromProps = (nextProps, prevState) => { + static getDerivedStateFromProps = ( + nextProps: ListControlEditorProps, + prevState: ListControlEditorState + ) => { const isNewFieldName = prevState.prevFieldName !== nextProps.controlParams.fieldName; if (!prevState.isLoadingFieldType && isNewFieldName) { return { @@ -63,13 +118,20 @@ export class ListControlEditor extends Component { } }; + async getIndexPatternSelect() { + const [, { data }] = await this.props.deps.core.getStartServices(); + this.setState({ + IndexPatternSelect: data.ui.IndexPatternSelect, + }); + } + loadIsStringField = async () => { if (!this.props.controlParams.indexPattern || !this.props.controlParams.fieldName) { this.setState({ isLoadingFieldType: false }); return; } - let indexPattern; + let indexPattern: IIndexPattern; try { indexPattern = await this.props.getIndexPattern(this.props.controlParams.indexPattern); } catch (err) { @@ -77,13 +139,13 @@ export class ListControlEditor extends Component { return; } - if (!this._isMounted) { + if (!this.isMounted) { return; } - const field = indexPattern.fields.find(field => { - return field.name === this.props.controlParams.fieldName; - }); + const field = (indexPattern.fields as IFieldType[]).find( + ({ name }) => name === this.props.controlParams.fieldName + ); if (!field) { return; } @@ -121,8 +183,8 @@ export class ListControlEditor extends Component { { - this.props.handleParentChange(this.props.controlIndex, evt); + onChange={event => { + this.props.handleParentChange(this.props.controlIndex, event); }} /> @@ -147,9 +209,9 @@ export class ListControlEditor extends Component { defaultMessage="Multiselect" /> } - checked={this.props.controlParams.options.multiselect} - onChange={evt => { - this.props.handleCheckboxOptionChange(this.props.controlIndex, 'multiselect', evt); + checked={this.props.controlParams.options.multiselect ?? true} + onChange={event => { + this.props.handleCheckboxOptionChange(this.props.controlIndex, 'multiselect', event); }} data-test-subj="listControlMultiselectInput" /> @@ -180,9 +242,9 @@ export class ListControlEditor extends Component { defaultMessage="Dynamic Options" /> } - checked={this.props.controlParams.options.dynamicOptions} - onChange={evt => { - this.props.handleCheckboxOptionChange(this.props.controlIndex, 'dynamicOptions', evt); + checked={this.props.controlParams.options.dynamicOptions ?? false} + onChange={event => { + this.props.handleCheckboxOptionChange(this.props.controlIndex, 'dynamicOptions', event); }} disabled={this.state.isStringField ? false : true} data-test-subj="listControlDynamicOptionsSwitch" @@ -212,8 +274,8 @@ export class ListControlEditor extends Component { { - this.props.handleNumberOptionChange(this.props.controlIndex, 'size', evt); + onChange={event => { + this.props.handleNumberOptionChange(this.props.controlIndex, 'size', event); }} data-test-subj="listControlSizeInput" /> @@ -225,12 +287,17 @@ export class ListControlEditor extends Component { }; render() { + if (this.state.IndexPatternSelect === null) { + return null; + } + return ( - + <> {this.renderOptions()} - + ); } } - -ListControlEditor.propTypes = { - getIndexPattern: PropTypes.func.isRequired, - controlIndex: PropTypes.number.isRequired, - controlParams: PropTypes.object.isRequired, - handleFieldNameChange: PropTypes.func.isRequired, - handleIndexPatternChange: PropTypes.func.isRequired, - handleCheckboxOptionChange: PropTypes.func.isRequired, - handleNumberOptionChange: PropTypes.func.isRequired, - parentCandidates: PropTypes.arrayOf( - PropTypes.shape({ - value: PropTypes.string.isRequired, - text: PropTypes.string.isRequired, - }) - ).isRequired, - handleParentChange: PropTypes.func.isRequired, -}; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.test.tsx similarity index 93% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.test.js rename to src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.test.tsx index ef84d37ca8de5..36ec4d4446fd6 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.test.tsx @@ -21,17 +21,19 @@ import React from 'react'; import { shallow } from 'enzyme'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { OptionsTab } from './options_tab'; +import { OptionsTab, OptionsTabProps } from './options_tab'; +import { Vis } from '../../legacy_imports'; describe('OptionsTab', () => { - let props; + let props: OptionsTabProps; beforeEach(() => { props = { - vis: {}, + vis: {} as Vis, stateParams: { updateFiltersOnChange: false, useTimeFilter: false, + pinFilters: false, }, setValue: jest.fn(), }; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.tsx similarity index 74% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.js rename to src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.tsx index 236624b11118c..43f9e15302e51 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.tsx @@ -17,24 +17,37 @@ * under the License. */ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; +import React, { PureComponent } from 'react'; import { EuiForm, EuiFormRow, EuiSwitch } from '@elastic/eui'; - import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSwitchEvent } from '@elastic/eui'; + +import { VisOptionsProps } from '../../legacy_imports'; + +interface OptionsTabParams { + updateFiltersOnChange: boolean; + useTimeFilter: boolean; + pinFilters: boolean; +} +type OptionsTabInjectedProps = Pick< + VisOptionsProps, + 'vis' | 'setValue' | 'stateParams' +>; + +export type OptionsTabProps = OptionsTabInjectedProps; -export class OptionsTab extends Component { - handleUpdateFiltersChange = evt => { - this.props.setValue('updateFiltersOnChange', evt.target.checked); +export class OptionsTab extends PureComponent { + handleUpdateFiltersChange = (event: EuiSwitchEvent) => { + this.props.setValue('updateFiltersOnChange', event.target.checked); }; - handleUseTimeFilter = evt => { - this.props.setValue('useTimeFilter', evt.target.checked); + handleUseTimeFilter = (event: EuiSwitchEvent) => { + this.props.setValue('useTimeFilter', event.target.checked); }; - handlePinFilters = evt => { - this.props.setValue('pinFilters', evt.target.checked); + handlePinFilters = (event: EuiSwitchEvent) => { + this.props.setValue('pinFilters', event.target.checked); }; render() { @@ -85,8 +98,3 @@ export class OptionsTab extends Component { ); } } - -OptionsTab.propTypes = { - vis: PropTypes.object.isRequired, - setValue: PropTypes.func.isRequired, -}; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.js deleted file mode 100644 index 6e1754b28647f..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.js +++ /dev/null @@ -1,102 +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 PropTypes from 'prop-types'; -import React, { Fragment } from 'react'; -import { IndexPatternSelectFormRow } from './index_pattern_select_form_row'; -import { FieldSelect } from './field_select'; - -import { EuiFormRow, EuiFieldNumber } from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -function filterField(field) { - return field.type === 'number'; -} - -export function RangeControlEditor(props) { - const stepSizeId = `stepSize-${props.controlIndex}`; - const decimalPlacesId = `decimalPlaces-${props.controlIndex}`; - const handleDecimalPlacesChange = evt => { - props.handleNumberOptionChange(props.controlIndex, 'decimalPlaces', evt); - }; - const handleStepChange = evt => { - props.handleNumberOptionChange(props.controlIndex, 'step', evt); - }; - return ( - - - - - - - } - > - - - - - } - > - - - - ); -} - -RangeControlEditor.propTypes = { - getIndexPattern: PropTypes.func.isRequired, - controlIndex: PropTypes.number.isRequired, - controlParams: PropTypes.object.isRequired, - handleFieldNameChange: PropTypes.func.isRequired, - handleIndexPatternChange: PropTypes.func.isRequired, - handleNumberOptionChange: PropTypes.func.isRequired, -}; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx similarity index 71% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js rename to src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx index 145b18a42dc15..e7f9b6083890c 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx @@ -18,30 +18,20 @@ */ import React from 'react'; -import sinon from 'sinon'; import { shallow } from 'enzyme'; +import { SinonSpy, spy, assert, match } from 'sinon'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -jest.mock('ui/new_platform', () => ({ - npStart: { - plugins: { - data: { - ui: { - IndexPatternSelect: () => { - return
; - }, - }, - }, - }, - }, -})); - +// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; import { getIndexPatternMock } from './__tests__/get_index_pattern_mock'; import { RangeControlEditor } from './range_control_editor'; +import { ControlParams } from '../../editor_utils'; +import { getDepsMock } from './__tests__/get_deps_mock'; +import { updateComponent } from './__tests__/update_component'; -const controlParams = { +const controlParams: ControlParams = { id: '1', indexPattern: 'indexPattern1', fieldName: 'numberField', @@ -51,20 +41,23 @@ const controlParams = { decimalPlaces: 0, step: 1, }, + parent: '', }; -let handleFieldNameChange; -let handleIndexPatternChange; -let handleNumberOptionChange; +const deps = getDepsMock(); +let handleFieldNameChange: SinonSpy; +let handleIndexPatternChange: SinonSpy; +let handleNumberOptionChange: SinonSpy; beforeEach(() => { - handleFieldNameChange = sinon.spy(); - handleIndexPatternChange = sinon.spy(); - handleNumberOptionChange = sinon.spy(); + handleFieldNameChange = spy(); + handleIndexPatternChange = spy(); + handleNumberOptionChange = spy(); }); -test('renders RangeControlEditor', () => { +test('renders RangeControlEditor', async () => { const component = shallow( { handleNumberOptionChange={handleNumberOptionChange} /> ); + + await updateComponent(component); + expect(component).toMatchSnapshot(); // eslint-disable-line }); -test('handleNumberOptionChange - step', () => { +test('handleNumberOptionChange - step', async () => { const component = mountWithIntl( { handleNumberOptionChange={handleNumberOptionChange} /> ); + + await updateComponent(component); + findTestSubject(component, 'rangeControlSizeInput0').simulate('change', { target: { value: 0.5 }, }); - sinon.assert.notCalled(handleFieldNameChange); - sinon.assert.notCalled(handleIndexPatternChange); + assert.notCalled(handleFieldNameChange); + assert.notCalled(handleIndexPatternChange); const expectedControlIndex = 0; const expectedOptionName = 'step'; - sinon.assert.calledWith( + assert.calledWith( handleNumberOptionChange, expectedControlIndex, expectedOptionName, - sinon.match(evt => { - if (evt.target.value === 0.5) { + match(event => { + if (event.target.value === 0.5) { return true; } return false; @@ -107,9 +107,10 @@ test('handleNumberOptionChange - step', () => { ); }); -test('handleNumberOptionChange - decimalPlaces', () => { +test('handleNumberOptionChange - decimalPlaces', async () => { const component = mountWithIntl( { handleNumberOptionChange={handleNumberOptionChange} /> ); + + await updateComponent(component); + findTestSubject(component, 'rangeControlDecimalPlacesInput0').simulate('change', { target: { value: 2 }, }); - sinon.assert.notCalled(handleFieldNameChange); - sinon.assert.notCalled(handleIndexPatternChange); + assert.notCalled(handleFieldNameChange); + assert.notCalled(handleIndexPatternChange); const expectedControlIndex = 0; const expectedOptionName = 'decimalPlaces'; - sinon.assert.calledWith( + assert.calledWith( handleNumberOptionChange, expectedControlIndex, expectedOptionName, - sinon.match(evt => { - if (evt.target.value === 2) { + match(event => { + if (event.target.value === 2) { return true; } return false; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx new file mode 100644 index 0000000000000..44477eafda6b1 --- /dev/null +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx @@ -0,0 +1,139 @@ +/* + * 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, { Component, Fragment, ChangeEvent, ComponentType } from 'react'; + +import { EuiFormRow, EuiFieldNumber } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { IndexPatternSelectFormRow } from './index_pattern_select_form_row'; +import { FieldSelect } from './field_select'; +import { ControlParams, ControlParamsOptions } from '../../editor_utils'; +import { + IIndexPattern, + IFieldType, + IndexPatternSelect, +} from '../../../../../../plugins/data/public'; +import { InputControlVisDependencies } from '../../plugin'; + +interface RangeControlEditorProps { + controlIndex: number; + controlParams: ControlParams; + getIndexPattern: (indexPatternId: string) => Promise; + handleFieldNameChange: (fieldName: string) => void; + handleIndexPatternChange: (indexPatternId: string) => void; + handleNumberOptionChange: ( + controlIndex: number, + optionName: keyof ControlParamsOptions, + event: ChangeEvent + ) => void; + deps: InputControlVisDependencies; +} + +interface RangeControlEditorState { + IndexPatternSelect: ComponentType | null; +} + +function filterField(field: IFieldType) { + return field.type === 'number'; +} + +export class RangeControlEditor extends Component< + RangeControlEditorProps, + RangeControlEditorState +> { + state: RangeControlEditorState = { + IndexPatternSelect: null, + }; + + componentDidMount() { + this.getIndexPatternSelect(); + } + + async getIndexPatternSelect() { + const [, { data }] = await this.props.deps.core.getStartServices(); + this.setState({ + IndexPatternSelect: data.ui.IndexPatternSelect, + }); + } + + render() { + const stepSizeId = `stepSize-${this.props.controlIndex}`; + const decimalPlacesId = `decimalPlaces-${this.props.controlIndex}`; + if (this.state.IndexPatternSelect === null) { + return null; + } + + return ( + + + + + + + } + > + { + this.props.handleNumberOptionChange(this.props.controlIndex, 'step', event); + }} + data-test-subj={`rangeControlSizeInput${this.props.controlIndex}`} + /> + + + + } + > + { + this.props.handleNumberOptionChange(this.props.controlIndex, 'decimalPlaces', event); + }} + data-test-subj={`rangeControlDecimalPlacesInput${this.props.controlIndex}`} + /> + + + ); + } +} diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.js.snap b/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.tsx.snap similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.js.snap rename to src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.tsx.snap index 6437cb19aef97..ba183cc40b126 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.js.snap +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.tsx.snap @@ -47,7 +47,6 @@ exports[`renders disabled control with tooltip 1`] = ` anchorClassName="eui-displayBlock" content="I am disabled for testing purposes" delay="regular" - placement="top" position="top" >
diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.js.snap b/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap similarity index 99% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.js.snap rename to src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap index 841421474e7b1..5a76967c71fbb 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.js.snap +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap @@ -18,7 +18,6 @@ exports[`Apply and Cancel change btns enabled when there are changes 1`] = ` > diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.test.js b/src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.test.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.test.js rename to src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.test.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.js b/src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.tsx similarity index 75% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.js rename to src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.tsx index fdd4fcb6e26ae..29385582924e7 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.tsx @@ -17,16 +17,24 @@ * under the License. */ -import PropTypes from 'prop-types'; -import React from 'react'; +import React, { ReactElement } from 'react'; import { EuiFormRow, EuiToolTip, EuiIcon } from '@elastic/eui'; -export function FormRow(props) { +export interface FormRowProps { + label: string; + warningMsg?: string; + id: string; + children: ReactElement; + controlIndex: number; + disableMsg?: string; +} + +export function FormRow(props: FormRowProps) { let control = props.children; if (props.disableMsg) { control = ( - + {control} ); @@ -49,12 +57,3 @@ export function FormRow(props) { ); } - -FormRow.propTypes = { - label: PropTypes.string.isRequired, - warningMsg: PropTypes.string, - id: PropTypes.string.isRequired, - children: PropTypes.node.isRequired, - controlIndex: PropTypes.number.isRequired, - disableMsg: PropTypes.string, -}; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.js b/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx similarity index 87% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.js rename to src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx index a835078ab4dc0..1712f024f5b7b 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx @@ -21,11 +21,16 @@ import React from 'react'; import sinon from 'sinon'; import { shallow } from 'enzyme'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; +// @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; import { InputControlVis } from './input_control_vis'; +import { ListControl } from '../../control/list_control_factory'; +import { RangeControl } from '../../control/range_control_factory'; -const mockListControl = { +jest.mock('ui/new_platform'); + +const mockListControl: ListControl = { id: 'mock-list-control', isEnabled: () => { return true; @@ -38,11 +43,9 @@ const mockListControl = { label: 'list control', value: [], selectOptions: ['choice1', 'choice2'], - format: value => { - return value; - }, -}; -const mockRangeControl = { + format: (value: any) => value, +} as ListControl; +const mockRangeControl: RangeControl = { id: 'mock-range-control', isEnabled: () => { return true; @@ -56,16 +59,16 @@ const mockRangeControl = { value: { min: 0, max: 0 }, min: 0, max: 100, - format: value => { - return value; - }, -}; + format: (value: any) => value, +} as RangeControl; const updateFiltersOnChange = false; -let stageFilter; -let submitFilters; -let resetControls; -let clearControls; +const refreshControlMock = () => Promise.resolve(); + +let stageFilter: sinon.SinonSpy; +let submitFilters: sinon.SinonSpy; +let resetControls: sinon.SinonSpy; +let clearControls: sinon.SinonSpy; beforeEach(() => { stageFilter = sinon.spy(); @@ -89,7 +92,7 @@ test('Renders list control', () => { hasValues={() => { return false; }} - refreshControl={() => {}} + refreshControl={refreshControlMock} /> ); expect(component).toMatchSnapshot(); // eslint-disable-line @@ -110,7 +113,7 @@ test('Renders range control', () => { hasValues={() => { return false; }} - refreshControl={() => {}} + refreshControl={refreshControlMock} /> ); expect(component).toMatchSnapshot(); // eslint-disable-line @@ -131,7 +134,7 @@ test('Apply and Cancel change btns enabled when there are changes', () => { hasValues={() => { return false; }} - refreshControl={() => {}} + refreshControl={refreshControlMock} /> ); expect(component).toMatchSnapshot(); // eslint-disable-line @@ -152,7 +155,7 @@ test('Clear btns enabled when there are values', () => { hasValues={() => { return true; }} - refreshControl={() => {}} + refreshControl={refreshControlMock} /> ); expect(component).toMatchSnapshot(); // eslint-disable-line @@ -173,7 +176,7 @@ test('clearControls', () => { hasValues={() => { return true; }} - refreshControl={() => {}} + refreshControl={refreshControlMock} /> ); findTestSubject(component, 'inputControlClearBtn').simulate('click'); @@ -198,7 +201,7 @@ test('submitFilters', () => { hasValues={() => { return true; }} - refreshControl={() => {}} + refreshControl={refreshControlMock} /> ); findTestSubject(component, 'inputControlSubmitBtn').simulate('click'); @@ -223,7 +226,7 @@ test('resetControls', () => { hasValues={() => { return true; }} - refreshControl={() => {}} + refreshControl={refreshControlMock} /> ); findTestSubject(component, 'inputControlCancelBtn').simulate('click'); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.js b/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.tsx similarity index 60% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.js rename to src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.tsx index 9e140155698f0..e2497287f35d0 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.tsx @@ -17,16 +17,37 @@ * under the License. */ -import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { RangeControl } from './range_control'; -import { ListControl } from './list_control'; import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; - import { FormattedMessage } from '@kbn/i18n/react'; +import { CONTROL_TYPES } from '../../editor_utils'; +import { ListControl } from '../../control/list_control_factory'; +import { RangeControl } from '../../control/range_control_factory'; +import { ListControl as ListControlComponent } from '../vis/list_control'; +import { RangeControl as RangeControlComponent } from '../vis/range_control'; + +function isListControl(control: RangeControl | ListControl): control is ListControl { + return control.type === CONTROL_TYPES.LIST; +} + +function isRangeControl(control: RangeControl | ListControl): control is RangeControl { + return control.type === CONTROL_TYPES.RANGE; +} + +interface InputControlVisProps { + stageFilter: (controlIndex: number, newValue: any) => void; + submitFilters: () => void; + resetControls: () => void; + clearControls: () => void; + controls: Array; + updateFiltersOnChange?: boolean; + hasChanges: () => boolean; + hasValues: () => boolean; + refreshControl: (controlIndex: number, query: any) => Promise; +} -export class InputControlVis extends Component { - constructor(props) { +export class InputControlVis extends Component { + constructor(props: InputControlVisProps) { super(props); this.handleSubmit = this.handleSubmit.bind(this); @@ -49,39 +70,38 @@ export class InputControlVis extends Component { renderControls() { return this.props.controls.map((control, index) => { let controlComponent = null; - switch (control.type) { - case 'list': - controlComponent = ( - { - this.props.refreshControl(index, query); - }} - /> - ); - break; - case 'range': - controlComponent = ( - - ); - break; - default: - throw new Error(`Unhandled control type ${control.type}`); + + if (isListControl(control)) { + controlComponent = ( + { + this.props.refreshControl(index, query); + }} + /> + ); + } else if (isRangeControl(control)) { + controlComponent = ( + + ); + } else { + throw new Error(`Unhandled control type ${control!.type}`); } + return ( { +const formatOptionLabel = (value: any) => { return `${value} + formatting`; }; -let stageFilter; +let stageFilter: sinon.SinonSpy; beforeEach(() => { stageFilter = sinon.spy(); @@ -46,6 +46,7 @@ test('renders ListControl', () => { controlIndex={0} stageFilter={stageFilter} formatOptionLabel={formatOptionLabel} + intl={{} as any} /> ); expect(component).toMatchSnapshot(); // eslint-disable-line @@ -56,11 +57,13 @@ test('disableMsg', () => { ); expect(component).toMatchSnapshot(); // eslint-disable-line diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.js b/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx similarity index 76% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.js rename to src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx index 7e92545e817e0..d62adfdce56b4 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx @@ -17,46 +17,76 @@ * under the License. */ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; +import React, { PureComponent } from 'react'; import _ from 'lodash'; -import { FormRow } from './form_row'; import { injectI18n } from '@kbn/i18n/react'; +import { InjectedIntlProps } from 'react-intl'; import { EuiFieldText, EuiComboBox } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormRow } from './form_row'; + +interface ListControlUiState { + isLoading: boolean; +} + +export type ListControlUiProps = InjectedIntlProps & { + id: string; + label: string; + selectedOptions: any[]; + options?: any[]; + formatOptionLabel: (option: any) => any; + disableMsg?: string; + multiselect?: boolean; + dynamicOptions?: boolean; + partialResults?: boolean; + controlIndex: number; + stageFilter: (controlIndex: number, value: any) => void; + fetchOptions?: (searchValue: string) => void; +}; + +class ListControlUi extends PureComponent { + static defaultProps = { + dynamicOptions: false, + multiselect: true, + selectedOptions: [], + options: [], + }; + + private isMounted: boolean = false; -class ListControlUi extends Component { state = { isLoading: false, }; componentDidMount = () => { - this._isMounted = true; + this.isMounted = true; }; componentWillUnmount = () => { - this._isMounted = false; + this.isMounted = false; }; - handleOnChange = selectedOptions => { + handleOnChange = (selectedOptions: any[]) => { const selectedValues = selectedOptions.map(({ value }) => { return value; }); this.props.stageFilter(this.props.controlIndex, selectedValues); }; - debouncedFetch = _.debounce(async searchValue => { - await this.props.fetchOptions(searchValue); + debouncedFetch = _.debounce(async (searchValue: string) => { + if (this.props.fetchOptions) { + await this.props.fetchOptions(searchValue); + } - if (this._isMounted) { + if (this.isMounted) { this.setState({ isLoading: false, }); } }, 300); - onSearchChange = searchValue => { + onSearchChange = (searchValue: string) => { this.setState( { isLoading: true, @@ -81,7 +111,7 @@ class ListControlUi extends Component { } const options = this.props.options - .map(option => { + ?.map(option => { return { label: this.props.formatOptionLabel(option).toString(), value: option, @@ -141,29 +171,4 @@ class ListControlUi extends Component { } } -ListControlUi.propTypes = { - id: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - selectedOptions: PropTypes.array.isRequired, - options: PropTypes.array, - formatOptionLabel: PropTypes.func.isRequired, - disableMsg: PropTypes.string, - multiselect: PropTypes.bool, - dynamicOptions: PropTypes.bool, - partialResults: PropTypes.bool, - controlIndex: PropTypes.number.isRequired, - stageFilter: PropTypes.func.isRequired, - fetchOptions: PropTypes.func, -}; - -ListControlUi.defaultProps = { - dynamicOptions: false, - multiselect: true, -}; - -ListControlUi.defaultProps = { - selectedOptions: [], - options: [], -}; - export const ListControl = injectI18n(ListControlUi); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.test.js b/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.test.tsx similarity index 89% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.test.js rename to src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.test.tsx index 8b72def2f2698..639616151a395 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.test.tsx @@ -21,8 +21,11 @@ import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { RangeControl, ceilWithPrecision, floorWithPrecision } from './range_control'; +import { RangeControl as RangeControlClass } from '../../control/range_control_factory'; -const control = { +jest.mock('ui/new_platform'); + +const control: RangeControlClass = { id: 'mock-range-control', isEnabled: () => { return true; @@ -39,7 +42,7 @@ const control = { hasValue: () => { return false; }, -}; +} as RangeControlClass; test('renders RangeControl', () => { const component = shallowWithIntl( @@ -49,7 +52,7 @@ test('renders RangeControl', () => { }); test('disabled', () => { - const disabledRangeControl = { + const disabledRangeControl: RangeControlClass = { id: 'mock-range-control', isEnabled: () => { return false; @@ -64,7 +67,7 @@ test('disabled', () => { hasValue: () => { return false; }, - }; + } as RangeControlClass; const component = shallowWithIntl( {}} /> ); diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.js b/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx similarity index 72% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.js rename to src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx index ee3e3c8fe4788..cd3982afd9afd 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx @@ -18,12 +18,17 @@ */ import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; +import React, { PureComponent } from 'react'; + +import { ValidatedDualRange } from '../../legacy_imports'; import { FormRow } from './form_row'; -import { ValidatedDualRange } from 'ui/validated_range'; +import { RangeControl as RangeControlClass } from '../../control/range_control_factory'; -function roundWithPrecision(value, decimalPlaces, roundFunction) { +function roundWithPrecision( + value: number, + decimalPlaces: number, + roundFunction: (n: number) => number +) { if (decimalPlaces <= 0) { return roundFunction(value); } @@ -35,18 +40,29 @@ function roundWithPrecision(value, decimalPlaces, roundFunction) { return results; } -export function ceilWithPrecision(value, decimalPlaces) { +export function ceilWithPrecision(value: number, decimalPlaces: number) { return roundWithPrecision(value, decimalPlaces, Math.ceil); } -export function floorWithPrecision(value, decimalPlaces) { +export function floorWithPrecision(value: number, decimalPlaces: number) { return roundWithPrecision(value, decimalPlaces, Math.floor); } -export class RangeControl extends Component { - state = {}; +export interface RangeControlState { + value?: [string, string]; + prevValue?: [string, string]; +} + +export interface RangeControlProps { + control: RangeControlClass; + controlIndex: number; + stageFilter: (controlIndex: number, value: any) => void; +} + +export class RangeControl extends PureComponent { + state: RangeControlState = {}; - static getDerivedStateFromProps(nextProps, prevState) { + static getDerivedStateFromProps(nextProps: RangeControlProps, prevState: RangeControlState) { const nextValue = nextProps.control.hasValue() ? [nextProps.control.value.min, nextProps.control.value.max] : ['', '']; @@ -68,7 +84,7 @@ export class RangeControl extends Component { return null; } - onChangeComplete = _.debounce(value => { + onChangeComplete = _.debounce((value: [string, string]) => { const controlValue = { min: value[0], max: value[1], @@ -111,16 +127,10 @@ export class RangeControl extends Component { id={this.props.control.id} label={this.props.control.label} controlIndex={this.props.controlIndex} - disableMsg={this.props.control.isEnabled() ? null : this.props.control.disabledReason} + disableMsg={this.props.control.isEnabled() ? undefined : this.props.control.disabledReason} > {this.renderControl()} ); } } - -RangeControl.propTypes = { - control: PropTypes.object.isRequired, - controlIndex: PropTypes.number.isRequired, - stageFilter: PropTypes.func.isRequired, -}; diff --git a/src/legacy/core_plugins/input_control_vis/public/control/control.test.js b/src/legacy/core_plugins/input_control_vis/public/control/control.test.ts similarity index 78% rename from src/legacy/core_plugins/input_control_vis/public/control/control.test.js rename to src/legacy/core_plugins/input_control_vis/public/control/control.test.ts index aa9bed44d031d..e76b199a0262c 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/control.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/control.test.ts @@ -19,34 +19,50 @@ import expect from '@kbn/expect'; import { Control } from './control'; +import { ControlParams } from '../editor_utils'; +import { FilterManager as BaseFilterManager } from './filter_manager/filter_manager'; +import { SearchSource } from '../legacy_imports'; -function createControlParams(id, label) { +function createControlParams(id: string, label: string): ControlParams { return { - id: id, + id, options: {}, - label: label, - }; + label, + } as ControlParams; } -let valueFromFilterBar; -const mockFilterManager = { +let valueFromFilterBar: any; +const mockFilterManager: BaseFilterManager = { getValueFromFilterBar: () => { return valueFromFilterBar; }, - createFilter: value => { - return `mockKbnFilter:${value}`; + createFilter: (value: any) => { + return `mockKbnFilter:${value}` as any; }, getIndexPattern: () => { return 'mockIndexPattern'; }, -}; -const mockKbnApi = {}; +} as any; + +class ControlMock extends Control { + fetch() { + return Promise.resolve(); + } + + destroy() {} +} +const mockKbnApi: SearchSource = {} as SearchSource; describe('hasChanged', () => { - let control; + let control: ControlMock; beforeEach(() => { - control = new Control(createControlParams(3, 'control'), mockFilterManager, mockKbnApi); + control = new ControlMock( + createControlParams('3', 'control'), + mockFilterManager, + false, + mockKbnApi + ); }); afterEach(() => { @@ -70,23 +86,26 @@ describe('hasChanged', () => { }); describe('ancestors', () => { - let grandParentControl; - let parentControl; - let childControl; + let grandParentControl: any; + let parentControl: any; + let childControl: any; beforeEach(() => { - grandParentControl = new Control( - createControlParams(1, 'grandparent control'), + grandParentControl = new ControlMock( + createControlParams('1', 'grandparent control'), mockFilterManager, + false, mockKbnApi ); - parentControl = new Control( - createControlParams(2, 'parent control'), + parentControl = new ControlMock( + createControlParams('2', 'parent control'), mockFilterManager, + false, mockKbnApi ); - childControl = new Control( - createControlParams(3, 'child control'), + childControl = new ControlMock( + createControlParams('3', 'child control'), mockFilterManager, + false, mockKbnApi ); }); @@ -122,7 +141,7 @@ describe('ancestors', () => { }); describe('getAncestorValues', () => { - let lastAncestorValues; + let lastAncestorValues: any[]; beforeEach(() => { grandParentControl.set('myGrandParentValue'); parentControl.set('myParentValue'); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/control.js b/src/legacy/core_plugins/input_control_vis/public/control/control.ts similarity index 66% rename from src/legacy/core_plugins/input_control_vis/public/control/control.js rename to src/legacy/core_plugins/input_control_vis/public/control/control.ts index 4035dc1adefe8..9dc03ecc23452 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/control.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/control.ts @@ -22,32 +22,53 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -export function noValuesDisableMsg(fieldName, indexPatternName) { +import { esFilters } from '../../../../../plugins/data/public'; +import { SearchSource as SearchSourceClass } from '../legacy_imports'; +import { ControlParams, ControlParamsOptions, CONTROL_TYPES } from '../editor_utils'; +import { RangeFilterManager } from './filter_manager/range_filter_manager'; +import { PhraseFilterManager } from './filter_manager/phrase_filter_manager'; +import { FilterManager as BaseFilterManager } from './filter_manager/filter_manager'; + +export function noValuesDisableMsg(fieldName: string, indexPatternName: string) { return i18n.translate('inputControl.control.noValuesDisableTooltip', { defaultMessage: 'Filtering occurs on the "{fieldName}" field, which doesn\'t exist on any documents in the "{indexPatternName}" \ index pattern. Choose a different field or index documents that contain values for this field.', - values: { fieldName: fieldName, indexPatternName: indexPatternName }, + values: { fieldName, indexPatternName }, }); } -export function noIndexPatternMsg(indexPatternId) { +export function noIndexPatternMsg(indexPatternId: string) { return i18n.translate('inputControl.control.noIndexPatternTooltip', { defaultMessage: 'Could not locate index-pattern id: {indexPatternId}.', values: { indexPatternId }, }); } -export class Control { - constructor(controlParams, filterManager, useTimeFilter, SearchSource) { +export abstract class Control { + private kbnFilter: esFilters.Filter | null = null; + + enable: boolean = false; + disabledReason: string = ''; + value: any; + + id: string; + options: ControlParamsOptions; + type: CONTROL_TYPES; + label: string; + ancestors: Array> = []; + + constructor( + public controlParams: ControlParams, + public filterManager: FilterManager, + public useTimeFilter: boolean, + public SearchSource: SearchSourceClass + ) { this.id = controlParams.id; this.controlParams = controlParams; this.options = controlParams.options; this.type = controlParams.type; this.label = controlParams.label ? controlParams.label : controlParams.fieldName; - this.useTimeFilter = useTimeFilter; - this.filterManager = filterManager; - this.SearchSource = SearchSource; // restore state from kibana filter context this.reset(); @@ -59,28 +80,20 @@ export class Control { ); } - async fetch() { - throw new Error('fetch method not defined, subclass are required to implement'); - } + abstract fetch(query: string): Promise; - destroy() { - throw new Error('destroy method not defined, subclass are required to implement'); - } + abstract destroy(): void; - format = value => { + format = (value: any) => { const field = this.filterManager.getField(); - if (field) { + if (field?.format?.convert) { return field.format.convert(value); } return value; }; - /** - * - * @param ancestors {array of Controls} - */ - setAncestors(ancestors) { + setAncestors(ancestors: Array>) { this.ancestors = ancestors; } @@ -110,17 +123,17 @@ export class Control { return this.enable; } - disable(reason) { + disable(reason: string) { this.enable = false; this.disabledReason = reason; } - set(newValue) { + set(newValue: any) { this.value = newValue; if (this.hasValue()) { - this._kbnFilter = this.filterManager.createFilter(this.value); + this.kbnFilter = this.filterManager.createFilter(this.value); } else { - this._kbnFilter = null; + this.kbnFilter = null; } } @@ -128,7 +141,7 @@ export class Control { * Remove any user changes to value by resetting value to that as provided by Kibana filter pills */ reset() { - this._kbnFilter = null; + this.kbnFilter = null; this.value = this.filterManager.getValueFromFilterBar(); } @@ -144,17 +157,17 @@ export class Control { } hasKbnFilter() { - if (this._kbnFilter) { + if (this.kbnFilter) { return true; } return false; } getKbnFilter() { - return this._kbnFilter; + return this.kbnFilter; } - hasValue() { + hasValue(): boolean { return this.value !== undefined; } } diff --git a/src/legacy/core_plugins/input_control_vis/public/control/control_factory.js b/src/legacy/core_plugins/input_control_vis/public/control/control_factory.ts similarity index 86% rename from src/legacy/core_plugins/input_control_vis/public/control/control_factory.js rename to src/legacy/core_plugins/input_control_vis/public/control/control_factory.ts index 6d3c7756f72aa..3dcc1d53d4211 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/control_factory.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/control_factory.ts @@ -19,14 +19,15 @@ import { rangeControlFactory } from './range_control_factory'; import { listControlFactory } from './list_control_factory'; +import { ControlParams, CONTROL_TYPES } from '../editor_utils'; -export function controlFactory(controlParams) { +export function getControlFactory(controlParams: ControlParams) { let factory = null; switch (controlParams.type) { - case 'range': + case CONTROL_TYPES.RANGE: factory = rangeControlFactory; break; - case 'list': + case CONTROL_TYPES.LIST: factory = listControlFactory; break; default: diff --git a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.js b/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts similarity index 71% rename from src/legacy/core_plugins/input_control_vis/public/control/create_search_source.js rename to src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts index 2917dda5e96a7..c8fa5af5e052b 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts @@ -16,15 +16,18 @@ * specific language governing permissions and limitations * under the License. */ -import { timefilter } from 'ui/timefilter'; + +import { esFilters, IndexPattern, TimefilterSetup } from '../../../../../plugins/data/public'; +import { SearchSource as SearchSourceClass, SearchSourceFields } from '../legacy_imports'; export function createSearchSource( - SearchSource, - initialState, - indexPattern, - aggs, - useTimeFilter, - filters = [] + SearchSource: SearchSourceClass, + initialState: SearchSourceFields | null, + indexPattern: IndexPattern, + aggs: any, + useTimeFilter: boolean, + filters: esFilters.PhraseFilter[] = [], + timefilter: TimefilterSetup['timefilter'] ) { const searchSource = initialState ? new SearchSource(initialState) : new SearchSource(); // Do not not inherit from rootSearchSource to avoid picking up time and globals @@ -32,7 +35,10 @@ export function createSearchSource( searchSource.setField('filter', () => { const activeFilters = [...filters]; if (useTimeFilter) { - activeFilters.push(timefilter.createFilter(indexPattern)); + const filter = timefilter.createFilter(indexPattern); + if (filter) { + activeFilters.push(filter); + } } return activeFilters; }); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.js b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts similarity index 66% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.js rename to src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts index 95277ac073d75..fd2cbae121b7e 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts @@ -18,30 +18,45 @@ */ import expect from '@kbn/expect'; + import { FilterManager } from './filter_manager'; +import { coreMock } from '../../../../../../core/public/mocks'; +import { + esFilters, + IndexPattern, + FilterManager as QueryFilterManager, +} from '../../../../../../plugins/data/public'; + +const setupMock = coreMock.createSetup(); + +class FilterManagerTest extends FilterManager { + createFilter() { + return {} as esFilters.Filter; + } + + getValueFromFilterBar() { + return null; + } +} describe('FilterManager', function() { const controlId = 'control1'; describe('findFilters', function() { - const indexPatternMock = {}; - let kbnFilters; - const queryFilterMock = { - getAppFilters: () => { - return kbnFilters; - }, - getGlobalFilters: () => { - return []; - }, - }; - let filterManager; + const indexPatternMock = {} as IndexPattern; + let kbnFilters: esFilters.Filter[]; + const queryFilterMock = new QueryFilterManager(setupMock.uiSettings); + queryFilterMock.getAppFilters = () => kbnFilters; + queryFilterMock.getGlobalFilters = () => []; + + let filterManager: FilterManagerTest; beforeEach(() => { kbnFilters = []; - filterManager = new FilterManager(controlId, 'field1', indexPatternMock, queryFilterMock); + filterManager = new FilterManagerTest(controlId, 'field1', indexPatternMock, queryFilterMock); }); test('should not find filters that are not controlled by any visualization', function() { - kbnFilters.push({}); + kbnFilters.push({} as esFilters.Filter); const foundFilters = filterManager.findFilters(); expect(foundFilters.length).to.be(0); }); @@ -51,7 +66,7 @@ describe('FilterManager', function() { meta: { controlledBy: 'anotherControl', }, - }); + } as esFilters.Filter); const foundFilters = filterManager.findFilters(); expect(foundFilters.length).to.be(0); }); @@ -61,7 +76,7 @@ describe('FilterManager', function() { meta: { controlledBy: controlId, }, - }); + } as esFilters.Filter); const foundFilters = filterManager.findFilters(); expect(foundFilters.length).to.be(1); }); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.js b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.ts similarity index 62% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.js rename to src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.ts index 672f56746cf80..d80a74ed46eae 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.ts @@ -19,15 +19,33 @@ import _ from 'lodash'; -export class FilterManager { - constructor(controlId, fieldName, indexPattern, queryFilter) { - this.controlId = controlId; - this.fieldName = fieldName; - this.indexPattern = indexPattern; - this.queryFilter = queryFilter; - } +import { + FilterManager as QueryFilterManager, + IndexPattern, + esFilters, +} from '../../../../../../plugins/data/public'; + +export abstract class FilterManager { + constructor( + public controlId: string, + public fieldName: string, + public indexPattern: IndexPattern, + public queryFilter: QueryFilterManager + ) {} + + /** + * Convert phrases into filter + * + * @param {any[]} phrases + * @returns PhraseFilter + * single phrase: match query + * multiple phrases: bool query with should containing list of match_phrase queries + */ + abstract createFilter(phrases: any): esFilters.Filter; + + abstract getValueFromFilterBar(): any; - getIndexPattern() { + getIndexPattern(): IndexPattern { return this.indexPattern; } @@ -35,11 +53,7 @@ export class FilterManager { return this.indexPattern.fields.getByName(this.fieldName); } - createFilter() { - throw new Error('Must implement createFilter.'); - } - - findFilters() { + findFilters(): esFilters.Filter[] { const kbnFilters = _.flatten([ this.queryFilter.getAppFilters(), this.queryFilter.getGlobalFilters(), @@ -48,8 +62,4 @@ export class FilterManager { return _.get(kbnFilter, 'meta.controlledBy') === this.controlId; }); } - - getValueFromFilterBar() { - throw new Error('Must implement getValueFromFilterBar.'); - } } diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.js b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts similarity index 79% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.js rename to src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts index 7aa1ec6632043..dc577ca7168d1 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts @@ -18,6 +18,12 @@ */ import expect from '@kbn/expect'; + +import { + esFilters, + IndexPattern, + FilterManager as QueryFilterManager, +} from '../../../../../../plugins/data/public'; import { PhraseFilterManager } from './phrase_filter_manager'; describe('PhraseFilterManager', function() { @@ -28,22 +34,20 @@ describe('PhraseFilterManager', function() { const fieldMock = { name: 'field1', format: { - convert: val => { - return val; - }, + convert: (value: any) => value, }, }; - const indexPatternMock = { + const indexPatternMock: IndexPattern = { id: indexPatternId, fields: { - getByName: name => { - const fields = { field1: fieldMock }; + getByName: (name: string) => { + const fields: any = { field1: fieldMock }; return fields[name]; }, }, - }; - const queryFilterMock = {}; - let filterManager; + } as IndexPattern; + const queryFilterMock: QueryFilterManager = {} as QueryFilterManager; + let filterManager: PhraseFilterManager; beforeEach(() => { filterManager = new PhraseFilterManager( controlId, @@ -83,22 +87,32 @@ describe('PhraseFilterManager', function() { }); describe('getValueFromFilterBar', function() { - const indexPatternMock = {}; - const queryFilterMock = {}; - let filterManager; - beforeEach(() => { - class MockFindFiltersPhraseFilterManager extends PhraseFilterManager { - constructor(controlId, fieldName, indexPattern, queryFilter, delimiter) { - super(controlId, fieldName, indexPattern, queryFilter, delimiter); - this.mockFilters = []; - } - findFilters() { - return this.mockFilters; - } - setMockFilters(mockFilters) { - this.mockFilters = mockFilters; - } + class MockFindFiltersPhraseFilterManager extends PhraseFilterManager { + mockFilters: esFilters.Filter[]; + + constructor( + id: string, + fieldName: string, + indexPattern: IndexPattern, + queryFilter: QueryFilterManager + ) { + super(id, fieldName, indexPattern, queryFilter); + this.mockFilters = []; } + + findFilters() { + return this.mockFilters; + } + + setMockFilters(mockFilters: esFilters.Filter[]) { + this.mockFilters = mockFilters; + } + } + + const indexPatternMock: IndexPattern = {} as IndexPattern; + const queryFilterMock: QueryFilterManager = {} as QueryFilterManager; + let filterManager: MockFindFiltersPhraseFilterManager; + beforeEach(() => { filterManager = new MockFindFiltersPhraseFilterManager( controlId, 'field1', @@ -119,7 +133,7 @@ describe('PhraseFilterManager', function() { }, }, }, - ]); + ] as esFilters.Filter[]); expect(filterManager.getValueFromFilterBar()).to.eql(['ios']); }); @@ -145,7 +159,7 @@ describe('PhraseFilterManager', function() { }, }, }, - ]); + ] as esFilters.Filter[]); expect(filterManager.getValueFromFilterBar()).to.eql(['ios', 'win xp']); }); @@ -169,7 +183,7 @@ describe('PhraseFilterManager', function() { }, }, }, - ]); + ] as esFilters.Filter[]); expect(filterManager.getValueFromFilterBar()).to.eql(['ios', 'win xp']); }); @@ -185,7 +199,7 @@ describe('PhraseFilterManager', function() { }, }, }, - ]); + ] as esFilters.Filter[]); expect(filterManager.getValueFromFilterBar()).to.eql(undefined); }); }); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.js b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts similarity index 68% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.js rename to src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts index 1e60f8c4ebb67..b0b46be86f1e8 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts @@ -18,37 +18,38 @@ */ import _ from 'lodash'; -import { FilterManager } from './filter_manager.js'; -import { esFilters } from '../../../../../../plugins/data/public'; + +import { FilterManager } from './filter_manager'; +import { + esFilters, + IndexPattern, + FilterManager as QueryFilterManager, +} from '../../../../../../plugins/data/public'; export class PhraseFilterManager extends FilterManager { - constructor(controlId, fieldName, indexPattern, queryFilter) { + constructor( + controlId: string, + fieldName: string, + indexPattern: IndexPattern, + queryFilter: QueryFilterManager + ) { super(controlId, fieldName, indexPattern, queryFilter); } - /** - * Convert phrases into filter - * - * @param {array} phrases - * @return {object} query filter - * single phrase: match query - * multiple phrases: bool query with should containing list of match_phrase queries - */ - createFilter(phrases) { - let newFilter; + createFilter(phrases: any): esFilters.PhraseFilter { + let newFilter: esFilters.PhraseFilter; + const value = this.indexPattern.fields.getByName(this.fieldName); + + if (!value) { + throw new Error(`Unable to find field with name: ${this.fieldName} on indexPattern`); + } + if (phrases.length === 1) { - newFilter = esFilters.buildPhraseFilter( - this.indexPattern.fields.getByName(this.fieldName), - phrases[0], - this.indexPattern - ); + newFilter = esFilters.buildPhraseFilter(value, phrases[0], this.indexPattern); } else { - newFilter = esFilters.buildPhrasesFilter( - this.indexPattern.fields.getByName(this.fieldName), - phrases, - this.indexPattern - ); + newFilter = esFilters.buildPhrasesFilter(value, phrases, this.indexPattern); } + newFilter.meta.key = this.fieldName; newFilter.meta.controlledBy = this.controlId; return newFilter; @@ -62,7 +63,7 @@ export class PhraseFilterManager extends FilterManager { const values = kbnFilters .map(kbnFilter => { - return this._getValueFromFilter(kbnFilter); + return this.getValueFromFilter(kbnFilter); }) .filter(value => value != null); @@ -78,15 +79,15 @@ export class PhraseFilterManager extends FilterManager { /** * Extract filtering value from kibana filters * - * @param {object} kbnFilter + * @param {esFilters.PhraseFilter} kbnFilter * @return {Array.} array of values pulled from filter */ - _getValueFromFilter(kbnFilter) { + private getValueFromFilter(kbnFilter: esFilters.PhraseFilter): any { // bool filter - multiple phrase filters if (_.has(kbnFilter, 'query.bool.should')) { - return _.get(kbnFilter, 'query.bool.should') - .map(kbnFilter => { - return this._getValueFromFilter(kbnFilter); + return _.get(kbnFilter, 'query.bool.should') + .map(kbnQueryFilter => { + return this.getValueFromFilter(kbnQueryFilter); }) .filter(value => { if (value) { diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.js b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts similarity index 68% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.js rename to src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts index ffe2ebdad53bc..f4993a60c5b39 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts @@ -18,7 +18,13 @@ */ import expect from '@kbn/expect'; + import { RangeFilterManager } from './range_filter_manager'; +import { + esFilters, + IndexPattern, + FilterManager as QueryFilterManager, +} from '../../../../../../plugins/data/public'; describe('RangeFilterManager', function() { const controlId = 'control1'; @@ -28,19 +34,19 @@ describe('RangeFilterManager', function() { const fieldMock = { name: 'field1', }; - const indexPatternMock = { + const indexPatternMock: IndexPattern = { id: indexPatternId, fields: { - getByName: name => { - const fields = { + getByName: (name: any) => { + const fields: any = { field1: fieldMock, }; return fields[name]; }, }, - }; - const queryFilterMock = {}; - let filterManager; + } as IndexPattern; + const queryFilterMock: QueryFilterManager = {} as QueryFilterManager; + let filterManager: RangeFilterManager; beforeEach(() => { filterManager = new RangeFilterManager( controlId, @@ -62,22 +68,32 @@ describe('RangeFilterManager', function() { }); describe('getValueFromFilterBar', function() { - const indexPatternMock = {}; - const queryFilterMock = {}; - let filterManager; - beforeEach(() => { - class MockFindFiltersRangeFilterManager extends RangeFilterManager { - constructor(controlId, fieldName, indexPattern, queryFilter) { - super(controlId, fieldName, indexPattern, queryFilter); - this.mockFilters = []; - } - findFilters() { - return this.mockFilters; - } - setMockFilters(mockFilters) { - this.mockFilters = mockFilters; - } + class MockFindFiltersRangeFilterManager extends RangeFilterManager { + mockFilters: esFilters.RangeFilter[]; + + constructor( + id: string, + fieldName: string, + indexPattern: IndexPattern, + queryFilter: QueryFilterManager + ) { + super(id, fieldName, indexPattern, queryFilter); + this.mockFilters = []; + } + + findFilters() { + return this.mockFilters; + } + + setMockFilters(mockFilters: esFilters.RangeFilter[]) { + this.mockFilters = mockFilters; } + } + + const indexPatternMock: IndexPattern = {} as IndexPattern; + const queryFilterMock: QueryFilterManager = {} as QueryFilterManager; + let filterManager: MockFindFiltersRangeFilterManager; + beforeEach(() => { filterManager = new MockFindFiltersRangeFilterManager( controlId, 'field1', @@ -95,14 +111,15 @@ describe('RangeFilterManager', function() { lt: 3, }, }, + meta: {} as esFilters.RangeFilterMeta, }, - ]); + ] as esFilters.RangeFilter[]); const value = filterManager.getValueFromFilterBar(); expect(value).to.be.a('object'); expect(value).to.have.property('min'); - expect(value.min).to.be(1); + expect(value?.min).to.be(1); expect(value).to.have.property('max'); - expect(value.max).to.be(3); + expect(value?.max).to.be(3); }); test('should return undefined when filter value can not be extracted from Kibana filter', function() { @@ -114,8 +131,9 @@ describe('RangeFilterManager', function() { lte: 3, }, }, + meta: {} as esFilters.RangeFilterMeta, }, - ]); + ] as esFilters.RangeFilter[]); expect(filterManager.getValueFromFilterBar()).to.eql(undefined); }); }); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.js b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts similarity index 77% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.js rename to src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts index 5a2e7b7d779bc..0a6819bd68e6f 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts @@ -18,11 +18,17 @@ */ import _ from 'lodash'; -import { FilterManager } from './filter_manager.js'; -import { esFilters } from '../../../../../../plugins/data/public'; + +import { FilterManager } from './filter_manager'; +import { esFilters, IFieldType } from '../../../../../../plugins/data/public'; + +interface SliderValue { + min?: string | number; + max?: string | number; +} // Convert slider value into ES range filter -function toRange(sliderValue) { +function toRange(sliderValue: SliderValue) { return { gte: sliderValue.min, lte: sliderValue.max, @@ -30,8 +36,8 @@ function toRange(sliderValue) { } // Convert ES range filter into slider value -function fromRange(range) { - const sliderValue = {}; +function fromRange(range: esFilters.RangeFilterParams): SliderValue { + const sliderValue: SliderValue = {}; if (_.has(range, 'gte')) { sliderValue.min = _.get(range, 'gte'); } @@ -54,9 +60,10 @@ export class RangeFilterManager extends FilterManager { * @param {object} react-input-range value - POJO with `min` and `max` properties * @return {object} range filter */ - createFilter(value) { + createFilter(value: SliderValue): esFilters.RangeFilter { const newFilter = esFilters.buildRangeFilter( - this.indexPattern.fields.getByName(this.fieldName), + // TODO: Fix type to be required + this.indexPattern.fields.getByName(this.fieldName) as IFieldType, toRange(value), this.indexPattern ); @@ -65,13 +72,13 @@ export class RangeFilterManager extends FilterManager { return newFilter; } - getValueFromFilterBar() { + getValueFromFilterBar(): SliderValue | undefined { const kbnFilters = this.findFilters(); if (kbnFilters.length === 0) { return; } - let range; + let range: esFilters.RangeFilterParams; if (_.has(kbnFilters[0], 'script')) { range = _.get(kbnFilters[0], 'script.script.params'); } else { diff --git a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.test.js b/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.test.ts similarity index 57% rename from src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.test.js rename to src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.test.ts index 3b5ef7372bc1f..2420907727638 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.test.ts @@ -17,92 +17,33 @@ * under the License. */ -import chrome from 'ui/chrome'; -import { listControlFactory } from './list_control_factory'; +import { listControlFactory, ListControl } from './list_control_factory'; +import { ControlParams, CONTROL_TYPES } from '../editor_utils'; +import { getDepsMock } from '../components/editor/__tests__/get_deps_mock'; +import { getSearchSourceMock } from '../components/editor/__tests__/get_search_service_mock'; -jest.mock('ui/timefilter', () => ({ - createFilter: jest.fn(), -})); +const MockSearchSource = getSearchSourceMock(); +const deps = getDepsMock(); -jest.mock('ui/new_platform', () => ({ - npStart: { - plugins: { - data: { - query: { - filterManager: { - fieldName: 'myNumberField', - getIndexPattern: () => ({ - fields: { - getByName: name => { - const fields = { myField: { name: 'myField' } }; - return fields[name]; - }, - }, - }), - getAppFilters: jest.fn().mockImplementation(() => []), - getGlobalFilters: jest.fn().mockImplementation(() => []), - }, - }, - indexPatterns: { - get: () => ({ - fields: { - getByName: name => { - const fields = { myField: { name: 'myField' } }; - return fields[name]; - }, - }, - }), - }, - }, - }, - }, +jest.doMock('./create_search_source.ts', () => ({ + createSearchSource: MockSearchSource, })); -chrome.getInjected.mockImplementation(key => { - switch (key) { - case 'autocompleteTimeout': - return 1000; - case 'autocompleteTerminateAfter': - return 100000; - } -}); - -function MockSearchSource() { - return { - setParent: () => {}, - setField: () => {}, - fetch: async () => { - return { - aggregations: { - termsAgg: { - buckets: [ - { - key: 'Zurich Airport', - doc_count: 691, - }, - { - key: 'Xi an Xianyang International Airport', - doc_count: 526, - }, - ], - }, - }, - }; - }, - }; -} - describe('hasValue', () => { - const controlParams = { + const controlParams: ControlParams = { id: '1', fieldName: 'myField', - options: {}, + options: {} as any, + type: CONTROL_TYPES.LIST, + label: 'test', + indexPattern: {} as any, + parent: 'parent', }; const useTimeFilter = false; - let listControl; + let listControl: ListControl; beforeEach(async () => { - listControl = await listControlFactory(controlParams, useTimeFilter, MockSearchSource); + listControl = await listControlFactory(controlParams, useTimeFilter, MockSearchSource, deps); }); test('should be false when control has no value', () => { @@ -121,22 +62,25 @@ describe('hasValue', () => { }); describe('fetch', () => { - const controlParams = { + const controlParams: ControlParams = { id: '1', fieldName: 'myField', - options: {}, + options: {} as any, + type: CONTROL_TYPES.LIST, + label: 'test', + indexPattern: {} as any, + parent: 'parent', }; const useTimeFilter = false; - const SearchSource = jest.fn(MockSearchSource); - let listControl; + let listControl: ListControl; beforeEach(async () => { - listControl = await listControlFactory(controlParams, useTimeFilter, SearchSource); + listControl = await listControlFactory(controlParams, useTimeFilter, MockSearchSource, deps); }); test('should pass in timeout parameters from injected vars', async () => { await listControl.fetch(); - expect(SearchSource).toHaveBeenCalledWith({ + expect(MockSearchSource).toHaveBeenCalledWith({ timeout: `1000ms`, terminate_after: 100000, }); @@ -152,24 +96,37 @@ describe('fetch', () => { }); describe('fetch with ancestors', () => { - const controlParams = { + const controlParams: ControlParams = { id: '1', fieldName: 'myField', - options: {}, + options: {} as any, + type: CONTROL_TYPES.LIST, + label: 'test', + indexPattern: {} as any, + parent: 'parent', }; const useTimeFilter = false; - let listControl; + let listControl: ListControl; let parentControl; beforeEach(async () => { - listControl = await listControlFactory(controlParams, useTimeFilter, MockSearchSource); + listControl = await listControlFactory(controlParams, useTimeFilter, MockSearchSource, deps); - const parentControlParams = { + const parentControlParams: ControlParams = { id: 'parent', fieldName: 'myField', - options: {}, + options: {} as any, + type: CONTROL_TYPES.LIST, + label: 'test', + indexPattern: {} as any, + parent: 'parent', }; - parentControl = await listControlFactory(parentControlParams, useTimeFilter, MockSearchSource); + parentControl = await listControlFactory( + parentControlParams, + useTimeFilter, + MockSearchSource, + deps + ); parentControl.clear(); listControl.setAncestors([parentControl]); }); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.js b/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts similarity index 63% rename from src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.js rename to src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts index d90b21eead5c6..56b42f295ce15 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts @@ -18,20 +18,30 @@ */ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; + +import { SearchSource as SearchSourceClass, SearchSourceFields } from '../legacy_imports'; import { Control, noValuesDisableMsg, noIndexPatternMsg } from './control'; import { PhraseFilterManager } from './filter_manager/phrase_filter_manager'; import { createSearchSource } from './create_search_source'; -import { i18n } from '@kbn/i18n'; -import { npStart } from 'ui/new_platform'; -import chrome from 'ui/chrome'; +import { ControlParams } from '../editor_utils'; +import { InputControlVisDependencies } from '../plugin'; +import { IFieldType, TimefilterSetup } from '../../../../../plugins/data/public'; function getEscapedQuery(query = '') { // https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html#_standard_operators return query.replace(/[.?+*|{}[\]()"\\#@&<>~]/g, match => `\\${match}`); } -const termsAgg = ({ field, size, direction, query }) => { - const terms = { +interface TermsAggArgs { + field?: IFieldType; + size: number | null; + direction: string; + query?: string; +} + +const termsAgg = ({ field, size, direction, query }: TermsAggArgs) => { + const terms: any = { order: { _count: direction, }, @@ -41,14 +51,14 @@ const termsAgg = ({ field, size, direction, query }) => { terms.size = size < 1 ? 1 : size; } - if (field.scripted) { + if (field?.scripted) { terms.script = { source: field.script, lang: field.lang, }; terms.value_type = field.type === 'number' ? 'float' : field.type; } else { - terms.field = field.name; + terms.field = field?.name; } if (query) { @@ -57,13 +67,34 @@ const termsAgg = ({ field, size, direction, query }) => { return { termsAgg: { - terms: terms, + terms, }, }; }; -class ListControl extends Control { - fetch = async query => { +export class ListControl extends Control { + private getInjectedVar: InputControlVisDependencies['core']['injectedMetadata']['getInjectedVar']; + private timefilter: TimefilterSetup['timefilter']; + + abortController?: AbortController; + lastAncestorValues: any; + lastQuery?: string; + partialResults?: boolean; + selectOptions?: string[]; + + constructor( + controlParams: ControlParams, + filterManager: PhraseFilterManager, + useTimeFilter: boolean, + SearchSource: SearchSourceClass, + deps: InputControlVisDependencies + ) { + super(controlParams, filterManager, useTimeFilter, SearchSource); + this.getInjectedVar = deps.core.injectedMetadata.getInjectedVar; + this.timefilter = deps.data.query.timefilter.timefilter; + } + + fetch = async (query?: string) => { // Abort any in-progress fetch if (this.abortController) { this.abortController.abort(); @@ -101,9 +132,9 @@ class ListControl extends Control { } const fieldName = this.filterManager.fieldName; - const initialSearchSourceState = { - timeout: `${chrome.getInjected('autocompleteTimeout')}ms`, - terminate_after: chrome.getInjected('autocompleteTerminateAfter'), + const initialSearchSourceState: SearchSourceFields = { + timeout: `${this.getInjectedVar('autocompleteTimeout')}ms`, + terminate_after: Number(this.getInjectedVar('autocompleteTerminateAfter')), }; const aggs = termsAgg({ field: indexPattern.fields.getByName(fieldName), @@ -117,7 +148,8 @@ class ListControl extends Control { indexPattern, aggs, this.useTimeFilter, - ancestorFilters + ancestorFilters, + this.timefilter ); const abortSignal = this.abortController.signal; @@ -143,8 +175,8 @@ class ListControl extends Control { return; } - const selectOptions = _.get(resp, 'aggregations.termsAgg.buckets', []).map(bucket => { - return bucket.key; + const selectOptions = _.get(resp, 'aggregations.termsAgg.buckets', []).map((bucket: any) => { + return bucket?.key; }); if (selectOptions.length === 0 && !query) { @@ -167,29 +199,34 @@ class ListControl extends Control { } } -export async function listControlFactory(controlParams, useTimeFilter, SearchSource) { - let indexPattern; - try { - indexPattern = await npStart.plugins.data.indexPatterns.get(controlParams.indexPattern); - - // dynamic options are only allowed on String fields but the setting defaults to true so it could - // be enabled for non-string fields (since UI input is hidden for non-string fields). - // If field is not string, then disable dynamic options. - const field = indexPattern.fields.find(field => { - return field.name === controlParams.fieldName; - }); - if (field && field.type !== 'string') { - controlParams.options.dynamicOptions = false; - } - } catch (err) { - // ignore not found error and return control so it can be displayed in disabled state. +export async function listControlFactory( + controlParams: ControlParams, + useTimeFilter: boolean, + SearchSource: SearchSourceClass, + deps: InputControlVisDependencies +) { + const [, { data: dataPluginStart }] = await deps.core.getStartServices(); + const indexPattern = await dataPluginStart.indexPatterns.get(controlParams.indexPattern); + + // dynamic options are only allowed on String fields but the setting defaults to true so it could + // be enabled for non-string fields (since UI input is hidden for non-string fields). + // If field is not string, then disable dynamic options. + const field = indexPattern.fields.find(({ name }) => name === controlParams.fieldName); + if (field && field.type !== 'string') { + controlParams.options.dynamicOptions = false; } - const { filterManager } = npStart.plugins.data.query; - return new ListControl( + const listControl = new ListControl( controlParams, - new PhraseFilterManager(controlParams.id, controlParams.fieldName, indexPattern, filterManager), + new PhraseFilterManager( + controlParams.id, + controlParams.fieldName, + indexPattern, + deps.data.query.filterManager + ), useTimeFilter, - SearchSource + SearchSource, + deps ); + return listControl; } diff --git a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.test.js b/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.test.ts similarity index 59% rename from src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.test.js rename to src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.test.ts index b545c6e2834f3..5328aeb6c6a47 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.test.ts @@ -18,74 +18,37 @@ */ import { rangeControlFactory } from './range_control_factory'; +import { ControlParams, CONTROL_TYPES } from '../editor_utils'; +import { getSearchSourceMock } from '../components/editor/__tests__/get_search_service_mock'; +import { getDepsMock } from '../components/editor/__tests__/get_deps_mock'; -let esSearchResponse; -class MockSearchSource { - setParent() {} - setField() {} - async fetch() { - return esSearchResponse; - } -} - -jest.mock('ui/timefilter', () => ({ - createFilter: jest.fn(), -})); - -jest.mock('ui/new_platform', () => ({ - npStart: { - plugins: { - data: { - query: { - filterManager: { - fieldName: 'myNumberField', - getIndexPattern: () => ({ - fields: { - getByName: name => { - const fields = { myNumberField: { name: 'myNumberField' } }; - return fields[name]; - }, - }, - }), - getAppFilters: jest.fn().mockImplementation(() => []), - getGlobalFilters: jest.fn().mockImplementation(() => []), - }, - }, - indexPatterns: { - get: () => ({ - fields: { - getByName: name => { - const fields = { myNumberField: { name: 'myNumberField' } }; - return fields[name]; - }, - }, - }), - }, - }, - }, - }, -})); +const deps = getDepsMock(); describe('fetch', () => { - const controlParams = { + const controlParams: ControlParams = { id: '1', fieldName: 'myNumberField', options: {}, + type: CONTROL_TYPES.RANGE, + label: 'test', + indexPattern: {} as any, + parent: {} as any, }; const useTimeFilter = false; - let rangeControl; - beforeEach(async () => { - rangeControl = await rangeControlFactory(controlParams, useTimeFilter, MockSearchSource); - }); - test('should set min and max from aggregation results', async () => { - esSearchResponse = { + const esSearchResponse = { aggregations: { maxAgg: { value: 100 }, minAgg: { value: 10 }, }, }; + const rangeControl = await rangeControlFactory( + controlParams, + useTimeFilter, + getSearchSourceMock(esSearchResponse), + deps + ); await rangeControl.fetch(); expect(rangeControl.isEnabled()).toBe(true); @@ -95,12 +58,18 @@ describe('fetch', () => { test('should disable control when there are 0 hits', async () => { // ES response when the query does not match any documents - esSearchResponse = { + const esSearchResponse = { aggregations: { maxAgg: { value: null }, minAgg: { value: null }, }, }; + const rangeControl = await rangeControlFactory( + controlParams, + useTimeFilter, + getSearchSourceMock(esSearchResponse), + deps + ); await rangeControl.fetch(); expect(rangeControl.isEnabled()).toBe(false); @@ -109,7 +78,13 @@ describe('fetch', () => { test('should disable control when response is empty', async () => { // ES response for dashboardonly user who does not have read permissions on index is 200 (which is weird) // and there is not aggregations key - esSearchResponse = {}; + const esSearchResponse = {}; + const rangeControl = await rangeControlFactory( + controlParams, + useTimeFilter, + getSearchSourceMock(esSearchResponse), + deps + ); await rangeControl.fetch(); expect(rangeControl.isEnabled()).toBe(false); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.js b/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts similarity index 63% rename from src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.js rename to src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts index c99c794c1fcd5..b9191436b5968 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.js +++ b/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts @@ -18,22 +18,29 @@ */ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; + +import { SearchSource as SearchSourceClass } from '../legacy_imports'; import { Control, noValuesDisableMsg, noIndexPatternMsg } from './control'; import { RangeFilterManager } from './filter_manager/range_filter_manager'; import { createSearchSource } from './create_search_source'; -import { i18n } from '@kbn/i18n'; -import { npStart } from 'ui/new_platform'; +import { ControlParams } from '../editor_utils'; +import { InputControlVisDependencies } from '../plugin'; +import { IFieldType, TimefilterSetup } from '../.../../../../../../plugins/data/public'; -const minMaxAgg = field => { - const aggBody = {}; - if (field.scripted) { - aggBody.script = { - source: field.script, - lang: field.lang, - }; - } else { - aggBody.field = field.name; +const minMaxAgg = (field?: IFieldType) => { + const aggBody: any = {}; + if (field) { + if (field.scripted) { + aggBody.script = { + source: field.script, + lang: field.lang, + }; + } else { + aggBody.field = field.name; + } } + return { maxAgg: { max: aggBody, @@ -44,7 +51,23 @@ const minMaxAgg = field => { }; }; -class RangeControl extends Control { +export class RangeControl extends Control { + timefilter: TimefilterSetup['timefilter']; + abortController: any; + min: any; + max: any; + + constructor( + controlParams: ControlParams, + filterManager: RangeFilterManager, + useTimeFilter: boolean, + SearchSource: SearchSourceClass, + deps: InputControlVisDependencies + ) { + super(controlParams, filterManager, useTimeFilter, SearchSource); + this.timefilter = deps.data.query.timefilter.timefilter; + } + async fetch() { // Abort any in-progress fetch if (this.abortController) { @@ -58,14 +81,15 @@ class RangeControl extends Control { } const fieldName = this.filterManager.fieldName; - const aggs = minMaxAgg(indexPattern.fields.getByName(fieldName)); const searchSource = createSearchSource( this.SearchSource, null, indexPattern, aggs, - this.useTimeFilter + this.useTimeFilter, + [], + this.timefilter ); const abortSignal = this.abortController.signal; @@ -102,18 +126,25 @@ class RangeControl extends Control { } } -export async function rangeControlFactory(controlParams, useTimeFilter, SearchSource) { - let indexPattern; - try { - indexPattern = await npStart.plugins.data.indexPatterns.get(controlParams.indexPattern); - } catch (err) { - // ignore not found error and return control so it can be displayed in disabled state. - } - const { filterManager } = npStart.plugins.data.query; +export async function rangeControlFactory( + controlParams: ControlParams, + useTimeFilter: boolean, + SearchSource: SearchSourceClass, + deps: InputControlVisDependencies +): Promise { + const [, { data: dataPluginStart }] = await deps.core.getStartServices(); + const indexPattern = await dataPluginStart.indexPatterns.get(controlParams.indexPattern); + return new RangeControl( controlParams, - new RangeFilterManager(controlParams.id, controlParams.fieldName, indexPattern, filterManager), + new RangeFilterManager( + controlParams.id, + controlParams.fieldName, + indexPattern, + deps.data.query.filterManager + ), useTimeFilter, - SearchSource + SearchSource, + deps ); } diff --git a/src/legacy/core_plugins/input_control_vis/public/editor_utils.js b/src/legacy/core_plugins/input_control_vis/public/editor_utils.ts similarity index 64% rename from src/legacy/core_plugins/input_control_vis/public/editor_utils.js rename to src/legacy/core_plugins/input_control_vis/public/editor_utils.ts index f5b4390342a0f..74def0a8d86f4 100644 --- a/src/legacy/core_plugins/input_control_vis/public/editor_utils.js +++ b/src/legacy/core_plugins/input_control_vis/public/editor_utils.ts @@ -16,21 +16,54 @@ * specific language governing permissions and limitations * under the License. */ +import { $Values } from '@kbn/utility-types'; export const CONTROL_TYPES = { - LIST: 'list', - RANGE: 'range', + LIST: 'list' as 'list', + RANGE: 'range' as 'range', }; +export type CONTROL_TYPES = $Values; -export const setControl = (controls, controlIndex, control) => [ +export interface ControlParamsOptions { + decimalPlaces?: number; + step?: number; + type?: string; + multiselect?: boolean; + dynamicOptions?: boolean; + size?: number; + order?: string; +} + +export interface ControlParams { + id: string; + type: CONTROL_TYPES; + label: string; + fieldName: string; + indexPattern: string; + parent: string; + options: ControlParamsOptions; +} + +export const setControl = ( + controls: ControlParams[], + controlIndex: number, + control: ControlParams +): ControlParams[] => [ ...controls.slice(0, controlIndex), control, ...controls.slice(controlIndex + 1), ]; -export const addControl = (controls, control) => [...controls, control]; +export const addControl = (controls: ControlParams[], control: ControlParams): ControlParams[] => [ + ...controls, + control, +]; -export const moveControl = (controls, controlIndex, direction) => { +export const moveControl = ( + controls: ControlParams[], + controlIndex: number, + direction: number +): ControlParams[] => { let newIndex; if (direction >= 0) { newIndex = controlIndex + 1; @@ -54,13 +87,13 @@ export const moveControl = (controls, controlIndex, direction) => { } }; -export const removeControl = (controls, controlIndex) => [ +export const removeControl = (controls: ControlParams[], controlIndex: number): ControlParams[] => [ ...controls.slice(0, controlIndex), ...controls.slice(controlIndex + 1), ]; -export const getDefaultOptions = type => { - const defaultOptions = {}; +export const getDefaultOptions = (type: CONTROL_TYPES): ControlParamsOptions => { + const defaultOptions: ControlParamsOptions = {}; switch (type) { case CONTROL_TYPES.RANGE: defaultOptions.decimalPlaces = 0; @@ -77,17 +110,17 @@ export const getDefaultOptions = type => { return defaultOptions; }; -export const newControl = type => ({ +export const newControl = (type: CONTROL_TYPES): ControlParams => ({ id: new Date().getTime().toString(), indexPattern: '', fieldName: '', parent: '', label: '', - type: type, + type, options: getDefaultOptions(type), }); -export const getTitle = (controlParams, controlIndex) => { +export const getTitle = (controlParams: ControlParams, controlIndex: number): string => { let title = `${controlParams.type}: ${controlIndex}`; if (controlParams.label) { title = `${controlParams.label}`; diff --git a/src/legacy/core_plugins/vis_type_timeseries/server/lib/get_vis_data.js b/src/legacy/core_plugins/input_control_vis/public/index.ts similarity index 71% rename from src/legacy/core_plugins/vis_type_timeseries/server/lib/get_vis_data.js rename to src/legacy/core_plugins/input_control_vis/public/index.ts index 055d4477825c7..e14c2cc4b69b6 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/server/lib/get_vis_data.js +++ b/src/legacy/core_plugins/input_control_vis/public/index.ts @@ -17,14 +17,9 @@ * under the License. */ -import _ from 'lodash'; -import { getPanelData } from './vis_data/get_panel_data'; +import { PluginInitializerContext } from '../../../../core/public'; +import { InputControlVisPlugin as Plugin } from './plugin'; -export function getVisData(req) { - const promises = req.payload.panels.map(getPanelData(req)); - return Promise.all(promises).then(res => { - return res.reduce((acc, data) => { - return _.assign(acc, data); - }, {}); - }); +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); } diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.test.js b/src/legacy/core_plugins/input_control_vis/public/input_control_fn.test.ts similarity index 83% rename from src/legacy/core_plugins/input_control_vis/public/input_control_fn.test.js rename to src/legacy/core_plugins/input_control_vis/public/input_control_fn.test.ts index 09c6749bcab94..aa1383587ea68 100644 --- a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/input_control_fn.test.ts @@ -17,14 +17,15 @@ * under the License. */ -jest.mock('ui/new_platform'); +import { createInputControlVisFn } from './input_control_fn'; // eslint-disable-next-line import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils'; -import { inputControlVis } from './input_control_fn'; + +jest.mock('./legacy_imports.ts'); describe('interpreter/functions#input_control_vis', () => { - const fn = functionWrapper(inputControlVis); + const fn = functionWrapper(createInputControlVisFn); const visConfig = { controls: [ { @@ -47,8 +48,8 @@ describe('interpreter/functions#input_control_vis', () => { pinFilters: false, }; - it('returns an object with the correct structure', () => { - const actual = fn(undefined, { visConfig: JSON.stringify(visConfig) }); + it('returns an object with the correct structure', async () => { + const actual = await fn(null, { visConfig: JSON.stringify(visConfig) }); expect(actual).toMatchSnapshot(); }); }); diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.js b/src/legacy/core_plugins/input_control_vis/public/input_control_fn.ts similarity index 70% rename from src/legacy/core_plugins/input_control_vis/public/input_control_fn.js rename to src/legacy/core_plugins/input_control_vis/public/input_control_fn.ts index 0bd435f502a5d..0482c0d2cbff3 100644 --- a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.js +++ b/src/legacy/core_plugins/input_control_vis/public/input_control_fn.ts @@ -17,10 +17,37 @@ * under the License. */ -import { functionsRegistry } from 'plugins/interpreter/registries'; import { i18n } from '@kbn/i18n'; -export const inputControlVis = () => ({ +import { + ExpressionFunction, + KibanaDatatable, + Render, +} from '../../../../plugins/expressions/public'; + +const name = 'input_control_vis'; + +type Context = KibanaDatatable; + +interface Arguments { + visConfig: string; +} + +type VisParams = Required; + +interface RenderValue { + visType: 'input_control_vis'; + visConfig: VisParams; +} + +type Return = Promise>; + +export const createInputControlVisFn = (): ExpressionFunction< + typeof name, + Context, + Arguments, + Return +> => ({ name: 'input_control_vis', type: 'render', context: { @@ -33,9 +60,10 @@ export const inputControlVis = () => ({ visConfig: { types: ['string'], default: '"{}"', + help: '', }, }, - fn(context, args) { + async fn(context, args) { const params = JSON.parse(args.visConfig); return { type: 'render', @@ -47,5 +75,3 @@ export const inputControlVis = () => ({ }; }, }); - -functionsRegistry.register(inputControlVis); diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts b/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts new file mode 100644 index 0000000000000..b6774aa87b43c --- /dev/null +++ b/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts @@ -0,0 +1,75 @@ +/* + * 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 { createInputControlVisController } from './vis_controller'; +import { getControlsTab } from './components/editor/controls_tab'; +import { OptionsTab } from './components/editor/options_tab'; +import { Status, defaultFeedbackMessage } from '../../visualizations/public'; +import { InputControlVisDependencies } from './plugin'; + +export function createInputControlVisTypeDefinition(deps: InputControlVisDependencies) { + const InputControlVisController = createInputControlVisController(deps); + const ControlsTab = getControlsTab(deps); + + return { + name: 'input_control_vis', + title: i18n.translate('inputControl.register.controlsTitle', { + defaultMessage: 'Controls', + }), + icon: 'visControls', + description: i18n.translate('inputControl.register.controlsDescription', { + defaultMessage: 'Create interactive controls for easy dashboard manipulation.', + }), + stage: 'experimental', + requiresUpdateStatus: [Status.PARAMS, Status.TIME], + feedbackMessage: defaultFeedbackMessage, + visualization: InputControlVisController, + visConfig: { + defaults: { + controls: [], + updateFiltersOnChange: false, + useTimeFilter: false, + pinFilters: false, + }, + }, + editor: 'default', + editorConfig: { + optionTabs: [ + { + name: 'controls', + title: i18n.translate('inputControl.register.tabs.controlsTitle', { + defaultMessage: 'Controls', + }), + editor: ControlsTab, + }, + { + name: 'options', + title: i18n.translate('inputControl.register.tabs.optionsTitle', { + defaultMessage: 'Options', + }), + editor: OptionsTab, + }, + ], + }, + requestHandler: 'none', + responseHandler: 'none', + }; +} diff --git a/src/legacy/core_plugins/input_control_vis/public/legacy.ts b/src/legacy/core_plugins/input_control_vis/public/legacy.ts new file mode 100644 index 0000000000000..438cdffdb323a --- /dev/null +++ b/src/legacy/core_plugins/input_control_vis/public/legacy.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 { PluginInitializerContext } from 'kibana/public'; +import { npSetup, npStart } from 'ui/new_platform'; + +import { plugin } from '.'; + +import { + InputControlVisPluginSetupDependencies, + InputControlVisPluginStartDependencies, +} from './plugin'; +import { + setup as visualizationsSetup, + start as visualizationsStart, +} from '../../visualizations/public/np_ready/public/legacy'; + +const setupPlugins: Readonly = { + expressions: npSetup.plugins.expressions, + data: npSetup.plugins.data, + visualizations: visualizationsSetup, +}; + +const startPlugins: Readonly = { + expressions: npStart.plugins.expressions, + data: npStart.plugins.data, + visualizations: visualizationsStart, +}; + +const pluginInstance = plugin({} as PluginInitializerContext); + +export const setup = pluginInstance.setup(npSetup.core, setupPlugins); +export const start = pluginInstance.start(npStart.core, startPlugins); diff --git a/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts b/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts new file mode 100644 index 0000000000000..864ce3b146689 --- /dev/null +++ b/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts @@ -0,0 +1,29 @@ +/* + * 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 { SearchSource as SearchSourceClass } from 'ui/courier'; +import { Class } from '@kbn/utility-types'; + +export { Vis, VisParams } from 'ui/vis'; +export { VisOptionsProps } from 'ui/vis/editors/default'; +export { ValidatedDualRange } from 'ui/validated_range'; +export { SearchSourceFields } from 'ui/courier/types'; + +export type SearchSource = Class; +export const SearchSource = SearchSourceClass; diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/index.js b/src/legacy/core_plugins/input_control_vis/public/lineage/index.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/lineage/index.js rename to src/legacy/core_plugins/input_control_vis/public/lineage/index.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.test.js b/src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.test.ts similarity index 94% rename from src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.test.js rename to src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.test.ts index de1b589b7dfa9..a0cd648007ecc 100644 --- a/src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.test.ts @@ -23,11 +23,11 @@ import { CONTROL_TYPES, newControl } from '../editor_utils'; test('creates lineage map', () => { const control1 = newControl(CONTROL_TYPES.LIST); - control1.id = 1; + control1.id = '1'; const control2 = newControl(CONTROL_TYPES.LIST); - control2.id = 2; + control2.id = '2'; const control3 = newControl(CONTROL_TYPES.LIST); - control3.id = 3; + control3.id = '3'; control2.parent = control1.id; control3.parent = control2.id; @@ -40,9 +40,9 @@ test('creates lineage map', () => { test('safely handles circular graph', () => { const control1 = newControl(CONTROL_TYPES.LIST); - control1.id = 1; + control1.id = '1'; const control2 = newControl(CONTROL_TYPES.LIST); - control2.id = 2; + control2.id = '2'; control1.parent = control2.id; control2.parent = control1.id; diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.js b/src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.ts similarity index 80% rename from src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.js rename to src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.ts index a08c5d1670a09..d74782c373942 100644 --- a/src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.js +++ b/src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.ts @@ -18,18 +18,19 @@ */ import _ from 'lodash'; +import { ControlParams } from '../editor_utils'; -export function getLineageMap(controlParamsList) { - function getControlParamsById(controlId) { +export function getLineageMap(controlParamsList: ControlParams[]) { + function getControlParamsById(controlId: string) { return controlParamsList.find(controlParams => { return controlParams.id === controlId; }); } - const lineageMap = new Map(); + const lineageMap = new Map(); controlParamsList.forEach(rootControlParams => { const lineage = [rootControlParams.id]; - const getLineage = controlParams => { + const getLineage = (controlParams: ControlParams) => { if ( _.has(controlParams, 'parent') && controlParams.parent !== '' && @@ -37,7 +38,10 @@ export function getLineageMap(controlParamsList) { ) { lineage.push(controlParams.parent); const parent = getControlParamsById(controlParams.parent); - getLineage(parent); + + if (parent) { + getLineage(parent); + } } }; diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.test.js b/src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.test.ts similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.test.js rename to src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.test.ts index fe180357067a9..af6e2444b486f 100644 --- a/src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.test.ts @@ -22,7 +22,7 @@ import { getLineageMap } from './lineage_map'; import { getParentCandidates } from './parent_candidates'; import { CONTROL_TYPES, newControl } from '../editor_utils'; -function createControlParams(id) { +function createControlParams(id: any) { const controlParams = newControl(CONTROL_TYPES.LIST); controlParams.id = id; controlParams.indexPattern = 'indexPatternId'; diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.js b/src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.ts similarity index 85% rename from src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.js rename to src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.ts index 17005c24dd41d..af4fddef19001 100644 --- a/src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.js +++ b/src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.ts @@ -17,9 +17,13 @@ * under the License. */ -import { getTitle } from '../editor_utils'; +import { getTitle, ControlParams } from '../editor_utils'; -export function getParentCandidates(controlParamsList, controlId, lineageMap) { +export function getParentCandidates( + controlParamsList: ControlParams[], + controlId: string, + lineageMap: Map +) { return controlParamsList .filter(controlParams => { // Ignore controls that do not have index pattern and field set @@ -28,7 +32,7 @@ export function getParentCandidates(controlParamsList, controlId, lineageMap) { } // Ignore controls that would create a circular graph const lineage = lineageMap.get(controlParams.id); - if (lineage.includes(controlId)) { + if (lineage?.includes(controlId)) { return false; } return true; diff --git a/src/legacy/core_plugins/input_control_vis/public/plugin.ts b/src/legacy/core_plugins/input_control_vis/public/plugin.ts new file mode 100644 index 0000000000000..e9ffad8b35f21 --- /dev/null +++ b/src/legacy/core_plugins/input_control_vis/public/plugin.ts @@ -0,0 +1,70 @@ +/* + * 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 { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; + +import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; +import { VisualizationsSetup, VisualizationsStart } from '../../visualizations/public'; +import { createInputControlVisFn } from './input_control_fn'; +import { createInputControlVisTypeDefinition } from './input_control_vis_type'; + +type InputControlVisCoreSetup = CoreSetup; + +export interface InputControlVisDependencies { + core: InputControlVisCoreSetup; + data: DataPublicPluginSetup; +} + +/** @internal */ +export interface InputControlVisPluginSetupDependencies { + expressions: ReturnType; + visualizations: VisualizationsSetup; + data: DataPublicPluginSetup; +} + +/** @internal */ +export interface InputControlVisPluginStartDependencies { + expressions: ReturnType; + visualizations: VisualizationsStart; + data: DataPublicPluginStart; +} + +/** @internal */ +export class InputControlVisPlugin implements Plugin, void> { + constructor(public initializerContext: PluginInitializerContext) {} + + public async setup( + core: InputControlVisCoreSetup, + { expressions, visualizations, data }: InputControlVisPluginSetupDependencies + ) { + const visualizationDependencies: Readonly = { + core, + data, + }; + + expressions.registerFunction(createInputControlVisFn); + visualizations.types.createBaseVisualization( + createInputControlVisTypeDefinition(visualizationDependencies) + ); + } + + public start(core: CoreStart, deps: InputControlVisPluginStartDependencies) { + // nothing to do here + } +} diff --git a/src/legacy/core_plugins/input_control_vis/public/register_vis.js b/src/legacy/core_plugins/input_control_vis/public/register_vis.js deleted file mode 100644 index 09993be3614f2..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/public/register_vis.js +++ /dev/null @@ -1,72 +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 { VisController } from './vis_controller'; -import { ControlsTab } from './components/editor/controls_tab'; -import { OptionsTab } from './components/editor/options_tab'; -import { i18n } from '@kbn/i18n'; -import { setup as visualizations } from '../../visualizations/public/np_ready/public/legacy'; -import { Status, defaultFeedbackMessage } from '../../visualizations/public'; - -export const inputControlVisDefinition = { - name: 'input_control_vis', - title: i18n.translate('inputControl.register.controlsTitle', { - defaultMessage: 'Controls', - }), - icon: 'visControls', - description: i18n.translate('inputControl.register.controlsDescription', { - defaultMessage: 'Create interactive controls for easy dashboard manipulation.', - }), - stage: 'experimental', - requiresUpdateStatus: [Status.PARAMS, Status.TIME], - feedbackMessage: defaultFeedbackMessage, - visualization: VisController, - visConfig: { - defaults: { - controls: [], - updateFiltersOnChange: false, - useTimeFilter: false, - pinFilters: false, - }, - }, - editor: 'default', - editorConfig: { - optionTabs: [ - { - name: 'controls', - title: i18n.translate('inputControl.register.tabs.controlsTitle', { - defaultMessage: 'Controls', - }), - editor: ControlsTab, - }, - { - name: 'options', - title: i18n.translate('inputControl.register.tabs.optionsTitle', { - defaultMessage: 'Options', - }), - editor: OptionsTab, - }, - ], - }, - requestHandler: 'none', - responseHandler: 'none', -}; - -// register the provider with the visTypes registry -visualizations.types.createBaseVisualization(inputControlVisDefinition); diff --git a/src/legacy/core_plugins/input_control_vis/public/vis_controller.js b/src/legacy/core_plugins/input_control_vis/public/vis_controller.js deleted file mode 100644 index 6a1e23769e28c..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/public/vis_controller.js +++ /dev/null @@ -1,207 +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 { render, unmountComponentAtNode } from 'react-dom'; -import { I18nContext } from 'ui/i18n'; -import { InputControlVis } from './components/vis/input_control_vis'; -import { controlFactory } from './control/control_factory'; -import { getLineageMap } from './lineage'; -import { npStart } from 'ui/new_platform'; -import { SearchSource } from '../../../ui/public/courier/search_source/search_source'; - -class VisController { - constructor(el, vis) { - this.el = el; - this.vis = vis; - this.controls = []; - - this.queryBarUpdateHandler = this.updateControlsFromKbn.bind(this); - - this.filterManager = npStart.plugins.data.query.filterManager; - this.updateSubsciption = this.filterManager.getUpdates$().subscribe(this.queryBarUpdateHandler); - } - - async render(visData, visParams, status) { - if (status.params || (visParams.useTimeFilter && status.time)) { - this.visParams = visParams; - this.controls = []; - this.controls = await this.initControls(); - this.drawVis(); - } - } - - destroy() { - this.updateSubsciption.unsubscribe(); - unmountComponentAtNode(this.el); - this.controls.forEach(control => control.destroy()); - } - - drawVis = () => { - render( - - - , - this.el - ); - }; - - async initControls() { - const controlParamsList = this.visParams.controls.filter(controlParams => { - // ignore controls that do not have indexPattern or field - return controlParams.indexPattern && controlParams.fieldName; - }); - - const controlFactoryPromises = controlParamsList.map(controlParams => { - const factory = controlFactory(controlParams); - return factory(controlParams, this.visParams.useTimeFilter, SearchSource); - }); - const controls = await Promise.all(controlFactoryPromises); - - const getControl = id => { - return controls.find(control => { - return id === control.id; - }); - }; - - const controlInitPromises = []; - getLineageMap(controlParamsList).forEach((lineage, controlId) => { - // first lineage item is the control. remove it - lineage.shift(); - const ancestors = []; - lineage.forEach(ancestorId => { - ancestors.push(getControl(ancestorId)); - }); - const control = getControl(controlId); - control.setAncestors(ancestors); - controlInitPromises.push(control.fetch()); - }); - - await Promise.all(controlInitPromises); - return controls; - } - - stageFilter = async (controlIndex, newValue) => { - this.controls[controlIndex].set(newValue); - if (this.visParams.updateFiltersOnChange) { - // submit filters on each control change - this.submitFilters(); - } else { - // Do not submit filters, just update vis so controls are updated with latest value - await this.updateNestedControls(); - this.drawVis(); - } - }; - - submitFilters = () => { - const stagedControls = this.controls.filter(control => { - return control.hasChanged(); - }); - - const newFilters = stagedControls - .filter(control => { - return control.hasKbnFilter(); - }) - .map(control => { - return control.getKbnFilter(); - }); - - stagedControls.forEach(control => { - // to avoid duplicate filters, remove any old filters for control - control.filterManager.findFilters().forEach(existingFilter => { - this.filterManager.removeFilter(existingFilter); - }); - }); - - // Clean up filter pills for nested controls that are now disabled because ancestors are not set. - // This has to be done after looking up the staged controls because otherwise removing a filter - // will re-sync the controls of all other filters. - this.controls.map(control => { - if (control.hasAncestors() && control.hasUnsetAncestor()) { - control.filterManager.findFilters().forEach(existingFilter => { - this.filterManager.removeFilter(existingFilter); - }); - } - }); - - this.filterManager.addFilters(newFilters, this.visParams.pinFilters); - }; - - clearControls = async () => { - this.controls.forEach(control => { - control.clear(); - }); - await this.updateNestedControls(); - this.drawVis(); - }; - - updateControlsFromKbn = async () => { - this.controls.forEach(control => { - control.reset(); - }); - await this.updateNestedControls(); - this.drawVis(); - }; - - async updateNestedControls() { - const fetchPromises = this.controls.map(async control => { - if (control.hasAncestors()) { - await control.fetch(); - } - }); - return await Promise.all(fetchPromises); - } - - hasChanges = () => { - return this.controls - .map(control => { - return control.hasChanged(); - }) - .reduce((a, b) => { - return a || b; - }); - }; - - hasValues = () => { - return this.controls - .map(control => { - return control.hasValue(); - }) - .reduce((a, b) => { - return a || b; - }); - }; - - refreshControl = async (controlIndex, query) => { - await this.controls[controlIndex].fetch(query); - this.drawVis(); - }; -} - -export { VisController }; diff --git a/src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx b/src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx new file mode 100644 index 0000000000000..849b58b8ee2da --- /dev/null +++ b/src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx @@ -0,0 +1,226 @@ +/* + * 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 { render, unmountComponentAtNode } from 'react-dom'; + +import { I18nStart } from 'kibana/public'; +import { Vis, VisParams, SearchSource } from './legacy_imports'; + +import { InputControlVis } from './components/vis/input_control_vis'; +import { getControlFactory } from './control/control_factory'; +import { getLineageMap } from './lineage'; +import { ControlParams } from './editor_utils'; +import { RangeControl } from './control/range_control_factory'; +import { ListControl } from './control/list_control_factory'; +import { InputControlVisDependencies } from './plugin'; +import { FilterManager, esFilters } from '../../../../plugins/data/public'; + +export const createInputControlVisController = (deps: InputControlVisDependencies) => { + return class InputControlVisController { + private I18nContext?: I18nStart['Context']; + + controls: Array; + queryBarUpdateHandler: () => void; + filterManager: FilterManager; + updateSubsciption: any; + visParams?: VisParams; + + constructor(public el: Element, public vis: Vis) { + this.controls = []; + + this.queryBarUpdateHandler = this.updateControlsFromKbn.bind(this); + + this.filterManager = deps.data.query.filterManager; + this.updateSubsciption = this.filterManager + .getUpdates$() + .subscribe(this.queryBarUpdateHandler); + } + + async render(visData: any, visParams: VisParams, status: any) { + if (status.params || (visParams.useTimeFilter && status.time)) { + this.visParams = visParams; + this.controls = []; + this.controls = await this.initControls(); + const [{ i18n }] = await deps.core.getStartServices(); + this.I18nContext = i18n.Context; + this.drawVis(); + } + } + + destroy() { + this.updateSubsciption.unsubscribe(); + unmountComponentAtNode(this.el); + this.controls.forEach(control => control.destroy()); + } + + drawVis = () => { + if (!this.I18nContext) { + throw new Error('no i18n context found'); + } + + render( + + + , + this.el + ); + }; + + async initControls() { + const controlParamsList = (this.visParams?.controls as ControlParams[])?.filter( + controlParams => { + // ignore controls that do not have indexPattern or field + return controlParams.indexPattern && controlParams.fieldName; + } + ); + + const controlFactoryPromises = controlParamsList.map(controlParams => { + const factory = getControlFactory(controlParams); + return factory(controlParams, this.visParams?.useTimeFilter, SearchSource, deps); + }); + const controls = await Promise.all(controlFactoryPromises); + + const getControl = (controlId: string) => { + return controls.find(({ id }) => id === controlId); + }; + + const controlInitPromises: Array> = []; + getLineageMap(controlParamsList).forEach((lineage, controlId) => { + // first lineage item is the control. remove it + lineage.shift(); + const ancestors: Array = []; + lineage.forEach(ancestorId => { + const control = getControl(ancestorId); + + if (control) { + ancestors.push(control); + } + }); + const control = getControl(controlId); + + if (control) { + control.setAncestors(ancestors); + controlInitPromises.push(control.fetch()); + } + }); + + await Promise.all(controlInitPromises); + return controls; + } + + stageFilter = async (controlIndex: number, newValue: any) => { + this.controls[controlIndex].set(newValue); + if (this.visParams?.updateFiltersOnChange) { + // submit filters on each control change + this.submitFilters(); + } else { + // Do not submit filters, just update vis so controls are updated with latest value + await this.updateNestedControls(); + this.drawVis(); + } + }; + + submitFilters = () => { + const stagedControls = this.controls.filter(control => { + return control.hasChanged(); + }); + + const newFilters = stagedControls + .map(control => control.getKbnFilter()) + .filter((filter): filter is esFilters.Filter => { + return filter !== null; + }); + + stagedControls.forEach(control => { + // to avoid duplicate filters, remove any old filters for control + control.filterManager.findFilters().forEach(existingFilter => { + this.filterManager.removeFilter(existingFilter); + }); + }); + + // Clean up filter pills for nested controls that are now disabled because ancestors are not set. + // This has to be done after looking up the staged controls because otherwise removing a filter + // will re-sync the controls of all other filters. + this.controls.map(control => { + if (control.hasAncestors() && control.hasUnsetAncestor()) { + control.filterManager.findFilters().forEach(existingFilter => { + this.filterManager.removeFilter(existingFilter); + }); + } + }); + + this.filterManager.addFilters(newFilters, this.visParams?.pinFilters); + }; + + clearControls = async () => { + this.controls.forEach(control => { + control.clear(); + }); + await this.updateNestedControls(); + this.drawVis(); + }; + + updateControlsFromKbn = async () => { + this.controls.forEach(control => { + control.reset(); + }); + await this.updateNestedControls(); + this.drawVis(); + }; + + async updateNestedControls() { + const fetchPromises = this.controls.map(async control => { + if (control.hasAncestors()) { + await control.fetch(); + } + }); + return await Promise.all(fetchPromises); + } + + hasChanges = () => { + return this.controls.map(control => control.hasChanged()).some(control => control); + }; + + hasValues = () => { + return this.controls + .map(control => { + return control.hasValue(); + }) + .reduce((a, b) => { + return a || b; + }); + }; + + refreshControl = async (controlIndex: number, query: any) => { + await this.controls[controlIndex].fetch(query); + this.drawVis(); + }; + }; +}; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/__snapshots__/dashboard_empty_screen.test.tsx.snap index 4ea658bcd03ef..178014a691be3 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/__snapshots__/dashboard_empty_screen.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -230,14 +230,18 @@ exports[`DashboardEmptyScreen renders correctly with visualize paragraph 1`] = ` type="dashboardApp" >
-
- - -
-
- - -
-
- - -
-
- -
- -
diff --git a/src/legacy/core_plugins/kibana/public/discover/helpers/build_services.ts b/src/legacy/core_plugins/kibana/public/discover/helpers/build_services.ts index 14ea4e99b0de4..27c7c00fb729c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/helpers/build_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/helpers/build_services.ts @@ -27,7 +27,7 @@ import { import * as docViewsRegistry from 'ui/registry/doc_views'; import { FilterManager, TimefilterContract, IndexPatternsContract } from 'src/plugins/data/public'; // @ts-ignore -import { createSavedSearchesService } from '../saved_searches/saved_searches'; +import { createSavedSearchesService } from '../saved_searches'; // @ts-ignore import { DiscoverStartPlugins } from '../plugin'; import { DataStart } from '../../../../data/public'; diff --git a/src/legacy/core_plugins/kibana/public/discover/index.ts b/src/legacy/core_plugins/kibana/public/discover/index.ts index 6ea658682c89d..e85408dc9bf6b 100644 --- a/src/legacy/core_plugins/kibana/public/discover/index.ts +++ b/src/legacy/core_plugins/kibana/public/discover/index.ts @@ -36,3 +36,5 @@ export const pluginInstance = plugin({} as PluginInitializerContext); SavedObjectRegistryProvider.register((savedSearches: any) => { return savedSearches; }); + +export { createSavedSearchesService } from './saved_searches/saved_searches'; diff --git a/src/legacy/core_plugins/kibana/public/discover/saved_searches/index.ts b/src/legacy/core_plugins/kibana/public/discover/saved_searches/index.ts index b9601e2fd257a..1dd99025b4b70 100644 --- a/src/legacy/core_plugins/kibana/public/discover/saved_searches/index.ts +++ b/src/legacy/core_plugins/kibana/public/discover/saved_searches/index.ts @@ -17,4 +17,5 @@ * under the License. */ -import './saved_searches'; +export * from './saved_searches'; +import './saved_searches_register'; diff --git a/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.ts b/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.ts index 46fdd3a7baedc..abd3d46820c18 100644 --- a/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.ts +++ b/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.ts @@ -16,22 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { npStart } from 'ui/new_platform'; -// @ts-ignore -import { uiModules } from 'ui/modules'; import { SavedObjectLoader } from 'ui/saved_objects'; import { SavedObjectKibanaServices } from 'ui/saved_objects/types'; -// @ts-ignore -import { savedObjectManagementRegistry } from '../../management/saved_object_registry'; import { createSavedSearchClass } from './_saved_search'; -// Register this service with the saved object registry so it can be -// edited by the object editor. -savedObjectManagementRegistry.register({ - service: 'savedSearches', - title: 'searches', -}); - export function createSavedSearchesService(services: SavedObjectKibanaServices) { const SavedSearchClass = createSavedSearchClass(services); const savedSearchLoader = new SavedObjectLoader( @@ -50,14 +38,3 @@ export function createSavedSearchesService(services: SavedObjectKibanaServices) return savedSearchLoader; } -// this is needed for saved object management -const module = uiModules.get('discover/saved_searches'); -module.service('savedSearches', () => { - const services = { - savedObjectsClient: npStart.core.savedObjects.client, - indexPatterns: npStart.plugins.data.indexPatterns, - chrome: npStart.core.chrome, - overlays: npStart.core.overlays, - }; - return createSavedSearchesService(services); -}); diff --git a/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches_register.ts b/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches_register.ts new file mode 100644 index 0000000000000..bdb1495a33925 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches_register.ts @@ -0,0 +1,43 @@ +/* + * 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 { npStart } from 'ui/new_platform'; +// @ts-ignore +import { uiModules } from 'ui/modules'; +// @ts-ignore +import { savedObjectManagementRegistry } from '../../management/saved_object_registry'; + +import { createSavedSearchesService } from './saved_searches'; + +// this is needed for saved object management +// Register this service with the saved object registry so it can be +// edited by the object editor. +savedObjectManagementRegistry.register({ + service: 'savedSearches', + title: 'searches', +}); +const module = uiModules.get('discover/saved_searches'); +module.service('savedSearches', () => { + const services = { + savedObjectsClient: npStart.core.savedObjects.client, + indexPatterns: npStart.plugins.data.indexPatterns, + chrome: npStart.core.chrome, + overlays: npStart.core.overlays, + }; + return createSavedSearchesService(services); +}); diff --git a/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/home.test.js.snap b/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/home.test.js.snap index 0bf8c808ae920..c1131cbe559f6 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/home.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/home.test.js.snap @@ -1072,8 +1072,8 @@ exports[`home welcome should show the normal home page if welcome screen is disa exports[`home welcome should show the welcome screen if enabled, and there are no index patterns defined 1`] = ` `; diff --git a/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/welcome.test.tsx.snap b/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/welcome.test.tsx.snap index 2007a3bb773cf..e36a6e0a5a9fb 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/welcome.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/welcome.test.tsx.snap @@ -62,10 +62,46 @@ exports[`should render a Welcome screen with no telemetry disclaimer 1`] = ` + + + + + + + + + + @@ -138,6 +174,231 @@ exports[`should render a Welcome screen with the telemetry disclaimer 1`] = ` + + + + + + + + + + + + + + +
+
+ +`; + +exports[`should render a Welcome screen with the telemetry disclaimer when optIn is false 1`] = ` + +
+
+
+ + + + + +

+ +

+
+ +

+ +

+
+ +
+
+
+ + + + + + + + + + + + + + + + + +
+
+
+`; + +exports[`should render a Welcome screen with the telemetry disclaimer when optIn is true 1`] = ` + +
+
+
+ + + + + +

+ +

+
+ +

+ +

+
+ +
+
+
+ + + diff --git a/src/legacy/core_plugins/kibana/public/home/components/home.js b/src/legacy/core_plugins/kibana/public/home/components/home.js index c87ceb9777c74..d552dd070c86d 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/home.js +++ b/src/legacy/core_plugins/kibana/public/home/components/home.js @@ -51,10 +51,7 @@ export class Home extends Component { getServices().getInjected('disableWelcomeScreen') || props.localStorage.getItem(KEY_ENABLE_WELCOME) === 'false' ); - const showTelemetryDisclaimer = getServices().getInjected( - 'telemetryNotifyUserAboutOptInDefault' - ); - + const currentOptInStatus = this.props.getOptInStatus(); this.state = { // If welcome is enabled, we wait for loading to complete // before rendering. This prevents an annoying flickering @@ -63,7 +60,7 @@ export class Home extends Component { isLoading: isWelcomeEnabled, isNewKibanaInstance: false, isWelcomeEnabled, - showTelemetryDisclaimer, + currentOptInStatus, }; } @@ -222,14 +219,13 @@ export class Home extends Component { renderLoading() { return ''; } - renderWelcome() { return ( ); } @@ -269,4 +265,5 @@ Home.propTypes = { urlBasePath: PropTypes.string.isRequired, mlEnabled: PropTypes.bool.isRequired, onOptInSeen: PropTypes.func.isRequired, + getOptInStatus: PropTypes.func.isRequired, }; diff --git a/src/legacy/core_plugins/kibana/public/home/components/home.test.js b/src/legacy/core_plugins/kibana/public/home/components/home.test.js index 780e2af695381..1f46cf2875fee 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/home.test.js +++ b/src/legacy/core_plugins/kibana/public/home/components/home.test.js @@ -63,6 +63,10 @@ describe('home', () => { setItem: sinon.mock(), }, urlBasePath: 'goober', + onOptInSeen() { + return false; + }, + getOptInStatus: jest.fn(), }; }); diff --git a/src/legacy/core_plugins/kibana/public/home/components/home_app.js b/src/legacy/core_plugins/kibana/public/home/components/home_app.js index 5a12eb0a66cf1..29f24f5b841a3 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/home_app.js +++ b/src/legacy/core_plugins/kibana/public/home/components/home_app.js @@ -29,14 +29,13 @@ import { getTutorial } from '../load_tutorials'; import { replaceTemplateStrings } from './tutorial/replace_template_strings'; import { getServices } from '../kibana_services'; import { npSetup } from 'ui/new_platform'; - export function HomeApp({ directories }) { const { getInjected, savedObjectsClient, getBasePath, addBasePath, - telemetryOptInProvider: { setOptInNoticeSeen }, + telemetryOptInProvider: { setOptInNoticeSeen, getOptIn }, } = getServices(); const { cloud } = npSetup.plugins; const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); @@ -87,6 +86,7 @@ export function HomeApp({ directories }) { localStorage={localStorage} urlBasePath={getBasePath()} onOptInSeen={setOptInNoticeSeen} + getOptInStatus={getOptIn} /> diff --git a/src/legacy/core_plugins/kibana/public/home/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap b/src/legacy/core_plugins/kibana/public/home/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap index 07744dd0f6f4b..161061868dab2 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/home/components/tutorial/__snapshots__/saved_objects_installer.test.js.snap @@ -508,6 +508,7 @@ exports[`bulkCreate should display success message when bulkCreate is successful aria-label="complete" className="euiIcon euiIcon--medium euiIcon-isLoaded euiStepNumber__icon" focusable="false" + role="img" style={null} > + diff --git a/src/legacy/core_plugins/kibana/public/home/components/welcome.test.tsx b/src/legacy/core_plugins/kibana/public/home/components/welcome.test.tsx index 21dcfd9ef15de..42c6e6ff6056a 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/welcome.test.tsx +++ b/src/legacy/core_plugins/kibana/public/home/components/welcome.test.tsx @@ -35,7 +35,25 @@ jest.mock('../kibana_services', () => ({ test('should render a Welcome screen with the telemetry disclaimer', () => { const component = shallow( // @ts-ignore - {}} showTelemetryDisclaimer={true} onOptInSeen={() => {}} /> + {}} onOptInSeen={() => {}} /> + ); + + expect(component).toMatchSnapshot(); +}); + +test('should render a Welcome screen with the telemetry disclaimer when optIn is true', () => { + const component = shallow( + // @ts-ignore + {}} onOptInSeen={() => {}} currentOptInStatus={true} /> + ); + + expect(component).toMatchSnapshot(); +}); + +test('should render a Welcome screen with the telemetry disclaimer when optIn is false', () => { + const component = shallow( + // @ts-ignore + {}} onOptInSeen={() => {}} currentOptInStatus={false} /> ); expect(component).toMatchSnapshot(); @@ -45,7 +63,7 @@ test('should render a Welcome screen with no telemetry disclaimer', () => { // @ts-ignore const component = shallow( // @ts-ignore - {}} showTelemetryDisclaimer={false} onOptInSeen={() => {}} /> + {}} onOptInSeen={() => {}} /> ); expect(component).toMatchSnapshot(); @@ -56,7 +74,7 @@ test('fires opt-in seen when mounted', () => { shallow( // @ts-ignore - {}} showTelemetryDisclaimer={true} onOptInSeen={seen} /> + {}} onOptInSeen={seen} /> ); expect(seen).toHaveBeenCalled(); diff --git a/src/legacy/core_plugins/kibana/public/home/components/welcome.tsx b/src/legacy/core_plugins/kibana/public/home/components/welcome.tsx index c8de0bf7bb936..435bf98ca7840 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/welcome.tsx +++ b/src/legacy/core_plugins/kibana/public/home/components/welcome.tsx @@ -23,7 +23,7 @@ * in Elasticsearch. */ -import React from 'react'; +import React, { Fragment } from 'react'; import { EuiLink, EuiTextColor, @@ -39,12 +39,11 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { getServices } from '../kibana_services'; import { SampleDataCard } from './sample_data'; - interface Props { urlBasePath: string; onSkip: () => void; onOptInSeen: () => any; - showTelemetryDisclaimer: boolean; + currentOptInStatus: boolean; } /** @@ -84,9 +83,42 @@ export class Welcome extends React.Component { document.removeEventListener('keydown', this.hideOnEsc); } - render() { - const { urlBasePath, showTelemetryDisclaimer } = this.props; + private renderTelemetryEnabledOrDisabledText = () => { + if (this.props.currentOptInStatus) { + return ( + + + + + + + ); + } else { + return ( + + + + + + + ); + } + }; + render() { + const { urlBasePath } = this.props; return (
@@ -121,34 +153,23 @@ export class Welcome extends React.Component { onDecline={this.onSampleDataDecline} /> - {showTelemetryDisclaimer && ( - - - - - + + + - - - - - )} + + {this.renderTelemetryEnabledOrDisabledText()} + diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap index ca04ac8fcfaab..f758511990d6f 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap @@ -93,7 +93,6 @@ exports[`Table should render normally 1`] = ` }, ] } - executeQueryOptions={Object {}} items={ Array [ Object { @@ -136,9 +135,11 @@ exports[`Table should render the boolean template (false) 1`] = ``; exports[`Table should render the boolean template (true) 1`] = ` `; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap index f2a55649fe4d7..4716fb8f77633 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap @@ -55,7 +55,6 @@ exports[`Table should render normally 1`] = ` }, ] } - executeQueryOptions={Object {}} items={ Array [ Object { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__jest__/__snapshots__/table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__jest__/__snapshots__/table.test.js.snap index 415bae7389e97..7856572373e79 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__jest__/__snapshots__/table.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__jest__/__snapshots__/table.test.js.snap @@ -78,7 +78,6 @@ exports[`Table should render normally 1`] = ` }, ] } - executeQueryOptions={Object {}} items={ Array [ Object { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index_pattern_table/index_pattern_table.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index_pattern_table/index_pattern_table.tsx index f54afc4a5e359..f254a6bc22a0d 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index_pattern_table/index_pattern_table.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index_pattern_table/index_pattern_table.tsx @@ -59,7 +59,7 @@ const columns = [ ))} ), - dataType: 'string', + dataType: 'string' as const, sortable: ({ sort }: { sort: string }) => sort, }, ]; @@ -72,7 +72,7 @@ const pagination = { const sorting = { sort: { field: 'title', - direction: 'asc', + direction: 'asc' as const, }, }; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap index 843c8207c88c3..731a3379491c1 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap @@ -54,7 +54,6 @@ exports[`ObjectsTable delete should show a confirm modal 1`] = ` }, ] } - executeQueryOptions={Object {}} items={ Array [ Object { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/__snapshots__/flyout.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/__snapshots__/flyout.test.js.snap index a9175e7b2a63e..ace06e0420a7c 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/__snapshots__/flyout.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/__snapshots__/flyout.test.js.snap @@ -90,7 +90,6 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = ` }, ] } - executeQueryOptions={Object {}} items={ Array [ Object { @@ -116,7 +115,6 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = ` } } responsive={true} - sorting={false} /> @@ -411,7 +409,6 @@ exports[`Flyout legacy conflicts should allow conflict resolution 1`] = ` }, ] } - executeQueryOptions={Object {}} items={ Array [ Object { @@ -448,7 +445,6 @@ exports[`Flyout legacy conflicts should allow conflict resolution 1`] = ` } } responsive={true} - sorting={false} /> diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/__snapshots__/relationships.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/__snapshots__/relationships.test.js.snap index 6060e96f3cfb6..941a0ffded820 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/__snapshots__/relationships.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/__snapshots__/relationships.test.js.snap @@ -83,7 +83,6 @@ exports[`Relationships should render dashboards normally 1`] = ` }, ] } - executeQueryOptions={Object {}} items={ Array [ Object { @@ -155,7 +154,6 @@ exports[`Relationships should render dashboards normally 1`] = ` ], } } - sorting={false} />
@@ -294,7 +292,6 @@ exports[`Relationships should render index patterns normally 1`] = ` }, ] } - executeQueryOptions={Object {}} items={ Array [ Object { @@ -371,7 +368,6 @@ exports[`Relationships should render index patterns normally 1`] = ` ], } } - sorting={false} />
@@ -461,7 +457,6 @@ exports[`Relationships should render searches normally 1`] = ` }, ] } - executeQueryOptions={Object {}} items={ Array [ Object { @@ -538,7 +533,6 @@ exports[`Relationships should render searches normally 1`] = ` ], } } - sorting={false} />
@@ -628,7 +622,6 @@ exports[`Relationships should render visualizations normally 1`] = ` }, ] } - executeQueryOptions={Object {}} items={ Array [ Object { @@ -700,7 +693,6 @@ exports[`Relationships should render visualizations normally 1`] = ` ], } } - sorting={false} />
diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts index 8e414984a0c08..45cc1dc5fb9dd 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts @@ -29,7 +29,7 @@ import { getTableAggs } from 'ui/visualize/loader/pipeline_helpers/utilities'; import { AppState } from 'ui/state_management/app_state'; import { npStart } from 'ui/new_platform'; import { IExpressionLoaderParams } from 'src/plugins/expressions/public'; -import { SearchSourceContract } from '../../../../../ui/public/courier'; +import { SearchSourceContract } from 'ui/courier'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; import { IIndexPattern, @@ -47,6 +47,7 @@ import { APPLY_FILTER_TRIGGER, } from '../../../../../../plugins/embeddable/public'; import { dispatchRenderComplete } from '../../../../../../plugins/kibana_utils/public'; +import { SavedSearch } from '../../discover/types'; const getKeys = (o: T): Array => Object.keys(o) as Array; @@ -57,6 +58,10 @@ export interface VisSavedObject extends SavedObject { title: string; uiStateJSON?: string; destroy: () => void; + savedSearchRefName?: string; + savedSearchId?: string; + savedSearch?: SavedSearch; + visState: any; } export interface VisualizeEmbeddableConfiguration { diff --git a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js deleted file mode 100644 index fd643115e9084..0000000000000 --- a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js +++ /dev/null @@ -1,150 +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. - */ - -/** - * @name SavedVis - * - * @extends SavedObject. - * - * NOTE: It's a type of SavedObject, but specific to visualizations. - */ - -import { Vis } from 'ui/vis'; -import { uiModules } from 'ui/modules'; -import { updateOldState } from '../../../../visualizations/public'; -import { VisualizeConstants } from '../visualize_constants'; -import { createLegacyClass } from 'ui/utils/legacy_class'; -import { SavedObjectProvider } from 'ui/saved_objects/saved_object'; -import { extractReferences, injectReferences } from './saved_visualization_references'; - -uiModules.get('app/visualize').factory('SavedVis', function(savedSearches, Private) { - const SavedObject = Private(SavedObjectProvider); - createLegacyClass(SavedVis).inherits(SavedObject); - function SavedVis(opts) { - const self = this; - opts = opts || {}; - if (typeof opts !== 'object') opts = { id: opts }; - - SavedVis.Super.call(self, { - type: SavedVis.type, - mapping: SavedVis.mapping, - searchSource: SavedVis.searchSource, - extractReferences: extractReferences, - injectReferences: injectReferences, - - id: opts.id, - indexPattern: opts.indexPattern, - defaults: { - title: '', - visState: (function() { - if (!opts.type) return null; - const def = {}; - def.type = opts.type; - return def; - })(), - uiStateJSON: '{}', - description: '', - savedSearchId: opts.savedSearchId, - version: 1, - }, - - afterESResp: this._afterEsResp, - }); - - this.showInRecentlyAccessed = true; - } - - SavedVis.type = 'visualization'; - - SavedVis.mapping = { - title: 'text', - visState: 'json', - uiStateJSON: 'text', - description: 'text', - savedSearchId: 'keyword', - version: 'integer', - }; - - // Order these fields to the top, the rest are alphabetical - SavedVis.fieldOrder = ['title', 'description']; - - SavedVis.searchSource = true; - - SavedVis.prototype.getFullPath = function() { - return `/app/kibana#${VisualizeConstants.EDIT_PATH}/${this.id}`; - }; - - SavedVis.prototype._afterEsResp = async function() { - const self = this; - - await self._getLinkedSavedSearch(); - self.searchSource.setField('size', 0); - return self.vis ? self._updateVis() : self._createVis(); - }; - - SavedVis.prototype._getLinkedSavedSearch = async function() { - const self = this; - const linkedSearch = !!self.savedSearchId; - const current = self.savedSearch; - - if (linkedSearch && current && current.id === self.savedSearchId) { - return; - } - - if (self.savedSearch) { - self.searchSource.setParent(self.savedSearch.searchSource.getParent()); - self.savedSearch.destroy(); - self.savedSearch = null; - } - - if (linkedSearch) { - self.savedSearch = await savedSearches.get(self.savedSearchId); - self.searchSource.setParent(self.savedSearch.searchSource); - } - }; - - SavedVis.prototype._createVis = function() { - const self = this; - - self.visState = updateOldState(self.visState); - - // visState doesn't yet exist when importing a visualization, so we can't - // assume that exists at this point. If it does exist, then we're not - // importing a visualization, so we want to sync the title. - if (self.visState) { - self.visState.title = self.title; - } - self.vis = new Vis(self.searchSource.getField('index'), self.visState); - - self.vis.savedSearchId = self.savedSearchId; - - return self.vis; - }; - - SavedVis.prototype._updateVis = function() { - const self = this; - - self.vis.indexPattern = self.searchSource.getField('index'); - self.visState.title = self.title; - self.vis.setState(self.visState); - self.vis.savedSearchId = self.savedSearchId; - }; - - return SavedVis; -}); diff --git a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.ts b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.ts new file mode 100644 index 0000000000000..3490e0ab127ed --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.ts @@ -0,0 +1,146 @@ +/* + * 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. + */ + +/** + * @name SavedVis + * + * @extends SavedObject. + * + * NOTE: It's a type of SavedObject, but specific to visualizations. + */ +// @ts-ignore +import { Vis } from 'ui/vis'; +import { SavedObject, SavedObjectKibanaServices } from 'ui/saved_objects/types'; +import { createSavedObjectClass } from 'ui/saved_objects/saved_object'; +import { updateOldState } from '../../../../visualizations/public'; +import { VisualizeConstants } from '../visualize_constants'; +import { extractReferences, injectReferences } from './saved_visualization_references'; +import { IIndexPattern } from '../../../../../../plugins/data/public'; +import { VisSavedObject } from '../legacy_imports'; + +import { createSavedSearchesService } from '../../discover'; + +async function _afterEsResp(savedVis: VisSavedObject, services: any) { + await _getLinkedSavedSearch(savedVis, services); + savedVis.searchSource!.setField('size', 0); + savedVis.vis = savedVis.vis ? _updateVis(savedVis) : await _createVis(savedVis); + return savedVis; +} + +async function _getLinkedSavedSearch(savedVis: VisSavedObject, services: any) { + const linkedSearch = !!savedVis.savedSearchId; + const current = savedVis.savedSearch; + + if (linkedSearch && current && current.id === savedVis.savedSearchId) { + return; + } + + if (savedVis.savedSearch) { + savedVis.searchSource!.setParent(savedVis.savedSearch.searchSource.getParent()); + savedVis.savedSearch.destroy(); + delete savedVis.savedSearch; + } + const savedSearches = createSavedSearchesService(services); + + if (linkedSearch) { + savedVis.savedSearch = await savedSearches.get(savedVis.savedSearchId!); + savedVis.searchSource!.setParent(savedVis.savedSearch!.searchSource); + } +} + +async function _createVis(savedVis: VisSavedObject) { + savedVis.visState = updateOldState(savedVis.visState); + + // visState doesn't yet exist when importing a visualization, so we can't + // assume that exists at this point. If it does exist, then we're not + // importing a visualization, so we want to sync the title. + if (savedVis.visState) { + savedVis.visState.title = savedVis.title; + } + // the typescript compiler is wrong here, will be right when vis.js -> vis.ts + // @ts-ignore + savedVis.vis = new Vis(savedVis.searchSource!.getField('index'), savedVis.visState); + + savedVis.vis!.savedSearchId = savedVis.savedSearchId; + + return savedVis.vis; +} + +function _updateVis(savedVis: VisSavedObject) { + if (savedVis.vis && savedVis.searchSource) { + savedVis.vis.indexPattern = savedVis.searchSource.getField('index'); + savedVis.visState.title = savedVis.title; + savedVis.vis.setState(savedVis.visState); + savedVis.vis.savedSearchId = savedVis.savedSearchId; + } + return savedVis.vis; +} + +export function createSavedVisClass(services: SavedObjectKibanaServices) { + const SavedObjectClass = createSavedObjectClass(services); + + class SavedVis extends SavedObjectClass { + public static type: string = 'visualization'; + public static mapping: Record = { + title: 'text', + visState: 'json', + uiStateJSON: 'text', + description: 'text', + savedSearchId: 'keyword', + version: 'integer', + }; + // Order these fields to the top, the rest are alphabetical + public static fieldOrder = ['title', 'description']; + public static searchSource = true; + + constructor(opts: Record | string = {}) { + if (typeof opts !== 'object') { + opts = { id: opts }; + } + const visState = !opts.type ? null : { type: opts.type }; + // Gives our SavedWorkspace the properties of a SavedObject + super({ + type: SavedVis.type, + mapping: SavedVis.mapping, + searchSource: SavedVis.searchSource, + extractReferences, + injectReferences, + id: (opts.id as string) || '', + indexPattern: opts.indexPattern as IIndexPattern, + defaults: { + title: '', + visState, + uiStateJSON: '{}', + description: '', + savedSearchId: opts.savedSearchId, + version: 1, + }, + afterESResp: (savedObject: SavedObject) => { + return _afterEsResp(savedObject as VisSavedObject, services) as Promise; + }, + }); + this.showInRecentlyAccessed = true; + this.getFullPath = () => { + return `/app/kibana#${VisualizeConstants.EDIT_PATH}/${this.id}`; + }; + } + } + + return SavedVis; +} diff --git a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/index.js b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/index.js rename to src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/index.ts diff --git a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualization_references.test.js b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualization_references.test.ts similarity index 92% rename from src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualization_references.test.js rename to src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualization_references.test.ts index cdc232b06cf51..6549b317d1634 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualization_references.test.js +++ b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualization_references.test.ts @@ -18,6 +18,7 @@ */ import { extractReferences, injectReferences } from './saved_visualization_references'; +import { VisSavedObject } from '../embeddable/visualize_embeddable'; describe('extractReferences', () => { test('extracts nothing if savedSearchId is empty', () => { @@ -26,6 +27,7 @@ describe('extractReferences', () => { attributes: { foo: true, }, + references: [], }; const updatedDoc = extractReferences(doc); expect(updatedDoc).toMatchInlineSnapshot(` @@ -45,6 +47,7 @@ Object { foo: true, savedSearchId: '123', }, + references: [], }; const updatedDoc = extractReferences(doc); expect(updatedDoc).toMatchInlineSnapshot(` @@ -83,6 +86,7 @@ Object { }, }), }, + references: [], }; const updatedDoc = extractReferences(doc); @@ -108,13 +112,13 @@ describe('injectReferences', () => { test('injects nothing when savedSearchRefName is null', () => { const context = { id: '1', - foo: true, - }; + title: 'test', + } as VisSavedObject; injectReferences(context, []); expect(context).toMatchInlineSnapshot(` Object { - "foo": true, "id": "1", + "title": "test", } `); }); @@ -122,7 +126,7 @@ Object { test('injects references into context', () => { const context = { id: '1', - foo: true, + title: 'test', savedSearchRefName: 'search_0', visState: { params: { @@ -137,7 +141,7 @@ Object { ], }, }, - }; + } as VisSavedObject; const references = [ { name: 'search_0', @@ -153,9 +157,9 @@ Object { injectReferences(context, references); expect(context).toMatchInlineSnapshot(` Object { - "foo": true, "id": "1", "savedSearchId": "123", + "title": "test", "visState": Object { "params": Object { "controls": Array [ @@ -176,9 +180,9 @@ Object { test(`fails when it can't find the saved search reference in the array`, () => { const context = { id: '1', - foo: true, savedSearchRefName: 'search_0', - }; + title: 'test', + } as VisSavedObject; expect(() => injectReferences(context, [])).toThrowErrorMatchingInlineSnapshot( `"Could not find saved search reference \\"search_0\\""` ); @@ -187,6 +191,7 @@ Object { test(`fails when it can't find the index pattern reference in the array`, () => { const context = { id: '1', + title: 'test', visState: { params: { controls: [ @@ -197,7 +202,7 @@ Object { ], }, }, - }; + } as VisSavedObject; expect(() => injectReferences(context, [])).toThrowErrorMatchingInlineSnapshot( `"Could not find index pattern reference \\"control_0_index_pattern\\""` ); diff --git a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualization_references.js b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualization_references.ts similarity index 77% rename from src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualization_references.js rename to src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualization_references.ts index 7f451076e239c..dd8c2e9d2b74f 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualization_references.js +++ b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualization_references.ts @@ -16,8 +16,16 @@ * specific language governing permissions and limitations * under the License. */ +import { SavedObjectAttributes, SavedObjectReference } from 'kibana/server'; +import { VisSavedObject } from '../embeddable/visualize_embeddable'; -export function extractReferences({ attributes, references = [] }) { +export function extractReferences({ + attributes, + references = [], +}: { + attributes: SavedObjectAttributes; + references: SavedObjectReference[]; +}) { const updatedAttributes = { ...attributes }; const updatedReferences = [...references]; @@ -26,7 +34,7 @@ export function extractReferences({ attributes, references = [] }) { updatedReferences.push({ name: 'search_0', type: 'search', - id: updatedAttributes.savedSearchId, + id: String(updatedAttributes.savedSearchId), }); delete updatedAttributes.savedSearchId; updatedAttributes.savedSearchRefName = 'search_0'; @@ -34,9 +42,9 @@ export function extractReferences({ attributes, references = [] }) { // Extract index patterns from controls if (updatedAttributes.visState) { - const visState = JSON.parse(updatedAttributes.visState); + const visState = JSON.parse(String(updatedAttributes.visState)); const controls = (visState.params && visState.params.controls) || []; - controls.forEach((control, i) => { + controls.forEach((control: Record, i: number) => { if (!control.indexPattern) { return; } @@ -57,7 +65,7 @@ export function extractReferences({ attributes, references = [] }) { }; } -export function injectReferences(savedObject, references) { +export function injectReferences(savedObject: VisSavedObject, references: SavedObjectReference[]) { if (savedObject.savedSearchRefName) { const savedSearchReference = references.find( reference => reference.name === savedObject.savedSearchRefName @@ -70,13 +78,11 @@ export function injectReferences(savedObject, references) { } if (savedObject.visState) { const controls = (savedObject.visState.params && savedObject.visState.params.controls) || []; - controls.forEach(control => { + controls.forEach((control: Record) => { if (!control.indexPatternRefName) { return; } - const reference = references.find( - reference => reference.name === control.indexPatternRefName - ); + const reference = references.find(ref => ref.name === control.indexPatternRefName); if (!reference) { throw new Error(`Could not find index pattern reference "${control.indexPatternRefName}"`); } diff --git a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualization_register.ts b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualization_register.ts new file mode 100644 index 0000000000000..803474b1f7b3f --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualization_register.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 { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; +// @ts-ignore +import { savedObjectManagementRegistry } from '../../management/saved_object_registry'; +import './saved_visualizations'; + +SavedObjectRegistryProvider.register((savedVisualizations: any) => { + return savedVisualizations; +}); + +// Register this service with the saved object registry so it can be +// edited by the object editor. +savedObjectManagementRegistry.register({ + service: 'savedVisualizations', + title: 'visualizations', +}); diff --git a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js deleted file mode 100644 index 784f45436e3a3..0000000000000 --- a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js +++ /dev/null @@ -1,92 +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 './_saved_vis'; -import { uiModules } from 'ui/modules'; -import { SavedObjectLoader, SavedObjectsClientProvider } from 'ui/saved_objects'; -import { savedObjectManagementRegistry } from '../../management/saved_object_registry'; -import { start as visualizations } from '../../../../visualizations/public/np_ready/public/legacy'; -import { createVisualizeEditUrl } from '../visualize_constants'; -import { findListItems } from './find_list_items'; -import { npStart } from '../../../../../ui/public/new_platform'; - -const app = uiModules.get('app/visualize'); - -// Register this service with the saved object registry so it can be -// edited by the object editor. -savedObjectManagementRegistry.register({ - service: 'savedVisualizations', - title: 'visualizations', -}); - -app.service('savedVisualizations', function(SavedVis, Private) { - const visTypes = visualizations.types; - const savedObjectClient = Private(SavedObjectsClientProvider); - const saveVisualizationLoader = new SavedObjectLoader( - SavedVis, - savedObjectClient, - npStart.core.chrome - ); - - saveVisualizationLoader.mapHitSource = function(source, id) { - source.id = id; - source.url = this.urlFor(id); - - let typeName = source.typeName; - if (source.visState) { - try { - typeName = JSON.parse(source.visState).type; - } catch (e) { - /* missing typename handled below */ - } // eslint-disable-line no-empty - } - - if (!typeName || !visTypes.get(typeName)) { - source.error = 'Unknown visualization type'; - return source; - } - - source.type = visTypes.get(typeName); - source.savedObjectType = 'visualization'; - source.icon = source.type.icon; - source.image = source.type.image; - source.typeTitle = source.type.title; - source.editUrl = `#${createVisualizeEditUrl(id)}`; - - return source; - }; - - saveVisualizationLoader.urlFor = function(id) { - return `#/visualize/edit/${encodeURIComponent(id)}`; - }; - - // This behaves similarly to find, except it returns visualizations that are - // defined as appExtensions and which may not conform to type: visualization - saveVisualizationLoader.findListItems = function(search = '', size = 100) { - return findListItems({ - search, - size, - mapSavedObjectApiHits: this.mapSavedObjectApiHits.bind(this), - savedObjectsClient: this.savedObjectsClient, - visTypes: visualizations.types.getAliases(), - }); - }; - - return saveVisualizationLoader; -}); diff --git a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.ts b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.ts new file mode 100644 index 0000000000000..7425250bffe1a --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.ts @@ -0,0 +1,86 @@ +/* + * 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 { npStart } from 'ui/new_platform'; +// @ts-ignore +import { uiModules } from 'ui/modules'; +import { SavedObjectLoader } from 'ui/saved_objects'; + +import { start as visualizations } from '../../../../visualizations/public/np_ready/public/legacy'; +import { createVisualizeEditUrl } from '../visualize_constants'; +// @ts-ignore +import { findListItems } from './find_list_items'; +import { createSavedVisClass } from './_saved_vis'; +const app = uiModules.get('app/visualize'); + +app.service('savedVisualizations', function() { + const savedObjectsClient = npStart.core.savedObjects.client; + const services = { + savedObjectsClient, + indexPatterns: npStart.plugins.data.indexPatterns, + chrome: npStart.core.chrome, + overlays: npStart.core.overlays, + }; + class SavedObjectLoaderVisualize extends SavedObjectLoader { + mapHitSource = (source: Record, id: string) => { + const visTypes = visualizations.types; + source.id = id; + source.url = this.urlFor(id); + + let typeName = source.typeName; + if (source.visState) { + try { + typeName = JSON.parse(String(source.visState)).type; + } catch (e) { + /* missing typename handled below */ + } // eslint-disable-line no-empty + } + + if (!typeName || !visTypes.get(typeName)) { + source.error = 'Unknown visualization type'; + return source; + } + + source.type = visTypes.get(typeName); + source.savedObjectType = 'visualization'; + source.icon = source.type.icon; + source.image = source.type.image; + source.typeTitle = source.type.title; + source.editUrl = `#${createVisualizeEditUrl(id)}`; + + return source; + }; + urlFor(id: string) { + return `#/visualize/edit/${encodeURIComponent(id)}`; + } + // This behaves similarly to find, except it returns visualizations that are + // defined as appExtensions and which may not conform to type: visualization + findListItems(search: string = '', size: number = 100) { + return findListItems({ + search, + size, + mapSavedObjectApiHits: this.mapSavedObjectApiHits.bind(this), + savedObjectsClient, + visTypes: visualizations.types.getAliases(), + }); + } + } + const SavedVis = createSavedVisClass(services); + + return new SavedObjectLoaderVisualize(SavedVis, savedObjectsClient, npStart.core.chrome); +}); diff --git a/src/legacy/core_plugins/kibana/public/visualize/wizard/__snapshots__/new_vis_modal.test.tsx.snap b/src/legacy/core_plugins/kibana/public/visualize/wizard/__snapshots__/new_vis_modal.test.tsx.snap index ca6b872c73f8f..0b44c7dc4e860 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/wizard/__snapshots__/new_vis_modal.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/visualize/wizard/__snapshots__/new_vis_modal.test.tsx.snap @@ -169,6 +169,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -226,6 +227,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -275,6 +277,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--medium euiIcon-isLoading euiBetaBadge__icon" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -290,6 +293,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--large euiIcon--secondary euiIcon-isLoading" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -325,6 +329,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--large euiIcon--secondary euiIcon-isLoading" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -361,6 +366,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--large euiIcon--secondary euiIcon-isLoading" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -494,6 +500,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -551,6 +558,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -600,6 +608,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--medium euiIcon-isLoading euiBetaBadge__icon" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -615,6 +624,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--large euiIcon--secondary euiIcon-isLoading" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -650,6 +660,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--large euiIcon--secondary euiIcon-isLoading" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -686,6 +697,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--large euiIcon--secondary euiIcon-isLoading" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -758,6 +770,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -815,6 +828,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -864,6 +878,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--medium euiIcon-isLoading euiBetaBadge__icon" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -879,6 +894,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--large euiIcon--secondary euiIcon-isLoading" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -914,6 +930,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--large euiIcon--secondary euiIcon-isLoading" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -950,6 +967,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--large euiIcon--secondary euiIcon-isLoading" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -1031,16 +1049,18 @@ exports[`NewVisModal filter for visualization types should render as expected 1` type="cross" >
-
-
- -
-
-
- -
-
- -
`; @@ -472,10 +452,11 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` className="euiButtonEmpty__content" >
- +
+ + + +
diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts index de423e967b0b3..f83daa4ea1a8a 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts @@ -10,7 +10,7 @@ import uuid from 'uuid'; import * as rest from '../../../../../services/rest/watcher'; import { createErrorGroupWatch } from '../createErrorGroupWatch'; import { esResponse } from './esResponse'; -import { HttpServiceBase } from 'kibana/public'; +import { HttpSetup } from 'kibana/public'; // disable html escaping since this is also disabled in watcher\s mustache implementation mustache.escape = value => value; @@ -30,7 +30,7 @@ describe('createErrorGroupWatch', () => { jest.spyOn(uuid, 'v4').mockReturnValue(Buffer.from('mocked-uuid')); createWatchResponse = await createErrorGroupWatch({ - http: {} as HttpServiceBase, + http: {} as HttpSetup, emails: ['my@email.dk', 'mySecond@email.dk'], schedule: { daily: { diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts index 1d21e35f122d9..d45453e24f1c9 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import url from 'url'; import uuid from 'uuid'; -import { HttpServiceBase } from 'kibana/public'; +import { HttpSetup } from 'kibana/public'; import { ERROR_CULPRIT, ERROR_EXC_HANDLED, @@ -35,7 +35,7 @@ export interface Schedule { } interface Arguments { - http: HttpServiceBase; + http: HttpSetup; emails: string[]; schedule: Schedule; serviceName: string; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap index 489d4f2908cbe..9b2a2c8f2490a 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap @@ -111,12 +111,14 @@ NodeList [ class="euiIcon euiIcon--medium euiIcon-isLoaded euiButton__icon" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" > + <path - d="M13.6 12.186l-1.357-1.358c-.025-.025-.058-.034-.084-.056.53-.794.84-1.746.84-2.773a4.977 4.977 0 0 0-.84-2.772c.026-.02.059-.03.084-.056L13.6 3.813a6.96 6.96 0 0 1 0 8.373zM8 15A6.956 6.956 0 0 1 3.814 13.6l1.358-1.358c.025-.025.034-.057.055-.084C6.02 12.688 6.974 13 8 13a4.978 4.978 0 0 0 2.773-.84c.02.026.03.058.056.083l1.357 1.358A6.956 6.956 0 0 1 8 15zm-5.601-2.813a6.963 6.963 0 0 1 0-8.373l1.359 1.358c.024.025.057.035.084.056A4.97 4.97 0 0 0 3 8c0 1.027.31 1.98.842 2.773-.027.022-.06.031-.084.056l-1.36 1.358zm5.6-.187A4 4 0 1 1 8 4a4 4 0 0 1 0 8zM8 1c1.573 0 3.019.525 4.187 1.4l-1.357 1.358c-.025.025-.035.057-.056.084A4.979 4.979 0 0 0 8 3a4.979 4.979 0 0 0-2.773.842c-.021-.027-.03-.059-.055-.084L3.814 2.4A6.957 6.957 0 0 1 8 1zm0-1a8.001 8.001 0 1 0 .003 16.002A8.001 8.001 0 0 0 8 0z" + d="M13.6 12.186l-1.357-1.358c-.025-.025-.058-.034-.084-.056.53-.794.84-1.746.84-2.773a4.977 4.977 0 00-.84-2.772c.026-.02.059-.03.084-.056L13.6 3.813a6.96 6.96 0 010 8.373zM8 15A6.956 6.956 0 013.814 13.6l1.358-1.358c.025-.025.034-.057.055-.084C6.02 12.688 6.974 13 8 13a4.978 4.978 0 002.773-.84c.02.026.03.058.056.083l1.357 1.358A6.956 6.956 0 018 15zm-5.601-2.813a6.963 6.963 0 010-8.373l1.359 1.358c.024.025.057.035.084.056A4.97 4.97 0 003 8c0 1.027.31 1.98.842 2.773-.027.022-.06.031-.084.056l-1.36 1.358zm5.6-.187A4 4 0 118 4a4 4 0 010 8zM8 1c1.573 0 3.019.525 4.187 1.4l-1.357 1.358c-.025.025-.035.057-.056.084A4.979 4.979 0 008 3a4.979 4.979 0 00-2.773.842c-.021-.027-.03-.059-.055-.084L3.814 2.4A6.957 6.957 0 018 1zm0-1a8.001 8.001 0 10.003 16.002A8.001 8.001 0 008 0z" fill-rule="evenodd" /> </svg> diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LoadingStatePrompt.tsx b/x-pack/legacy/plugins/apm/public/components/shared/LoadingStatePrompt.tsx index f68e2978f680f..e1cf07c03dee9 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/LoadingStatePrompt.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/LoadingStatePrompt.tsx @@ -5,20 +5,14 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiEmptyPrompt } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; export function LoadingStatePrompt() { return ( - <EuiEmptyPrompt - title={ - <div> - {i18n.translate('xpack.apm.loading.prompt', { - defaultMessage: 'Loading...' - })} - </div> - } - titleSize="s" - /> + <EuiFlexGroup justifyContent="spaceAround"> + <EuiFlexItem grow={false}> + <EuiLoadingSpinner size="l" /> + </EuiFlexItem> + </EuiFlexGroup> ); } diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ManagedTable/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/ManagedTable/index.tsx index 53c66e04c468a..c8404b02afe70 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/ManagedTable/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/ManagedTable/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiBasicTable } from '@elastic/eui'; +import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; import { sortByOrder } from 'lodash'; import React, { useMemo, useCallback, ReactNode } from 'react'; import { useUrlParams } from '../../../hooks/useUrlParams'; @@ -69,8 +69,8 @@ function UnoptimizedManagedTable<T>(props: Props<T>) { const sort = useMemo(() => { return { sort: { - field: sortField, - direction: sortDirection + field: sortField as keyof T, + direction: sortDirection as 'asc' | 'desc' } }; }, [sortField, sortDirection]); @@ -78,7 +78,7 @@ function UnoptimizedManagedTable<T>(props: Props<T>) { const onTableChange = useCallback( (options: { page: { index: number; size: number }; - sort: { field: string; direction: 'asc' | 'desc' }; + sort?: { field: keyof T; direction: 'asc' | 'desc' }; }) => { history.push({ ...history.location, @@ -86,8 +86,8 @@ function UnoptimizedManagedTable<T>(props: Props<T>) { ...toQuery(history.location.search), page: options.page.index, pageSize: options.page.size, - sortField: options.sort.field, - sortDirection: options.sort.direction + sortField: options.sort!.field, + sortDirection: options.sort!.direction }) }); }, @@ -107,7 +107,7 @@ function UnoptimizedManagedTable<T>(props: Props<T>) { <EuiBasicTable noItemsMessage={noItemsMessage} items={renderedItems} - columns={columns} + columns={(columns as unknown) as Array<EuiBasicTableColumn<T>>} // EuiBasicTableColumn is stricter than ITableColumn pagination={pagination} sorting={sort} onChange={onTableChange} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap index 5b17b124a321d..ea1b825c856ad 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap @@ -185,14 +185,18 @@ exports[`Stackframe when stackframe has source lines should render correctly 1`] type="arrowRight" > <EuiIconEmpty + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiAccordion__icon" focusable="false" + role="img" style={null} > <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiAccordion__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap index 63f56b8db5c50..48e442ce734cf 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap @@ -21,12 +21,14 @@ exports[`TransactionActionMenu component should match the snapshot 1`] = ` class="euiIcon euiIcon--medium euiIcon-isLoaded euiButtonEmpty__icon" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" > + <title /> <path - d="M13.069 5.157L8.384 9.768a.546.546 0 0 1-.768 0L2.93 5.158a.552.552 0 0 0-.771 0 .53.53 0 0 0 0 .759l4.684 4.61c.641.631 1.672.63 2.312 0l4.684-4.61a.53.53 0 0 0 0-.76.552.552 0 0 0-.771 0z" + d="M13.069 5.157L8.384 9.768a.546.546 0 01-.768 0L2.93 5.158a.552.552 0 00-.771 0 .53.53 0 000 .759l4.684 4.61c.641.631 1.672.63 2.312 0l4.684-4.61a.53.53 0 000-.76.552.552 0 00-.771 0z" fill-rule="non-zero" /> </svg> diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx new file mode 100644 index 0000000000000..fb087612f8e3d --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx @@ -0,0 +1,74 @@ +/* + * 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 { VerticalGridLines } from 'react-vis'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { + EuiIcon, + EuiToolTip, + EuiFlexGroup, + EuiFlexItem, + EuiText +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Maybe } from '../../../../../typings/common'; +import { Annotation } from '../../../../../common/annotations'; +import { PlotValues, SharedPlot } from './plotUtils'; +import { asAbsoluteDateTime } from '../../../../utils/formatters'; + +interface Props { + annotations: Annotation[]; + plotValues: PlotValues; + width: number; + overlay: Maybe<HTMLElement>; +} + +const style = { + stroke: theme.euiColorSecondary, + strokeDasharray: 'none' +}; + +export function AnnotationsPlot(props: Props) { + const { plotValues, annotations } = props; + + const tickValues = annotations.map(annotation => annotation.time); + + return ( + <> + <SharedPlot plotValues={plotValues}> + <VerticalGridLines tickValues={tickValues} style={style} /> + </SharedPlot> + {annotations.map(annotation => ( + <div + key={annotation.id} + style={{ + position: 'absolute', + left: plotValues.x(annotation.time) - 8, + top: -2 + }} + > + <EuiToolTip + title={asAbsoluteDateTime(annotation.time, 'seconds')} + content={ + <EuiFlexGroup> + <EuiFlexItem grow={true}> + <EuiText> + {i18n.translate('xpack.apm.version', { + defaultMessage: 'Version' + })} + </EuiText> + </EuiFlexItem> + <EuiFlexItem grow={false}>{annotation.text}</EuiFlexItem> + </EuiFlexGroup> + } + > + <EuiIcon type="tag" color={theme.euiColorSecondary} /> + </EuiToolTip> + </div> + ))} + </> + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js index 11755e13bfdd6..848c975942ff6 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js @@ -16,6 +16,8 @@ import { truncate } from '../../../../style/variables'; import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { i18n } from '@kbn/i18n'; +import { EuiIcon } from '@elastic/eui'; const Container = styled.div` display: flex; @@ -73,9 +75,12 @@ export default function Legends({ noHits, series, seriesEnabledState, - truncateLegends + truncateLegends, + hasAnnotations, + showAnnotations, + onAnnotationsToggle }) { - if (noHits) { + if (noHits && !hasAnnotations) { return null; } @@ -107,6 +112,30 @@ export default function Legends({ /> ); })} + {hasAnnotations && ( + <Legend + key="annotations" + onClick={() => { + if (onAnnotationsToggle) { + onAnnotationsToggle(); + } + }} + text={ + <LegendContent> + {i18n.translate('xpack.apm.serviceVersion', { + defaultMessage: 'Service version' + })} + </LegendContent> + } + indicator={() => ( + <div style={{ marginRight: px(units.quarter) }}> + <EuiIcon type="tag" color={theme.euiColorSecondary} /> + </div> + )} + disabled={!showAnnotations} + color={theme.euiColorSecondary} + /> + )} <MoreSeries hiddenSeriesCount={hiddenSeriesCount} /> </Container> ); @@ -118,5 +147,8 @@ Legends.propTypes = { noHits: PropTypes.bool.isRequired, series: PropTypes.array.isRequired, seriesEnabledState: PropTypes.array.isRequired, - truncateLegends: PropTypes.bool.isRequired + truncateLegends: PropTypes.bool.isRequired, + hasAnnotations: PropTypes.bool, + showAnnotations: PropTypes.bool, + onAnnotationsToggle: PropTypes.func }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/index.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/index.js index e87d60c9b3fe8..f59c30d2f4d2f 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/index.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/index.js @@ -13,6 +13,7 @@ import Legends from './Legends'; import StaticPlot from './StaticPlot'; import InteractivePlot from './InteractivePlot'; import VoronoiPlot from './VoronoiPlot'; +import { AnnotationsPlot } from './AnnotationsPlot'; import { createSelector } from 'reselect'; import { getPlotValues } from './plotUtils'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; @@ -28,7 +29,8 @@ export class InnerCustomPlot extends PureComponent { seriesEnabledState: [], isDrawing: false, selectionStart: null, - selectionEnd: null + selectionEnd: null, + showAnnotations: true }; getEnabledSeries = createSelector( @@ -122,7 +124,7 @@ export class InnerCustomPlot extends PureComponent { } render() { - const { series, truncateLegends, width } = this.props; + const { series, truncateLegends, width, annotations } = this.props; if (!width) { return null; @@ -166,6 +168,14 @@ export class InnerCustomPlot extends PureComponent { tickFormatX={this.props.tickFormatX} /> + {this.state.showAnnotations && !isEmpty(annotations) && ( + <AnnotationsPlot + plotValues={plotValues} + width={width} + annotations={annotations || []} + /> + )} + <InteractivePlot plotValues={plotValues} hoverX={this.props.hoverX} @@ -192,6 +202,13 @@ export class InnerCustomPlot extends PureComponent { hiddenSeriesCount={hiddenSeriesCount} clickLegend={this.clickLegend} seriesEnabledState={this.state.seriesEnabledState} + hasAnnotations={!isEmpty(annotations)} + showAnnotations={this.state.showAnnotations} + onAnnotationsToggle={() => { + this.setState(({ showAnnotations }) => ({ + showAnnotations: !showAnnotations + })); + }} /> </Fragment> ); @@ -209,7 +226,14 @@ InnerCustomPlot.propTypes = { truncateLegends: PropTypes.bool, width: PropTypes.number.isRequired, height: PropTypes.number, - stackBy: PropTypes.string + stackBy: PropTypes.string, + annotations: PropTypes.arrayOf( + PropTypes.shape({ + type: PropTypes.string, + id: PropTypes.string, + firstSeen: PropTypes.number + }) + ) }; InnerCustomPlot.defaultProps = { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts index 55bfb490e8588..b130deed7f098 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts @@ -6,6 +6,7 @@ // @ts-ignore import * as plotUtils from './plotUtils'; +import { TimeSeries, Coordinate } from '../../../../../typings/timeseries'; describe('plotUtils', () => { describe('getPlotValues', () => { @@ -34,7 +35,10 @@ describe('plotUtils', () => { expect( plotUtils .getPlotValues( - [{ data: { x: 0, y: 200 } }, { data: { x: 0, y: 300 } }], + [ + { data: [{ x: 0, y: 200 }] }, + { data: [{ x: 0, y: 300 }] } + ] as Array<TimeSeries<Coordinate>>, [], { height: 1, diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx similarity index 74% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.js rename to x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx index 4186f6c899750..10eb4659ea695 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx @@ -11,6 +11,7 @@ import d3 from 'd3'; import PropTypes from 'prop-types'; import React from 'react'; +import { TimeSeries, Coordinate } from '../../../../../typings/timeseries'; import { unit } from '../../../../style/variables'; import { getTimezoneOffsetInMs } from './getTimezoneOffsetInMs'; @@ -22,20 +23,23 @@ const XY_MARGIN = { bottom: unit * 2 }; -const getXScale = (xMin, xMax, width) => { +const getXScale = (xMin: number, xMax: number, width: number) => { return scaleLinear() .domain([xMin, xMax]) .range([XY_MARGIN.left, width - XY_MARGIN.right]); }; -const getYScale = (yMin, yMax) => { +const getYScale = (yMin: number, yMax: number) => { return scaleLinear() .domain([yMin, yMax]) .range([XY_HEIGHT, 0]) .nice(); }; -function getFlattenedCoordinates(visibleSeries, enabledSeries) { +function getFlattenedCoordinates( + visibleSeries: Array<TimeSeries<Coordinate>>, + enabledSeries: Array<TimeSeries<Coordinate>> +) { const enabledCoordinates = flatten(enabledSeries.map(serie => serie.data)); if (!isEmpty(enabledCoordinates)) { return enabledCoordinates; @@ -44,10 +48,24 @@ function getFlattenedCoordinates(visibleSeries, enabledSeries) { return flatten(visibleSeries.map(serie => serie.data)); } +export type PlotValues = ReturnType<typeof getPlotValues>; + export function getPlotValues( - visibleSeries, - enabledSeries, - { width, yMin = 0, yMax = 'max', height, stackBy } + visibleSeries: Array<TimeSeries<Coordinate>>, + enabledSeries: Array<TimeSeries<Coordinate>>, + { + width, + yMin = 0, + yMax = 'max', + height, + stackBy + }: { + width: number; + yMin?: number | 'min'; + yMax?: number | 'max'; + height: number; + stackBy?: 'x' | 'y'; + } ) { const flattenedCoordinates = getFlattenedCoordinates( visibleSeries, @@ -59,10 +77,10 @@ export function getPlotValues( const xMax = d3.max(flattenedCoordinates, d => d.x); if (yMax === 'max') { - yMax = d3.max(flattenedCoordinates, d => d.y); + yMax = d3.max(flattenedCoordinates, d => d.y ?? 0); } if (yMin === 'min') { - yMin = d3.min(flattenedCoordinates, d => d.y); + yMin = d3.min(flattenedCoordinates, d => d.y ?? 0); } const [xMinZone, xMaxZone] = [xMin, xMax].map(x => { @@ -101,11 +119,19 @@ export function getPlotValues( }; } -export function SharedPlot({ plotValues, ...props }) { +export function SharedPlot({ + plotValues, + ...props +}: { + plotValues: PlotValues; + children: React.ReactNode; +}) { const { XY_HEIGHT: height, XY_MARGIN: margin, XY_WIDTH: width } = plotValues; return ( - <div style={{ position: 'absolute', top: 0, left: 0 }}> + <div + style={{ position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }} + > <XYPlot dontCheckIfEmpty height={height} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap index ff917dd95bf96..c46cbbbcccc0b 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap @@ -46,6 +46,7 @@ Array [ style={ Object { "left": 0, + "pointerEvents": "none", "position": "absolute", "top": 0, } @@ -2094,6 +2095,7 @@ Array [ style={ Object { "left": 0, + "pointerEvents": "none", "position": "absolute", "top": 0, } @@ -2126,6 +2128,7 @@ Array [ style={ Object { "left": 0, + "pointerEvents": "none", "position": "absolute", "top": 0, } @@ -2818,6 +2821,7 @@ Object { "selectionEnd": null, "selectionStart": null, "seriesEnabledState": Array [], + "showAnnotations": true, } `; @@ -2969,6 +2973,7 @@ Array [ style={ Object { "left": 0, + "pointerEvents": "none", "position": "absolute", "top": 0, } @@ -5017,6 +5022,7 @@ Array [ style={ Object { "left": 0, + "pointerEvents": "none", "position": "absolute", "top": 0, } @@ -5283,6 +5289,7 @@ Array [ style={ Object { "left": 0, + "pointerEvents": "none", "position": "absolute", "top": 0, } @@ -5975,6 +5982,7 @@ Object { "selectionEnd": null, "selectionStart": null, "seriesEnabledState": Array [], + "showAnnotations": true, } `; @@ -5992,6 +6000,7 @@ Array [ style={ Object { "left": 0, + "pointerEvents": "none", "position": "absolute", "top": 0, } @@ -6355,6 +6364,7 @@ Array [ style={ Object { "left": 0, + "pointerEvents": "none", "position": "absolute", "top": 0, } @@ -6394,5 +6404,6 @@ Object { "selectionEnd": null, "selectionStart": null, "seriesEnabledState": Array [], + "showAnnotations": true, } `; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.js index 912859f6e0419..601482430b00f 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.js @@ -37,6 +37,7 @@ export default class Legend extends PureComponent { radius = units.minus - 1, disabled = false, clickable = false, + indicator, ...rest } = this.props; return ( @@ -47,7 +48,7 @@ export default class Legend extends PureComponent { fontSize={fontSize} {...rest} > - <Indicator color={color} radius={radius} /> + {indicator ? indicator() : <Indicator color={color} radius={radius} />} {text} </Container> ); diff --git a/x-pack/legacy/plugins/apm/public/context/ChartsSyncContext.tsx b/x-pack/legacy/plugins/apm/public/context/ChartsSyncContext.tsx index c2676a35d8e78..afce0811b48f6 100644 --- a/x-pack/legacy/plugins/apm/public/context/ChartsSyncContext.tsx +++ b/x-pack/legacy/plugins/apm/public/context/ChartsSyncContext.tsx @@ -7,6 +7,8 @@ import React, { useMemo, useState } from 'react'; import { toQuery, fromQuery } from '../components/shared/Links/url_helpers'; import { history } from '../utils/history'; +import { useUrlParams } from '../hooks/useUrlParams'; +import { useFetcher } from '../hooks/useFetcher'; const ChartsSyncContext = React.createContext<{ hoverX: number | null; @@ -17,6 +19,31 @@ const ChartsSyncContext = React.createContext<{ const ChartsSyncContextProvider: React.FC = ({ children }) => { const [time, setTime] = useState<number | null>(null); + const { urlParams, uiFilters } = useUrlParams(); + + const { start, end, serviceName } = urlParams; + const { environment } = uiFilters; + + const { data = { annotations: [] } } = useFetcher( + callApmApi => { + if (start && end && serviceName) { + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/annotations', + params: { + path: { + serviceName + }, + query: { + start, + end, + environment + } + } + }); + } + }, + [start, end, environment, serviceName] + ); const value = useMemo(() => { const hoverXHandlers = { @@ -43,11 +70,12 @@ const ChartsSyncContextProvider: React.FC = ({ children }) => { }) }); }, - hoverX: time + hoverX: time, + annotations: data.annotations }; return { ...hoverXHandlers }; - }, [time, setTime]); + }, [time, data.annotations]); return <ChartsSyncContext.Provider value={value} children={children} />; }; diff --git a/x-pack/legacy/plugins/apm/public/services/__test__/callApi.test.ts b/x-pack/legacy/plugins/apm/public/services/__test__/callApi.test.ts index 31ba1e8d40aaa..95ebed1fcb2a6 100644 --- a/x-pack/legacy/plugins/apm/public/services/__test__/callApi.test.ts +++ b/x-pack/legacy/plugins/apm/public/services/__test__/callApi.test.ts @@ -7,10 +7,10 @@ import { mockNow } from '../../utils/testHelpers'; import { clearCache, callApi } from '../rest/callApi'; import { SessionStorageMock } from './SessionStorageMock'; -import { HttpServiceBase } from 'kibana/public'; +import { HttpSetup } from 'kibana/public'; -type HttpMock = HttpServiceBase & { - get: jest.SpyInstance<HttpServiceBase['get']>; +type HttpMock = HttpSetup & { + get: jest.SpyInstance<HttpSetup['get']>; }; describe('callApi', () => { diff --git a/x-pack/legacy/plugins/apm/public/services/__test__/callApmApi.test.ts b/x-pack/legacy/plugins/apm/public/services/__test__/callApmApi.test.ts index e8a9fa74bd1da..9cca9469bba0e 100644 --- a/x-pack/legacy/plugins/apm/public/services/__test__/callApmApi.test.ts +++ b/x-pack/legacy/plugins/apm/public/services/__test__/callApmApi.test.ts @@ -6,7 +6,7 @@ import * as callApiExports from '../rest/callApi'; import { createCallApmApi, APMClient } from '../rest/createCallApmApi'; -import { HttpServiceBase } from 'kibana/public'; +import { HttpSetup } from 'kibana/public'; const callApi = jest .spyOn(callApiExports, 'callApi') @@ -15,7 +15,7 @@ const callApi = jest describe('callApmApi', () => { let callApmApi: APMClient; beforeEach(() => { - callApmApi = createCallApmApi({} as HttpServiceBase); + callApmApi = createCallApmApi({} as HttpSetup); }); afterEach(() => { diff --git a/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts b/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts index 853ba5023f6fd..43ecb860a1f1a 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts @@ -7,7 +7,7 @@ import { isString, startsWith } from 'lodash'; import LRU from 'lru-cache'; import hash from 'object-hash'; -import { HttpServiceBase, HttpFetchOptions } from 'kibana/public'; +import { HttpSetup, HttpFetchOptions } from 'kibana/public'; export type FetchOptions = Omit<HttpFetchOptions, 'body'> & { pathname: string; @@ -42,7 +42,7 @@ export function clearCache() { export type CallApi = typeof callApi; export async function callApi<T = void>( - http: HttpServiceBase, + http: HttpSetup, fetchOptions: FetchOptions ): Promise<T> { const cacheKey = getCacheKey(fetchOptions); diff --git a/x-pack/legacy/plugins/apm/public/services/rest/createCallApmApi.ts b/x-pack/legacy/plugins/apm/public/services/rest/createCallApmApi.ts index 964cc12794075..b4d060adec5a1 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/createCallApmApi.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/createCallApmApi.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { HttpServiceBase } from 'kibana/public'; +import { HttpSetup } from 'kibana/public'; import { callApi, FetchOptions } from './callApi'; import { APMAPI } from '../../../server/routes/create_apm_api'; import { Client } from '../../../server/routes/typings'; @@ -17,7 +17,7 @@ export type APMClientOptions = Omit<FetchOptions, 'query' | 'body'> & { }; }; -export const createCallApmApi = (http: HttpServiceBase) => +export const createCallApmApi = (http: HttpSetup) => ((options: APMClientOptions) => { const { pathname, params = {}, ...opts } = options; diff --git a/x-pack/legacy/plugins/apm/public/services/rest/index_pattern.ts b/x-pack/legacy/plugins/apm/public/services/rest/index_pattern.ts index 477a9f96cc4fb..8e1234dd55e69 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/index_pattern.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/index_pattern.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HttpServiceBase } from 'kibana/public'; +import { HttpSetup } from 'kibana/public'; import { createCallApmApi } from './createCallApmApi'; -export const createStaticIndexPattern = async (http: HttpServiceBase) => { +export const createStaticIndexPattern = async (http: HttpSetup) => { const callApmApi = createCallApmApi(http); return await callApmApi({ method: 'POST', diff --git a/x-pack/legacy/plugins/apm/public/services/rest/ml.ts b/x-pack/legacy/plugins/apm/public/services/rest/ml.ts index e495a8968a7f3..e42b9536362e0 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/ml.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/ml.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HttpServiceBase } from 'kibana/public'; +import { HttpSetup } from 'kibana/public'; import { PROCESSOR_EVENT, SERVICE_NAME, @@ -32,7 +32,7 @@ interface StartedMLJobApiResponse { jobs: MlResponseItem[]; } -async function getTransactionIndices(http: HttpServiceBase) { +async function getTransactionIndices(http: HttpSetup) { const callApmApi: APMClient = createCallApmApi(http); const indices = await callApmApi({ method: 'GET', @@ -48,7 +48,7 @@ export async function startMLJob({ }: { serviceName: string; transactionType: string; - http: HttpServiceBase; + http: HttpSetup; }) { const transactionIndices = await getTransactionIndices(http); const groups = ['apm', serviceName.toLowerCase()]; @@ -90,7 +90,7 @@ export async function getHasMLJob({ }: { serviceName: string; transactionType: string; - http: HttpServiceBase; + http: HttpSetup; }) { try { await callApi<MLJobApiResponse>(http, { diff --git a/x-pack/legacy/plugins/apm/public/services/rest/watcher.ts b/x-pack/legacy/plugins/apm/public/services/rest/watcher.ts index dfa64b5368ee9..259f2af33ba9a 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/watcher.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/watcher.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HttpServiceBase } from 'kibana/public'; +import { HttpSetup } from 'kibana/public'; import { callApi } from './callApi'; export async function createWatch({ @@ -12,7 +12,7 @@ export async function createWatch({ watch, http }: { - http: HttpServiceBase; + http: HttpSetup; id: string; watch: any; }) { diff --git a/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx b/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx index 0c8a7cbc17884..862c982d6b5ac 100644 --- a/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx @@ -19,7 +19,11 @@ import { MemoryRouter } from 'react-router-dom'; import { APMConfig } from '../../../../../plugins/apm/server'; import { LocationProvider } from '../context/LocationContext'; import { PromiseReturnType } from '../../typings/common'; -import { ESFilter } from '../../typings/elasticsearch'; +import { + ESFilter, + ESSearchResponse, + ESSearchRequest +} from '../../typings/elasticsearch'; import { ApmPluginContext, ApmPluginContextValue @@ -117,29 +121,41 @@ interface MockSetup { }; } +interface Options { + mockResponse?: ( + request: ESSearchRequest + ) => ESSearchResponse<unknown, ESSearchRequest>; +} + export async function inspectSearchParams( - fn: (mockSetup: MockSetup) => Promise<any> + fn: (mockSetup: MockSetup) => Promise<any>, + options: Options = {} ) { - const clientSpy = jest.fn().mockReturnValueOnce({ - hits: { - total: 0 - } + const spy = jest.fn().mockImplementation(async request => { + return options.mockResponse + ? options.mockResponse(request) + : { + hits: { + hits: { + total: { + value: 0 + } + } + } + }; }); - const internalClientSpy = jest.fn().mockReturnValueOnce({ - hits: { - total: 0 - } - }); + let response; + let error; const mockSetup = { start: 1528113600000, end: 1528977600000, client: { - search: clientSpy + search: spy } as any, internalClient: { - search: internalClientSpy + search: spy } as any, config: new Proxy( {}, @@ -164,21 +180,18 @@ export async function inspectSearchParams( dynamicIndexPattern: null as any }; try { - await fn(mockSetup); - } catch { + response = await fn(mockSetup); + } catch (err) { + error = err; // we're only extracting the search params } - let params; - if (clientSpy.mock.calls.length) { - params = clientSpy.mock.calls[0][0]; - } else { - params = internalClientSpy.mock.calls[0][0]; - } - return { - params, - teardown: () => clientSpy.mockClear() + params: spy.mock.calls[0][0], + response, + error, + spy, + teardown: () => spy.mockClear() }; } diff --git a/x-pack/legacy/plugins/apm/readme.md b/x-pack/legacy/plugins/apm/readme.md index 0f928fe626bd3..6b21f08b7695e 100644 --- a/x-pack/legacy/plugins/apm/readme.md +++ b/x-pack/legacy/plugins/apm/readme.md @@ -31,11 +31,8 @@ _Docker Compose is required_ ### Setup default APM users -APM behaves differently depending on which the role and permissions a logged in user has. -For testing purposes APM has invented 4 custom users: - - -**elastic**: Apps: read/write. Indices: read/write (all) +APM behaves differently depending on which the role and permissions a logged in user has. +For testing purposes APM uses 3 custom users: **apm_read_user**: Apps: read. Indices: read (`apm-*`) @@ -44,10 +41,10 @@ For testing purposes APM has invented 4 custom users: **kibana_write_user** Apps: read/write. Indices: None -To create the 4 users with the correct roles run the following script: +To create the users with the correct roles run the following script: ```sh -node x-pack/legacy/plugins/apm/scripts/setup-kibana-security.js --username <github-username> +node x-pack/legacy/plugins/apm/scripts/setup-kibana-security.js --role-suffix <github-username-or-something-unique> ``` The users will be created with the password specified in kibana.dev.yml for `elasticsearch.password` diff --git a/x-pack/legacy/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts b/x-pack/legacy/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts index 66f2a8d1ac79f..85aa43f78f7dd 100644 --- a/x-pack/legacy/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts +++ b/x-pack/legacy/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts @@ -20,13 +20,13 @@ const config = yaml.safeLoad( ) ); -const GITHUB_USERNAME = argv.username as string; const KIBANA_INDEX = config['kibana.index'] as string; const TASK_MANAGER_INDEX = config['xpack.task_manager.index'] as string; -const ELASTICSEARCH_USERNAME = (argv.esUsername as string) || 'elastic'; -const ELASTICSEARCH_PASSWORD = (argv.esPassword || +const KIBANA_ROLE_SUFFIX = argv.roleSuffix as string; +const ELASTICSEARCH_USERNAME = (argv.username as string) || 'elastic'; +const ELASTICSEARCH_PASSWORD = (argv.password || config['elasticsearch.password']) as string; -const KIBANA_BASE_URL = (argv.baseUrl as string) || 'http://localhost:5601'; +const KIBANA_BASE_URL = (argv.kibanaUrl as string) || 'http://localhost:5601'; interface User { username: string; @@ -40,51 +40,76 @@ const getKibanaBasePath = once(async () => { try { await axios.request({ url: KIBANA_BASE_URL, maxRedirects: 0 }); } catch (e) { - const err = e as AxiosError; - const { location } = err.response?.headers; - const isBasePath = RegExp(/^\/\w{3}$/).test(location); - return isBasePath ? location : ''; + if (isAxiosError(e)) { + const location = e.response?.headers?.location; + const isBasePath = RegExp(/^\/\w{3}$/).test(location); + return isBasePath ? location : ''; + } + + throw e; } return ''; }); init().catch(e => { - if (e.response) { - console.log( - JSON.stringify({ request: e.config, response: e.response.data }, null, 2) + if (e instanceof AbortError) { + console.error(e.message); + } else if (isAxiosError(e)) { + console.error( + `${e.config.method?.toUpperCase() || 'GET'} ${e.config.url} (Code: ${ + e.response?.status + })` ); - return; - } - console.log(e); + if (e.response) { + console.error( + JSON.stringify( + { request: e.config, response: e.response.data }, + null, + 2 + ) + ); + } + } else { + console.error(e); + } }); async function init() { + const version = await getKibanaVersion(); + console.log(`Connected to Kibana ${version}`); + + const isKibanaLocal = KIBANA_BASE_URL.startsWith('http://localhost'); + // kibana.index must be different from `.kibana` - if (KIBANA_INDEX === '.kibana') { + if (isKibanaLocal && KIBANA_INDEX === '.kibana') { console.log( 'kibana.dev.yml: Please use a custom "kibana.index". Example: "kibana.index: .kibana-john"' ); return; } - if (!KIBANA_INDEX.startsWith('.kibana')) { + if (isKibanaLocal && !KIBANA_INDEX.startsWith('.kibana')) { console.log( 'kibana.dev.yml: "kibana.index" must be prefixed with `.kibana`. Example: "kibana.index: .kibana-john"' ); return; } - if (TASK_MANAGER_INDEX && !TASK_MANAGER_INDEX.startsWith('.kibana')) { + if ( + isKibanaLocal && + TASK_MANAGER_INDEX && + !TASK_MANAGER_INDEX.startsWith('.kibana') + ) { console.log( 'kibana.dev.yml: "xpack.task_manager.index" must be prefixed with `.kibana`. Example: "xpack.task_manager.index: .kibana-task-manager-john"' ); return; } - if (!GITHUB_USERNAME) { + if (!KIBANA_ROLE_SUFFIX) { console.log( - 'Please specify your github username with `--username <username>` ' + 'Please specify a unique suffix that will be added to your roles with `--role-suffix <suffix>` ' ); return; } @@ -95,8 +120,8 @@ async function init() { return; } - const KIBANA_READ_ROLE = `kibana_read_${GITHUB_USERNAME}`; - const KIBANA_WRITE_ROLE = `kibana_write_${GITHUB_USERNAME}`; + const KIBANA_READ_ROLE = `kibana_read_${KIBANA_ROLE_SUFFIX}`; + const KIBANA_WRITE_ROLE = `kibana_write_${KIBANA_ROLE_SUFFIX}`; // create roles await createRole({ roleName: KIBANA_READ_ROLE, privilege: 'read' }); @@ -132,16 +157,18 @@ async function isSecurityEnabled() { } async function callKibana<T>(options: AxiosRequestConfig): Promise<T> { - const basePath = await getKibanaBasePath(); - const { data } = await axios.request({ + const kibanaBasePath = await getKibanaBasePath(); + const reqOptions = { ...options, - baseURL: KIBANA_BASE_URL + basePath, + baseURL: KIBANA_BASE_URL + kibanaBasePath, auth: { username: ELASTICSEARCH_USERNAME, password: ELASTICSEARCH_PASSWORD }, headers: { 'kbn-xsrf': 'true', ...options.headers } - }); + }; + + const { data } = await axios.request(reqOptions); return data; } @@ -222,10 +249,8 @@ async function getUser(username: string) { url: `/internal/security/users/${username}` }); } catch (e) { - const err = e as AxiosError; - // return empty if user doesn't exist - if (err.response?.status === 404) { + if (isAxiosError(e) && e.response?.status === 404) { return null; } @@ -240,13 +265,51 @@ async function getRole(roleName: string) { url: `/api/security/role/${roleName}` }); } catch (e) { - const err = e as AxiosError; - // return empty if role doesn't exist - if (err.response?.status === 404) { + if (isAxiosError(e) && e.response?.status === 404) { return null; } throw e; } } + +async function getKibanaVersion() { + try { + const res: { version: { number: number } } = await callKibana({ + method: 'GET', + url: `/api/status` + }); + return res.version.number; + } catch (e) { + if (isAxiosError(e)) { + switch (e.response?.status) { + case 401: + throw new AbortError( + `Could not access Kibana with the provided credentials. Username: "${e.config.auth?.username}". Password: "${e.config.auth?.password}"` + ); + + case 404: + throw new AbortError( + `Could not get version on ${e.config.url} (Code: 404)` + ); + + default: + throw new AbortError( + `Cannot access Kibana on ${e.config.baseURL}. Please specify Kibana with: "--kibana-url <url>"` + ); + } + } + throw e; + } +} + +function isAxiosError(e: AxiosError | Error): e is AxiosError { + return 'isAxiosError' in e; +} + +class AbortError extends Error { + constructor(message: string) { + super(message); + } +} diff --git a/x-pack/legacy/plugins/apm/scripts/setup-kibana-security.js b/x-pack/legacy/plugins/apm/scripts/setup-kibana-security.js index a3dc3d66f56a0..825c1a526fcc5 100644 --- a/x-pack/legacy/plugins/apm/scripts/setup-kibana-security.js +++ b/x-pack/legacy/plugins/apm/scripts/setup-kibana-security.js @@ -12,7 +12,7 @@ * The two roles will be assigned to the already existing users: `apm_read_user`, `apm_write_user`, `kibana_write_user` * * This makes it possible to use the existing cloud users locally - * Usage: node setup-kibana-security.js --username YOUR-GITHUB-USERNAME + * Usage: node setup-kibana-security.js --role-suffix <YOUR-GITHUB-USERNAME-OR-SOMETHING-UNIQUE> ******************************/ // compile typescript on the fly diff --git a/x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/multiple-versions.json b/x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/multiple-versions.json new file mode 100644 index 0000000000000..7e2d2405d681c --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/multiple-versions.json @@ -0,0 +1,34 @@ +{ + "took": 444, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 10000, + "relation": "gte" + }, + "max_score": null, + "hits": [] + }, + "aggregations": { + "versions": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "8.0.0", + "doc_count": 615285 + }, + { + "key": "7.5.0", + "doc_count": 615285 + } + ] + } + } +} diff --git a/x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/no-versions.json b/x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/no-versions.json new file mode 100644 index 0000000000000..fa5c63f1b9a54 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/no-versions.json @@ -0,0 +1,25 @@ +{ + "took": 398, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 10000, + "relation": "gte" + }, + "max_score": null, + "hits": [] + }, + "aggregations": { + "versions": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [] + } + } +} diff --git a/x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/one-version.json b/x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/one-version.json new file mode 100644 index 0000000000000..56303909bcd6f --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/one-version.json @@ -0,0 +1,30 @@ +{ + "took": 444, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 10000, + "relation": "gte" + }, + "max_score": null, + "hits": [] + }, + "aggregations": { + "versions": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "8.0.0", + "doc_count": 615285 + } + ] + } + } +} diff --git a/x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/versions-first-seen.json b/x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/versions-first-seen.json new file mode 100644 index 0000000000000..c53b28c8bf594 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/versions-first-seen.json @@ -0,0 +1,24 @@ +{ + "took": 4750, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 10000, + "relation": "gte" + }, + "max_score": null, + "hits": [] + }, + "aggregations": { + "first_seen": { + "value": 1.5281138E12, + "value_as_string": "2018-06-04T12:00:00.000Z" + } + } +} diff --git a/x-pack/legacy/plugins/apm/server/lib/services/annotations/index.test.ts b/x-pack/legacy/plugins/apm/server/lib/services/annotations/index.test.ts new file mode 100644 index 0000000000000..75ac0642a1b8c --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/services/annotations/index.test.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 { getServiceAnnotations } from '.'; +import { + SearchParamsMock, + inspectSearchParams +} from '../../../../public/utils/testHelpers'; +import noVersions from './__fixtures__/no-versions.json'; +import oneVersion from './__fixtures__/one-version.json'; +import multipleVersions from './__fixtures__/multiple-versions.json'; +import versionsFirstSeen from './__fixtures__/versions-first-seen.json'; + +describe('getServiceAnnotations', () => { + let mock: SearchParamsMock; + + afterEach(() => { + mock.teardown(); + }); + + describe('with 0 versions', () => { + it('returns no annotations', async () => { + mock = await inspectSearchParams( + setup => + getServiceAnnotations({ + setup, + serviceName: 'foo', + environment: 'bar' + }), + { + mockResponse: () => noVersions + } + ); + + expect(mock.response).toEqual({ annotations: [] }); + }); + }); + + describe('with 1 version', () => { + it('returns no annotations', async () => { + mock = await inspectSearchParams( + setup => + getServiceAnnotations({ + setup, + serviceName: 'foo', + environment: 'bar' + }), + { + mockResponse: () => oneVersion + } + ); + + expect(mock.response).toEqual({ annotations: [] }); + }); + }); + + describe('with more than 1 version', () => { + it('returns two annotations', async () => { + const responses = [ + multipleVersions, + versionsFirstSeen, + versionsFirstSeen + ]; + mock = await inspectSearchParams( + setup => + getServiceAnnotations({ + setup, + serviceName: 'foo', + environment: 'bar' + }), + { + mockResponse: () => responses.shift() + } + ); + + expect(mock.spy.mock.calls.length).toBe(3); + + expect(mock.response).toEqual({ + annotations: [ + { + id: '8.0.0', + text: '8.0.0', + time: 1.5281138e12, + type: 'version' + }, + { + id: '7.5.0', + text: '7.5.0', + time: 1.5281138e12, + type: 'version' + } + ] + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/services/annotations/index.ts b/x-pack/legacy/plugins/apm/server/lib/services/annotations/index.ts new file mode 100644 index 0000000000000..c03746ca220ee --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/services/annotations/index.ts @@ -0,0 +1,114 @@ +/* + * 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 { isNumber } from 'lodash'; +import { Annotation, AnnotationType } from '../../../../common/annotations'; +import { ESFilter } from '../../../../typings/elasticsearch'; +import { + SERVICE_NAME, + SERVICE_ENVIRONMENT, + PROCESSOR_EVENT +} from '../../../../common/elasticsearch_fieldnames'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { rangeFilter } from '../../helpers/range_filter'; +import { SERVICE_VERSION } from '../../../../common/elasticsearch_fieldnames'; + +export async function getServiceAnnotations({ + setup, + serviceName, + environment +}: { + serviceName: string; + environment?: string; + setup: Setup & SetupTimeRange; +}) { + const { start, end, client, indices } = setup; + + const filter: ESFilter[] = [ + { term: { [PROCESSOR_EVENT]: 'transaction' } }, + { range: rangeFilter(start, end) }, + { term: { [SERVICE_NAME]: serviceName } } + ]; + + if (environment) { + filter.push({ term: { [SERVICE_ENVIRONMENT]: environment } }); + } + + const versions = + ( + await client.search({ + index: indices['apm_oss.transactionIndices'], + body: { + size: 0, + track_total_hits: false, + query: { + bool: { + filter + } + }, + aggs: { + versions: { + terms: { + field: SERVICE_VERSION + } + } + } + } + }) + ).aggregations?.versions.buckets.map(bucket => bucket.key) ?? []; + + if (versions.length > 1) { + const annotations = await Promise.all( + versions.map(async version => { + const response = await client.search({ + index: indices['apm_oss.transactionIndices'], + body: { + size: 0, + query: { + bool: { + filter: filter + .filter(esFilter => !Object.keys(esFilter).includes('range')) + .concat({ + term: { + [SERVICE_VERSION]: version + } + }) + } + }, + aggs: { + first_seen: { + min: { + field: '@timestamp' + } + } + }, + track_total_hits: false + } + }); + + const firstSeen = response.aggregations?.first_seen.value; + + if (!isNumber(firstSeen)) { + throw new Error( + 'First seen for version was unexpectedly undefined or null.' + ); + } + + if (firstSeen < start || firstSeen > end) { + return null; + } + + return { + type: AnnotationType.VERSION, + id: version, + time: firstSeen, + text: version + }; + }) + ); + return { annotations: annotations.filter(Boolean) as Annotation[] }; + } + return { annotations: [] }; +} diff --git a/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts b/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts index 95488591d4b89..e98842151da84 100644 --- a/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts @@ -17,7 +17,8 @@ import { serviceAgentNameRoute, serviceTransactionTypesRoute, servicesRoute, - serviceNodeMetadataRoute + serviceNodeMetadataRoute, + serviceAnnotationsRoute } from './services'; import { agentConfigurationRoute, @@ -75,6 +76,7 @@ const createApmApi = () => { .add(serviceTransactionTypesRoute) .add(servicesRoute) .add(serviceNodeMetadataRoute) + .add(serviceAnnotationsRoute) // Agent configuration .add(agentConfigurationAgentNameRoute) diff --git a/x-pack/legacy/plugins/apm/server/routes/services.ts b/x-pack/legacy/plugins/apm/server/routes/services.ts index 91495bb96b032..78cb092b85db6 100644 --- a/x-pack/legacy/plugins/apm/server/routes/services.ts +++ b/x-pack/legacy/plugins/apm/server/routes/services.ts @@ -19,6 +19,7 @@ import { getServiceNodeMetadata } from '../lib/services/get_service_node_metadat import { createRoute } from './create_route'; import { uiFiltersRt, rangeRt } from './default_api_types'; import { getServiceMap } from '../lib/services/map'; +import { getServiceAnnotations } from '../lib/services/annotations'; export const servicesRoute = createRoute(() => ({ path: '/api/apm/services', @@ -98,3 +99,29 @@ export const serviceMapRoute = createRoute(() => ({ return new Boom('Not found', { statusCode: 404 }); } })); + +export const serviceAnnotationsRoute = createRoute(() => ({ + path: '/api/apm/services/{serviceName}/annotations', + params: { + path: t.type({ + serviceName: t.string + }), + query: t.intersection([ + rangeRt, + t.partial({ + environment: t.string + }) + ]) + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { serviceName } = context.params.path; + const { environment } = context.params.query; + + return getServiceAnnotations({ + setup, + serviceName, + environment + }); + } +})); diff --git a/x-pack/legacy/plugins/infra/public/apps/kibana_app.ts b/x-pack/legacy/plugins/apm/typings/react-vis.d.ts similarity index 67% rename from x-pack/legacy/plugins/infra/public/apps/kibana_app.ts rename to x-pack/legacy/plugins/apm/typings/react-vis.d.ts index 5c5687c6c1283..aef8efc30d555 100644 --- a/x-pack/legacy/plugins/infra/public/apps/kibana_app.ts +++ b/x-pack/legacy/plugins/apm/typings/react-vis.d.ts @@ -4,6 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { compose } from '../lib/compose/kibana_compose'; -import { startApp } from './start_app'; -startApp(compose()); +declare module 'react-vis'; diff --git a/x-pack/legacy/plugins/apm/typings/timeseries.ts b/x-pack/legacy/plugins/apm/typings/timeseries.ts index d64486d8e71e9..600be15ea229f 100644 --- a/x-pack/legacy/plugins/apm/typings/timeseries.ts +++ b/x-pack/legacy/plugins/apm/typings/timeseries.ts @@ -15,12 +15,14 @@ export interface RectCoordinate { x0: number; } -export interface TimeSeries { +export interface TimeSeries< + TCoordinate extends { x: number } = Coordinate | RectCoordinate +> { title: string; titleShort?: string; hideLegend?: boolean; hideTooltipValue?: boolean; - data: Array<Coordinate | RectCoordinate>; + data: TCoordinate[]; legendValue?: string; type: string; color: string; diff --git a/x-pack/legacy/plugins/beats_management/public/components/table/table.tsx b/x-pack/legacy/plugins/beats_management/public/components/table/table.tsx index 8b101196d21ee..26ddd682405cb 100644 --- a/x-pack/legacy/plugins/beats_management/public/components/table/table.tsx +++ b/x-pack/legacy/plugins/beats_management/public/components/table/table.tsx @@ -93,7 +93,7 @@ export class Table extends React.Component<TableProps, TableState> { }; const selectionOptions = hideTableControls - ? null + ? undefined : { onSelectionChange: this.setSelection, selectable: () => true, @@ -148,7 +148,7 @@ export class Table extends React.Component<TableProps, TableState> { ); } - private onTableChange = (page: { index: number; size: number } = { index: 0, size: 50 }) => { + private onTableChange = ({ page }: { page: { index: number; size: number } }) => { if (this.props.onTableChange) { this.props.onTableChange(page.index, page.size); } diff --git a/x-pack/legacy/plugins/beats_management/public/components/table/table_type_configs.tsx b/x-pack/legacy/plugins/beats_management/public/components/table/table_type_configs.tsx index 6f03f884563e1..6fff7574c39e6 100644 --- a/x-pack/legacy/plugins/beats_management/public/components/table/table_type_configs.tsx +++ b/x-pack/legacy/plugins/beats_management/public/components/table/table_type_configs.tsx @@ -14,7 +14,7 @@ import { ConnectedLink } from '../navigation/connected_link'; import { TagBadge } from '../tag'; export interface ColumnDefinition { - align?: string; + align?: 'left' | 'right' | 'center' | undefined; field: string; name: string; sortable?: boolean; diff --git a/x-pack/legacy/plugins/beats_management/public/pages/beat/details.tsx b/x-pack/legacy/plugins/beats_management/public/pages/beat/details.tsx index 3eaf550cb8c77..3952b44f82561 100644 --- a/x-pack/legacy/plugins/beats_management/public/pages/beat/details.tsx +++ b/x-pack/legacy/plugins/beats_management/public/pages/beat/details.tsx @@ -13,6 +13,7 @@ import { EuiSpacer, EuiText, EuiTitle, + EuiBasicTableColumn, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; @@ -98,7 +99,7 @@ class BeatDetailPageUi extends React.PureComponent<PageProps, PageState> { ), })); - const columns = [ + const columns: Array<EuiBasicTableColumn<ConfigurationBlock>> = [ { field: 'displayValue', name: intl.formatMessage({ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/component/__examples__/__snapshots__/dropdown_filter.examples.storyshot b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/component/__examples__/__snapshots__/dropdown_filter.examples.storyshot index 7d37bf184b49b..b9c6d258821f2 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/component/__examples__/__snapshots__/dropdown_filter.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/component/__examples__/__snapshots__/dropdown_filter.examples.storyshot @@ -16,9 +16,11 @@ exports[`Storyshots renderers/DropdownFilter default 1`] = ` </option> </select> <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading canvasDropdownFilter__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -61,9 +63,11 @@ exports[`Storyshots renderers/DropdownFilter with choices 1`] = ` </option> </select> <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading canvasDropdownFilter__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -107,9 +111,11 @@ exports[`Storyshots renderers/DropdownFilter with choices and new value 1`] = ` </option> </select> <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading canvasDropdownFilter__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -153,9 +159,11 @@ exports[`Storyshots renderers/DropdownFilter with choices and value 1`] = ` </option> </select> <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading canvasDropdownFilter__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -181,9 +189,11 @@ exports[`Storyshots renderers/DropdownFilter with new value 1`] = ` </option> </select> <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading canvasDropdownFilter__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.examples.storyshot b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.examples.storyshot index 6c9d4ce4459b3..be4db66a84d83 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.examples.storyshot @@ -60,10 +60,11 @@ exports[`Storyshots arguments/AxisConfig extended 1`] = ` className="euiFormControlLayoutCustomIcon" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -141,10 +142,11 @@ exports[`Storyshots arguments/AxisConfig/components extended 1`] = ` className="euiFormControlLayoutCustomIcon" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__examples__/__snapshots__/date_format.examples.storyshot b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__examples__/__snapshots__/date_format.examples.storyshot index 4bed74ddb6c60..014c365055fa3 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__examples__/__snapshots__/date_format.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/__examples__/__snapshots__/date_format.examples.storyshot @@ -43,10 +43,11 @@ Array [ className="euiFormControlLayoutCustomIcon" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -120,10 +121,11 @@ Array [ className="euiFormControlLayoutCustomIcon" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -196,10 +198,11 @@ exports[`Storyshots arguments/DateFormat with preset format 1`] = ` className="euiFormControlLayoutCustomIcon" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/__snapshots__/number_format.examples.storyshot b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/__snapshots__/number_format.examples.storyshot index 6de7df3454dde..e93ad3be31f1d 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/__snapshots__/number_format.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/__examples__/__snapshots__/number_format.examples.storyshot @@ -53,10 +53,11 @@ Array [ className="euiFormControlLayoutCustomIcon" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -140,10 +141,11 @@ Array [ className="euiFormControlLayoutCustomIcon" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -226,10 +228,11 @@ exports[`Storyshots arguments/NumberFormat with preset format 1`] = ` className="euiFormControlLayoutCustomIcon" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot index 89ed05a1eea93..454ef0a79d10a 100644 --- a/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot @@ -80,10 +80,11 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -113,10 +114,11 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -147,10 +149,11 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -177,10 +180,11 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -275,10 +279,11 @@ exports[`Storyshots components/Assets/Asset marker 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -308,10 +313,11 @@ exports[`Storyshots components/Assets/Asset marker 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -342,10 +348,11 @@ exports[`Storyshots components/Assets/Asset marker 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -372,10 +379,11 @@ exports[`Storyshots components/Assets/Asset marker 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} diff --git a/x-pack/legacy/plugins/canvas/public/components/color_dot/__examples__/__snapshots__/color_dot.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/color_dot/__examples__/__snapshots__/color_dot.examples.storyshot index 9ffc95c82531e..77359957b8443 100644 --- a/x-pack/legacy/plugins/canvas/public/components/color_dot/__examples__/__snapshots__/color_dot.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/color_dot/__examples__/__snapshots__/color_dot.examples.storyshot @@ -127,9 +127,11 @@ Array [ } > <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading" focusable="false" height={16} + role="img" style={ Object { "fill": "#000", @@ -156,9 +158,11 @@ Array [ } > <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading" focusable="false" height={16} + role="img" style={ Object { "fill": "#fff", @@ -185,9 +189,11 @@ Array [ } > <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading" focusable="false" height={16} + role="img" style={ Object { "fill": "#fff", @@ -214,9 +220,11 @@ Array [ } > <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading" focusable="false" height={16} + role="img" style={ Object { "fill": "#fff", diff --git a/x-pack/legacy/plugins/canvas/public/components/color_manager/__examples__/__snapshots__/color_manager.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/color_manager/__examples__/__snapshots__/color_manager.examples.storyshot index 25198ac2e79f3..5da0ea8738c37 100644 --- a/x-pack/legacy/plugins/canvas/public/components/color_manager/__examples__/__snapshots__/color_manager.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/color_manager/__examples__/__snapshots__/color_manager.examples.storyshot @@ -389,10 +389,11 @@ exports[`Storyshots components/Color/ColorManager interactive 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -407,10 +408,11 @@ exports[`Storyshots components/Color/ColorManager interactive 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -807,10 +809,11 @@ Array [ type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -825,10 +828,11 @@ Array [ type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -894,10 +898,11 @@ Array [ type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -912,10 +917,11 @@ Array [ type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -981,10 +987,11 @@ Array [ type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -999,10 +1006,11 @@ Array [ type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} diff --git a/x-pack/legacy/plugins/canvas/public/components/color_palette/__examples__/__snapshots__/color_palette.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/color_palette/__examples__/__snapshots__/color_palette.examples.storyshot index 05dd67960db05..badbf96029f12 100644 --- a/x-pack/legacy/plugins/canvas/public/components/color_palette/__examples__/__snapshots__/color_palette.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/color_palette/__examples__/__snapshots__/color_palette.examples.storyshot @@ -364,9 +364,11 @@ Array [ } > <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading selected-color" focusable="false" height={16} + role="img" style={ Object { "fill": "#333", @@ -713,9 +715,11 @@ exports[`Storyshots components/Color/ColorPalette six colors, wrap at 4 1`] = ` } > <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading selected-color" focusable="false" height={16} + role="img" style={ Object { "fill": "#333", @@ -985,9 +989,11 @@ Array [ } > <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading selected-color" focusable="false" height={16} + role="img" style={ Object { "fill": "#333", diff --git a/x-pack/legacy/plugins/canvas/public/components/color_picker/__examples__/__snapshots__/color_picker.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/color_picker/__examples__/__snapshots__/color_picker.examples.storyshot index 1af31bdef47fe..6e25ce3d4b0d4 100644 --- a/x-pack/legacy/plugins/canvas/public/components/color_picker/__examples__/__snapshots__/color_picker.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/color_picker/__examples__/__snapshots__/color_picker.examples.storyshot @@ -220,10 +220,11 @@ exports[`Storyshots components/Color/ColorPicker interactive 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -238,10 +239,11 @@ exports[`Storyshots components/Color/ColorPicker interactive 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -307,9 +309,11 @@ exports[`Storyshots components/Color/ColorPicker six colors 1`] = ` } > <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading selected-color" focusable="false" height={16} + role="img" style={ Object { "fill": "#333", @@ -511,10 +515,11 @@ exports[`Storyshots components/Color/ColorPicker six colors 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -529,10 +534,11 @@ exports[`Storyshots components/Color/ColorPicker six colors 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -767,10 +773,11 @@ exports[`Storyshots components/Color/ColorPicker six colors, value missing 1`] = type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -785,10 +792,11 @@ exports[`Storyshots components/Color/ColorPicker six colors, value missing 1`] = type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -833,9 +841,11 @@ exports[`Storyshots components/Color/ColorPicker three colors 1`] = ` } > <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading selected-color" focusable="false" height={16} + role="img" style={ Object { "fill": "#333", @@ -959,10 +969,11 @@ exports[`Storyshots components/Color/ColorPicker three colors 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -977,10 +988,11 @@ exports[`Storyshots components/Color/ColorPicker three colors 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} diff --git a/x-pack/legacy/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.examples.storyshot index ec282e9a44fde..efaa34001971e 100644 --- a/x-pack/legacy/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.examples.storyshot @@ -55,10 +55,11 @@ Array [ type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -209,10 +210,11 @@ Array [ className="euiFilePicker__prompt" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--large euiIcon-isLoading euiFilePicker__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -255,9 +257,11 @@ Array [ className="euiCard__top" > <svg + aria-hidden={true} className="euiIcon euiIcon--xxLarge euiIcon--app euiIcon-isLoading euiCard__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -412,10 +416,11 @@ Array [ type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -566,10 +571,11 @@ Array [ className="euiFilePicker__prompt" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--large euiIcon-isLoading euiFilePicker__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -766,10 +772,11 @@ Array [ type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -920,10 +927,11 @@ Array [ className="euiFilePicker__prompt" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--large euiIcon-isLoading euiFilePicker__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -966,9 +974,11 @@ Array [ className="euiCard__top" > <svg + aria-hidden={true} className="euiIcon euiIcon--xxLarge euiIcon--app euiIcon-isLoading euiCard__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -1122,10 +1132,11 @@ Array [ type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -1276,10 +1287,11 @@ Array [ className="euiFilePicker__prompt" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--large euiIcon-isLoading euiFilePicker__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -1322,9 +1334,11 @@ Array [ className="euiCard__top" > <svg + aria-hidden={true} className="euiIcon euiIcon--xxLarge euiIcon--app euiIcon-isLoading euiCard__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} diff --git a/x-pack/legacy/plugins/canvas/public/components/element_card/__examples__/__snapshots__/element_card.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/element_card/__examples__/__snapshots__/element_card.examples.storyshot index f09921607ef46..5eedf32020e4c 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_card/__examples__/__snapshots__/element_card.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/element_card/__examples__/__snapshots__/element_card.examples.storyshot @@ -16,9 +16,11 @@ exports[`Storyshots components/Elements/ElementCard with click handler 1`] = ` className="euiCard__top" > <svg + aria-hidden={true} className="euiIcon euiIcon--xxLarge euiIcon--app euiIcon-isLoading euiCard__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -118,9 +120,11 @@ exports[`Storyshots components/Elements/ElementCard with tags 1`] = ` className="euiCard__top" > <svg + aria-hidden={true} className="euiIcon euiIcon--xxLarge euiIcon--app euiIcon-isLoading euiCard__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -289,9 +293,11 @@ exports[`Storyshots components/Elements/ElementCard with title and description 1 className="euiCard__top" > <svg + aria-hidden={true} className="euiIcon euiIcon--xxLarge euiIcon--app euiIcon-isLoading euiCard__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_controls.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_controls.examples.storyshot index c88869ea22f1a..27a2a60180930 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_controls.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_controls.examples.storyshot @@ -28,10 +28,11 @@ exports[`Storyshots components/Elements/ElementControls has two buttons 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -57,10 +58,11 @@ exports[`Storyshots components/Elements/ElementControls has two buttons 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_grid.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_grid.examples.storyshot index 01774f849dfe7..c9fb77061572d 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_grid.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_grid.examples.storyshot @@ -75,10 +75,11 @@ exports[`Storyshots components/Elements/ElementGrid with controls 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -104,10 +105,11 @@ exports[`Storyshots components/Elements/ElementGrid with controls 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -182,10 +184,11 @@ exports[`Storyshots components/Elements/ElementGrid with controls 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -211,10 +214,11 @@ exports[`Storyshots components/Elements/ElementGrid with controls 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -289,10 +293,11 @@ exports[`Storyshots components/Elements/ElementGrid with controls 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -318,10 +323,11 @@ exports[`Storyshots components/Elements/ElementGrid with controls 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -411,10 +417,11 @@ exports[`Storyshots components/Elements/ElementGrid with controls and filter 1`] type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -440,10 +447,11 @@ exports[`Storyshots components/Elements/ElementGrid with controls and filter 1`] type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} diff --git a/x-pack/legacy/plugins/canvas/public/components/file_upload/__snapshots__/file_upload.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/file_upload/__snapshots__/file_upload.examples.storyshot index 085662eeb65c7..7f18c2dc85215 100644 --- a/x-pack/legacy/plugins/canvas/public/components/file_upload/__snapshots__/file_upload.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/file_upload/__snapshots__/file_upload.examples.storyshot @@ -20,10 +20,11 @@ exports[`Storyshots components/FileUpload default 1`] = ` className="euiFilePicker__prompt" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--large euiIcon-isLoading euiFilePicker__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} diff --git a/x-pack/legacy/plugins/canvas/public/components/font_picker/__snapshots__/font_picker.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/font_picker/__snapshots__/font_picker.examples.storyshot index 20996df68747c..c1cb45123f04b 100644 --- a/x-pack/legacy/plugins/canvas/public/components/font_picker/__snapshots__/font_picker.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/font_picker/__snapshots__/font_picker.examples.storyshot @@ -47,10 +47,11 @@ exports[`Storyshots components/FontPicker default 1`] = ` className="euiFormControlLayoutCustomIcon" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -129,10 +130,11 @@ exports[`Storyshots components/FontPicker with value 1`] = ` className="euiFormControlLayoutCustomIcon" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} diff --git a/x-pack/legacy/plugins/canvas/public/components/item_grid/__examples__/__snapshots__/item_grid.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/item_grid/__examples__/__snapshots__/item_grid.examples.storyshot index 79b547102b089..5ee984885bbc0 100644 --- a/x-pack/legacy/plugins/canvas/public/components/item_grid/__examples__/__snapshots__/item_grid.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/item_grid/__examples__/__snapshots__/item_grid.examples.storyshot @@ -71,9 +71,11 @@ exports[`Storyshots components/ItemGrid complex grid 1`] = ` } > <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading" focusable="false" height={16} + role="img" style={ Object { "fill": "#333", @@ -100,9 +102,11 @@ exports[`Storyshots components/ItemGrid complex grid 1`] = ` } > <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading" focusable="false" height={16} + role="img" style={ Object { "fill": "#FFF", @@ -129,9 +133,11 @@ exports[`Storyshots components/ItemGrid complex grid 1`] = ` } > <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading" focusable="false" height={16} + role="img" style={ Object { "fill": "#FFF", @@ -151,27 +157,33 @@ exports[`Storyshots components/ItemGrid icon grid 1`] = ` className="item-grid-row" > <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} xmlns="http://www.w3.org/2000/svg" /> <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} xmlns="http://www.w3.org/2000/svg" /> <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} diff --git a/x-pack/legacy/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.examples.storyshot index e02d64e3e0647..9397a67402e51 100644 --- a/x-pack/legacy/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.examples.storyshot @@ -55,10 +55,11 @@ exports[`Storyshots components/KeyboardShortcutsDoc default 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.examples.storyshot index 5a5472e422963..09fc81c382d11 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.examples.storyshot @@ -43,10 +43,11 @@ exports[`Storyshots components/Sidebar/SidebarHeader default 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -85,10 +86,11 @@ exports[`Storyshots components/Sidebar/SidebarHeader default 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -148,10 +150,11 @@ exports[`Storyshots components/Sidebar/SidebarHeader without layer controls 1`] type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -190,10 +193,11 @@ exports[`Storyshots components/Sidebar/SidebarHeader without layer controls 1`] type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} diff --git a/x-pack/legacy/plugins/canvas/public/components/tag/__examples__/__snapshots__/tag.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/tag/__examples__/__snapshots__/tag.examples.storyshot index ec6d0cbe4d1ec..562feb8111e41 100644 --- a/x-pack/legacy/plugins/canvas/public/components/tag/__examples__/__snapshots__/tag.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/tag/__examples__/__snapshots__/tag.examples.storyshot @@ -55,9 +55,11 @@ exports[`Storyshots components/Tags/Tag as health 1`] = ` className="euiFlexItem euiFlexItem--flexGrowZero" > <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading" focusable="false" height={16} + role="img" style={ Object { "fill": "#666666", @@ -88,9 +90,11 @@ exports[`Storyshots components/Tags/Tag as health with color 1`] = ` className="euiFlexItem euiFlexItem--flexGrowZero" > <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading" focusable="false" height={16} + role="img" style={ Object { "fill": "#9b3067", diff --git a/x-pack/legacy/plugins/canvas/public/components/tag_list/__examples__/__snapshots__/tag_list.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/tag_list/__examples__/__snapshots__/tag_list.examples.storyshot index 1e41a56b82f9e..9dcf55642c66f 100644 --- a/x-pack/legacy/plugins/canvas/public/components/tag_list/__examples__/__snapshots__/tag_list.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/tag_list/__examples__/__snapshots__/tag_list.examples.storyshot @@ -76,9 +76,11 @@ Array [ className="euiFlexItem euiFlexItem--flexGrowZero" > <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading" focusable="false" height={16} + role="img" style={ Object { "fill": "#cc3b54", @@ -106,9 +108,11 @@ Array [ className="euiFlexItem euiFlexItem--flexGrowZero" > <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading" focusable="false" height={16} + role="img" style={ Object { "fill": "#9b3067", @@ -136,9 +140,11 @@ Array [ className="euiFlexItem euiFlexItem--flexGrowZero" > <svg + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading" focusable="false" height={16} + role="img" style={ Object { "fill": "#d41e93", diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/pdf_panel.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/pdf_panel.examples.storyshot index a94f593661cf3..1888d01a3ac93 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/pdf_panel.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/pdf_panel.examples.storyshot @@ -71,10 +71,11 @@ exports[`Storyshots components/Export/PDFPanel default 1`] = ` className="euiButton__content" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButton__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/workpad_export.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/workpad_export.examples.storyshot index c9f5cf67f734a..9c7fca6d78190 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/workpad_export.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/workpad_export.examples.storyshot @@ -28,10 +28,11 @@ exports[`Storyshots components/Export/WorkpadExport disabled 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -72,10 +73,11 @@ exports[`Storyshots components/Export/WorkpadExport enabled 1`] = ` type="button" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/extended_template.examples.storyshot b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/extended_template.examples.storyshot index 9e3bebbd71273..2915d3bfef57b 100644 --- a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/extended_template.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/extended_template.examples.storyshot @@ -139,10 +139,11 @@ exports[`Storyshots arguments/ContainerStyle extended 1`] = ` className="euiFormControlLayoutCustomIcon" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -208,10 +209,11 @@ exports[`Storyshots arguments/ContainerStyle extended 1`] = ` className="euiFormControlLayoutCustomIcon" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -367,10 +369,11 @@ exports[`Storyshots arguments/ContainerStyle extended 1`] = ` className="euiFormControlLayoutCustomIcon" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -626,10 +629,11 @@ exports[`Storyshots arguments/ContainerStyle/components appearance form 1`] = ` className="euiFormControlLayoutCustomIcon" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -695,10 +699,11 @@ exports[`Storyshots arguments/ContainerStyle/components appearance form 1`] = ` className="euiFormControlLayoutCustomIcon" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -853,10 +858,11 @@ exports[`Storyshots arguments/ContainerStyle/components border form 1`] = ` className="euiFormControlLayoutCustomIcon" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -1123,10 +1129,11 @@ exports[`Storyshots arguments/ContainerStyle/components extended template 1`] = className="euiFormControlLayoutCustomIcon" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -1192,10 +1199,11 @@ exports[`Storyshots arguments/ContainerStyle/components extended template 1`] = className="euiFormControlLayoutCustomIcon" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -1351,10 +1359,11 @@ exports[`Storyshots arguments/ContainerStyle/components extended template 1`] = className="euiFormControlLayoutCustomIcon" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.examples.storyshot b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.examples.storyshot index 3b41da4cd3d54..8fa2d406831a4 100644 --- a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.examples.storyshot @@ -66,10 +66,11 @@ exports[`Storyshots arguments/SeriesStyle extended 1`] = ` className="euiFormControlLayoutCustomIcon" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -163,10 +164,11 @@ exports[`Storyshots arguments/SeriesStyle extended 1`] = ` className="euiFormControlLayoutCustomIcon" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -252,10 +254,11 @@ exports[`Storyshots arguments/SeriesStyle extended 1`] = ` className="euiFormControlLayoutCustomIcon" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} @@ -341,10 +344,11 @@ exports[`Storyshots arguments/SeriesStyle extended 1`] = ` className="euiFormControlLayoutCustomIcon" > <svg - aria-hidden="true" + aria-hidden={true} className="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height={16} + role="img" style={null} viewBox="0 0 16 16" width={16} diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/simple_template.examples.storyshot b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/simple_template.examples.storyshot index c843a62702b16..72477cec2603b 100644 --- a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/simple_template.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/simple_template.examples.storyshot @@ -173,12 +173,14 @@ exports[`Storyshots arguments/SeriesStyle/components simple: no series 1`] = ` onMouseOver={[Function]} > <svg + aria-hidden={true} aria-label="Info" className="euiIcon euiIcon--medium euiIcon--warning euiIcon-isLoading" focusable="true" height={16} onBlur={[Function]} onFocus={[Function]} + role="img" style={null} tabIndex={0} viewBox="0 0 16 16" diff --git a/x-pack/legacy/plugins/canvas/public/lib/loading_indicator.ts b/x-pack/legacy/plugins/canvas/public/lib/loading_indicator.ts index a95f4278b6a69..4b9e548d5c718 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/loading_indicator.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/loading_indicator.ts @@ -16,7 +16,7 @@ export interface LoadingIndicatorInterface { const loadingCount$ = new Rx.BehaviorSubject(0); -export const initLoadingIndicator = (addLoadingCount: CoreStart['http']['addLoadingCount']) => +export const initLoadingIndicator = (addLoadingCount: CoreStart['http']['addLoadingCountSource']) => addLoadingCount(loadingCount$); export const loadingIndicator = { diff --git a/x-pack/legacy/plugins/canvas/public/plugin.tsx b/x-pack/legacy/plugins/canvas/public/plugin.tsx index 5190e8521101b..9828845d9ffa9 100644 --- a/x-pack/legacy/plugins/canvas/public/plugin.tsx +++ b/x-pack/legacy/plugins/canvas/public/plugin.tsx @@ -77,7 +77,7 @@ export class CanvasPlugin initLocationProvider(core, plugins); initStore(core, plugins); initClipboard(plugins.__LEGACY.storage); - initLoadingIndicator(core.http.addLoadingCount); + initLoadingIndicator(core.http.addLoadingCountSource); const CanvasRootController = CanvasRootControllerFactory(core, plugins); plugins.__LEGACY.setRootController('canvas', CanvasRootController); diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/api/__tests__/__snapshots__/shareable.test.tsx.snap b/x-pack/legacy/plugins/canvas/shareable_runtime/api/__tests__/__snapshots__/shareable.test.tsx.snap index 782d5364de821..acd68622f1af0 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/api/__tests__/__snapshots__/shareable.test.tsx.snap +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/api/__tests__/__snapshots__/shareable.test.tsx.snap @@ -9,7 +9,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with default propertie </style><div class=\\"content\\"><div class=\\"renderContainer\\"><div data-renderer=\\"markdown\\" class=\\"render\\"><div>markdown mock</div></div></div></div></div></div></div></div></div></div><div class=\\"root\\" style=\\"height: 48px;\\"><div class=\\"root\\"><div class=\\"slideContainer\\"><div class=\\"root\\" style=\\"height: 100px; width: 150px;\\"><div class=\\"preview\\" style=\\"height: 720px; width: 1080px;\\"><div id=\\"page-7186b301-f8a7-4c65-8b89-38d68d31cfc4\\" class=\\"root\\" style=\\"height: 720px; width: 1080px; background: rgb(119, 119, 119);\\"><div class=\\"canvasPositionable canvasInteractable\\" style=\\"width: 1082px; height: 205.37748344370857px; margin-left: -541px; margin-top: -102.68874172185429px; position: absolute;\\"><div class=\\"root\\"><div class=\\"container s2042575598\\" style=\\"overflow: hidden;\\"><style type=\\"text/css\\">.s2042575598 .canvasRenderEl h1 { font-size: 150px; text-align: center; color: #d3d3d3; } -</style><div class=\\"content\\"><div class=\\"renderContainer\\"><div data-renderer=\\"markdown\\" class=\\"render\\"><div>markdown mock</div></div></div></div></div></div></div></div></div></div></div></div><div class=\\"bar\\" style=\\"bottom: 0px;\\"><div class=\\"euiFlexGroup euiFlexGroup--directionRow euiFlexGroup--responsive\\"><div class=\\"euiFlexItem title\\"><div class=\\"euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive\\"><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><a class=\\"euiLink euiLink--primary\\" href=\\"https://www.elastic.co\\" rel=\\"\\" title=\\"Powered by Elastic.co\\"><svg width=\\"32\\" height=\\"32\\" viewBox=\\"0 0 32 32\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--large euiIcon-isLoaded\\" focusable=\\"false\\"><g fill=\\"none\\"><path fill=\\"#FDD009\\" d=\\"M11.934 13.152l7.353 3.356 7.42-6.507c.107-.537.16-1.072.16-1.633 0-4.578-3.721-8.303-8.295-8.303a8.288 8.288 0 0 0-6.84 3.61l-1.234 6.409 1.436 3.068z\\"></path><path fill=\\"#23BAB1\\" d=\\"M4.322 20.947a8.461 8.461 0 0 0-.162 1.657c0 4.59 3.731 8.326 8.317 8.326a8.288 8.288 0 0 0 6.873-3.646l1.224-6.387-1.634-3.127-7.383-3.368-7.235 6.545z\\"></path><path fill=\\"#EE5097\\" d=\\"M4.276 8.208L9.315 9.4l1.104-5.736a3.976 3.976 0 0 0-2.413-.815 3.978 3.978 0 0 0-3.971 3.976c0 .484.08.948.24 1.383\\"></path><path fill=\\"#17A7E0\\" d=\\"M3.838 9.41c-2.251.747-3.817 2.907-3.817 5.284 0 2.314 1.43 4.38 3.576 5.198l7.07-6.398-1.298-2.776-5.53-1.308z\\"></path><path fill=\\"#92C73D\\" d=\\"M20.642 27.284a3.945 3.945 0 0 0 2.4.822 3.977 3.977 0 0 0 3.972-3.975c0-.484-.08-.948-.24-1.383l-5.036-1.18-1.096 5.716z\\"></path><path fill=\\"#0678A0\\" d=\\"M21.667 20.247l5.543 1.298c2.252-.745 3.818-2.907 3.818-5.284a5.553 5.553 0 0 0-3.583-5.19l-7.25 6.36 1.472 2.816z\\"></path></g></svg></a></div><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\" style=\\"min-width: 0; cursor: default;\\"><div class=\\"euiText euiText--small\\"><div class=\\"euiTextColor euiTextColor--ghost\\"><div class=\\"eui-textTruncate\\">My Canvas Workpad</div></div></div></div></div></div><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><div class=\\"euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--directionRow euiFlexGroup--responsive\\"><div class=\\"euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive\\" style=\\"margin: 0px 12px;\\"><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><button disabled=\\"\\" class=\\"euiButtonIcon euiButtonIcon--ghost\\" type=\\"button\\" data-test-subj=\\"pageControlsPrevPage\\" aria-label=\\"Previous Page\\"><svg width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--medium euiIcon-isLoaded euiButtonIcon__icon\\" focusable=\\"false\\" aria-hidden=\\"true\\"><path fill-rule=\\"nonzero\\" d=\\"M10.843 13.069L6.232 8.384a.546.546 0 0 1 0-.768l4.61-4.685a.552.552 0 0 0 0-.771.53.53 0 0 0-.759 0l-4.61 4.684a1.65 1.65 0 0 0 0 2.312l4.61 4.684a.53.53 0 0 0 .76 0 .552.552 0 0 0 0-.771z\\"></path></svg></button></div><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><button class=\\"euiButtonEmpty euiButtonEmpty--ghost euiButtonEmpty--small\\" type=\\"button\\" data-test-subj=\\"pageControlsCurrentPage\\"><span class=\\"euiButtonEmpty__content\\"><span class=\\"euiButtonEmpty__text\\"><div class=\\"euiText euiText--small\\"><div class=\\"euiTextColor euiTextColor--ghost\\">Page 1</div></div></span></span></button></div><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><button disabled=\\"\\" class=\\"euiButtonIcon euiButtonIcon--ghost\\" type=\\"button\\" data-test-subj=\\"pageControlsNextPage\\" aria-label=\\"Next Page\\"><svg width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--medium euiIcon-isLoaded euiButtonIcon__icon\\" focusable=\\"false\\" aria-hidden=\\"true\\"><path fill-rule=\\"nonzero\\" d=\\"M5.157 13.069l4.611-4.685a.546.546 0 0 0 0-.768L5.158 2.93a.552.552 0 0 1 0-.771.53.53 0 0 1 .759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 0 1-.76 0 .552.552 0 0 1 0-.771z\\"></path></svg></button></div></div><div class=\\"euiFlexGroup euiFlexGroup--alignItemsFlexEnd euiFlexGroup--justifyContentCenter euiFlexGroup--directionColumn euiFlexGroup--responsive\\"><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><div class=\\"euiPopover euiPopover--anchorUpRight euiPopover--withTitle\\" id=\\"settings\\"><div class=\\"euiPopover__anchor\\"><button class=\\"euiButtonIcon euiButtonIcon--ghost\\" type=\\"button\\" aria-label=\\"Settings\\"><svg width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--medium euiIcon-isLoaded euiButtonIcon__icon\\" focusable=\\"false\\" aria-hidden=\\"true\\"><path d=\\"M.164 10.329L1.87 8 .163 5.67c.18-.601.43-1.19.758-1.757a8.197 8.197 0 0 1 1.142-1.535l2.872.313L6.099.05a8.166 8.166 0 0 1 3.8-.003l1.166 2.644 2.872-.313a8.166 8.166 0 0 1 1.899 3.293L14.13 8l1.706 2.33c-.18.601-.43 1.19-.758 1.757a8.197 8.197 0 0 1-1.142 1.535l-2.872-.313-1.164 2.641a8.166 8.166 0 0 1-3.8.003l-1.166-2.644-2.872.313a8.166 8.166 0 0 1-1.899-3.293zm4.663 1.986a1 1 0 0 1 1.023.591l.957 2.17c.79.134 1.597.132 2.387-.001l.956-2.169a1 1 0 0 1 1.023-.59l2.358.256a7.23 7.23 0 0 0 1.194-2.068l-1.401-1.913a1 1 0 0 1 0-1.182l1.4-1.912a7.165 7.165 0 0 0-1.192-2.069l-2.359.257a1 1 0 0 1-1.023-.591L9.193.924a7.165 7.165 0 0 0-2.387.001L5.85 3.094a1 1 0 0 1-1.023.59l-2.358-.256a7.23 7.23 0 0 0-1.194 2.068l1.401 1.913a1 1 0 0 1 0 1.182l-1.4 1.912c.28.751.681 1.45 1.192 2.069l2.359-.257zM8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0-1a2 2 0 1 0 0-4 2 2 0 0 0 0 4z\\"></path></svg></button></div></div></div></div></div></div></div></div></div></div></div>" +</style><div class=\\"content\\"><div class=\\"renderContainer\\"><div data-renderer=\\"markdown\\" class=\\"render\\"><div>markdown mock</div></div></div></div></div></div></div></div></div></div></div></div><div class=\\"bar\\" style=\\"bottom: 0px;\\"><div class=\\"euiFlexGroup euiFlexGroup--directionRow euiFlexGroup--responsive\\"><div class=\\"euiFlexItem title\\"><div class=\\"euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive\\"><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><a class=\\"euiLink euiLink--primary\\" href=\\"https://www.elastic.co\\" rel=\\"\\" title=\\"Powered by Elastic.co\\"><svg width=\\"32\\" height=\\"32\\" viewBox=\\"0 0 32 32\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--large euiIcon-isLoaded\\" focusable=\\"false\\" role=\\"img\\" aria-hidden=\\"true\\"><title>
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with height specified 1`] = `"
"`; @@ -21,7 +21,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with height specified
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with page specified 1`] = `"
"`; @@ -33,7 +33,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with page specified 2`
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with width and height specified 1`] = `"
"`; @@ -45,7 +45,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with width and height
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with width specified 1`] = `"
"`; @@ -57,5 +57,5 @@ exports[`Canvas Shareable Workpad API Placed successfully with width specified 2
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/canvas.examples.storyshot b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/canvas.examples.storyshot index c3352b52c591d..6a33dba76c126 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/canvas.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/canvas.examples.storyshot @@ -1325,9 +1325,11 @@ exports[`Storyshots shareables/Canvas component 1`] = ` title="Powered by Elastic.co" >
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.examples.storyshot b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.examples.storyshot index 6570016336d9e..7b3ac299f80ad 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.examples.storyshot @@ -1278,9 +1278,11 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` title="Powered by Elastic.co" > can navigate Autoplay Settings 1`] = ` Auto Play @@ -95,13 +101,16 @@ exports[` can navigate Autoplay Settings 1`] = ` class="euiContextMenu__itemLayout" > can navigate Autoplay Settings 2`] = ` Auto Play @@ -237,13 +255,16 @@ exports[` can navigate Autoplay Settings 2`] = ` class="euiContextMenu__itemLayout" > can navigate Toolbar Settings, closes when activated 1`] = Auto Play @@ -558,13 +597,16 @@ exports[` can navigate Toolbar Settings, closes when activated 1`] = class="euiContextMenu__itemLayout" > can navigate Toolbar Settings, closes when activated 2`] = Auto Play @@ -700,13 +751,16 @@ exports[` can navigate Toolbar Settings, closes when activated 2`] = class="euiContextMenu__itemLayout" >
Settings
Hide Toolbar
Hide the toolbar when the mouse is not within the Canvas?
"`; +exports[` can navigate Toolbar Settings, closes when activated 3`] = `"
Settings
Hide Toolbar
Hide the toolbar when the mouse is not within the Canvas?
"`; diff --git a/x-pack/legacy/plugins/graph/public/angular/__tests__/workspace.js b/x-pack/legacy/plugins/graph/public/angular/graph_client_workspace.test.js similarity index 75% rename from x-pack/legacy/plugins/graph/public/angular/__tests__/workspace.js rename to x-pack/legacy/plugins/graph/public/angular/graph_client_workspace.test.js index a09f94ca6ce9f..6179467966764 100644 --- a/x-pack/legacy/plugins/graph/public/angular/__tests__/workspace.js +++ b/x-pack/legacy/plugins/graph/public/angular/graph_client_workspace.test.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -const gws = require('../graph_client_workspace.js'); -const expect = require('@kbn/expect'); +import gws from './graph_client_workspace'; + describe('graphui-workspace', function() { describe('createWorkspace()', function() { // var fooResource=null; @@ -47,7 +47,7 @@ describe('graphui-workspace', function() { }); it('initializeWorkspace', function() { const { workspace } = init(); - expect(workspace.nodes).to.have.length(0); + expect(workspace.nodes.length).toEqual(0); }); it('simpleSearch', function() { //Test that a graph is loaded from a free-text search @@ -79,16 +79,16 @@ describe('graphui-workspace', function() { }; workspace.simpleSearch('myquery', {}, 2); - expect(workspace.nodes).to.have.length(2); - expect(workspace.edges).to.have.length(1); - expect(workspace.selectedNodes).to.have.length(0); - expect(workspace.blacklistedNodes).to.have.length(0); + expect(workspace.nodes.length).toEqual(2); + expect(workspace.edges.length).toEqual(1); + expect(workspace.selectedNodes.length).toEqual(0); + expect(workspace.blacklistedNodes.length).toEqual(0); const nodeA = workspace.getNode(workspace.makeNodeId('field1', 'a')); - expect(nodeA).to.be.an(Object); + expect(typeof nodeA).toBe('object'); const nodeD = workspace.getNode(workspace.makeNodeId('field1', 'd')); - expect(nodeD).to.be(undefined); + expect(nodeD).toBe(undefined); }); it('expandTest', function() { @@ -121,10 +121,10 @@ describe('graphui-workspace', function() { }; workspace.simpleSearch('myquery', {}, 2); - expect(workspace.nodes).to.have.length(2); - expect(workspace.edges).to.have.length(1); - expect(workspace.selectedNodes).to.have.length(0); - expect(workspace.blacklistedNodes).to.have.length(0); + expect(workspace.nodes.length).toEqual(2); + expect(workspace.edges.length).toEqual(1); + expect(workspace.selectedNodes.length).toEqual(0); + expect(workspace.blacklistedNodes.length).toEqual(0); mockedResult = { vertices: [ @@ -151,8 +151,8 @@ describe('graphui-workspace', function() { ], }; workspace.expandGraph(); - expect(workspace.nodes).to.have.length(3); //we already had b from initial query - expect(workspace.edges).to.have.length(2); + expect(workspace.nodes.length).toEqual(3); //we already had b from initial query + expect(workspace.edges.length).toEqual(2); }); it('selectionTest', function() { @@ -203,38 +203,38 @@ describe('graphui-workspace', function() { }; workspace.simpleSearch('myquery', {}, 2); - expect(workspace.selectedNodes).to.have.length(0); + expect(workspace.selectedNodes.length).toEqual(0); const nodeA1 = workspace.getNode(workspace.makeNodeId('field1', 'a1')); - expect(nodeA1).to.be.an(Object); + expect(typeof nodeA1).toEqual('object'); const nodeA2 = workspace.getNode(workspace.makeNodeId('field1', 'a2')); - expect(nodeA2).to.be.an(Object); + expect(typeof nodeA2).toEqual('object'); const nodeB1 = workspace.getNode(workspace.makeNodeId('field1', 'b1')); - expect(nodeB1).to.be.an(Object); + expect(typeof nodeB1).toEqual('object'); const nodeB2 = workspace.getNode(workspace.makeNodeId('field1', 'b2')); - expect(nodeB2).to.be.an(Object); + expect(typeof nodeB2).toEqual('object'); - expect(workspace.selectedNodes).to.have.length(0); + expect(workspace.selectedNodes.length).toEqual(0); workspace.selectNode(nodeA1); - expect(workspace.selectedNodes).to.have.length(1); + expect(workspace.selectedNodes.length).toEqual(1); workspace.selectInvert(); - expect(workspace.selectedNodes).to.have.length(3); + expect(workspace.selectedNodes.length).toEqual(3); workspace.selectInvert(); - expect(workspace.selectedNodes).to.have.length(1); + expect(workspace.selectedNodes.length).toEqual(1); workspace.deselectNode(nodeA1); - expect(workspace.selectedNodes).to.have.length(0); + expect(workspace.selectedNodes.length).toEqual(0); workspace.selectAll(); - expect(workspace.selectedNodes).to.have.length(4); + expect(workspace.selectedNodes.length).toEqual(4); workspace.selectInvert(); - expect(workspace.selectedNodes).to.have.length(0); + expect(workspace.selectedNodes.length).toEqual(0); workspace.selectNode(nodeA1); - expect(workspace.selectedNodes).to.have.length(1); + expect(workspace.selectedNodes.length).toEqual(1); workspace.selectNeighbours(); - expect(workspace.selectedNodes).to.have.length(2); + expect(workspace.selectedNodes.length).toEqual(2); workspace.selectNeighbours(); //Should have reached full extent of a1-a2 island. - expect(workspace.selectedNodes).to.have.length(2); + expect(workspace.selectedNodes.length).toEqual(2); }); it('undoRedoDeletes', function() { @@ -266,31 +266,31 @@ describe('graphui-workspace', function() { }; workspace.simpleSearch('myquery', {}, 2); - expect(workspace.nodes).to.have.length(2); + expect(workspace.nodes.length).toEqual(2); let nodeA1 = workspace.getNode(workspace.makeNodeId('field1', 'a1')); - expect(nodeA1).to.be.an(Object); + expect(typeof nodeA1).toEqual('object'); const nodeA2 = workspace.getNode(workspace.makeNodeId('field1', 'a2')); - expect(nodeA2).to.be.an(Object); + expect(typeof nodeA2).toEqual('object'); workspace.selectNode(nodeA1); workspace.deleteSelection(); - expect(workspace.nodes).to.have.length(1); + expect(workspace.nodes.length).toEqual(1); nodeA1 = workspace.getNode(workspace.makeNodeId('field1', 'a1')); - expect(nodeA1).to.be(undefined); + expect(nodeA1).toBe(undefined); workspace.undo(); - expect(workspace.nodes).to.have.length(2); + expect(workspace.nodes.length).toEqual(2); nodeA1 = workspace.getNode(workspace.makeNodeId('field1', 'a1')); - expect(nodeA1).to.be.an(Object); + expect(typeof nodeA1).toEqual('object'); workspace.redo(); - expect(workspace.nodes).to.have.length(1); + expect(workspace.nodes.length).toEqual(1); nodeA1 = workspace.getNode(workspace.makeNodeId('field1', 'a1')); - expect(nodeA1).to.be(undefined); + expect(nodeA1).toBe(undefined); workspace.undo(); - expect(workspace.nodes).to.have.length(2); + expect(workspace.nodes.length).toEqual(2); }); it('undoRedoGroupings', function() { @@ -322,36 +322,36 @@ describe('graphui-workspace', function() { }; workspace.simpleSearch('myquery', {}, 2); - expect(workspace.nodes).to.have.length(2); + expect(workspace.nodes.length).toEqual(2); const nodeA1 = workspace.getNode(workspace.makeNodeId('field1', 'a1')); - expect(nodeA1).to.be.an(Object); + expect(typeof nodeA1).toEqual('object'); const nodeA2 = workspace.getNode(workspace.makeNodeId('field1', 'a2')); - expect(nodeA2).to.be.an(Object); + expect(typeof nodeA2).toEqual('object'); workspace.selectNode(nodeA2); workspace.mergeSelections(nodeA1); let groupedItems = workspace.returnUnpackedGroupeds([nodeA1]); - expect(groupedItems).to.have.length(2); + expect(groupedItems.length).toEqual(2); workspace.undo(); groupedItems = workspace.returnUnpackedGroupeds([nodeA1]); - expect(groupedItems).to.have.length(1); + expect(groupedItems.length).toEqual(1); workspace.redo(); groupedItems = workspace.returnUnpackedGroupeds([nodeA1]); - expect(groupedItems).to.have.length(2); + expect(groupedItems.length).toEqual(2); //Grouped deletes delete all grouped items workspace.selectNone(); workspace.selectNode(nodeA1); workspace.deleteSelection(); - expect(workspace.nodes).to.have.length(0); - expect(workspace.selectedNodes).to.have.length(0); + expect(workspace.nodes.length).toEqual(0); + expect(workspace.selectedNodes.length).toEqual(0); workspace.undo(); - expect(workspace.nodes).to.have.length(2); + expect(workspace.nodes.length).toEqual(2); groupedItems = workspace.returnUnpackedGroupeds([nodeA1]); - expect(groupedItems).to.have.length(2); + expect(groupedItems.length).toEqual(2); }); }); }); diff --git a/x-pack/legacy/plugins/graph/public/app.js b/x-pack/legacy/plugins/graph/public/app.js index 6ada589f3740c..d0dbf34abc055 100644 --- a/x-pack/legacy/plugins/graph/public/app.js +++ b/x-pack/legacy/plugins/graph/public/app.js @@ -12,7 +12,7 @@ import { Provider } from 'react-redux'; import { isColorDark, hexToRgb } from '@elastic/eui'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; +import { showSaveModal } from './legacy_imports'; import appTemplate from './angular/templates/index.html'; import listingTemplate from './angular/templates/listing_ng_wrapper.html'; @@ -32,24 +32,21 @@ import { asAngularSyncedObservable } from './helpers/as_observable'; import { colorChoices } from './helpers/style_choices'; import { createGraphStore, datasourceSelector, hasFieldsSelector } from './state_management'; import { formatHttpError } from './helpers/format_http_error'; -import { checkLicense } from '../../../../plugins/graph/common/check_license'; export function initGraphApp(angularModule, deps) { const { chrome, - savedGraphWorkspaces, toastNotifications, savedObjectsClient, indexPatterns, - kbnBaseUrl, addBasePath, getBasePath, npData, config, - savedObjectRegistry, + savedWorkspaceLoader, capabilities, coreStart, - Storage, + storage, canEditDrillDownUrls, graphSavePolicy, } = deps; @@ -110,22 +107,19 @@ export function initGraphApp(angularModule, deps) { template: listingTemplate, badge: getReadonlyBadge, controller($location, $scope) { - const services = savedObjectRegistry.byLoaderPropertiesName; - const graphService = services['Graph workspace']; - $scope.listingLimit = config.get('savedObjects:listingLimit'); $scope.create = () => { $location.url(getNewPath()); }; $scope.find = search => { - return graphService.find(search, $scope.listingLimit); + return savedWorkspaceLoader.find(search, $scope.listingLimit); }; $scope.editItem = workspace => { $location.url(getEditPath(workspace)); }; $scope.getViewUrl = workspace => getEditUrl(addBasePath, workspace); $scope.delete = workspaces => { - return graphService.delete(workspaces.map(({ id }) => id)); + return savedWorkspaceLoader.delete(workspaces.map(({ id }) => id)); }; $scope.capabilities = capabilities; $scope.initialFilter = $location.search().filter || ''; @@ -139,14 +133,14 @@ export function initGraphApp(angularModule, deps) { resolve: { savedWorkspace: function($route) { return $route.current.params.id - ? savedGraphWorkspaces.get($route.current.params.id).catch(function() { + ? savedWorkspaceLoader.get($route.current.params.id).catch(function() { toastNotifications.addDanger( i18n.translate('xpack.graph.missingWorkspaceErrorMessage', { defaultMessage: 'Missing workspace', }) ); }) - : savedGraphWorkspaces.get(); + : savedWorkspaceLoader.get(); }, indexPatterns: function() { return savedObjectsClient @@ -300,7 +294,7 @@ export function initGraphApp(angularModule, deps) { // register things on scope passed down to react components $scope.pluginDataStart = npData; - $scope.storage = new Storage(window.localStorage); + $scope.storage = storage; $scope.coreStart = coreStart; $scope.loading = false; $scope.reduxStore = store; diff --git a/x-pack/legacy/plugins/graph/public/render_app.ts b/x-pack/legacy/plugins/graph/public/application.ts similarity index 82% rename from x-pack/legacy/plugins/graph/public/render_app.ts rename to x-pack/legacy/plugins/graph/public/application.ts index e892643cf8031..69bc789974632 100644 --- a/x-pack/legacy/plugins/graph/public/render_app.ts +++ b/x-pack/legacy/plugins/graph/public/application.ts @@ -11,16 +11,6 @@ import { EuiConfirmModal } from '@elastic/eui'; // They can stay even after NP cutover import angular from 'angular'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; -import 'ui/angular-bootstrap'; -import 'ace'; -import 'ui/kbn_top_nav'; -import { configureAppAngularModule } from 'ui/legacy_compat'; -// @ts-ignore -import { createTopNavDirective, createTopNavHelper } from 'ui/kbn_top_nav/kbn_top_nav'; -// @ts-ignore -import { confirmModalFactory } from 'ui/modals/confirm_modal'; -// @ts-ignore -import { addAppRedirectMessageToUrl } from 'ui/notify'; // type imports import { @@ -31,6 +21,13 @@ import { ToastsStart, IUiSettingsClient, } from 'kibana/public'; +import { + configureAppAngularModule, + createTopNavDirective, + createTopNavHelper, + confirmModalFactory, + addAppRedirectMessageToUrl, +} from './legacy_imports'; // @ts-ignore import { initGraphApp } from './app'; import { @@ -40,6 +37,8 @@ import { import { LicensingPluginSetup } from '../../../../plugins/licensing/public'; import { checkLicense } from '../../../../plugins/graph/common/check_license'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../src/plugins/navigation/public'; +import { createSavedWorkspacesLoader } from './services/persistence/saved_workspace_loader'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; /** * These are dependencies of the Graph app besides the base dependencies @@ -47,7 +46,7 @@ import { NavigationPublicPluginStart as NavigationStart } from '../../../../../s * plugins in LP-world, but if they are migrated only the import path in the plugin * itself changes */ -export interface GraphDependencies extends LegacyAngularInjectedDependencies { +export interface GraphDependencies { element: HTMLElement; appBasePath: string; capabilities: Record>; @@ -62,27 +61,11 @@ export interface GraphDependencies extends LegacyAngularInjectedDependencies { savedObjectsClient: SavedObjectsClientContract; addBasePath: (url: string) => string; getBasePath: () => string; - Storage: any; + storage: Storage; canEditDrillDownUrls: boolean; graphSavePolicy: string; } -/** - * Dependencies of the Graph app which rely on the global angular instance. - * These dependencies have to be migrated to their NP counterparts. - */ -export interface LegacyAngularInjectedDependencies { - /** - * Instance of SavedObjectRegistryProvider - */ - savedObjectRegistry: any; - kbnBaseUrl: any; - /** - * Instance of SavedWorkspacesProvider - */ - savedGraphWorkspaces: any; -} - export const renderApp = ({ appBasePath, element, ...deps }: GraphDependencies) => { const graphAngularModule = createLocalAngularModule(deps.navigation); configureAppAngularModule(graphAngularModule, deps.coreStart as LegacyCoreStart, true); @@ -92,12 +75,20 @@ export const renderApp = ({ appBasePath, element, ...deps }: GraphDependencies) const licenseAllowsToShowThisPage = info.showAppLink && info.enableAppLink; if (!licenseAllowsToShowThisPage) { - const newUrl = addAppRedirectMessageToUrl(deps.addBasePath(deps.kbnBaseUrl), info.message); + const newUrl = addAppRedirectMessageToUrl(deps.addBasePath('/app/kibana'), info.message); window.location.href = newUrl; } }); - initGraphApp(graphAngularModule, deps); + const savedWorkspaceLoader = createSavedWorkspacesLoader({ + chrome: deps.coreStart.chrome, + indexPatterns: deps.npData.indexPatterns, + overlays: deps.coreStart.overlays, + savedObjectsClient: deps.coreStart.savedObjects.client, + basePath: deps.coreStart.http.basePath, + }); + + initGraphApp(graphAngularModule, { ...deps, savedWorkspaceLoader }); const $injector = mountGraphApp(appBasePath, element); return () => { licenseSubscription.unsubscribe(); diff --git a/x-pack/legacy/plugins/graph/public/index.ts b/x-pack/legacy/plugins/graph/public/index.ts index 995e261a5c7a7..d9854acb9332c 100644 --- a/x-pack/legacy/plugins/graph/public/index.ts +++ b/x-pack/legacy/plugins/graph/public/index.ts @@ -4,41 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -// legacy imports currently necessary to power Graph -// for a cutover all of these have to be resolved -import 'uiExports/savedObjectTypes'; -import 'uiExports/autocompleteProviders'; -import 'ui/autoload/all'; -import chrome from 'ui/chrome'; -import { IPrivate } from 'ui/private'; -// @ts-ignore -import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; - import { npSetup, npStart } from 'ui/new_platform'; -import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { LicensingPluginSetup } from '../../../../plugins/licensing/public'; import { GraphPlugin } from './plugin'; -// @ts-ignore -import { SavedWorkspacesProvider } from './angular/services/saved_workspaces'; -import { LegacyAngularInjectedDependencies } from './render_app'; - -/** - * Get dependencies relying on the global angular context. - * They also have to get resolved together with the legacy imports above - */ -async function getAngularInjectedDependencies(): Promise { - const injector = await chrome.dangerouslyGetActiveInjector(); - - const Private = injector.get('Private'); - - return { - savedObjectRegistry: Private(SavedObjectRegistryProvider), - kbnBaseUrl: injector.get('kbnBaseUrl'), - savedGraphWorkspaces: Private(SavedWorkspacesProvider), - }; -} - type XpackNpSetupDeps = typeof npSetup.plugins & { licensing: LicensingPluginSetup; }; @@ -46,16 +15,10 @@ type XpackNpSetupDeps = typeof npSetup.plugins & { (async () => { const instance = new GraphPlugin(); instance.setup(npSetup.core, { - __LEGACY: { - Storage, - }, ...(npSetup.plugins as XpackNpSetupDeps), }); instance.start(npStart.core, { npData: npStart.plugins.data, navigation: npStart.plugins.navigation, - __LEGACY: { - angularDependencies: await getAngularInjectedDependencies(), - }, }); })(); diff --git a/x-pack/legacy/plugins/graph/public/legacy_imports.ts b/x-pack/legacy/plugins/graph/public/legacy_imports.ts new file mode 100644 index 0000000000000..7ea2cf6dd901b --- /dev/null +++ b/x-pack/legacy/plugins/graph/public/legacy_imports.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 'ui/angular-bootstrap'; +import 'ace'; + +export { SavedObject, SavedObjectKibanaServices } from 'ui/saved_objects/types'; +export { configureAppAngularModule } from 'ui/legacy_compat'; +// @ts-ignore +export { createTopNavDirective, createTopNavHelper } from 'ui/kbn_top_nav/kbn_top_nav'; +// @ts-ignore +export { confirmModalFactory } from 'ui/modals/confirm_modal'; +// @ts-ignore +export { addAppRedirectMessageToUrl } from 'ui/notify'; +export { SaveResult } from 'ui/saved_objects/show_saved_object_save_modal'; +export { createSavedObjectClass } from 'ui/saved_objects/saved_object'; +export { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; diff --git a/x-pack/legacy/plugins/graph/public/plugin.ts b/x-pack/legacy/plugins/graph/public/plugin.ts index f30742f2c00d2..ab610d76be101 100644 --- a/x-pack/legacy/plugins/graph/public/plugin.ts +++ b/x-pack/legacy/plugins/graph/public/plugin.ts @@ -7,7 +7,7 @@ // NP type imports import { CoreSetup, CoreStart, Plugin, SavedObjectsClientContract } from 'src/core/public'; import { Plugin as DataPlugin } from 'src/plugins/data/public'; -import { LegacyAngularInjectedDependencies } from './render_app'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { LicensingPluginSetup } from '../../../../plugins/licensing/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../src/plugins/navigation/public'; @@ -17,30 +17,20 @@ export interface GraphPluginStartDependencies { } export interface GraphPluginSetupDependencies { - __LEGACY: { - Storage: any; - }; licensing: LicensingPluginSetup; } -export interface GraphPluginStartDependencies { - __LEGACY: { - angularDependencies: LegacyAngularInjectedDependencies; - }; -} - export class GraphPlugin implements Plugin { private navigationStart: NavigationStart | null = null; private npDataStart: ReturnType | null = null; private savedObjectsClient: SavedObjectsClientContract | null = null; - private angularDependencies: LegacyAngularInjectedDependencies | null = null; - setup(core: CoreSetup, { __LEGACY: { Storage }, licensing }: GraphPluginSetupDependencies) { + setup(core: CoreSetup, { licensing }: GraphPluginSetupDependencies) { core.application.register({ id: 'graph', title: 'Graph', mount: async ({ core: contextCore }, params) => { - const { renderApp } = await import('./render_app'); + const { renderApp } = await import('./application'); return renderApp({ ...params, licensing, @@ -53,26 +43,21 @@ export class GraphPlugin implements Plugin { 'canEditDrillDownUrls' ) as boolean, graphSavePolicy: core.injectedMetadata.getInjectedVar('graphSavePolicy') as string, - Storage, + storage: new Storage(window.localStorage), capabilities: contextCore.application.capabilities.graph, coreStart: contextCore, chrome: contextCore.chrome, config: contextCore.uiSettings, toastNotifications: contextCore.notifications.toasts, indexPatterns: this.npDataStart!.indexPatterns, - ...this.angularDependencies!, }); }, }); } - start( - core: CoreStart, - { npData, navigation, __LEGACY: { angularDependencies } }: GraphPluginStartDependencies - ) { + start(core: CoreStart, { npData, navigation }: GraphPluginStartDependencies) { this.navigationStart = navigation; this.npDataStart = npData; - this.angularDependencies = angularDependencies; this.savedObjectsClient = core.savedObjects.client; } diff --git a/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace.ts b/x-pack/legacy/plugins/graph/public/services/persistence/saved_workspace.ts similarity index 93% rename from x-pack/legacy/plugins/graph/public/angular/services/saved_workspace.ts rename to x-pack/legacy/plugins/graph/public/services/persistence/saved_workspace.ts index bcde72a02f02e..d25fcc89d6a1f 100644 --- a/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace.ts +++ b/x-pack/legacy/plugins/graph/public/services/persistence/saved_workspace.ts @@ -3,10 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { SavedObject, SavedObjectKibanaServices } from 'ui/saved_objects/types'; -import { createSavedObjectClass } from 'ui/saved_objects/saved_object'; import { i18n } from '@kbn/i18n'; import { extractReferences, injectReferences } from './saved_workspace_references'; +import { + createSavedObjectClass, + SavedObject, + SavedObjectKibanaServices, +} from '../../legacy_imports'; export interface SavedWorkspace extends SavedObject { wsState?: string; diff --git a/x-pack/legacy/plugins/graph/public/angular/services/saved_workspaces.ts b/x-pack/legacy/plugins/graph/public/services/persistence/saved_workspace_loader.ts similarity index 78% rename from x-pack/legacy/plugins/graph/public/angular/services/saved_workspaces.ts rename to x-pack/legacy/plugins/graph/public/services/persistence/saved_workspace_loader.ts index e28bb60fb466b..8ddff0be0d06d 100644 --- a/x-pack/legacy/plugins/graph/public/angular/services/saved_workspaces.ts +++ b/x-pack/legacy/plugins/graph/public/services/persistence/saved_workspace_loader.ts @@ -4,24 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { npSetup, npStart } from 'ui/new_platform'; -import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; +import { IBasePath } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { createSavedWorkspaceClass } from './saved_workspace'; +import { SavedObjectKibanaServices } from '../../legacy_imports'; -export function SavedWorkspacesProvider() { - const savedObjectsClient = npStart.core.savedObjects.client; - const services = { - savedObjectsClient, - indexPatterns: npStart.plugins.data.indexPatterns, - chrome: npStart.core.chrome, - overlays: npStart.core.overlays, - }; - +export function createSavedWorkspacesLoader( + services: SavedObjectKibanaServices & { basePath: IBasePath } +) { + const { savedObjectsClient, basePath } = services; const SavedWorkspace = createSavedWorkspaceClass(services); const urlFor = (id: string) => - npSetup.core.http.basePath.prepend(`/app/graph#/workspace/${encodeURIComponent(id)}`); + basePath.prepend(`/app/graph#/workspace/${encodeURIComponent(id)}`); const mapHits = (hit: { id: string; attributes: Record }) => { const source = hit.attributes; source.id = hit.id; @@ -72,5 +67,3 @@ export function SavedWorkspacesProvider() { }, }; } - -SavedObjectRegistryProvider.register(SavedWorkspacesProvider); diff --git a/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace_references.test.ts b/x-pack/legacy/plugins/graph/public/services/persistence/saved_workspace_references.test.ts similarity index 100% rename from x-pack/legacy/plugins/graph/public/angular/services/saved_workspace_references.test.ts rename to x-pack/legacy/plugins/graph/public/services/persistence/saved_workspace_references.test.ts diff --git a/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace_references.ts b/x-pack/legacy/plugins/graph/public/services/persistence/saved_workspace_references.ts similarity index 100% rename from x-pack/legacy/plugins/graph/public/angular/services/saved_workspace_references.ts rename to x-pack/legacy/plugins/graph/public/services/persistence/saved_workspace_references.ts diff --git a/x-pack/legacy/plugins/graph/public/services/save_modal.tsx b/x-pack/legacy/plugins/graph/public/services/save_modal.tsx index 5930d2283b7c0..d949ac1d4a600 100644 --- a/x-pack/legacy/plugins/graph/public/services/save_modal.tsx +++ b/x-pack/legacy/plugins/graph/public/services/save_modal.tsx @@ -5,9 +5,9 @@ */ import React from 'react'; -import { SaveResult } from 'ui/saved_objects/show_saved_object_save_modal'; import { GraphWorkspaceSavedObject, GraphSavePolicy } from '../types'; import { SaveModal, OnSaveGraphProps } from '../components/save_modal'; +import { SaveResult } from '../legacy_imports'; export type SaveWorkspaceHandler = ( saveOptions: { diff --git a/x-pack/legacy/plugins/graph/public/types/persistence.ts b/x-pack/legacy/plugins/graph/public/types/persistence.ts index 7883e81fb9b8e..7fc5e15d9ea72 100644 --- a/x-pack/legacy/plugins/graph/public/types/persistence.ts +++ b/x-pack/legacy/plugins/graph/public/types/persistence.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObject } from 'ui/saved_objects/types'; import { AdvancedSettings, UrlTemplate, WorkspaceField } from './app_state'; import { WorkspaceNode, WorkspaceEdge } from './workspace_state'; +import { SavedObject } from '../legacy_imports'; type Omit = Pick>; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap b/x-pack/legacy/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap index a7cc8843140c8..bd42346e36135 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap +++ b/x-pack/legacy/plugins/index_lifecycle_management/__jest__/__snapshots__/extend_index_management.test.js.snap @@ -263,16 +263,18 @@ exports[`ilm summary extension should return extension when index has lifecycle type="cross" >