diff --git a/.browserslistrc b/.browserslistrc index a788f9544ab8a..89114f393c462 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -1,3 +1,9 @@ +[production] last 2 versions > 5% Safari 7 # for PhantomJS support: https://github.com/elastic/kibana/issues/27136 + +[dev] +last 1 chrome versions +last 1 firefox versions +last 1 safari versions diff --git a/.eslintrc.js b/.eslintrc.js index abfe5e0a6cc27..9b00135df5bac 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -355,13 +355,7 @@ module.exports = { settings: { // instructs import/no-extraneous-dependencies to treat certain modules // as core modules, even if they aren't listed in package.json - 'import/core-modules': [ - 'plugins', - 'legacy/ui', - 'uiExports', - // TODO: Remove once https://github.com/benmosher/eslint-plugin-import/issues/1374 is fixed - 'querystring', - ], + 'import/core-modules': ['plugins', 'legacy/ui', 'uiExports'], 'import/resolver': { '@kbn/eslint-import-resolver-kibana': { diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7901bd331edff..bf1e341c796fa 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -103,6 +103,7 @@ /packages/*babel*/ @elastic/kibana-operations /packages/kbn-dev-utils*/ @elastic/kibana-operations /packages/kbn-es/ @elastic/kibana-operations +/packages/kbn-optimizer/ @elastic/kibana-operations /packages/kbn-pm/ @elastic/kibana-operations /packages/kbn-test/ @elastic/kibana-operations /packages/kbn-ui-shared-deps/ @elastic/kibana-operations diff --git a/.i18nrc.json b/.i18nrc.json index c171b842254ee..6874d02304e49 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -37,7 +37,10 @@ "savedObjects": "src/plugins/saved_objects", "server": "src/legacy/server", "statusPage": "src/legacy/core_plugins/status_page", - "telemetry": "src/legacy/core_plugins/telemetry", + "telemetry": [ + "src/legacy/core_plugins/telemetry", + "src/plugins/telemetry" + ], "tileMap": "src/legacy/core_plugins/tile_map", "timelion": ["src/legacy/core_plugins/timelion", "src/legacy/core_plugins/vis_type_timelion", "src/plugins/timelion"], "uiActions": "src/plugins/ui_actions", diff --git a/Jenkinsfile b/Jenkinsfile index 4e6f3141a12e7..1b4350d5b91e9 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -14,11 +14,11 @@ stage("Kibana Pipeline") { // This stage is just here to help the BlueOcean UI a 'kibana-intake-agent': kibanaPipeline.intakeWorker('kibana-intake', './test/scripts/jenkins_unit.sh'), 'x-pack-intake-agent': kibanaPipeline.intakeWorker('x-pack-intake', './test/scripts/jenkins_xpack.sh'), 'kibana-oss-agent': kibanaPipeline.withWorkers('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ - 'oss-firefoxSmoke': kibanaPipeline.getPostBuildWorker('firefoxSmoke', { - retryable('kibana-firefoxSmoke') { - runbld('./test/scripts/jenkins_firefox_smoke.sh', 'Execute kibana-firefoxSmoke') - } - }), + // 'oss-firefoxSmoke': kibanaPipeline.getPostBuildWorker('firefoxSmoke', { + // retryable('kibana-firefoxSmoke') { + // runbld('./test/scripts/jenkins_firefox_smoke.sh', 'Execute kibana-firefoxSmoke') + // } + // }), 'oss-ciGroup1': kibanaPipeline.getOssCiGroupWorker(1), 'oss-ciGroup2': kibanaPipeline.getOssCiGroupWorker(2), 'oss-ciGroup3': kibanaPipeline.getOssCiGroupWorker(3), @@ -39,11 +39,11 @@ stage("Kibana Pipeline") { // This stage is just here to help the BlueOcean UI a // 'oss-visualRegression': kibanaPipeline.getPostBuildWorker('visualRegression', { runbld('./test/scripts/jenkins_visual_regression.sh', 'Execute kibana-visualRegression') }), ]), 'kibana-xpack-agent': kibanaPipeline.withWorkers('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ - 'xpack-firefoxSmoke': kibanaPipeline.getPostBuildWorker('xpack-firefoxSmoke', { - retryable('xpack-firefoxSmoke') { - runbld('./test/scripts/jenkins_xpack_firefox_smoke.sh', 'Execute xpack-firefoxSmoke') - } - }), + // 'xpack-firefoxSmoke': kibanaPipeline.getPostBuildWorker('xpack-firefoxSmoke', { + // retryable('xpack-firefoxSmoke') { + // runbld('./test/scripts/jenkins_xpack_firefox_smoke.sh', 'Execute xpack-firefoxSmoke') + // } + // }), 'xpack-ciGroup1': kibanaPipeline.getXpackCiGroupWorker(1), 'xpack-ciGroup2': kibanaPipeline.getXpackCiGroupWorker(2), 'xpack-ciGroup3': kibanaPipeline.getXpackCiGroupWorker(3), diff --git a/NOTICE.txt b/NOTICE.txt index 69be6db72cff2..33c1d535d7df3 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -218,28 +218,3 @@ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---- -This product includes code that was extracted from angular@1.3. -Original license: -The MIT License - -Copyright (c) 2010-2014 Google, Inc. http://angularjs.org - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc index c4611f3b41e55..c6174e1786c78 100644 --- a/docs/apm/troubleshooting.asciidoc +++ b/docs/apm/troubleshooting.asciidoc @@ -73,10 +73,9 @@ You can also use the Agent's public API to manually set a name for the transacti ==== Fields are not searchable -In Elasticsearch, index patterns are used to define settings and mappings that determine how fields should be analyzed. -The recommended index template file for APM Server is installed when Kibana starts. -This template defines which fields are available in Kibana for features like the Kuery bar, -or for linking to other plugins like Logs, Uptime, and Discover. +In Elasticsearch, index templates are used to define settings and mappings that determine how fields should be analyzed. +The recommended index template file for APM Server is installed by the APM Server packages. +This template, by default, enables and disables indexing on certain fields. As an example, some agents store cookie values in `http.request.cookies`. Since `http.request` has disabled dynamic indexing, and `http.request.cookies` is not declared in a custom mapping, diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.currentappid_.md b/docs/development/core/public/kibana-plugin-public.applicationstart.currentappid_.md new file mode 100644 index 0000000000000..d3ceeabcd81f4 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.currentappid_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationStart](./kibana-plugin-public.applicationstart.md) > [currentAppId$](./kibana-plugin-public.applicationstart.currentappid_.md) + +## ApplicationStart.currentAppId$ property + +An observable that emits the current application id and each subsequent id update. + +Signature: + +```typescript +currentAppId$: Observable; +``` diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.geturlforapp.md b/docs/development/core/public/kibana-plugin-public.applicationstart.geturlforapp.md index 7eadd4d4e9d44..1ae368a11674f 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.geturlforapp.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.geturlforapp.md @@ -4,13 +4,16 @@ ## ApplicationStart.getUrlForApp() method -Returns a relative URL to a given app, including the global base path. +Returns an URL to a given app, including the global base path. By default, the URL is relative (/basePath/app/my-app). Use the `absolute` option to generate an absolute url (http://host:port/basePath/app/my-app) + +Note that when generating absolute urls, the protocol, host and port are determined from the browser location. Signature: ```typescript getUrlForApp(appId: string, options?: { path?: string; + absolute?: boolean; }): string; ``` @@ -19,7 +22,7 @@ getUrlForApp(appId: string, options?: { | Parameter | Type | Description | | --- | --- | --- | | appId | string | | -| options | {
path?: string;
} | | +| options | {
path?: string;
absolute?: boolean;
} | | Returns: diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.md b/docs/development/core/public/kibana-plugin-public.applicationstart.md index 3ad7e3b1656d8..d5a0bef9470f7 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.md @@ -16,12 +16,13 @@ export interface ApplicationStart | Property | Type | Description | | --- | --- | --- | | [capabilities](./kibana-plugin-public.applicationstart.capabilities.md) | RecursiveReadonly<Capabilities> | Gets the read-only capabilities. | +| [currentAppId$](./kibana-plugin-public.applicationstart.currentappid_.md) | Observable<string | undefined> | An observable that emits the current application id and each subsequent id update. | ## Methods | Method | Description | | --- | --- | -| [getUrlForApp(appId, options)](./kibana-plugin-public.applicationstart.geturlforapp.md) | Returns a relative URL to a given app, including the global base path. | +| [getUrlForApp(appId, options)](./kibana-plugin-public.applicationstart.geturlforapp.md) | Returns an URL to a given app, including the global base path. By default, the URL is relative (/basePath/app/my-app). Use the absolute option to generate an absolute url (http://host:port/basePath/app/my-app)Note that when generating absolute urls, the protocol, host and port are determined from the browser location. | | [navigateToApp(appId, options)](./kibana-plugin-public.applicationstart.navigatetoapp.md) | Navigate to a given app | | [registerMountContext(contextName, provider)](./kibana-plugin-public.applicationstart.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). | diff --git a/docs/development/core/public/kibana-plugin-public.ihttpfetcherror.md b/docs/development/core/public/kibana-plugin-public.ihttpfetcherror.md index 6109671bb1aa6..49287cc6e261e 100644 --- a/docs/development/core/public/kibana-plugin-public.ihttpfetcherror.md +++ b/docs/development/core/public/kibana-plugin-public.ihttpfetcherror.md @@ -16,6 +16,7 @@ export interface IHttpFetchError extends Error | Property | Type | Description | | --- | --- | --- | | [body](./kibana-plugin-public.ihttpfetcherror.body.md) | any | | +| [name](./kibana-plugin-public.ihttpfetcherror.name.md) | string | | | [req](./kibana-plugin-public.ihttpfetcherror.req.md) | Request | | | [request](./kibana-plugin-public.ihttpfetcherror.request.md) | Request | | | [res](./kibana-plugin-public.ihttpfetcherror.res.md) | Response | | diff --git a/docs/images/controls/controls_in_dashboard.png b/docs/images/controls/controls_in_dashboard.png deleted file mode 100644 index 5ea6b3ad0ca88..0000000000000 Binary files a/docs/images/controls/controls_in_dashboard.png and /dev/null differ diff --git a/docs/images/dashboard-controls.png b/docs/images/dashboard-controls.png new file mode 100644 index 0000000000000..d121ce561e341 Binary files /dev/null and b/docs/images/dashboard-controls.png differ diff --git a/docs/images/markdown-example.png b/docs/images/markdown-example.png new file mode 100644 index 0000000000000..79daa1298883d Binary files /dev/null and b/docs/images/markdown-example.png differ diff --git a/docs/images/markdown_example_1.png b/docs/images/markdown_example_1.png new file mode 100644 index 0000000000000..71dd9b76b8caf Binary files /dev/null and b/docs/images/markdown_example_1.png differ diff --git a/docs/images/markdown_example_2.png b/docs/images/markdown_example_2.png new file mode 100644 index 0000000000000..f2094c3cbb3f1 Binary files /dev/null and b/docs/images/markdown_example_2.png differ diff --git a/docs/images/markdown_example_3.png b/docs/images/markdown_example_3.png new file mode 100644 index 0000000000000..eca9735b495d0 Binary files /dev/null and b/docs/images/markdown_example_3.png differ diff --git a/docs/images/markdown_example_4.png b/docs/images/markdown_example_4.png new file mode 100644 index 0000000000000..d4a0829fef64e Binary files /dev/null and b/docs/images/markdown_example_4.png differ diff --git a/docs/infrastructure/images/infra-sysmon.png b/docs/infrastructure/images/infra-sysmon.png index 5b82d8c9b4e19..dd653bb046f45 100644 Binary files a/docs/infrastructure/images/infra-sysmon.png and b/docs/infrastructure/images/infra-sysmon.png differ diff --git a/docs/infrastructure/images/infra-view-metrics.png b/docs/infrastructure/images/infra-view-metrics.png index 9ad862ec6515d..6001f18d283fe 100644 Binary files a/docs/infrastructure/images/infra-view-metrics.png and b/docs/infrastructure/images/infra-view-metrics.png differ diff --git a/docs/infrastructure/images/metrics-add-data.png b/docs/infrastructure/images/metrics-add-data.png index d9640e0d9f5da..f96c30f0e1848 100644 Binary files a/docs/infrastructure/images/metrics-add-data.png and b/docs/infrastructure/images/metrics-add-data.png differ diff --git a/docs/infrastructure/images/metrics-explorer-screen.png b/docs/infrastructure/images/metrics-explorer-screen.png index 7ccf8891678af..6d56491f7d485 100644 Binary files a/docs/infrastructure/images/metrics-explorer-screen.png and b/docs/infrastructure/images/metrics-explorer-screen.png differ diff --git a/docs/limitations.asciidoc b/docs/limitations.asciidoc index 818cc766bf6a9..30a716641cc5d 100644 --- a/docs/limitations.asciidoc +++ b/docs/limitations.asciidoc @@ -1,22 +1,38 @@ +[chapter] [[limitations]] = Limitations -[partintro] --- -{kib} currently has the following limitations. +Following are the known limitations in {kib}. -* <> -* <> -* <> +[float] +=== Exporting data -These {stack} features also have limitations that affect {kib}: +Exporting a data table or saved search from a dashboard or visualization report +has known limitations. The PDF report only includes the data visible on the screen. -* {ref}/watcher-limitations.html[Alerting] -* {ml-docs}/ml-limitations.html[Machine learning] -* {ref}/security-limitations.html[Security] +[float] +=== Nested objects + +Kibana cannot perform aggregations across fields that contain nested objects. +It also cannot search on nested objects when Lucene Query Syntax is used in +the query bar. + +[IMPORTANT] +============================================== +Using `include_in_parent` or `copy_to` as a workaround is not supported and may stop functioning in future releases. +============================================== --- +[float] +=== Graph -include::limitations/nested-objects.asciidoc[] +Graph has limited support for multiple indices. +Go to <> for details. -include::limitations/export-data.asciidoc[] \ No newline at end of file +[float] +=== Other limitations + +These {stack} features have limitations that affect {kib}: + +* {ref}/watcher-limitations.html[Alerting] +* {ml-docs}/ml-limitations.html[Machine learning] +* {ref}/security-limitations.html[Security] diff --git a/docs/limitations/export-data.asciidoc b/docs/limitations/export-data.asciidoc deleted file mode 100644 index 442460c67017c..0000000000000 --- a/docs/limitations/export-data.asciidoc +++ /dev/null @@ -1,5 +0,0 @@ -[[export-data]] -== Exporting data - -Exporting a data table or saved search from a dashboard or visualization report -has known limitations. The PDF report only includes the data visible on the screen. \ No newline at end of file diff --git a/docs/limitations/nested-objects.asciidoc b/docs/limitations/nested-objects.asciidoc deleted file mode 100644 index 214f33eef5c42..0000000000000 --- a/docs/limitations/nested-objects.asciidoc +++ /dev/null @@ -1,11 +0,0 @@ -[[nested-objects]] -== Nested Objects - -Kibana cannot perform aggregations across fields that contain nested objects. -It also cannot search on nested objects when Lucene Query Syntax is used in -the query bar. - -[IMPORTANT] -============================================== -Using `include_in_parent` or `copy_to` as a workaround is not supported and may stop functioning in future releases. -============================================== diff --git a/docs/logs/images/log-rate-anomalies.png b/docs/logs/images/log-rate-anomalies.png index ac9ff7c9a5235..74ce8d682e1cc 100644 Binary files a/docs/logs/images/log-rate-anomalies.png and b/docs/logs/images/log-rate-anomalies.png differ diff --git a/docs/logs/images/log-rate-entries.png b/docs/logs/images/log-rate-entries.png index f8a3acc9883e0..efa693a2ac529 100644 Binary files a/docs/logs/images/log-rate-entries.png and b/docs/logs/images/log-rate-entries.png differ diff --git a/docs/logs/images/log-time-filter.png b/docs/logs/images/log-time-filter.png index 863e488e6c6c0..ffba6f972aeb7 100644 Binary files a/docs/logs/images/log-time-filter.png and b/docs/logs/images/log-time-filter.png differ diff --git a/docs/logs/images/logs-add-data.png b/docs/logs/images/logs-add-data.png index 2c4a65590aa1b..176c71466aa38 100644 Binary files a/docs/logs/images/logs-add-data.png and b/docs/logs/images/logs-add-data.png differ diff --git a/docs/logs/images/logs-console.png b/docs/logs/images/logs-console.png index 5feb3d9608974..8e94c31c6862a 100644 Binary files a/docs/logs/images/logs-console.png and b/docs/logs/images/logs-console.png differ diff --git a/docs/logs/using.asciidoc b/docs/logs/using.asciidoc index f191f7d746cf8..d84a9260521c7 100644 --- a/docs/logs/using.asciidoc +++ b/docs/logs/using.asciidoc @@ -8,7 +8,6 @@ You can also view related application traces or uptime information where availab [role="screenshot"] image::logs/images/logs-console.png[Logs Console in Kibana] -// ++ Update this [float] [[logs-search]] diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 4eddb1779a26a..c1f06aff722b5 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -447,7 +447,7 @@ us improve your user experience. Your data is never shared with anyone. Set to `false` to disable telemetry capabilities entirely. You can alternatively opt out through the *Advanced Settings* in {kib}. -`vega.enableExternalUrls:`:: *Default: false* Set this value to true to allow Vega to use any URL to access external data sources and images. If false, Vega can only get data from Elasticsearch. +`vis_type_vega.enableExternalUrls:`:: *Default: false* Set this value to true to allow Vega to use any URL to access external data sources and images. If false, Vega can only get data from Elasticsearch. `xpack.license_management.enabled`:: *Default: true* Set this value to false to disable the License Management user interface. diff --git a/docs/user/visualize.asciidoc b/docs/user/visualize.asciidoc index 5692fe6d1ae01..1bcbd51a9629a 100644 --- a/docs/user/visualize.asciidoc +++ b/docs/user/visualize.asciidoc @@ -53,9 +53,9 @@ data sets. * *<>* [horizontal] -<>:: Provides the ability to add interactive inputs to a Dashboard. +Controls:: Adds interactive inputs to a Dashboard. -<>:: Display free-form information or instructions. +Markdown widget:: Display free-form information or instructions. * *For developers* [horizontal] diff --git a/docs/visualize/for-dashboard.asciidoc b/docs/visualize/for-dashboard.asciidoc index a197998ecdc9d..d6e39d35b7b23 100644 --- a/docs/visualize/for-dashboard.asciidoc +++ b/docs/visualize/for-dashboard.asciidoc @@ -1,117 +1,51 @@ [[for-dashboard]] -== Markdown and controls - -[float] -[[markdown-widget]] -=== Markdown widget - -The Markdown widget is a text entry field that accepts GitHub-flavored Markdown text. Kibana renders the text you enter -in this field and displays the results on the dashboard. You can click the *Help* link to go to the -https://help.github.com/articles/github-flavored-markdown/[help page] for GitHub flavored Markdown. From the widget -you can: - -* Click *Apply* to display the rendered text in the Preview panel -* Click *Discard* to revert to a previously saved version +== Dashboard tools +Visualize comes with controls and Markdown tools that you can add to dashboards for an interactive experience. [float] [[controls]] -=== Controls widget +=== Controls experimental[] -The Controls widget enables you to add interactive inputs -to a dashboard. You can create two types of inputs: +The controls tool enables you to add interactive inputs +on a dashboard. -* Dropdown menu -* Radio slider +You can add two types of interactive inputs: -[role="screenshot"] -image::images/controls/controls_in_dashboard.png[] +* *Options list* - Filters content based on one or more specified options. The dropdown menu is dynamically populated with the results of a terms aggregation. For example, use the options list on the sample flight dashboard when you want to filter the data by origin city and destination city. -[float] -[[add-input-controls]] -=== Add input controls - -To start a *Controls* visualization, open the Visualization application -and click the *+* button. Scroll to the *Others* section and -select *Controls*. - -In the visualization builder, choose the type of control to add to -your visualization. - -[float] -==== Dropdown menu - -A dropdown menu allows users to filter content by selecting -one or more options from a list. The dropdown menu is dynamically populated -with the results of a terms aggregation. +* *Range slider* - Filters data within a specified range of numbers. The minimum and maximum values are dynamically populated with the results of a min and max aggregation. For example, use the range slider when you want to filter the sample flight dashboard by a specific average ticket price. [role="screenshot"] -image::images/controls/dropdown_control_editor.png[] - -*Control Label*:: The label for the dropdown menu. By default, the -label is the field name. - -*Index Pattern*:: The <> that contains -the data set to visualize. - -*Field*:: The field used to populate the list of options -and filter on when users interact with the input. -The list of available fields is derived from the specified -index pattern. - -*Parent control*:: The control for chaining dropdown menus so that the -selection in the first menu -filters the terms in the second menu. Only available when -creating multiple dropdown menus. - -*Multiselect*:: When enabled, the dropdown menu allows users to select multiple options. - -*Size*:: The number of options to include in the list. +image::images/dashboard-controls.png[] [float] -==== Range slider +[[markdown-widget]] +=== Markdown -A range sliders allow users to filter content within a range of numbers. -The range slider minimum and maximum values are dynamically populated with -the results of a min and max aggregation. +The Markdown tool is a text entry field that accepts GitHub-flavored Markdown text. When you enter the text, the tool populates the results on the dashboard. -[role="screenshot"] -image::images/controls/range_slider_editor.png[] +Markdown is helpful when you want to include important information, instructions, and images on your dashboard. -*Control Label*:: The label for the range slider. By default, the -label is the field name. +For information about GitHub-flavored Markdown text, click *Help*. -*Index Pattern*:: The <> that contains -the data set to visualize. +For example, when you enter: -*Field*:: The field used to populate the range slider -and filter on when users interact with the input. -The list of available fields is derived from the -specified index pattern. - -*Step Size*:: The increment/decrement size of the slider. +[role="screenshot"] +image::images/markdown_example_1.png[] -*Decimal Places*:: The number of decimal places. +The following instructions are displayed: -[float] -[[global-options]] -=== Global options +[role="screenshot"] +image::images/markdown_example_2.png[] -Open the *Options* tab to configure settings that apply to all input -controls in a Controls visualization. +Or when you enter: [role="screenshot"] -image::images/controls/controls_options.png[] - -*Update Kibana filters on each change*:: When enabled, all input interactions -immediately create filters that cause the dashboard to refresh. When disabled, -Kibana filters are only created -when the user clicks *Apply changes* image:images/apply-changes-button.png[]. +image::images/markdown_example_3.png[] -*Use time filter*:: When enabled, the aggregations used to generate -the dropdown options list and range minimum and maximum are bound -to <>. +The following image is displayed: -*Pin filters to global state*:: When enabled, all filters created by -interacting with the inputs are automatically pinned. +[role="screenshot"] +image::images/markdown_example_4.png[] diff --git a/docs/visualize/most-frequent.asciidoc b/docs/visualize/most-frequent.asciidoc index 2cb8aa7cb3c1f..ba291e3cc6859 100644 --- a/docs/visualize/most-frequent.asciidoc +++ b/docs/visualize/most-frequent.asciidoc @@ -11,6 +11,8 @@ The most frequently used visualizations include: * Metric, goal, and gauge * Tag cloud +[[metric-chart]] + [float] [[frequently-used-viz-aggregation]] === Supported aggregations diff --git a/package.json b/package.json index c3762c2eabd28..26e1112ead697 100644 --- a/package.json +++ b/package.json @@ -137,12 +137,6 @@ "@kbn/test-subj-selector": "0.2.1", "@kbn/ui-framework": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", - "@types/flot": "^0.0.31", - "@types/json-stable-stringify": "^1.0.32", - "@types/lodash.clonedeep": "^4.5.4", - "@types/node-forge": "^0.9.0", - "@types/react-grid-layout": "^0.16.7", - "@types/recompose": "^0.30.5", "JSONStream": "1.3.5", "abort-controller": "^3.0.0", "angular": "^1.7.9", @@ -152,11 +146,12 @@ "angular-route": "^1.7.9", "angular-sanitize": "^1.7.9", "angular-sortable-view": "^0.0.17", - "autoprefixer": "9.6.1", + "autoprefixer": "^9.7.4", "babel-loader": "^8.0.6", "bluebird": "3.5.5", "boom": "^7.2.0", "brace": "0.11.1", + "browserslist-useragent": "^3.0.2", "cache-loader": "^4.1.0", "chalk": "^2.4.2", "check-disk-space": "^2.1.0", @@ -165,7 +160,7 @@ "commander": "3.0.2", "compare-versions": "3.5.1", "core-js": "^3.2.1", - "css-loader": "2.1.1", + "css-loader": "^3.4.2", "d3": "3.5.17", "d3-cloud": "1.2.5", "deep-freeze-strict": "^1.1.1", @@ -226,11 +221,11 @@ "opn": "^5.5.0", "oppsy": "^2.0.0", "pegjs": "0.10.0", - "postcss-loader": "3.0.0", + "postcss-loader": "^3.0.0", "prop-types": "15.6.0", "proxy-from-env": "1.0.0", "pug": "^2.0.4", - "querystring-browser": "1.0.4", + "query-string": "6.10.1", "raw-loader": "3.1.0", "react": "^16.12.0", "react-color": "^2.13.8", @@ -259,7 +254,7 @@ "seedrandom": "^3.0.5", "semver": "^5.5.0", "style-it": "^2.1.3", - "style-loader": "0.23.1", + "style-loader": "^1.1.3", "symbol-observable": "^1.2.0", "tar": "4.4.13", "terser-webpack-plugin": "^2.3.4", @@ -279,7 +274,7 @@ "vega-schema-url-parser": "1.0.0", "vega-tooltip": "^0.12.0", "vision": "^5.3.3", - "webpack": "4.41.0", + "webpack": "^4.41.5", "webpack-merge": "4.2.2", "whatwg-fetch": "^3.0.0", "wrapper-webpack-plugin": "^2.1.0", @@ -300,6 +295,7 @@ "@kbn/eslint-plugin-eslint": "1.0.0", "@kbn/expect": "1.0.0", "@kbn/plugin-generator": "1.0.0", + "@kbn/optimizer": "1.0.0", "@kbn/test": "1.0.0", "@kbn/utility-types": "1.0.0", "@microsoft/api-documenter": "7.7.2", @@ -312,6 +308,7 @@ "@types/babel__core": "^7.1.2", "@types/bluebird": "^3.1.1", "@types/boom": "^7.2.0", + "@types/browserslist-useragent": "^3.0.0", "@types/chance": "^1.0.0", "@types/cheerio": "^0.22.10", "@types/chromedriver": "^2.38.0", @@ -324,6 +321,7 @@ "@types/enzyme": "^3.9.0", "@types/eslint": "^6.1.3", "@types/fetch-mock": "^7.3.1", + "@types/flot": "^0.0.31", "@types/getopts": "^2.0.1", "@types/glob": "^7.1.1", "@types/globby": "^8.0.0", @@ -337,10 +335,12 @@ "@types/joi": "^13.4.2", "@types/jquery": "^3.3.31", "@types/js-yaml": "^3.11.1", + "@types/json-stable-stringify": "^1.0.32", "@types/json5": "^0.0.30", "@types/license-checker": "15.0.0", "@types/listr": "^0.14.0", "@types/lodash": "^3.10.1", + "@types/lodash.clonedeep": "^4.5.4", "@types/lru-cache": "^5.1.0", "@types/markdown-it": "^0.0.7", "@types/minimatch": "^2.0.29", @@ -348,6 +348,7 @@ "@types/moment-timezone": "^0.5.12", "@types/mustache": "^0.8.31", "@types/node": "^10.12.27", + "@types/node-forge": "^0.9.0", "@types/numeral": "^0.0.26", "@types/opn": "^5.1.0", "@types/pegjs": "^0.10.1", @@ -357,11 +358,13 @@ "@types/reach__router": "^1.2.6", "@types/react": "^16.9.11", "@types/react-dom": "^16.9.4", + "@types/react-grid-layout": "^0.16.7", "@types/react-redux": "^6.0.6", "@types/react-resize-detector": "^4.0.1", "@types/react-router": "^5.1.3", "@types/react-router-dom": "^5.1.3", "@types/react-virtualized": "^9.18.7", + "@types/recompose": "^0.30.6", "@types/redux": "^3.6.31", "@types/redux-actions": "^2.6.1", "@types/request": "^2.48.2", @@ -461,7 +464,7 @@ "pixelmatch": "^5.1.0", "pkg-up": "^2.0.0", "pngjs": "^3.4.0", - "postcss": "^7.0.5", + "postcss": "^7.0.26", "postcss-url": "^8.0.0", "prettier": "^1.19.1", "proxyquire": "1.8.0", diff --git a/packages/kbn-babel-preset/node_preset.js b/packages/kbn-babel-preset/node_preset.js index c7809f28fec7b..ee06e2588b022 100644 --- a/packages/kbn-babel-preset/node_preset.js +++ b/packages/kbn-babel-preset/node_preset.js @@ -54,7 +54,12 @@ module.exports = (_, options = {}) => { // on their own useBuiltIns: 'entry', modules: 'cjs', - corejs: 3, + // right now when using `corejs: 3` babel does not use the latest available + // core-js version due to a bug: https://github.com/babel/babel/issues/10816 + // Because of that we should use for that value the same version we install + // in the package.json in order to have the same polyfills between the environment + // and the tests + corejs: '3.2.1', ...(options['@babel/preset-env'] || {}), }, diff --git a/packages/kbn-babel-preset/webpack_preset.js b/packages/kbn-babel-preset/webpack_preset.js index e6a8bd81b602e..d76a3e9714838 100644 --- a/packages/kbn-babel-preset/webpack_preset.js +++ b/packages/kbn-babel-preset/webpack_preset.js @@ -25,7 +25,9 @@ module.exports = () => { { useBuiltIns: 'entry', modules: false, - corejs: 3, + // Please read the explanation for this + // in node_preset.js + corejs: '3.2.1', }, ], require('./common_preset'), diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index 714ed56ac4703..305e29a0e41df 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -18,12 +18,7 @@ */ export { withProcRunner, ProcRunner } from './proc_runner'; -export { - ToolingLog, - ToolingLogTextWriter, - pickLevelFromFlags, - ToolingLogCollectingWriter, -} from './tooling_log'; +export * from './tooling_log'; export { createAbsolutePathSerializer } from './serializers'; export { CA_CERT_PATH, diff --git a/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.ts b/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.ts index 661ed7329347f..9edc63dd7d842 100644 --- a/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.ts +++ b/packages/kbn-dev-utils/src/serializers/absolute_path_serializer.ts @@ -17,7 +17,9 @@ * under the License. */ -export function createAbsolutePathSerializer(rootPath: string) { +import { REPO_ROOT } from '../repo_root'; + +export function createAbsolutePathSerializer(rootPath: string = REPO_ROOT) { return { print: (value: string) => value.replace(rootPath, '').replace(/\\/g, '/'), test: (value: any) => typeof value === 'string' && value.startsWith(rootPath), diff --git a/packages/kbn-dev-utils/src/tooling_log/index.ts b/packages/kbn-dev-utils/src/tooling_log/index.ts index 1f5afac26d561..f8009a255f010 100644 --- a/packages/kbn-dev-utils/src/tooling_log/index.ts +++ b/packages/kbn-dev-utils/src/tooling_log/index.ts @@ -19,5 +19,5 @@ export { ToolingLog } from './tooling_log'; export { ToolingLogTextWriter, ToolingLogTextWriterConfig } from './tooling_log_text_writer'; -export { pickLevelFromFlags, LogLevel } from './log_levels'; +export { pickLevelFromFlags, parseLogLevel, LogLevel } from './log_levels'; export { ToolingLogCollectingWriter } from './tooling_log_collecting_writer'; diff --git a/packages/kbn-dev-utils/src/tooling_log/tooling_log_text_writer.ts b/packages/kbn-dev-utils/src/tooling_log/tooling_log_text_writer.ts index 65b625de9f308..b8c12433a0ebb 100644 --- a/packages/kbn-dev-utils/src/tooling_log/tooling_log_text_writer.ts +++ b/packages/kbn-dev-utils/src/tooling_log/tooling_log_text_writer.ts @@ -82,20 +82,28 @@ export class ToolingLogTextWriter implements Writer { } } - write({ type, indent, args }: Message) { - if (!shouldWriteType(this.level, type)) { + write(msg: Message) { + if (!shouldWriteType(this.level, msg.type)) { return false; } - const txt = type === 'error' ? stringifyError(args[0]) : format(args[0], ...args.slice(1)); - const prefix = has(MSG_PREFIXES, type) ? MSG_PREFIXES[type] : ''; + const prefix = has(MSG_PREFIXES, msg.type) ? MSG_PREFIXES[msg.type] : ''; + ToolingLogTextWriter.write(this.writeTo, prefix, msg); + return true; + } + + static write(writeTo: ToolingLogTextWriter['writeTo'], prefix: string, msg: Message) { + const txt = + msg.type === 'error' + ? stringifyError(msg.args[0]) + : format(msg.args[0], ...msg.args.slice(1)); (prefix + txt).split('\n').forEach((line, i) => { let lineIndent = ''; - if (indent > 0) { + if (msg.indent > 0) { // if we are indenting write some spaces followed by a symbol - lineIndent += ' '.repeat(indent - 1); + lineIndent += ' '.repeat(msg.indent - 1); lineIndent += line.startsWith('-') ? '└' : '│'; } @@ -105,9 +113,7 @@ export class ToolingLogTextWriter implements Writer { lineIndent += PREFIX_INDENT; } - this.writeTo.write(`${lineIndent}${line}\n`); + writeTo.write(`${lineIndent}${line}\n`); }); - - return true; } } diff --git a/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js b/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js index e02c38494991a..da0b799b338ed 100755 --- a/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js +++ b/packages/kbn-eslint-import-resolver-kibana/lib/get_webpack_config.js @@ -29,7 +29,6 @@ exports.getWebpackConfig = function(kibanaPath, projectRoot, config) { // Kibana defaults https://github.com/elastic/kibana/blob/6998f074542e8c7b32955db159d15661aca253d7/src/legacy/ui/ui_bundler_env.js#L30-L36 ui: fromKibana('src/legacy/ui/public'), test_harness: fromKibana('src/test_harness/public'), - querystring: 'querystring-browser', // Dev defaults for test bundle https://github.com/elastic/kibana/blob/6998f074542e8c7b32955db159d15661aca253d7/src/core_plugins/tests_bundle/index.js#L73-L78 ng_mock$: fromKibana('src/test_utils/public/ng_mock'), diff --git a/packages/kbn-eslint-import-resolver-kibana/package.json b/packages/kbn-eslint-import-resolver-kibana/package.json index 9fae27011767e..332f7e8a20cc2 100755 --- a/packages/kbn-eslint-import-resolver-kibana/package.json +++ b/packages/kbn-eslint-import-resolver-kibana/package.json @@ -16,6 +16,6 @@ "glob-all": "^3.1.0", "lru-cache": "^4.1.5", "resolve": "^1.7.1", - "webpack": "^4.41.0" + "webpack": "^4.41.5" } } diff --git a/packages/kbn-interpreter/package.json b/packages/kbn-interpreter/package.json index 4faa1bc8e542f..d2f0b0c358284 100644 --- a/packages/kbn-interpreter/package.json +++ b/packages/kbn-interpreter/package.json @@ -23,15 +23,15 @@ "@kbn/dev-utils": "1.0.0", "babel-loader": "^8.0.6", "copy-webpack-plugin": "^5.0.4", - "css-loader": "2.1.1", + "css-loader": "^3.4.2", "del": "^5.1.0", "getopts": "^2.2.4", "pegjs": "0.10.0", - "sass-loader": "^7.3.1", - "style-loader": "0.23.1", + "sass-loader": "^8.0.2", + "style-loader": "^1.1.3", "supports-color": "^7.0.0", "url-loader": "2.2.0", - "webpack": "4.41.0", - "webpack-cli": "^3.3.9" + "webpack": "^4.41.5", + "webpack-cli": "^3.3.10" } } diff --git a/packages/kbn-optimizer/README.md b/packages/kbn-optimizer/README.md new file mode 100644 index 0000000000000..c7f50c6af8dfd --- /dev/null +++ b/packages/kbn-optimizer/README.md @@ -0,0 +1,110 @@ +# @kbn/optimizer + +`@kbn/optimizer` is a package for building Kibana platform UI plugins (and hopefully more soon). + +Kibana Platform plugins with `"ui": true` in their `kibana.json` file will have their `public/index.ts` file (and all of its dependencies) bundled into the `target/public` directory of the plugin. The build output does not need to be updated when other plugins are updated and is included in the distributable without requiring that we ship `@kbn/optimizer` 🎉. + +## Webpack config + +The [Webpack config][WebpackConfig] is designed to provide the majority of what was available in the legacy optimizer and is the same for all plugins to promote consistency and keep things sane for the operations team. It has support for JS/TS built with babel, url imports of image and font files, and support for importing `scss` and `css` files. SCSS is pre-processed by [postcss][PostCss], built for both light and dark mode and injected automatically into the page when the parent module is loaded (page reloads are still required for switching between light/dark mode). CSS is injected into the DOM as it is written on disk when the parent module is loaded (no postcss support). + +Source maps are enabled except when building the distributable. They show the code actually being executed by the browser to strike a balance between debuggability and performance. They are not configurable at this time but will be configurable once we have a developer configuration solution that doesn't rely on the server (see [#55656](https://github.com/elastic/kibana/issues/55656)). + +### IE Support + +To make front-end code easier to debug the optimizer uses the `BROWSERSLIST_ENV=dev` environment variable (by default) to build JS and CSS that is compatible with modern browsers. In order to support older browsers like IE in development you will need to specify the `BROWSERSLIST_ENV=production` environment variable or build a distributable for testing. + +## Running the optimizer + +The `@kbn/optimizer` is automatically executed from the dev cli, the Kibana build scripts, and in CI. If you're running Kibana locally in some other way you might need to build the plugins manually, which you can do by running `node scripts/build_kibana_platform_plugins` (pass `--help` for options). + +### Worker count + +You can limit the number of workers the optimizer uses by setting the `KBN_OPTIMIZER_MAX_WORKERS` environment variable. You might want to do this if your system struggles to keep up while the optimizer is getting started and building all plugins as fast as possible. Setting `KBN_OPTIMIZER_MAX_WORKERS=1` will cause the optimizer to take the longest amount of time but will have the smallest impact on other components of your system. + +We only limit the number of workers we will start at any given time. If we start more workers later we will limit the number of workers we start at that time by the maximum, but we don't take into account the number of workers already started because it is assumed that those workers are doing very little work. This greatly simplifies the logic as we don't ever have to reallocate workers and provides the best performance in most cases. + +### Caching + +Bundles built by the the optimizer include a cache file which describes the information needed to determine if the bundle needs to be rebuilt when the optimizer is restarted. Caching is enabled by default and is very aggressive about invalidating the cache output, but if you need to disable caching you can pass `--no-cache` to `node scripts/build_kibana_platform_plugins`, or set the `KBN_OPTIMIZER_NO_CACHE` environment variable to anything (env overrides everything). + +When a bundle is determined to be up-to-date a worker is not started for the bundle. If running the optimizer with the `--dev/--watch` flag, then all the files referenced by cached bundles are watched for changes. Once a change is detected in any of the files referenced by the built bundle a worker is started. If a file is changed that is referenced by several bundles then workers will be started for each bundle, combining workers together to respect the worker limit. + +## API + +To run the optimizer from code, you can import the [`OptimizerConfig`][OptimizerConfig] class and [`runOptimizer`][Optimizer] function. Create an [`OptimizerConfig`][OptimizerConfig] instance by calling it's static `create()` method with some options, then pass it to the [`runOptimizer`][Optimizer] function. `runOptimizer()` returns an observable of update objects, which are summaries of the optimizer state plus an optional `event` property which describes the internal events occuring and may be of use. You can use the [`logOptimizerState()`][LogOptimizerState] helper to write the relevant bits of state to a tooling log or checkout it's implementation to see how the internal events like [`WorkerStdio`][ObserveWorker] and [`WorkerStarted`][ObserveWorker] are used. + +Example: +```ts +import { runOptimizer, OptimizerConfig, logOptimizerState } from '@kbn/optimizer'; +import { REPO_ROOT, ToolingLog } from '@kbn/dev-utils'; + +const log = new ToolingLog({ + level: 'verbose', + writeTo: process.stdout, +}) + +const config = OptimizerConfig.create({ + repoRoot: Path.resolve(__dirname, '../../..'), + watch: false, + oss: true, + dist: true +}); + +await runOptimizer(config) + .pipe(logOptimizerState(log, config)) + .toPromise(); +``` + +This is essentially what we're doing in [`script/build_kibana_platform_plugins`][Cli] and the new [build system task][BuildTask]. + +## Internals + +The optimizer runs webpack instances in worker processes. Each worker is configured via a [`WorkerConfig`][WorkerConfig] object and an array of [`Bundle`][Bundle] objects which are JSON serialized and passed to the worker as it's arguments. + +Plugins/bundles are assigned to workers based on the number of modules historically seen in each bundle in an effort to evenly distribute the load across the worker pool (see [`assignBundlesToWorkers`][AssignBundlesToWorkers]). + +The number of workers that will be started at any time is automatically chosen by dividing the number of cores available by 3 (minimum of 2). + +The [`WorkerConfig`][WorkerConfig] includes the location of the repo (it might be one of many builds, or the main repo), wether we are running in watch mode, wether we are building a distributable, and other global config items. + +The [`Bundle`][Bundle] objects which include the details necessary to create a webpack config for a specific plugin's bundle (created using [`webpack.config.ts`][WebpackConfig]). + +Each worker communicates state back to the main process by sending [`WorkerMsg`][WorkerMsg] and [`CompilerMsg`][CompilerMsg] objects using IPC. + +The Optimizer captures all of these messages and produces a stream of update objects. + +Optimizer phases: +
+
'initializing'
+
Initial phase, during this state the optimizer is validating caches and determining which builds should be built initially.
+
'initialized'
+
Emitted by the optimizer once it's don't initializing its internal state and determined which bundles are going to be built initially.
+
'running'
+
Emitted when any worker is in a running state. To determine which compilers are running, look for BundleState objects with type 'running'.
+
'issue'
+
Emitted when all workers are done running and any compiler completed with a 'compiler issue' status. Compiler issues include things like "unable to resolve module" or syntax errors in the source modules and can be fixed by users when running in watch mode.
+
'success'
+
Emitted when all workers are done running and all compilers completed with 'compiler success'.
+
'reallocating'
+
Emitted when the files referenced by a cached bundle have changed, before the worker has been started up to update that bundle.
+
+ +Workers have several error message they may emit which indicate unrecoverable errors. When any of those messages are received the stream will error and the workers will be torn down. + +For an example of how to handle these states checkout the [`logOptimizerState()`][LogOptimizerState] helper. + +[PostCss]: https://postcss.org/ +[Cli]: src/cli.ts +[Optimizer]: src/optimizer.ts +[ObserveWorker]: src/observe_worker.ts +[CompilerMsg]: src/common/compiler_messages.ts +[WorkerMsg]: src/common/worker_messages.ts +[Bundle]: src/common/bundle.ts +[WebpackConfig]: src/worker/webpack.config.ts +[BundleDefinition]: src/common/bundle_definition.ts +[WorkerConfig]: src/common/worker_config.ts +[OptimizerConfig]: src/optimizer_config.ts +[LogOptimizerState]: src/log_optimizer_state.ts +[AssignBundlesToWorkers]: src/assign_bundles_to_workers.ts +[BuildTask]: ../../src/dev/build/tasks/build_kibana_platform_plugins.js \ No newline at end of file diff --git a/packages/kbn-optimizer/babel.config.js b/packages/kbn-optimizer/babel.config.js new file mode 100644 index 0000000000000..ff657603f4c8d --- /dev/null +++ b/packages/kbn-optimizer/babel.config.js @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = { + presets: ['@kbn/babel-preset/node_preset'], + ignore: ['**/*.test.js'], +}; diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/index.js b/packages/kbn-optimizer/index.d.ts similarity index 94% rename from src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/index.js rename to packages/kbn-optimizer/index.d.ts index ffb0e88c60a0d..aa55df9215c2f 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/index.js +++ b/packages/kbn-optimizer/index.d.ts @@ -17,4 +17,4 @@ * under the License. */ -export { injectBanner } from './inject_banner'; +export * from './src/index'; diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json new file mode 100644 index 0000000000000..e8bb31f1e365d --- /dev/null +++ b/packages/kbn-optimizer/package.json @@ -0,0 +1,44 @@ +{ + "name": "@kbn/optimizer", + "version": "1.0.0", + "private": true, + "license": "Apache-2.0", + "main": "./target/index.js", + "scripts": { + "build": "babel src --out-dir target --copy-files --delete-dir-on-start --extensions .ts --ignore *.test.ts --source-maps=inline", + "kbn:bootstrap": "yarn build", + "kbn:watch": "yarn build --watch" + }, + "dependencies": { + "@babel/cli": "^7.5.5", + "@kbn/babel-preset": "1.0.0", + "@kbn/dev-utils": "1.0.0", + "@kbn/ui-shared-deps": "1.0.0", + "@types/loader-utils": "^1.1.3", + "@types/watchpack": "^1.1.5", + "@types/webpack": "^4.41.3", + "autoprefixer": "^9.7.4", + "babel-loader": "^8.0.6", + "clean-webpack-plugin": "^3.0.0", + "cpy": "^8.0.0", + "css-loader": "^3.4.2", + "del": "^5.1.0", + "file-loader": "^4.2.0", + "istanbul-instrumenter-loader": "^3.0.1", + "jest-diff": "^25.1.0", + "json-stable-stringify": "^1.0.1", + "loader-utils": "^1.2.3", + "node-sass": "^4.13.0", + "postcss-loader": "^3.0.0", + "raw-loader": "^3.1.0", + "rxjs": "^6.5.3", + "sass-loader": "^8.0.2", + "style-loader": "^1.1.3", + "terser-webpack-plugin": "^2.1.2", + "tinymath": "1.2.1", + "url-loader": "^2.2.0", + "watchpack": "^1.6.0", + "webpack": "^4.41.5", + "webpack-merge": "^4.2.2" + } +} \ No newline at end of file diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json new file mode 100644 index 0000000000000..20c8046daa65e --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json @@ -0,0 +1,4 @@ +{ + "id": "bar", + "ui": true +} diff --git a/src/legacy/core_plugins/telemetry/public/services/index.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts similarity index 88% rename from src/legacy/core_plugins/telemetry/public/services/index.ts rename to packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts index 8b02f8ce4c5b0..66fa55479f3b9 100644 --- a/src/legacy/core_plugins/telemetry/public/services/index.ts +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/index.ts @@ -17,5 +17,6 @@ * under the License. */ -export { TelemetryOptInProvider } from './telemetry_opt_in'; -export { isUnauthenticated } from './path'; +import { fooLibFn } from '../../foo/public/index'; +export * from './lib'; +export { fooLibFn }; diff --git a/webpackShims/tinymath.js b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/lib.ts similarity index 93% rename from webpackShims/tinymath.js rename to packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/lib.ts index 45aa86a6ef64a..091fae72ad635 100644 --- a/webpackShims/tinymath.js +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/lib.ts @@ -17,4 +17,6 @@ * under the License. */ -module.exports = require('tinymath/lib/tinymath.es5.js'); +export function barLibFn() { + return 'bar'; +} diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/kibana.json new file mode 100644 index 0000000000000..6e4e9c70a115c --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/kibana.json @@ -0,0 +1,3 @@ +{ + "id": "baz" +} diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/server/index.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/server/index.ts new file mode 100644 index 0000000000000..12e580bbb76b3 --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/server/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './lib'; diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/server/lib.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/server/lib.ts new file mode 100644 index 0000000000000..870e5a8045280 --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/server/lib.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function bazLibFn() { + return 'baz'; +} diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/kibana.json new file mode 100644 index 0000000000000..256856181ccd8 --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/kibana.json @@ -0,0 +1,4 @@ +{ + "id": "foo", + "ui": true +} diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/ext.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/ext.ts new file mode 100644 index 0000000000000..3064d6814e2b1 --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/ext.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const ext = 'TRUE'; diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/index.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/index.ts new file mode 100644 index 0000000000000..9d3871df24739 --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './lib'; +export * from './ext'; diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/lib.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/lib.ts new file mode 100644 index 0000000000000..04a8c7e5b1eec --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/public/lib.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function fooLibFn() { + return 'foo'; +} diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/kibana.json new file mode 100644 index 0000000000000..b9e044523a6a5 --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/kibana.json @@ -0,0 +1,3 @@ +{ + "id": "test_baz" +} diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/server/index.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/server/index.ts new file mode 100644 index 0000000000000..12e580bbb76b3 --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/server/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './lib'; diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/server/lib.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/server/lib.ts new file mode 100644 index 0000000000000..870e5a8045280 --- /dev/null +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/server/lib.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function bazLibFn() { + return 'baz'; +} diff --git a/packages/kbn-optimizer/src/cli.ts b/packages/kbn-optimizer/src/cli.ts new file mode 100644 index 0000000000000..dcb4dcd35698d --- /dev/null +++ b/packages/kbn-optimizer/src/cli.ts @@ -0,0 +1,118 @@ +/* + * 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 'source-map-support/register'; + +import Path from 'path'; + +import { run, REPO_ROOT, createFlagError } from '@kbn/dev-utils'; + +import { logOptimizerState } from './log_optimizer_state'; +import { OptimizerConfig } from './optimizer'; +import { runOptimizer } from './run_optimizer'; + +run( + async ({ log, flags }) => { + const watch = flags.watch ?? false; + if (typeof watch !== 'boolean') { + throw createFlagError('expected --watch to have no value'); + } + + const oss = flags.oss ?? false; + if (typeof oss !== 'boolean') { + throw createFlagError('expected --oss to have no value'); + } + + const cache = flags.cache ?? true; + if (typeof cache !== 'boolean') { + throw createFlagError('expected --cache to have no value'); + } + + const dist = flags.dist ?? false; + if (typeof dist !== 'boolean') { + throw createFlagError('expected --dist to have no value'); + } + + const examples = flags.examples ?? false; + if (typeof examples !== 'boolean') { + throw createFlagError('expected --no-examples to have no value'); + } + + const profileWebpack = flags.profile ?? false; + if (typeof profileWebpack !== 'boolean') { + throw createFlagError('expected --profile to have no value'); + } + + const inspectWorkers = flags['inspect-workers'] ?? false; + if (typeof inspectWorkers !== 'boolean') { + throw createFlagError('expected --no-inspect-workers to have no value'); + } + + const maxWorkerCount = flags.workers ? Number.parseInt(String(flags.workers), 10) : undefined; + if (maxWorkerCount !== undefined && (!Number.isFinite(maxWorkerCount) || maxWorkerCount < 1)) { + throw createFlagError('expected --workers to be a number greater than 0'); + } + + const extraPluginScanDirs = ([] as string[]) + .concat((flags['scan-dir'] as string | string[]) || []) + .map(p => Path.resolve(p)); + if (!extraPluginScanDirs.every(s => typeof s === 'string')) { + throw createFlagError('expected --scan-dir to be a string'); + } + + const config = OptimizerConfig.create({ + repoRoot: REPO_ROOT, + watch, + maxWorkerCount, + oss, + dist, + cache, + examples, + profileWebpack, + extraPluginScanDirs, + inspectWorkers, + }); + + await runOptimizer(config) + .pipe(logOptimizerState(log, config)) + .toPromise(); + }, + { + flags: { + boolean: ['watch', 'oss', 'examples', 'dist', 'cache', 'profile', 'inspect-workers'], + string: ['workers', 'scan-dir'], + default: { + examples: true, + cache: true, + 'inspect-workers': true, + }, + help: ` + --watch run the optimizer in watch mode + --workers max number of workers to use + --oss only build oss plugins + --profile profile the webpack builds and write stats.json files to build outputs + --no-cache disable the cache + --no-examples don't build the example plugins + --dist create bundles that are suitable for inclusion in the Kibana distributable + --scan-dir add a directory to the list of directories scanned for plugins (specify as many times as necessary) + --no-inspect-workers when inspecting the parent process, don't inspect the workers + `, + }, + } +); diff --git a/packages/kbn-optimizer/src/common/array_helpers.test.ts b/packages/kbn-optimizer/src/common/array_helpers.test.ts new file mode 100644 index 0000000000000..9d45217486ee8 --- /dev/null +++ b/packages/kbn-optimizer/src/common/array_helpers.test.ts @@ -0,0 +1,112 @@ +/* + * 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 { ascending, descending } from './array_helpers'; + +describe('ascending/descending', () => { + interface Item { + a: number; + b: number | string; + c?: number; + } + + const a = (x: Item) => x.a; + const b = (x: Item) => x.b; + const c = (x: Item) => x.c; + const print = (x: Item) => `${x.a}/${x.b}/${x.c}`; + const values: Item[] = [ + { a: 1, b: 2, c: 3 }, + { a: 3, b: 2, c: 1 }, + { a: 9, b: 9, c: 9 }, + { a: 8, b: 5, c: 8 }, + { a: 8, b: 5 }, + { a: 8, b: 4 }, + { a: 8, b: 3, c: 8 }, + { a: 8, b: 2 }, + { a: 8, b: 1, c: 8 }, + { a: 8, b: 1 }, + { a: 8, b: 0 }, + { a: 8, b: -1, c: 8 }, + { a: 8, b: -2 }, + { a: 8, b: -3, c: 8 }, + { a: 8, b: -4 }, + { a: 8, b: 'foo', c: 8 }, + { a: 8, b: 'foo' }, + { a: 8, b: 'bar', c: 8 }, + { a: 8, b: 'bar' }, + ].sort(() => 0.5 - Math.random()); + + it('sorts items using getters', () => { + expect( + Array.from(values) + .sort(ascending(a, b, c)) + .map(print) + ).toMatchInlineSnapshot(` + Array [ + "1/2/3", + "3/2/1", + "8/-4/undefined", + "8/-3/8", + "8/-2/undefined", + "8/-1/8", + "8/0/undefined", + "8/1/undefined", + "8/1/8", + "8/2/undefined", + "8/3/8", + "8/4/undefined", + "8/5/undefined", + "8/5/8", + "8/bar/undefined", + "8/bar/8", + "8/foo/undefined", + "8/foo/8", + "9/9/9", + ] + `); + + expect( + Array.from(values) + .sort(descending(a, b, c)) + .map(print) + ).toMatchInlineSnapshot(` + Array [ + "9/9/9", + "8/foo/8", + "8/foo/undefined", + "8/bar/8", + "8/bar/undefined", + "8/5/8", + "8/5/undefined", + "8/4/undefined", + "8/3/8", + "8/2/undefined", + "8/1/8", + "8/1/undefined", + "8/0/undefined", + "8/-1/8", + "8/-2/undefined", + "8/-3/8", + "8/-4/undefined", + "3/2/1", + "1/2/3", + ] + `); + }); +}); diff --git a/packages/kbn-optimizer/src/common/array_helpers.ts b/packages/kbn-optimizer/src/common/array_helpers.ts new file mode 100644 index 0000000000000..740f018d19298 --- /dev/null +++ b/packages/kbn-optimizer/src/common/array_helpers.ts @@ -0,0 +1,84 @@ +/* + * 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. + */ + +type SortPropGetter = (x: T) => number | string | undefined; +type Comparator = (a: T, b: T) => number; + +/** + * create a sort comparator that sorts objects in ascending + * order based on the ...getters. getters are called for each + * item and return the value to compare against the other items. + * + * - if a getter returns undefined the item will be sorted + * before all other items + * - if a getter returns a string it will be compared using + * `String#localeCompare` + * - otherwise comparison is done using subtraction + * - If the values for a getter are equal the next getter is + * used to compare the items. + */ +export const ascending = (...getters: Array>): Comparator => (a, b) => { + for (const getter of getters) { + const valA = getter(a); + const valB = getter(b); + + if (valA === valB) { + continue; + } + if (valA === undefined) { + return -1; + } + if (valB === undefined) { + return 1; + } + + return typeof valA === 'string' || typeof valB === 'string' + ? String(valA).localeCompare(String(valB)) + : valA - valB; + } + + return 0; +}; + +/** + * create a sort comparator that sorts values in descending + * order based on the ...getters + * + * See docs for ascending() + */ +export const descending = (...getters: Array>): Comparator => { + const sorter = ascending(...getters); + return (a, b) => sorter(b, a); +}; + +/** + * Alternate Array#includes() implementation with sane types, functions as a type guard + */ +export const includes = (array: T[], value: any): value is T => array.includes(value); + +/** + * Ponyfill for Object.fromEntries() + */ +export const entriesToObject = (entries: Array): Record => { + const object: Record = {}; + for (const [key, value] of entries) { + object[key] = value; + } + return object; +}; diff --git a/packages/kbn-optimizer/src/common/bundle.test.ts b/packages/kbn-optimizer/src/common/bundle.test.ts new file mode 100644 index 0000000000000..ec78a1bdf020e --- /dev/null +++ b/packages/kbn-optimizer/src/common/bundle.test.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 { Bundle, BundleSpec, parseBundles } from './bundle'; + +jest.mock('fs'); + +const SPEC: BundleSpec = { + contextDir: '/foo/bar', + entry: 'entry', + id: 'bar', + outputDir: '/foo/bar/target', + sourceRoot: '/foo', + type: 'plugin', +}; + +it('creates cache keys', () => { + const bundle = new Bundle(SPEC); + expect( + bundle.createCacheKey( + ['/foo/bar/a', '/foo/bar/c'], + new Map([ + ['/foo/bar/a', 123], + ['/foo/bar/b', 456], + ['/foo/bar/c', 789], + ]) + ) + ).toMatchInlineSnapshot(` + Object { + "mtimes": Object { + "/foo/bar/a": 123, + "/foo/bar/c": 789, + }, + "spec": Object { + "contextDir": "/foo/bar", + "entry": "entry", + "id": "bar", + "outputDir": "/foo/bar/target", + "sourceRoot": "/foo", + "type": "plugin", + }, + } + `); +}); + +it('provides serializable versions of itself', () => { + const bundle = new Bundle(SPEC); + expect(bundle.toSpec()).toEqual(SPEC); +}); + +it('provides the module count from the cache', () => { + const bundle = new Bundle(SPEC); + expect(bundle.cache.getModuleCount()).toBe(undefined); + bundle.cache.set({ moduleCount: 123 }); + expect(bundle.cache.getModuleCount()).toBe(123); +}); + +it('parses bundles from JSON specs', () => { + const bundles = parseBundles(JSON.stringify([SPEC])); + + expect(bundles).toMatchInlineSnapshot(` + Array [ + Bundle { + "cache": BundleCache { + "path": "/foo/bar/target/.kbn-optimizer-cache", + "state": undefined, + }, + "contextDir": "/foo/bar", + "entry": "entry", + "id": "bar", + "outputDir": "/foo/bar/target", + "sourceRoot": "/foo", + "type": "plugin", + }, + ] + `); +}); diff --git a/packages/kbn-optimizer/src/common/bundle.ts b/packages/kbn-optimizer/src/common/bundle.ts new file mode 100644 index 0000000000000..f1bc0965a46cc --- /dev/null +++ b/packages/kbn-optimizer/src/common/bundle.ts @@ -0,0 +1,170 @@ +/* + * 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 Path from 'path'; + +import { BundleCache } from './bundle_cache'; +import { UnknownVals } from './ts_helpers'; +import { includes, ascending, entriesToObject } from './array_helpers'; + +const VALID_BUNDLE_TYPES = ['plugin' as const]; + +export interface BundleSpec { + readonly type: typeof VALID_BUNDLE_TYPES[0]; + /** Unique id for this bundle */ + readonly id: string; + /** Webpack entry request for this plugin, relative to the contextDir */ + readonly entry: string; + /** Absolute path to the plugin source directory */ + readonly contextDir: string; + /** Absolute path to the root of the repository */ + readonly sourceRoot: string; + /** Absolute path to the directory where output should be written */ + readonly outputDir: string; +} + +export class Bundle { + /** Bundle type, only "plugin" is supported for now */ + public readonly type: BundleSpec['type']; + /** Unique identifier for this bundle */ + public readonly id: BundleSpec['id']; + /** Path, relative to `contextDir`, to the entry file for the Webpack bundle */ + public readonly entry: BundleSpec['entry']; + /** + * Absolute path to the root of the bundle context (plugin directory) + * where the entry is resolved relative to and the default output paths + * are relative to + */ + public readonly contextDir: BundleSpec['contextDir']; + /** Absolute path to the root of the whole project source, repo root */ + public readonly sourceRoot: BundleSpec['sourceRoot']; + /** Absolute path to the output directory for this bundle */ + public readonly outputDir: BundleSpec['outputDir']; + + public readonly cache: BundleCache; + + constructor(spec: BundleSpec) { + this.type = spec.type; + this.id = spec.id; + this.entry = spec.entry; + this.contextDir = spec.contextDir; + this.sourceRoot = spec.sourceRoot; + this.outputDir = spec.outputDir; + + this.cache = new BundleCache(Path.resolve(this.outputDir, '.kbn-optimizer-cache')); + } + + /** + * Calculate the cache key for this bundle based from current + * mtime values. + * + * @param mtimes pre-fetched mtimes (ms || undefined) for all referenced files + */ + createCacheKey(files: string[], mtimes: Map): unknown { + return { + spec: this.toSpec(), + mtimes: entriesToObject( + files.map(p => [p, mtimes.get(p)] as const).sort(ascending(e => e[0])) + ), + }; + } + + /** + * Get the raw "specification" for the bundle, this object is JSON serialized + * in the cache key, passed to worker processes so they know what bundles + * to build, and passed to the Bundle constructor to rebuild the Bundle object. + */ + toSpec(): BundleSpec { + return { + type: this.type, + id: this.id, + entry: this.entry, + contextDir: this.contextDir, + sourceRoot: this.sourceRoot, + outputDir: this.outputDir, + }; + } +} + +/** + * Parse a JSON string containing an array of BundleSpec objects into an array + * of Bundle objects, validating everything. + */ +export function parseBundles(json: string) { + try { + if (typeof json !== 'string') { + throw new Error('must be a JSON string'); + } + + const specs: Array> = JSON.parse(json); + + if (!Array.isArray(specs)) { + throw new Error('must be an array'); + } + + return specs.map( + (spec: UnknownVals): Bundle => { + if (!(spec && typeof spec === 'object')) { + throw new Error('`bundles[]` must be an object'); + } + + const { type } = spec; + if (!includes(VALID_BUNDLE_TYPES, type)) { + throw new Error('`bundles[]` must have a valid `type`'); + } + + const { id } = spec; + if (!(typeof id === 'string')) { + throw new Error('`bundles[]` must have a string `id` property'); + } + + const { entry } = spec; + if (!(typeof entry === 'string')) { + throw new Error('`bundles[]` must have a string `entry` property'); + } + + const { contextDir } = spec; + if (!(typeof contextDir === 'string' && Path.isAbsolute(contextDir))) { + throw new Error('`bundles[]` must have an absolute path `contextDir` property'); + } + + const { sourceRoot } = spec; + if (!(typeof sourceRoot === 'string' && Path.isAbsolute(sourceRoot))) { + throw new Error('`bundles[]` must have an absolute path `sourceRoot` property'); + } + + const { outputDir } = spec; + if (!(typeof outputDir === 'string' && Path.isAbsolute(outputDir))) { + throw new Error('`bundles[]` must have an absolute path `outputDir` property'); + } + + return new Bundle({ + type, + id, + entry, + contextDir, + sourceRoot, + outputDir, + }); + } + ); + } catch (error) { + throw new Error(`unable to parse bundles: ${error.message}`); + } +} diff --git a/packages/kbn-optimizer/src/common/bundle_cache.test.ts b/packages/kbn-optimizer/src/common/bundle_cache.test.ts new file mode 100644 index 0000000000000..f6118739045ba --- /dev/null +++ b/packages/kbn-optimizer/src/common/bundle_cache.test.ts @@ -0,0 +1,118 @@ +/* + * 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 { BundleCache, State } from './bundle_cache'; + +jest.mock('fs'); +const mockReadFileSync: jest.Mock = jest.requireMock('fs').readFileSync; +const mockMkdirSync: jest.Mock = jest.requireMock('fs').mkdirSync; +const mockWriteFileSync: jest.Mock = jest.requireMock('fs').writeFileSync; + +const SOME_STATE: State = { + cacheKey: 'abc', + files: ['123'], + moduleCount: 123, + optimizerCacheKey: 'abc', +}; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +it(`doesn't complain if files are not on disk`, () => { + const cache = new BundleCache('/foo/bar.json'); + expect(cache.get()).toEqual({}); +}); + +it(`updates files on disk when calling set()`, () => { + const cache = new BundleCache('/foo/bar.json'); + cache.set(SOME_STATE); + expect(mockReadFileSync).not.toHaveBeenCalled(); + expect(mockMkdirSync.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/foo", + Object { + "recursive": true, + }, + ], + ] + `); + expect(mockWriteFileSync.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/foo/bar.json", + "{ + \\"cacheKey\\": \\"abc\\", + \\"files\\": [ + \\"123\\" + ], + \\"moduleCount\\": 123, + \\"optimizerCacheKey\\": \\"abc\\" + }", + ], + ] + `); +}); + +it(`serves updated state from memory`, () => { + const cache = new BundleCache('/foo/bar.json'); + cache.set(SOME_STATE); + jest.clearAllMocks(); + + expect(cache.get()).toEqual(SOME_STATE); + expect(mockReadFileSync).not.toHaveBeenCalled(); + expect(mockMkdirSync).not.toHaveBeenCalled(); + expect(mockWriteFileSync).not.toHaveBeenCalled(); +}); + +it('reads state from disk on get() after refresh()', () => { + const cache = new BundleCache('/foo/bar.json'); + cache.set(SOME_STATE); + cache.refresh(); + jest.clearAllMocks(); + + cache.get(); + expect(mockMkdirSync).not.toHaveBeenCalled(); + expect(mockWriteFileSync).not.toHaveBeenCalled(); + expect(mockReadFileSync.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/foo/bar.json", + "utf8", + ], + ] + `); +}); + +it('provides accessors to specific state properties', () => { + const cache = new BundleCache('/foo/bar.json'); + + expect(cache.getModuleCount()).toBe(undefined); + expect(cache.getReferencedFiles()).toEqual(undefined); + expect(cache.getCacheKey()).toEqual(undefined); + expect(cache.getOptimizerCacheKey()).toEqual(undefined); + + cache.set(SOME_STATE); + + expect(cache.getModuleCount()).toBe(123); + expect(cache.getReferencedFiles()).toEqual(['123']); + expect(cache.getCacheKey()).toEqual('abc'); + expect(cache.getOptimizerCacheKey()).toEqual('abc'); +}); diff --git a/packages/kbn-optimizer/src/common/bundle_cache.ts b/packages/kbn-optimizer/src/common/bundle_cache.ts new file mode 100644 index 0000000000000..1dbc7f1d1b6b0 --- /dev/null +++ b/packages/kbn-optimizer/src/common/bundle_cache.ts @@ -0,0 +1,97 @@ +/* + * 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 Fs from 'fs'; +import Path from 'path'; + +export interface State { + optimizerCacheKey?: unknown; + cacheKey?: unknown; + moduleCount?: number; + files?: string[]; +} + +const DEFAULT_STATE: State = {}; +const DEFAULT_STATE_JSON = JSON.stringify(DEFAULT_STATE); + +/** + * Helper to read and update metadata for bundles. + */ +export class BundleCache { + private state: State | undefined = undefined; + constructor(private readonly path: string | false) {} + + refresh() { + this.state = undefined; + } + + get() { + if (!this.state) { + let json; + try { + if (this.path) { + json = Fs.readFileSync(this.path, 'utf8'); + } + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + } + + let partialCache: Partial; + try { + partialCache = JSON.parse(json || DEFAULT_STATE_JSON); + } catch (error) { + partialCache = {}; + } + + this.state = { + ...DEFAULT_STATE, + ...partialCache, + }; + } + + return this.state; + } + + set(updated: State) { + this.state = updated; + if (this.path) { + const directory = Path.dirname(this.path); + Fs.mkdirSync(directory, { recursive: true }); + Fs.writeFileSync(this.path, JSON.stringify(this.state, null, 2)); + } + } + + public getModuleCount() { + return this.get().moduleCount; + } + + public getReferencedFiles() { + return this.get().files; + } + + public getCacheKey() { + return this.get().cacheKey; + } + + public getOptimizerCacheKey() { + return this.get().optimizerCacheKey; + } +} diff --git a/packages/kbn-optimizer/src/common/compiler_messages.ts b/packages/kbn-optimizer/src/common/compiler_messages.ts new file mode 100644 index 0000000000000..5f2e9d518bfa6 --- /dev/null +++ b/packages/kbn-optimizer/src/common/compiler_messages.ts @@ -0,0 +1,98 @@ +/* + * 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. + */ + +/** + * Message sent when a compiler encouters an unresolvable error. + * The worker will be shut down following this message. + */ +export interface CompilerErrorMsg { + type: 'compiler error'; + id: string; + errorMsg: string; + errorStack?: string; +} + +/** + * Message sent when a compiler starts running, either for the first + * time or because of changes detected when watching. + */ +export interface CompilerRunningMsg { + type: 'running'; + bundleId: string; +} + +/** + * Message sent when a compiler encounters an error that + * prevents the bundle from building correctly. When in + * watch mode these issues can be fixed by the user. + * (ie. unresolved import, syntax error, etc.) + */ +export interface CompilerIssueMsg { + type: 'compiler issue'; + bundleId: string; + failure: string; +} + +/** + * Message sent when a compiler completes successfully and + * the bundle has been written to disk or updated on disk. + */ +export interface CompilerSuccessMsg { + type: 'compiler success'; + bundleId: string; + moduleCount: number; +} + +export type CompilerMsg = CompilerRunningMsg | CompilerIssueMsg | CompilerSuccessMsg; + +export class CompilerMsgs { + constructor(private bundle: string) {} + + running(): CompilerRunningMsg { + return { + bundleId: this.bundle, + type: 'running', + }; + } + + compilerFailure(options: { failure: string }): CompilerIssueMsg { + return { + bundleId: this.bundle, + type: 'compiler issue', + failure: options.failure, + }; + } + + compilerSuccess(options: { moduleCount: number }): CompilerSuccessMsg { + return { + bundleId: this.bundle, + type: 'compiler success', + moduleCount: options.moduleCount, + }; + } + + error(error: Error): CompilerErrorMsg { + return { + id: this.bundle, + type: 'compiler error', + errorMsg: error.message, + errorStack: error.stack, + }; + } +} diff --git a/packages/kbn-optimizer/src/common/event_stream_helpers.test.ts b/packages/kbn-optimizer/src/common/event_stream_helpers.test.ts new file mode 100644 index 0000000000000..60982abff2d87 --- /dev/null +++ b/packages/kbn-optimizer/src/common/event_stream_helpers.test.ts @@ -0,0 +1,69 @@ +/* + * 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 * as Rx from 'rxjs'; +import { toArray } from 'rxjs/operators'; + +import { summarizeEvent$ } from './event_stream_helpers'; + +it('emits each state with each event, ignoring events when reducer returns undefined', async () => { + const values = await summarizeEvent$( + Rx.of(1, 2, 3, 4, 5), + { + sum: 0, + }, + (state, event) => { + if (event % 2) { + return { + sum: state.sum + event, + }; + } + } + ) + .pipe(toArray()) + .toPromise(); + + expect(values).toMatchInlineSnapshot(` + Array [ + Object { + "state": Object { + "sum": 0, + }, + }, + Object { + "event": 1, + "state": Object { + "sum": 1, + }, + }, + Object { + "event": 3, + "state": Object { + "sum": 4, + }, + }, + Object { + "event": 5, + "state": Object { + "sum": 9, + }, + }, + ] + `); +}); diff --git a/packages/kbn-optimizer/src/common/event_stream_helpers.ts b/packages/kbn-optimizer/src/common/event_stream_helpers.ts new file mode 100644 index 0000000000000..c1585f79ede6e --- /dev/null +++ b/packages/kbn-optimizer/src/common/event_stream_helpers.ts @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import * as Rx from 'rxjs'; +import { scan, distinctUntilChanged, startWith } from 'rxjs/operators'; + +export interface Update { + event?: Event; + state: State; +} + +export type Summarizer = (prev: State, event: Event) => State | undefined; + +/** + * Transform an event stream into a state update stream which emits + * the events and individual states for each event. + */ +export const summarizeEvent$ = ( + event$: Rx.Observable, + initialState: State, + reducer: Summarizer +) => { + const initUpdate: Update = { + state: initialState, + }; + + return event$.pipe( + scan((prev, event): Update => { + const newState = reducer(prev.state, event); + return newState === undefined + ? prev + : { + event, + state: newState, + }; + }, initUpdate), + distinctUntilChanged(), + startWith(initUpdate) + ); +}; diff --git a/packages/kbn-optimizer/src/common/index.ts b/packages/kbn-optimizer/src/common/index.ts new file mode 100644 index 0000000000000..ea0560f132153 --- /dev/null +++ b/packages/kbn-optimizer/src/common/index.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './bundle'; +export * from './bundle_cache'; +export * from './worker_config'; +export * from './worker_messages'; +export * from './compiler_messages'; +export * from './ts_helpers'; +export * from './rxjs_helpers'; +export * from './array_helpers'; +export * from './event_stream_helpers'; diff --git a/packages/kbn-optimizer/src/common/rxjs_helpers.test.ts b/packages/kbn-optimizer/src/common/rxjs_helpers.test.ts new file mode 100644 index 0000000000000..72be71e6bf7ec --- /dev/null +++ b/packages/kbn-optimizer/src/common/rxjs_helpers.test.ts @@ -0,0 +1,140 @@ +/* + * 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 * as Rx from 'rxjs'; +import { toArray, map } from 'rxjs/operators'; + +import { pipeClosure, debounceTimeBuffer, maybeMap, maybe } from './rxjs_helpers'; + +jest.useFakeTimers(); + +describe('pipeClosure()', () => { + it('calls closure on each subscription to setup unique state', async () => { + let counter = 0; + + const foo$ = Rx.of(1, 2, 3).pipe( + pipeClosure(source$ => { + const multiplier = ++counter; + return source$.pipe(map(i => i * multiplier)); + }), + toArray() + ); + + await expect(foo$.toPromise()).resolves.toMatchInlineSnapshot(` + Array [ + 1, + 2, + 3, + ] + `); + await expect(foo$.toPromise()).resolves.toMatchInlineSnapshot(` + Array [ + 2, + 4, + 6, + ] + `); + await expect(foo$.toPromise()).resolves.toMatchInlineSnapshot(` + Array [ + 3, + 6, + 9, + ] + `); + }); +}); + +describe('maybe()', () => { + it('filters out undefined values from the stream', async () => { + const foo$ = Rx.of(1, undefined, 2, undefined, 3).pipe(maybe(), toArray()); + + await expect(foo$.toPromise()).resolves.toEqual([1, 2, 3]); + }); +}); + +describe('maybeMap()', () => { + it('calls map fn and filters out undefined values returned', async () => { + const foo$ = Rx.of(1, 2, 3, 4, 5).pipe( + maybeMap(i => (i % 2 ? i : undefined)), + toArray() + ); + + await expect(foo$.toPromise()).resolves.toEqual([1, 3, 5]); + }); +}); + +describe('debounceTimeBuffer()', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('buffers items until there is n milliseconds of silence, then flushes buffer to stream', async () => { + const foo$ = new Rx.Subject(); + const dest = new Rx.BehaviorSubject(undefined); + foo$ + .pipe( + debounceTimeBuffer(100), + map(items => items.reduce((sum, n) => sum + n)) + ) + .subscribe(dest); + + foo$.next(1); + expect(dest.getValue()).toBe(undefined); + + // only wait 99 milliseconds before sending the next value + jest.advanceTimersByTime(99); + foo$.next(1); + expect(dest.getValue()).toBe(undefined); + + // only wait 99 milliseconds before sending the next value + jest.advanceTimersByTime(99); + foo$.next(1); + expect(dest.getValue()).toBe(undefined); + + // send the next value after 100 milliseconds and observe that it was forwarded + jest.advanceTimersByTime(100); + foo$.next(1); + expect(dest.getValue()).toBe(3); + + foo$.complete(); + if (!dest.isStopped) { + throw new Error('Expected destination to stop as soon as the source is completed'); + } + }); + + it('clears queue as soon as source completes if source completes before time is up', () => { + const foo$ = new Rx.Subject(); + const dest = new Rx.BehaviorSubject(undefined); + foo$ + .pipe( + debounceTimeBuffer(100), + map(items => items.reduce((sum, n) => sum + n)) + ) + .subscribe(dest); + + foo$.next(1); + expect(dest.getValue()).toBe(undefined); + foo$.complete(); + expect(dest.getValue()).toBe(1); + }); +}); diff --git a/packages/kbn-optimizer/src/common/rxjs_helpers.ts b/packages/kbn-optimizer/src/common/rxjs_helpers.ts new file mode 100644 index 0000000000000..1114f65bacb19 --- /dev/null +++ b/packages/kbn-optimizer/src/common/rxjs_helpers.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 * as Rx from 'rxjs'; +import { mergeMap, tap, debounceTime, map } from 'rxjs/operators'; + +type Operator = (source: Rx.Observable) => Rx.Observable; +type MapFn = (item: T1, index: number) => T2; + +/** + * Wrap an operator chain in a closure so that is can have some local + * state. The `fn` is called each time the final observable is + * subscribed so the pipeline/closure is setup for each subscription. + */ +export const pipeClosure = (fn: Operator): Operator => { + return (source: Rx.Observable) => { + return Rx.defer(() => fn(source)); + }; +}; + +/** + * An operator that filters out undefined values from the stream while + * supporting TypeScript + */ +export const maybe = (): Operator => { + return mergeMap(item => (item === undefined ? Rx.EMPTY : [item])); +}; + +/** + * An operator like map(), but undefined values are filered out automatically + * with TypeScript support. For some reason TS doesn't have great support for + * filter's without defining an explicit type assertion in the signature of + * the filter. + */ +export const maybeMap = (fn: MapFn): Operator => { + return mergeMap((item, index) => { + const result = fn(item, index); + return result === undefined ? Rx.EMPTY : [result]; + }); +}; + +/** + * Debounce received notifications and write them to a buffer. Once the source + * has been silent for `ms` milliseconds the buffer is flushed as a single array + * to the destination stream + */ +export const debounceTimeBuffer = (ms: number) => + pipeClosure((source$: Rx.Observable) => { + const buffer: T[] = []; + return source$.pipe( + tap(item => buffer.push(item)), + debounceTime(ms), + map(() => { + const items = Array.from(buffer); + buffer.length = 0; + return items; + }) + ); + }); diff --git a/src/legacy/core_plugins/inspector_views/index.js b/packages/kbn-optimizer/src/common/ts_helpers.ts similarity index 80% rename from src/legacy/core_plugins/inspector_views/index.js rename to packages/kbn-optimizer/src/common/ts_helpers.ts index a37b6bb3db426..8c0b857d212ac 100644 --- a/src/legacy/core_plugins/inspector_views/index.js +++ b/packages/kbn-optimizer/src/common/ts_helpers.ts @@ -17,12 +17,10 @@ * under the License. */ -import { resolve } from 'path'; - -export default function(kibana) { - return new kibana.Plugin({ - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }, - }); -} +/** + * Convert an object type into an object with the same keys + * but with each value type replaced with `unknown` + */ +export type UnknownVals = { + [k in keyof T]: unknown; +}; diff --git a/packages/kbn-optimizer/src/common/worker_config.ts b/packages/kbn-optimizer/src/common/worker_config.ts new file mode 100644 index 0000000000000..c999260872d0f --- /dev/null +++ b/packages/kbn-optimizer/src/common/worker_config.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 Path from 'path'; + +import { UnknownVals } from './ts_helpers'; + +export interface WorkerConfig { + readonly repoRoot: string; + readonly watch: boolean; + readonly dist: boolean; + readonly cache: boolean; + readonly profileWebpack: boolean; + readonly browserslistEnv: string; + readonly optimizerCacheKey: unknown; +} + +export function parseWorkerConfig(json: string): WorkerConfig { + try { + if (typeof json !== 'string') { + throw new Error('expected worker config to be a JSON string'); + } + + const parsed: UnknownVals = JSON.parse(json); + + if (!(typeof parsed === 'object' && parsed)) { + throw new Error('config must be an object'); + } + + const repoRoot = parsed.repoRoot; + if (typeof repoRoot !== 'string' || !Path.isAbsolute(repoRoot)) { + throw new Error('`repoRoot` config must be an absolute path'); + } + + const cache = parsed.cache; + if (typeof cache !== 'boolean') { + throw new Error('`cache` config must be a boolean'); + } + + const watch = parsed.watch; + if (typeof watch !== 'boolean') { + throw new Error('`watch` config must be a boolean'); + } + + const dist = parsed.dist; + if (typeof dist !== 'boolean') { + throw new Error('`dist` config must be a boolean'); + } + + const profileWebpack = parsed.profileWebpack; + if (typeof profileWebpack !== 'boolean') { + throw new Error('`profileWebpack` must be a boolean'); + } + + const optimizerCacheKey = parsed.optimizerCacheKey; + if (optimizerCacheKey === undefined) { + throw new Error('`optimizerCacheKey` must be defined'); + } + + const browserslistEnv = parsed.browserslistEnv; + if (typeof browserslistEnv !== 'string') { + throw new Error('`browserslistEnv` must be a string'); + } + + return { + repoRoot, + cache, + watch, + dist, + profileWebpack, + optimizerCacheKey, + browserslistEnv, + }; + } catch (error) { + throw new Error(`unable to parse worker config: ${error.message}`); + } +} diff --git a/packages/kbn-optimizer/src/common/worker_messages.ts b/packages/kbn-optimizer/src/common/worker_messages.ts new file mode 100644 index 0000000000000..d3c03f483d7e8 --- /dev/null +++ b/packages/kbn-optimizer/src/common/worker_messages.ts @@ -0,0 +1,64 @@ +/* + * 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 { + CompilerRunningMsg, + CompilerIssueMsg, + CompilerSuccessMsg, + CompilerErrorMsg, +} from './compiler_messages'; + +export type WorkerMsg = + | CompilerRunningMsg + | CompilerIssueMsg + | CompilerSuccessMsg + | CompilerErrorMsg + | WorkerErrorMsg; + +/** + * Message sent when the worker encounters an error that it can't + * recover from, no more messages will be sent and the worker + * will exit after this message. + */ +export interface WorkerErrorMsg { + type: 'worker error'; + errorMsg: string; + errorStack?: string; +} + +const WORKER_STATE_TYPES: ReadonlyArray = [ + 'running', + 'compiler issue', + 'compiler success', + 'compiler error', + 'worker error', +]; + +export const isWorkerMsg = (value: any): value is WorkerMsg => + typeof value === 'object' && value && WORKER_STATE_TYPES.includes(value.type); + +export class WorkerMsgs { + error(error: Error): WorkerErrorMsg { + return { + type: 'worker error', + errorMsg: error.message, + errorStack: error.stack, + }; + } +} diff --git a/packages/kbn-optimizer/src/index.ts b/packages/kbn-optimizer/src/index.ts new file mode 100644 index 0000000000000..48777f1d54aaf --- /dev/null +++ b/packages/kbn-optimizer/src/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { OptimizerConfig } from './optimizer'; +export * from './run_optimizer'; +export * from './log_optimizer_state'; diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap new file mode 100644 index 0000000000000..706f79978beee --- /dev/null +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -0,0 +1,557 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`builds expected bundles, saves bundle counts to metadata: OptimizerConfig 1`] = ` +OptimizerConfig { + "bundles": Array [ + Bundle { + "cache": BundleCache { + "path": /plugins/bar/target/public/.kbn-optimizer-cache, + "state": undefined, + }, + "contextDir": /plugins/bar, + "entry": "./public/index", + "id": "bar", + "outputDir": /plugins/bar/target/public, + "sourceRoot": , + "type": "plugin", + }, + Bundle { + "cache": BundleCache { + "path": /plugins/foo/target/public/.kbn-optimizer-cache, + "state": undefined, + }, + "contextDir": /plugins/foo, + "entry": "./public/index", + "id": "foo", + "outputDir": /plugins/foo/target/public, + "sourceRoot": , + "type": "plugin", + }, + ], + "cache": true, + "dist": false, + "inspectWorkers": false, + "maxWorkerCount": 1, + "plugins": Array [ + Object { + "directory": /plugins/bar, + "id": "bar", + "isUiPlugin": true, + }, + Object { + "directory": /plugins/baz, + "id": "baz", + "isUiPlugin": false, + }, + Object { + "directory": /plugins/foo, + "id": "foo", + "isUiPlugin": true, + }, + ], + "profileWebpack": false, + "repoRoot": , + "watch": false, +} +`; + +exports[`builds expected bundles, saves bundle counts to metadata: bar bundle 1`] = ` +"var __kbnBundles__ = typeof __kbnBundles__ === \\"object\\" ? __kbnBundles__ : {}; __kbnBundles__[\\"plugin/bar\\"] = +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); +/******/ } +/******/ }; +/******/ +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ +/******/ // create a fake namespace object +/******/ // mode & 1: value is a module id, require it +/******/ // mode & 2: merge all properties of value into the ns +/******/ // mode & 4: return value when already ns object +/******/ // mode & 8|1: behave like require +/******/ __webpack_require__.t = function(value, mode) { +/******/ if(mode & 1) value = __webpack_require__(value); +/******/ if(mode & 8) return value; +/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; +/******/ var ns = Object.create(null); +/******/ __webpack_require__.r(ns); +/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); +/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); +/******/ return ns; +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = \\"__REPLACE_WITH_PUBLIC_PATH__\\"; +/******/ +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = \\"./public/index.ts\\"); +/******/ }) +/************************************************************************/ +/******/ ({ + +/***/ \\"../foo/public/ext.ts\\": +/*!****************************!*\\\\ + !*** ../foo/public/ext.ts ***! + \\\\****************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +\\"use strict\\"; + + +Object.defineProperty(exports, \\"__esModule\\", { + value: true +}); +exports.ext = void 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. + */ +const ext = 'TRUE'; +exports.ext = ext; + +/***/ }), + +/***/ \\"../foo/public/index.ts\\": +/*!******************************!*\\\\ + !*** ../foo/public/index.ts ***! + \\\\******************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +\\"use strict\\"; + + +Object.defineProperty(exports, \\"__esModule\\", { + value: true +}); + +var _lib = __webpack_require__(/*! ./lib */ \\"../foo/public/lib.ts\\"); + +Object.keys(_lib).forEach(function (key) { + if (key === \\"default\\" || key === \\"__esModule\\") return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _lib[key]; + } + }); +}); + +var _ext = __webpack_require__(/*! ./ext */ \\"../foo/public/ext.ts\\"); + +Object.keys(_ext).forEach(function (key) { + if (key === \\"default\\" || key === \\"__esModule\\") return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _ext[key]; + } + }); +}); + +/***/ }), + +/***/ \\"../foo/public/lib.ts\\": +/*!****************************!*\\\\ + !*** ../foo/public/lib.ts ***! + \\\\****************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +\\"use strict\\"; + + +Object.defineProperty(exports, \\"__esModule\\", { + value: true +}); +exports.fooLibFn = fooLibFn; + +/* + * 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. + */ +function fooLibFn() { + return 'foo'; +} + +/***/ }), + +/***/ \\"./public/index.ts\\": +/*!*************************!*\\\\ + !*** ./public/index.ts ***! + \\\\*************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +\\"use strict\\"; + + +Object.defineProperty(exports, \\"__esModule\\", { + value: true +}); +var _exportNames = { + fooLibFn: true +}; +Object.defineProperty(exports, \\"fooLibFn\\", { + enumerable: true, + get: function () { + return _index.fooLibFn; + } +}); + +var _index = __webpack_require__(/*! ../../foo/public/index */ \\"../foo/public/index.ts\\"); + +var _lib = __webpack_require__(/*! ./lib */ \\"./public/lib.ts\\"); + +Object.keys(_lib).forEach(function (key) { + if (key === \\"default\\" || key === \\"__esModule\\") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _lib[key]; + } + }); +}); + +/***/ }), + +/***/ \\"./public/lib.ts\\": +/*!***********************!*\\\\ + !*** ./public/lib.ts ***! + \\\\***********************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +\\"use strict\\"; + + +Object.defineProperty(exports, \\"__esModule\\", { + value: true +}); +exports.barLibFn = barLibFn; + +/* + * 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. + */ +function barLibFn() { + return 'bar'; +} + +/***/ }) + +/******/ })[\\"plugin\\"]; +//# sourceMappingURL=bar.plugin.js.map" +`; + +exports[`builds expected bundles, saves bundle counts to metadata: foo bundle 1`] = ` +"var __kbnBundles__ = typeof __kbnBundles__ === \\"object\\" ? __kbnBundles__ : {}; __kbnBundles__[\\"plugin/foo\\"] = +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); +/******/ } +/******/ }; +/******/ +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ +/******/ // create a fake namespace object +/******/ // mode & 1: value is a module id, require it +/******/ // mode & 2: merge all properties of value into the ns +/******/ // mode & 4: return value when already ns object +/******/ // mode & 8|1: behave like require +/******/ __webpack_require__.t = function(value, mode) { +/******/ if(mode & 1) value = __webpack_require__(value); +/******/ if(mode & 8) return value; +/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; +/******/ var ns = Object.create(null); +/******/ __webpack_require__.r(ns); +/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); +/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); +/******/ return ns; +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = \\"__REPLACE_WITH_PUBLIC_PATH__\\"; +/******/ +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = \\"./public/index.ts\\"); +/******/ }) +/************************************************************************/ +/******/ ({ + +/***/ \\"./public/ext.ts\\": +/*!***********************!*\\\\ + !*** ./public/ext.ts ***! + \\\\***********************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +\\"use strict\\"; + + +Object.defineProperty(exports, \\"__esModule\\", { + value: true +}); +exports.ext = void 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. + */ +const ext = 'TRUE'; +exports.ext = ext; + +/***/ }), + +/***/ \\"./public/index.ts\\": +/*!*************************!*\\\\ + !*** ./public/index.ts ***! + \\\\*************************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +\\"use strict\\"; + + +Object.defineProperty(exports, \\"__esModule\\", { + value: true +}); + +var _lib = __webpack_require__(/*! ./lib */ \\"./public/lib.ts\\"); + +Object.keys(_lib).forEach(function (key) { + if (key === \\"default\\" || key === \\"__esModule\\") return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _lib[key]; + } + }); +}); + +var _ext = __webpack_require__(/*! ./ext */ \\"./public/ext.ts\\"); + +Object.keys(_ext).forEach(function (key) { + if (key === \\"default\\" || key === \\"__esModule\\") return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _ext[key]; + } + }); +}); + +/***/ }), + +/***/ \\"./public/lib.ts\\": +/*!***********************!*\\\\ + !*** ./public/lib.ts ***! + \\\\***********************/ +/*! no static exports found */ +/***/ (function(module, exports, __webpack_require__) { + +\\"use strict\\"; + + +Object.defineProperty(exports, \\"__esModule\\", { + value: true +}); +exports.fooLibFn = fooLibFn; + +/* + * 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. + */ +function fooLibFn() { + return 'foo'; +} + +/***/ }) + +/******/ })[\\"plugin\\"]; +//# sourceMappingURL=foo.plugin.js.map" +`; diff --git a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts new file mode 100644 index 0000000000000..dda818875db23 --- /dev/null +++ b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts @@ -0,0 +1,155 @@ +/* + * 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 Path from 'path'; +import Fs from 'fs'; +import { inspect } from 'util'; + +import cpy from 'cpy'; +import del from 'del'; +import { toArray, tap } from 'rxjs/operators'; +import { createAbsolutePathSerializer } from '@kbn/dev-utils'; +import { runOptimizer, OptimizerConfig, OptimizerUpdate } from '@kbn/optimizer'; + +const TMP_DIR = Path.resolve(__dirname, '../__fixtures__/__tmp__'); +const MOCK_REPO_SRC = Path.resolve(__dirname, '../__fixtures__/mock_repo'); +const MOCK_REPO_DIR = Path.resolve(TMP_DIR, 'mock_repo'); + +expect.addSnapshotSerializer(createAbsolutePathSerializer(MOCK_REPO_DIR)); + +beforeEach(async () => { + await del(TMP_DIR); + await cpy('**/*', MOCK_REPO_DIR, { + cwd: MOCK_REPO_SRC, + parents: true, + deep: true, + }); +}); + +afterEach(async () => { + await del(TMP_DIR); +}); + +it('builds expected bundles, saves bundle counts to metadata', async () => { + const config = OptimizerConfig.create({ + repoRoot: MOCK_REPO_DIR, + pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], + maxWorkerCount: 1, + }); + + expect(config).toMatchSnapshot('OptimizerConfig'); + + const msgs = await runOptimizer(config) + .pipe( + tap(state => { + if (state.event?.type === 'worker stdio') { + // eslint-disable-next-line no-console + console.log('worker', state.event.stream, state.event.chunk.toString('utf8')); + } + }), + toArray() + ) + .toPromise(); + + const assert = (statement: string, truth: boolean, altStates?: OptimizerUpdate[]) => { + if (!truth) { + throw new Error( + `expected optimizer to ${statement}, states: ${inspect(altStates || msgs, { + colors: true, + depth: Infinity, + })}` + ); + } + }; + + const initializingStates = msgs.filter(msg => msg.state.phase === 'initializing'); + assert('produce at least one initializing event', initializingStates.length >= 1); + + const bundleCacheStates = msgs.filter( + msg => + (msg.event?.type === 'bundle cached' || msg.event?.type === 'bundle not cached') && + msg.state.phase === 'initializing' + ); + assert('produce two bundle cache events while initializing', bundleCacheStates.length === 2); + + const initializedStates = msgs.filter(msg => msg.state.phase === 'initialized'); + assert('produce at least one initialized event', initializedStates.length >= 1); + + const workerStarted = msgs.filter(msg => msg.event?.type === 'worker started'); + assert('produce one worker started event', workerStarted.length === 1); + + const runningStates = msgs.filter(msg => msg.state.phase === 'running'); + assert( + 'produce two or three "running" states', + runningStates.length === 2 || runningStates.length === 3 + ); + + const bundleNotCachedEvents = msgs.filter(msg => msg.event?.type === 'bundle not cached'); + assert('produce two "bundle not cached" events', bundleNotCachedEvents.length === 2); + + const successStates = msgs.filter(msg => msg.state.phase === 'success'); + assert( + 'produce one or two "compiler success" states', + successStates.length === 1 || successStates.length === 2 + ); + + const otherStates = msgs.filter( + msg => + msg.state.phase !== 'initializing' && + msg.state.phase !== 'success' && + msg.state.phase !== 'running' && + msg.state.phase !== 'initialized' && + msg.event?.type !== 'bundle not cached' + ); + assert('produce zero unexpected states', otherStates.length === 0, otherStates); + + expect( + Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, 'plugins/foo/target/public/foo.plugin.js'), 'utf8') + ).toMatchSnapshot('foo bundle'); + + expect( + Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, 'plugins/bar/target/public/bar.plugin.js'), 'utf8') + ).toMatchSnapshot('bar bundle'); + + const foo = config.bundles.find(b => b.id === 'foo')!; + expect(foo).toBeTruthy(); + foo.cache.refresh(); + expect(foo.cache.getModuleCount()).toBe(3); + expect(foo.cache.getReferencedFiles()).toMatchInlineSnapshot(` + Array [ + /plugins/foo/public/ext.ts, + /plugins/foo/public/index.ts, + /plugins/foo/public/lib.ts, + ] + `); + + const bar = config.bundles.find(b => b.id === 'bar')!; + expect(bar).toBeTruthy(); + bar.cache.refresh(); + expect(bar.cache.getModuleCount()).toBe(5); + expect(bar.cache.getReferencedFiles()).toMatchInlineSnapshot(` + Array [ + /plugins/foo/public/ext.ts, + /plugins/foo/public/index.ts, + /plugins/foo/public/lib.ts, + /plugins/bar/public/index.ts, + /plugins/bar/public/lib.ts, + ] + `); +}); diff --git a/packages/kbn-optimizer/src/integration_tests/bundle_cache.test.ts b/packages/kbn-optimizer/src/integration_tests/bundle_cache.test.ts new file mode 100644 index 0000000000000..1bfd8d3fd073a --- /dev/null +++ b/packages/kbn-optimizer/src/integration_tests/bundle_cache.test.ts @@ -0,0 +1,301 @@ +/* + * 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 Path from 'path'; + +import cpy from 'cpy'; +import del from 'del'; +import { toArray } from 'rxjs/operators'; +import { createAbsolutePathSerializer } from '@kbn/dev-utils'; + +import { getMtimes } from '../optimizer/get_mtimes'; +import { OptimizerConfig } from '../optimizer/optimizer_config'; +import { Bundle } from '../common/bundle'; +import { getBundleCacheEvent$ } from '../optimizer/bundle_cache'; + +const TMP_DIR = Path.resolve(__dirname, '../__fixtures__/__tmp__'); +const MOCK_REPO_SRC = Path.resolve(__dirname, '../__fixtures__/mock_repo'); +const MOCK_REPO_DIR = Path.resolve(TMP_DIR, 'mock_repo'); + +expect.addSnapshotSerializer({ + print: () => '', + test: v => v instanceof Bundle, +}); +expect.addSnapshotSerializer(createAbsolutePathSerializer(MOCK_REPO_DIR)); + +beforeEach(async () => { + await del(TMP_DIR); + await cpy('**/*', MOCK_REPO_DIR, { + cwd: MOCK_REPO_SRC, + parents: true, + deep: true, + }); +}); + +afterEach(async () => { + await del(TMP_DIR); +}); + +it('emits "bundle cached" event when everything is updated', async () => { + const config = OptimizerConfig.create({ + repoRoot: MOCK_REPO_DIR, + pluginScanDirs: [], + pluginPaths: [Path.resolve(MOCK_REPO_DIR, 'plugins/foo')], + maxWorkerCount: 1, + }); + const [bundle] = config.bundles; + + const optimizerCacheKey = 'optimizerCacheKey'; + const files = [ + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/ext.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/index.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/lib.ts'), + ]; + const mtimes = await getMtimes(files); + const cacheKey = bundle.createCacheKey(files, mtimes); + + bundle.cache.set({ + cacheKey, + optimizerCacheKey, + files, + moduleCount: files.length, + }); + + const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) + .pipe(toArray()) + .toPromise(); + + expect(cacheEvents).toMatchInlineSnapshot(` + Array [ + Object { + "bundle": , + "type": "bundle cached", + }, + ] + `); +}); + +it('emits "bundle not cached" event when cacheKey is up to date but caching is disabled in config', async () => { + const config = OptimizerConfig.create({ + repoRoot: MOCK_REPO_DIR, + pluginScanDirs: [], + pluginPaths: [Path.resolve(MOCK_REPO_DIR, 'plugins/foo')], + maxWorkerCount: 1, + cache: false, + }); + const [bundle] = config.bundles; + + const optimizerCacheKey = 'optimizerCacheKey'; + const files = [ + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/ext.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/index.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/lib.ts'), + ]; + const mtimes = await getMtimes(files); + const cacheKey = bundle.createCacheKey(files, mtimes); + + bundle.cache.set({ + cacheKey, + optimizerCacheKey, + files, + moduleCount: files.length, + }); + + const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) + .pipe(toArray()) + .toPromise(); + + expect(cacheEvents).toMatchInlineSnapshot(` + Array [ + Object { + "bundle": , + "reason": "cache disabled", + "type": "bundle not cached", + }, + ] + `); +}); + +it('emits "bundle not cached" event when optimizerCacheKey is missing', async () => { + const config = OptimizerConfig.create({ + repoRoot: MOCK_REPO_DIR, + pluginScanDirs: [], + pluginPaths: [Path.resolve(MOCK_REPO_DIR, 'plugins/foo')], + maxWorkerCount: 1, + }); + const [bundle] = config.bundles; + + const optimizerCacheKey = 'optimizerCacheKey'; + const files = [ + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/ext.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/index.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/lib.ts'), + ]; + const mtimes = await getMtimes(files); + const cacheKey = bundle.createCacheKey(files, mtimes); + + bundle.cache.set({ + cacheKey, + optimizerCacheKey: undefined, + files, + moduleCount: files.length, + }); + + const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) + .pipe(toArray()) + .toPromise(); + + expect(cacheEvents).toMatchInlineSnapshot(` + Array [ + Object { + "bundle": , + "reason": "missing optimizer cache key", + "type": "bundle not cached", + }, + ] + `); +}); + +it('emits "bundle not cached" event when optimizerCacheKey is outdated, includes diff', async () => { + const config = OptimizerConfig.create({ + repoRoot: MOCK_REPO_DIR, + pluginScanDirs: [], + pluginPaths: [Path.resolve(MOCK_REPO_DIR, 'plugins/foo')], + maxWorkerCount: 1, + }); + const [bundle] = config.bundles; + + const optimizerCacheKey = 'optimizerCacheKey'; + const files = [ + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/ext.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/index.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/lib.ts'), + ]; + const mtimes = await getMtimes(files); + const cacheKey = bundle.createCacheKey(files, mtimes); + + bundle.cache.set({ + cacheKey, + optimizerCacheKey: 'old', + files, + moduleCount: files.length, + }); + + const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) + .pipe(toArray()) + .toPromise(); + + expect(cacheEvents).toMatchInlineSnapshot(` + Array [ + Object { + "bundle": , + "diff": "- Expected + + Received + + - old + + optimizerCacheKey", + "reason": "optimizer cache key mismatch", + "type": "bundle not cached", + }, + ] + `); +}); + +it('emits "bundle not cached" event when cacheKey is missing', async () => { + const config = OptimizerConfig.create({ + repoRoot: MOCK_REPO_DIR, + pluginScanDirs: [], + pluginPaths: [Path.resolve(MOCK_REPO_DIR, 'plugins/foo')], + maxWorkerCount: 1, + }); + const [bundle] = config.bundles; + + const optimizerCacheKey = 'optimizerCacheKey'; + const files = [ + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/ext.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/index.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/lib.ts'), + ]; + + bundle.cache.set({ + cacheKey: undefined, + optimizerCacheKey, + files, + moduleCount: files.length, + }); + + const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) + .pipe(toArray()) + .toPromise(); + + expect(cacheEvents).toMatchInlineSnapshot(` + Array [ + Object { + "bundle": , + "reason": "missing cache key", + "type": "bundle not cached", + }, + ] + `); +}); + +it('emits "bundle not cached" event when cacheKey is outdated', async () => { + const config = OptimizerConfig.create({ + repoRoot: MOCK_REPO_DIR, + pluginScanDirs: [], + pluginPaths: [Path.resolve(MOCK_REPO_DIR, 'plugins/foo')], + maxWorkerCount: 1, + }); + const [bundle] = config.bundles; + + const optimizerCacheKey = 'optimizerCacheKey'; + const files = [ + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/ext.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/index.ts'), + Path.resolve(MOCK_REPO_DIR, 'plugins/foo/public/lib.ts'), + ]; + + bundle.cache.set({ + cacheKey: 'old', + optimizerCacheKey, + files, + moduleCount: files.length, + }); + + jest.spyOn(bundle, 'createCacheKey').mockImplementation(() => 'new'); + + const cacheEvents = await getBundleCacheEvent$(config, optimizerCacheKey) + .pipe(toArray()) + .toPromise(); + + expect(cacheEvents).toMatchInlineSnapshot(` + Array [ + Object { + "bundle": , + "diff": "- Expected + + Received + + - old + + new", + "reason": "cache key mismatch", + "type": "bundle not cached", + }, + ] + `); +}); diff --git a/packages/kbn-optimizer/src/integration_tests/watch_bundles_for_changes.test.ts b/packages/kbn-optimizer/src/integration_tests/watch_bundles_for_changes.test.ts new file mode 100644 index 0000000000000..c02a857883a98 --- /dev/null +++ b/packages/kbn-optimizer/src/integration_tests/watch_bundles_for_changes.test.ts @@ -0,0 +1,143 @@ +/* + * 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 * as Rx from 'rxjs'; +import { map } from 'rxjs/operators'; +import ActualWatchpack from 'watchpack'; + +import { Bundle, ascending } from '../common'; +import { watchBundlesForChanges$ } from '../optimizer/watch_bundles_for_changes'; +import { BundleCacheEvent } from '../optimizer'; + +jest.mock('fs'); +jest.mock('watchpack'); + +const MockWatchPack: jest.MockedClass = jest.requireMock('watchpack'); +const bundleEntryPath = (bundle: Bundle) => `${bundle.contextDir}/${bundle.entry}`; + +const makeTestBundle = (id: string) => { + const bundle = new Bundle({ + type: 'plugin', + id, + contextDir: `/repo/plugins/${id}/public`, + entry: 'index.ts', + outputDir: `/repo/plugins/${id}/target/public`, + sourceRoot: `/repo`, + }); + + bundle.cache.set({ + cacheKey: 'abc', + moduleCount: 1, + optimizerCacheKey: 'abc', + files: [bundleEntryPath(bundle)], + }); + + return bundle; +}; + +const FOO_BUNDLE = makeTestBundle('foo'); +const BAR_BUNDLE = makeTestBundle('bar'); +const BAZ_BUNDLE = makeTestBundle('baz'); +const BOX_BUNDLE = makeTestBundle('box'); +const CAR_BUNDLE = makeTestBundle('car'); +const BUNDLES = [FOO_BUNDLE, BAR_BUNDLE, BAZ_BUNDLE, BOX_BUNDLE, CAR_BUNDLE]; + +const bundleCacheEvent$ = Rx.from(BUNDLES).pipe( + map( + (bundle): BundleCacheEvent => ({ + type: 'bundle cached', + bundle, + }) + ) +); + +beforeEach(async () => { + jest.useFakeTimers(); +}); + +afterEach(async () => { + jest.useRealTimers(); +}); + +it('notifies of changes and completes once all bundles have changed', async () => { + expect.assertions(18); + + const promise = watchBundlesForChanges$(bundleCacheEvent$, Date.now()) + .pipe( + map((event, i) => { + // each time we trigger a change event we get a 'changed detected' event + if (i === 0 || i === 2 || i === 4 || i === 6) { + expect(event).toHaveProperty('type', 'changes detected'); + return; + } + + expect(event).toHaveProperty('type', 'changes'); + // to teach TS what we're doing + if (event.type !== 'changes') { + return; + } + + // first we change foo and bar, and after 1 second get that change comes though + if (i === 1) { + expect(event.bundles).toHaveLength(2); + const [bar, foo] = event.bundles.sort(ascending(b => b.id)); + expect(bar).toHaveProperty('id', 'bar'); + expect(foo).toHaveProperty('id', 'foo'); + } + + // next we change just the baz package and it's represented on its own + if (i === 3) { + expect(event.bundles).toHaveLength(1); + expect(event.bundles[0]).toHaveProperty('id', 'baz'); + } + + // finally we change box and car together + if (i === 5) { + expect(event.bundles).toHaveLength(2); + const [bar, foo] = event.bundles.sort(ascending(b => b.id)); + expect(bar).toHaveProperty('id', 'box'); + expect(foo).toHaveProperty('id', 'car'); + } + }) + ) + .toPromise(); + + expect(MockWatchPack.mock.instances).toHaveLength(1); + const [watcher] = (MockWatchPack.mock.instances as any) as Array>; + expect(watcher.on).toHaveBeenCalledTimes(1); + expect(watcher.on).toHaveBeenCalledWith('change', expect.any(Function)); + const [, changeListener] = watcher.on.mock.calls[0]; + + // foo and bar are changes without 1sec so they are batched + changeListener(bundleEntryPath(FOO_BUNDLE), 'modified'); + jest.advanceTimersByTime(900); + changeListener(bundleEntryPath(BAR_BUNDLE), 'modified'); + jest.advanceTimersByTime(1000); + + // baz is the only change in 1sec so it is on its own + changeListener(bundleEntryPath(BAZ_BUNDLE), 'modified'); + jest.advanceTimersByTime(1000); + + // finish by changing box and car + changeListener(bundleEntryPath(BOX_BUNDLE), 'deleted'); + changeListener(bundleEntryPath(CAR_BUNDLE), 'deleted'); + jest.advanceTimersByTime(1000); + + await expect(promise).resolves.toEqual(undefined); +}); diff --git a/packages/kbn-optimizer/src/log_optimizer_state.ts b/packages/kbn-optimizer/src/log_optimizer_state.ts new file mode 100644 index 0000000000000..1ee4e47bfd9ee --- /dev/null +++ b/packages/kbn-optimizer/src/log_optimizer_state.ts @@ -0,0 +1,137 @@ +/* + * 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 { inspect } from 'util'; + +import { ToolingLog } from '@kbn/dev-utils'; +import { tap } from 'rxjs/operators'; + +import { OptimizerConfig } from './optimizer'; +import { OptimizerUpdate$ } from './run_optimizer'; +import { CompilerMsg, pipeClosure } from './common'; + +export function logOptimizerState(log: ToolingLog, config: OptimizerConfig) { + return pipeClosure((update$: OptimizerUpdate$) => { + const bundleStates = new Map(); + const bundlesThatWereBuilt = new Set(); + let loggedInit = false; + + return update$.pipe( + tap(update => { + const { event, state } = update; + + if (event?.type === 'worker stdio') { + const chunk = event.chunk.toString('utf8'); + log.warning( + `worker`, + event.stream, + chunk.slice(0, chunk.length - (chunk.endsWith('\n') ? 1 : 0)) + ); + } + + if (event?.type === 'bundle not cached') { + log.debug( + `[${event.bundle.id}] bundle not cached because [${event.reason}]${ + event.diff ? `, diff:\n${event.diff}` : '' + }` + ); + } + + if (event?.type === 'bundle cached') { + log.debug(`[${event.bundle.id}] bundle cached`); + } + + if (event?.type === 'worker started') { + let moduleCount = 0; + for (const bundle of event.bundles) { + moduleCount += bundle.cache.getModuleCount() ?? NaN; + } + const mcString = isFinite(moduleCount) ? String(moduleCount) : '?'; + const bcString = String(event.bundles.length); + log.info(`starting worker [${bcString} bundles, ${mcString} modules]`); + } + + if (state.phase === 'reallocating') { + log.debug(`changes detected...`); + return; + } + + if (state.phase === 'initialized') { + if (!loggedInit) { + loggedInit = true; + log.info(`initialized, ${state.offlineBundles.length} bundles cached`); + } + + if (state.onlineBundles.length === 0) { + log.success(`all bundles cached, success after ${state.durSec}`); + } + return; + } + + for (const { bundleId: id, type } of state.compilerStates) { + const prevBundleState = bundleStates.get(id); + + if (type === prevBundleState) { + continue; + } + + if (type === 'running') { + bundlesThatWereBuilt.add(id); + } + + bundleStates.set(id, type); + log.debug( + `[${id}] state = "${type}"${type !== 'running' ? ` after ${state.durSec} sec` : ''}` + ); + } + + if (state.phase === 'running' || state.phase === 'initializing') { + return true; + } + + if (state.phase === 'issue') { + log.error(`webpack compile errors`); + log.indent(4); + for (const b of state.compilerStates) { + if (b.type === 'compiler issue') { + log.error(`[${b.bundleId}] build`); + log.indent(4); + log.error(b.failure); + log.indent(-4); + } + } + log.indent(-4); + return true; + } + + if (state.phase === 'success') { + const buildCount = bundlesThatWereBuilt.size; + bundlesThatWereBuilt.clear(); + log.success( + `${buildCount} bundles compiled successfully after ${state.durSec} sec` + + (config.watch ? ', watching for changes' : '') + ); + return true; + } + + throw new Error(`unhandled optimizer message: ${inspect(update)}`); + }) + ); + }); +} diff --git a/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.test.ts b/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.test.ts new file mode 100644 index 0000000000000..dd4d5c294dfc8 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.test.ts @@ -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. + */ + +jest.mock('fs'); + +import { Bundle } from '../common'; + +import { assignBundlesToWorkers, Assignments } from './assign_bundles_to_workers'; + +const hasModuleCount = (b: Bundle) => b.cache.getModuleCount() !== undefined; +const noModuleCount = (b: Bundle) => b.cache.getModuleCount() === undefined; +const summarizeBundles = (w: Assignments) => + [ + w.moduleCount ? `${w.moduleCount} known modules` : '', + w.newBundles ? `${w.newBundles} new bundles` : '', + ] + .filter(Boolean) + .join(', '); + +const readConfigs = (workers: Assignments[]) => + workers.map( + (w, i) => `worker ${i} (${summarizeBundles(w)}) => ${w.bundles.map(b => b.id).join(',')}` + ); + +const assertReturnVal = (workers: Assignments[]) => { + expect(workers).toBeInstanceOf(Array); + for (const worker of workers) { + expect(worker).toEqual({ + moduleCount: expect.any(Number), + newBundles: expect.any(Number), + bundles: expect.any(Array), + }); + + expect(worker.bundles.filter(noModuleCount).length).toBe(worker.newBundles); + expect( + worker.bundles.filter(hasModuleCount).reduce((sum, b) => sum + b.cache.getModuleCount()!, 0) + ).toBe(worker.moduleCount); + } +}; + +const testBundle = (id: string) => + new Bundle({ + contextDir: `/repo/plugin/${id}/public`, + entry: 'index.ts', + id, + outputDir: `/repo/plugins/${id}/target/public`, + sourceRoot: `/repo`, + type: 'plugin', + }); + +const getBundles = ({ + withCounts = 0, + withoutCounts = 0, +}: { + withCounts?: number; + withoutCounts?: number; +}) => { + const bundles: Bundle[] = []; + + for (let i = 1; i <= withCounts; i++) { + const id = `foo${i}`; + const bundle = testBundle(id); + bundle.cache.set({ moduleCount: i % 5 === 0 ? i * 10 : i }); + bundles.push(bundle); + } + + for (let i = 0; i < withoutCounts; i++) { + const id = `bar${i}`; + bundles.push(testBundle(id)); + } + + return bundles; +}; + +it('creates less workers if maxWorkersCount is larger than bundle count', () => { + const workers = assignBundlesToWorkers(getBundles({ withCounts: 2 }), 10); + + assertReturnVal(workers); + expect(workers.length).toBe(2); + expect(readConfigs(workers)).toMatchInlineSnapshot(` + Array [ + "worker 0 (1 known modules) => foo1", + "worker 1 (2 known modules) => foo2", + ] + `); +}); + +it('assigns unknown plugin counts as evenly as possible', () => { + const workers = assignBundlesToWorkers(getBundles({ withoutCounts: 10 }), 3); + + assertReturnVal(workers); + expect(readConfigs(workers)).toMatchInlineSnapshot(` + Array [ + "worker 0 (4 new bundles) => bar9,bar6,bar3,bar0", + "worker 1 (3 new bundles) => bar8,bar5,bar2", + "worker 2 (3 new bundles) => bar7,bar4,bar1", + ] + `); +}); + +it('distributes bundles without module counts evenly after assigning modules with known counts evenly', () => { + const bundles = getBundles({ withCounts: 16, withoutCounts: 10 }); + const workers = assignBundlesToWorkers(bundles, 4); + + assertReturnVal(workers); + expect(readConfigs(workers)).toMatchInlineSnapshot(` + Array [ + "worker 0 (78 known modules, 3 new bundles) => foo5,foo11,foo8,foo6,foo2,foo1,bar9,bar5,bar1", + "worker 1 (78 known modules, 3 new bundles) => foo16,foo14,foo13,foo12,foo9,foo7,foo4,foo3,bar8,bar4,bar0", + "worker 2 (100 known modules, 2 new bundles) => foo10,bar7,bar3", + "worker 3 (150 known modules, 2 new bundles) => foo15,bar6,bar2", + ] + `); +}); + +it('distributes 2 bundles to workers evenly', () => { + const workers = assignBundlesToWorkers(getBundles({ withCounts: 2 }), 4); + + assertReturnVal(workers); + expect(readConfigs(workers)).toMatchInlineSnapshot(` + Array [ + "worker 0 (1 known modules) => foo1", + "worker 1 (2 known modules) => foo2", + ] + `); +}); + +it('distributes 5 bundles to workers evenly', () => { + const workers = assignBundlesToWorkers(getBundles({ withCounts: 5 }), 4); + + assertReturnVal(workers); + expect(readConfigs(workers)).toMatchInlineSnapshot(` + Array [ + "worker 0 (3 known modules) => foo2,foo1", + "worker 1 (3 known modules) => foo3", + "worker 2 (4 known modules) => foo4", + "worker 3 (50 known modules) => foo5", + ] + `); +}); + +it('distributes 10 bundles to workers evenly', () => { + const workers = assignBundlesToWorkers(getBundles({ withCounts: 10 }), 4); + + assertReturnVal(workers); + expect(readConfigs(workers)).toMatchInlineSnapshot(` + Array [ + "worker 0 (20 known modules) => foo9,foo6,foo4,foo1", + "worker 1 (20 known modules) => foo8,foo7,foo3,foo2", + "worker 2 (50 known modules) => foo5", + "worker 3 (100 known modules) => foo10", + ] + `); +}); + +it('distributes 15 bundles to workers evenly', () => { + const workers = assignBundlesToWorkers(getBundles({ withCounts: 15 }), 4); + + assertReturnVal(workers); + expect(readConfigs(workers)).toMatchInlineSnapshot(` + Array [ + "worker 0 (70 known modules) => foo14,foo13,foo12,foo11,foo9,foo6,foo4,foo1", + "worker 1 (70 known modules) => foo5,foo8,foo7,foo3,foo2", + "worker 2 (100 known modules) => foo10", + "worker 3 (150 known modules) => foo15", + ] + `); +}); + +it('distributes 20 bundles to workers evenly', () => { + const workers = assignBundlesToWorkers(getBundles({ withCounts: 20 }), 4); + + assertReturnVal(workers); + expect(readConfigs(workers)).toMatchInlineSnapshot(` + Array [ + "worker 0 (153 known modules) => foo15,foo3", + "worker 1 (153 known modules) => foo10,foo16,foo13,foo11,foo7,foo6", + "worker 2 (154 known modules) => foo5,foo19,foo18,foo17,foo14,foo12,foo9,foo8,foo4,foo2,foo1", + "worker 3 (200 known modules) => foo20", + ] + `); +}); + +it('distributes 25 bundles to workers evenly', () => { + const workers = assignBundlesToWorkers(getBundles({ withCounts: 25 }), 4); + + assertReturnVal(workers); + expect(readConfigs(workers)).toMatchInlineSnapshot(` + Array [ + "worker 0 (250 known modules) => foo20,foo17,foo13,foo9,foo8,foo2,foo1", + "worker 1 (250 known modules) => foo15,foo23,foo22,foo18,foo16,foo11,foo7,foo3", + "worker 2 (250 known modules) => foo10,foo5,foo24,foo21,foo19,foo14,foo12,foo6,foo4", + "worker 3 (250 known modules) => foo25", + ] + `); +}); + +it('distributes 30 bundles to workers evenly', () => { + const workers = assignBundlesToWorkers(getBundles({ withCounts: 30 }), 4); + + assertReturnVal(workers); + expect(readConfigs(workers)).toMatchInlineSnapshot(` + Array [ + "worker 0 (352 known modules) => foo30,foo22,foo14,foo11,foo4,foo1", + "worker 1 (352 known modules) => foo15,foo10,foo28,foo24,foo19,foo16,foo9,foo6", + "worker 2 (353 known modules) => foo20,foo5,foo29,foo23,foo21,foo13,foo12,foo3,foo2", + "worker 3 (353 known modules) => foo25,foo27,foo26,foo18,foo17,foo8,foo7", + ] + `); +}); diff --git a/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.ts b/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.ts new file mode 100644 index 0000000000000..001783b167c7a --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.ts @@ -0,0 +1,121 @@ +/* + * 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 { Bundle, descending, ascending } from '../common'; + +// helper types used inside getWorkerConfigs so we don't have +// to calculate moduleCounts over and over + +export interface Assignments { + moduleCount: number; + newBundles: number; + bundles: Bundle[]; +} + +/** assign a wrapped bundle to a worker */ +const assignBundle = (worker: Assignments, bundle: Bundle) => { + const moduleCount = bundle.cache.getModuleCount(); + if (moduleCount !== undefined) { + worker.moduleCount += moduleCount; + } else { + worker.newBundles += 1; + } + + worker.bundles.push(bundle); +}; + +/** + * Create WorkerConfig objects for each worker we will use to build the bundles. + * + * We need to evenly assign bundles to workers so that each worker will have + * about the same amount of work to do. We do this by tracking the module count + * of each bundle in the OptimizerCache and determining the overall workload + * of a worker by the sum of modules it will have to compile for all of its + * bundles. + * + * We only know the module counts after the first build of a new bundle, so + * when we encounter a bundle without a module count in the cache we just + * assign them to workers round-robin, starting with the workers which have + * the smallest number of modules to build. + */ +export function assignBundlesToWorkers(bundles: Bundle[], maxWorkerCount: number) { + const workerCount = Math.min(bundles.length, maxWorkerCount); + const workers: Assignments[] = []; + for (let i = 0; i < workerCount; i++) { + workers.push({ + moduleCount: 0, + newBundles: 0, + bundles: [], + }); + } + + /** + * separate the bundles which do and don't have module + * counts and sort them by [moduleCount, id] + */ + const bundlesWithCountsDesc = bundles + .filter(b => b.cache.getModuleCount() !== undefined) + .sort( + descending( + b => b.cache.getModuleCount(), + b => b.id + ) + ); + const bundlesWithoutModuleCounts = bundles + .filter(b => b.cache.getModuleCount() === undefined) + .sort(descending(b => b.id)); + + /** + * assign largest bundles to the smallest worker until it is + * no longer the smallest worker and repeat until all bundles + * with module counts are assigned + */ + while (bundlesWithCountsDesc.length) { + const [smallestWorker, nextSmallestWorker] = workers.sort(ascending(w => w.moduleCount)); + + while (!nextSmallestWorker || smallestWorker.moduleCount <= nextSmallestWorker.moduleCount) { + const bundle = bundlesWithCountsDesc.shift(); + + if (!bundle) { + break; + } + + assignBundle(smallestWorker, bundle); + } + } + + /** + * assign bundles without module counts to workers round-robin + * starting with the smallest workers + */ + workers.sort(ascending(w => w.moduleCount)); + while (bundlesWithoutModuleCounts.length) { + for (const worker of workers) { + const bundle = bundlesWithoutModuleCounts.shift(); + + if (!bundle) { + break; + } + + assignBundle(worker, bundle); + } + } + + return workers; +} diff --git a/packages/kbn-optimizer/src/optimizer/bundle_cache.ts b/packages/kbn-optimizer/src/optimizer/bundle_cache.ts new file mode 100644 index 0000000000000..55e8e1d3fd084 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/bundle_cache.ts @@ -0,0 +1,132 @@ +/* + * 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 * as Rx from 'rxjs'; +import { mergeAll } from 'rxjs/operators'; + +import { Bundle } from '../common'; + +import { OptimizerConfig } from './optimizer_config'; +import { getMtimes } from './get_mtimes'; +import { diffCacheKey } from './cache_keys'; + +export type BundleCacheEvent = BundleNotCachedEvent | BundleCachedEvent; + +export interface BundleNotCachedEvent { + type: 'bundle not cached'; + reason: + | 'missing optimizer cache key' + | 'optimizer cache key mismatch' + | 'missing cache key' + | 'cache key mismatch' + | 'cache disabled'; + diff?: string; + bundle: Bundle; +} + +export interface BundleCachedEvent { + type: 'bundle cached'; + bundle: Bundle; +} + +export function getBundleCacheEvent$( + config: OptimizerConfig, + optimizerCacheKey: unknown +): Rx.Observable { + return Rx.defer(async () => { + const events: BundleCacheEvent[] = []; + const eligibleBundles: Bundle[] = []; + + for (const bundle of config.bundles) { + if (!config.cache) { + events.push({ + type: 'bundle not cached', + reason: 'cache disabled', + bundle, + }); + continue; + } + + const cachedOptimizerCacheKeys = bundle.cache.getOptimizerCacheKey(); + if (!cachedOptimizerCacheKeys) { + events.push({ + type: 'bundle not cached', + reason: 'missing optimizer cache key', + bundle, + }); + continue; + } + + const optimizerCacheKeyDiff = diffCacheKey(cachedOptimizerCacheKeys, optimizerCacheKey); + if (optimizerCacheKeyDiff !== undefined) { + events.push({ + type: 'bundle not cached', + reason: 'optimizer cache key mismatch', + diff: optimizerCacheKeyDiff, + bundle, + }); + continue; + } + + if (!bundle.cache.getCacheKey()) { + events.push({ + type: 'bundle not cached', + reason: 'missing cache key', + bundle, + }); + continue; + } + + eligibleBundles.push(bundle); + } + + const mtimes = await getMtimes( + new Set( + eligibleBundles.reduce( + (acc: string[], bundle) => [...acc, ...(bundle.cache.getReferencedFiles() || [])], + [] + ) + ) + ); + + for (const bundle of eligibleBundles) { + const diff = diffCacheKey( + bundle.cache.getCacheKey(), + bundle.createCacheKey(bundle.cache.getReferencedFiles() || [], mtimes) + ); + + if (diff) { + events.push({ + type: 'bundle not cached', + reason: 'cache key mismatch', + diff, + bundle, + }); + continue; + } + + events.push({ + type: 'bundle cached', + bundle, + }); + } + + return events; + }).pipe(mergeAll()); +} diff --git a/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts b/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts new file mode 100644 index 0000000000000..44234acd897dc --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts @@ -0,0 +1,178 @@ +/* + * 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 jestDiff from 'jest-diff'; +import { REPO_ROOT, createAbsolutePathSerializer } from '@kbn/dev-utils'; + +import { reformatJestDiff, getOptimizerCacheKey, diffCacheKey } from './cache_keys'; +import { OptimizerConfig } from './optimizer_config'; + +jest.mock('./get_changes.ts'); +jest.mock('execa'); +expect.addSnapshotSerializer(createAbsolutePathSerializer()); + +jest.requireMock('execa').mockImplementation(async (cmd: string, args: string[], opts: object) => { + expect(cmd).toBe('git'); + expect(args).toEqual([ + 'log', + '-n', + '1', + '--pretty=format:%H', + '--', + expect.stringContaining('kbn-optimizer'), + ]); + expect(opts).toEqual({ + cwd: REPO_ROOT, + }); + + return { + stdout: '', + }; +}); + +jest.requireMock('./get_changes.ts').getChanges.mockImplementation( + async () => + new Map([ + ['/foo/bar/a', 'modified'], + ['/foo/bar/b', 'modified'], + ['/foo/bar/c', 'deleted'], + ]) +); + +describe('getOptimizerCacheKey()', () => { + it('uses latest commit and changes files to create unique value', async () => { + const config = OptimizerConfig.create({ + repoRoot: REPO_ROOT, + }); + + await expect(getOptimizerCacheKey(config)).resolves.toMatchInlineSnapshot(` + Object { + "deletedPaths": Array [ + "/foo/bar/c", + ], + "lastCommit": "", + "modifiedPaths": Object {}, + "workerConfig": Object { + "browserslistEnv": "dev", + "cache": true, + "dist": false, + "optimizerCacheKey": "♻", + "profileWebpack": false, + "repoRoot": , + "watch": false, + }, + } + `); + }); +}); + +describe('diffCacheKey()', () => { + it('returns undefined if values are equal', () => { + expect(diffCacheKey('1', '1')).toBe(undefined); + expect(diffCacheKey(1, 1)).toBe(undefined); + expect(diffCacheKey(['1', '2', { a: 'b' }], ['1', '2', { a: 'b' }])).toBe(undefined); + expect( + diffCacheKey( + { + a: '1', + b: '2', + }, + { + b: '2', + a: '1', + } + ) + ).toBe(undefined); + }); + + it('returns a diff if the values are different', () => { + expect(diffCacheKey(['1', '2', { a: 'b' }], ['1', '2', { b: 'a' }])).toMatchInlineSnapshot(` + "- Expected + + Received + +  Array [ +  \\"1\\", +  \\"2\\", +  Object { + - \\"a\\": \\"b\\", + + \\"b\\": \\"a\\", +  }, +  ]" + `); + expect( + diffCacheKey( + { + a: '1', + b: '1', + }, + { + b: '2', + a: '2', + } + ) + ).toMatchInlineSnapshot(` + "- Expected + + Received + +  Object { + - \\"a\\": \\"1\\", + - \\"b\\": \\"1\\", + + \\"a\\": \\"2\\", + + \\"b\\": \\"2\\", +  }" + `); + }); +}); + +describe('reformatJestDiff()', () => { + it('reformats large jestDiff output to focus on the changed lines', () => { + const diff = jestDiff( + { + a: ['1', '1', '1', '1', '1', '1', '1', '2', '1', '1', '1', '1', '1', '1', '1', '1', '1'], + }, + { + b: ['1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '2', '1', '1', '1', '1'], + } + ); + + expect(reformatJestDiff(diff)).toMatchInlineSnapshot(` + "- Expected + + Received + +  Object { + - \\"a\\": Array [ + + \\"b\\": Array [ +  \\"1\\", +  \\"1\\", +  ... +  \\"1\\", +  \\"1\\", + - \\"2\\", +  \\"1\\", +  \\"1\\", +  ... +  \\"1\\", +  \\"1\\", + + \\"2\\", +  \\"1\\", +  \\"1\\", +  ..." + `); + }); +}); diff --git a/packages/kbn-optimizer/src/optimizer/cache_keys.ts b/packages/kbn-optimizer/src/optimizer/cache_keys.ts new file mode 100644 index 0000000000000..3529ffa587f16 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/cache_keys.ts @@ -0,0 +1,155 @@ +/* + * 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 Path from 'path'; + +import Chalk from 'chalk'; +import execa from 'execa'; +import { REPO_ROOT } from '@kbn/dev-utils'; +import stripAnsi from 'strip-ansi'; + +import jestDiff from 'jest-diff'; +import jsonStable from 'json-stable-stringify'; +import { ascending, WorkerConfig } from '../common'; + +import { getMtimes } from './get_mtimes'; +import { getChanges } from './get_changes'; +import { OptimizerConfig } from './optimizer_config'; + +const OPTIMIZER_DIR = Path.dirname(require.resolve('../../package.json')); +const RELATIVE_DIR = Path.relative(REPO_ROOT, OPTIMIZER_DIR); + +export function diffCacheKey(expected?: unknown, actual?: unknown) { + if (jsonStable(expected) === jsonStable(actual)) { + return; + } + + return reformatJestDiff(jestDiff(expected, actual)); +} + +export function reformatJestDiff(diff: string | null) { + const diffLines = diff?.split('\n') || []; + + if ( + diffLines.length < 4 || + stripAnsi(diffLines[0]) !== '- Expected' || + stripAnsi(diffLines[1]) !== '+ Received' + ) { + throw new Error(`unexpected diff format: ${diff}`); + } + + const outputLines = [diffLines.shift(), diffLines.shift(), diffLines.shift()]; + + /** + * buffer which contains between 0 and 5 lines from the diff which aren't additions or + * deletions. The first three are the first three lines seen since the buffer was cleared + * and the last two lines are the last two lines seen. + * + * When flushContext() is called we write the first two lines to output, an elipses if there + * are five lines, and then the last two lines. + * + * At the very end we will write the last two lines of context if they're defined + */ + const contextBuffer: string[] = []; + + /** + * Convert a line to an empty line with elipses placed where the text on that line starts + */ + const toElipses = (line: string) => { + return stripAnsi(line).replace(/^(\s*).*/, '$1...'); + }; + + while (diffLines.length) { + const line = diffLines.shift()!; + const plainLine = stripAnsi(line); + if (plainLine.startsWith('+ ') || plainLine.startsWith('- ')) { + // write contextBuffer to the outputLines + if (contextBuffer.length) { + outputLines.push( + ...contextBuffer.slice(0, 2), + ...(contextBuffer.length === 5 + ? [Chalk.dim(toElipses(contextBuffer[2])), ...contextBuffer.slice(3, 5)] + : contextBuffer.slice(2, 4)) + ); + + contextBuffer.length = 0; + } + + // add this line to the outputLines + outputLines.push(line); + } else { + // update the contextBuffer with this line which doesn't represent a change + if (contextBuffer.length === 5) { + contextBuffer[3] = contextBuffer[4]; + contextBuffer[4] = line; + } else { + contextBuffer.push(line); + } + } + } + + if (contextBuffer.length) { + outputLines.push( + ...contextBuffer.slice(0, 2), + ...(contextBuffer.length > 2 ? [Chalk.dim(toElipses(contextBuffer[2]))] : []) + ); + } + + return outputLines.join('\n'); +} + +export interface OptimizerCacheKey { + readonly lastCommit: string | undefined; + readonly workerConfig: WorkerConfig; + readonly deletedPaths: string[]; + readonly modifiedPaths: Record; +} + +async function getLastCommit() { + const { stdout } = await execa( + 'git', + ['log', '-n', '1', '--pretty=format:%H', '--', RELATIVE_DIR], + { + cwd: REPO_ROOT, + } + ); + + return stdout.trim() || undefined; +} + +export async function getOptimizerCacheKey(config: OptimizerConfig) { + const changes = Array.from((await getChanges(OPTIMIZER_DIR)).entries()); + + const cacheKeys: OptimizerCacheKey = { + lastCommit: await getLastCommit(), + workerConfig: config.getWorkerConfig('♻'), + deletedPaths: changes.filter(e => e[1] === 'deleted').map(e => e[0]), + modifiedPaths: {} as Record, + }; + + const modified = changes.filter(e => e[1] === 'modified').map(e => e[0]); + const mtimes = await getMtimes(modified); + for (const [path, mtime] of Array.from(mtimes.entries()).sort(ascending(e => e[0]))) { + if (typeof mtime === 'number') { + cacheKeys.modifiedPaths[path] = mtime; + } + } + + return cacheKeys; +} diff --git a/packages/kbn-optimizer/src/optimizer/get_bundles.test.ts b/packages/kbn-optimizer/src/optimizer/get_bundles.test.ts new file mode 100644 index 0000000000000..9d95d883d605c --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/get_bundles.test.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 { createAbsolutePathSerializer } from '@kbn/dev-utils'; + +import { getBundles } from './get_bundles'; + +expect.addSnapshotSerializer(createAbsolutePathSerializer('/repo')); + +it('returns a bundle for each plugin', () => { + expect( + getBundles( + [ + { + directory: '/repo/plugins/foo', + id: 'foo', + isUiPlugin: true, + }, + { + directory: '/repo/plugins/bar', + id: 'bar', + isUiPlugin: false, + }, + { + directory: '/outside/of/repo/plugins/baz', + id: 'baz', + isUiPlugin: true, + }, + ], + '/repo' + ).map(b => b.toSpec()) + ).toMatchInlineSnapshot(` + Array [ + Object { + "contextDir": /plugins/foo, + "entry": "./public/index", + "id": "foo", + "outputDir": /plugins/foo/target/public, + "sourceRoot": , + "type": "plugin", + }, + Object { + "contextDir": "/outside/of/repo/plugins/baz", + "entry": "./public/index", + "id": "baz", + "outputDir": "/outside/of/repo/plugins/baz/target/public", + "sourceRoot": , + "type": "plugin", + }, + ] + `); +}); diff --git a/packages/kbn-optimizer/src/optimizer/get_bundles.ts b/packages/kbn-optimizer/src/optimizer/get_bundles.ts new file mode 100644 index 0000000000000..7cd7bf15317e0 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/get_bundles.ts @@ -0,0 +1,40 @@ +/* + * 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 Path from 'path'; + +import { Bundle } from '../common'; + +import { KibanaPlatformPlugin } from './kibana_platform_plugins'; + +export function getBundles(plugins: KibanaPlatformPlugin[], repoRoot: string) { + return plugins + .filter(p => p.isUiPlugin) + .map( + p => + new Bundle({ + type: 'plugin', + id: p.id, + entry: './public/index', + sourceRoot: repoRoot, + contextDir: p.directory, + outputDir: Path.resolve(p.directory, 'target/public'), + }) + ); +} diff --git a/packages/kbn-optimizer/src/optimizer/get_changes.test.ts b/packages/kbn-optimizer/src/optimizer/get_changes.test.ts new file mode 100644 index 0000000000000..04a6dfb3e3625 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/get_changes.test.ts @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('execa'); + +import { getChanges } from './get_changes'; + +const execa: jest.Mock = jest.requireMock('execa'); + +it('parses git ls-files output', async () => { + expect.assertions(4); + + execa.mockImplementation((cmd, args, options) => { + expect(cmd).toBe('git'); + expect(args).toEqual(['ls-files', '-dmt', '--', '/foo/bar/x']); + expect(options).toEqual({ + cwd: '/foo/bar/x', + }); + + return { + stdout: [ + 'C kbn-optimizer/package.json', + 'C kbn-optimizer/src/common/bundle.ts', + 'R kbn-optimizer/src/common/bundles.ts', + 'C kbn-optimizer/src/common/bundles.ts', + 'R kbn-optimizer/src/get_bundle_definitions.test.ts', + 'C kbn-optimizer/src/get_bundle_definitions.test.ts', + ].join('\n'), + }; + }); + + await expect(getChanges('/foo/bar/x')).resolves.toMatchInlineSnapshot(` + Map { + "/foo/bar/x/kbn-optimizer/package.json" => "modified", + "/foo/bar/x/kbn-optimizer/src/common/bundle.ts" => "modified", + "/foo/bar/x/kbn-optimizer/src/common/bundles.ts" => "deleted", + "/foo/bar/x/kbn-optimizer/src/get_bundle_definitions.test.ts" => "deleted", + } + `); +}); diff --git a/packages/kbn-optimizer/src/optimizer/get_changes.ts b/packages/kbn-optimizer/src/optimizer/get_changes.ts new file mode 100644 index 0000000000000..0c03b029c0dc4 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/get_changes.ts @@ -0,0 +1,63 @@ +/* + * 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 Path from 'path'; + +import execa from 'execa'; + +export type Changes = Map; + +/** + * get the changes in all the context directories (plugin public paths) + */ +export async function getChanges(dir: string) { + const { stdout } = await execa('git', ['ls-files', '-dmt', '--', dir], { + cwd: dir, + }); + + const changes: Changes = new Map(); + const output = stdout.trim(); + + if (output) { + for (const line of output.split('\n')) { + const [tag, ...pathParts] = line.trim().split(' '); + const path = Path.resolve(dir, pathParts.join(' ')); + switch (tag) { + case 'M': + case 'C': + // for some reason ls-files returns deleted files as both deleted + // and modified, so make sure not to overwrite changes already + // tracked as "deleted" + if (changes.get(path) !== 'deleted') { + changes.set(path, 'modified'); + } + break; + + case 'R': + changes.set(path, 'deleted'); + break; + + default: + throw new Error(`unexpected path status ${tag} for path ${path}`); + } + } + } + + return changes; +} diff --git a/src/legacy/core_plugins/kibana/server/lib/__tests__/system_api.js b/packages/kbn-optimizer/src/optimizer/get_mtimes.test.ts similarity index 57% rename from src/legacy/core_plugins/kibana/server/lib/__tests__/system_api.js rename to packages/kbn-optimizer/src/optimizer/get_mtimes.test.ts index a63a93f3a70d5..e1ecd3f1078ad 100644 --- a/src/legacy/core_plugins/kibana/server/lib/__tests__/system_api.js +++ b/packages/kbn-optimizer/src/optimizer/get_mtimes.test.ts @@ -17,25 +17,30 @@ * under the License. */ -import expect from '@kbn/expect'; -import { isSystemApiRequest } from '../system_api'; +jest.mock('fs'); -describe('system_api', () => { - describe('#isSystemApiRequest', () => { - it('returns true for a system API HTTP request', () => { - const mockHapiRequest = { - headers: { - 'kbn-system-api': true, - }, - }; - expect(isSystemApiRequest(mockHapiRequest)).to.be(true); - }); +import { getMtimes } from './get_mtimes'; - it('returns false for a non-system API HTTP request', () => { - const mockHapiRequest = { - headers: {}, - }; - expect(isSystemApiRequest(mockHapiRequest)).to.be(false); - }); +const { stat }: { stat: jest.Mock } = jest.requireMock('fs'); + +it('returns mtimes Map', async () => { + stat.mockImplementation((path, cb) => { + if (path.includes('missing')) { + const error = new Error('file not found'); + (error as any).code = 'ENOENT'; + cb(error); + } else { + cb(null, { + mtimeMs: 1234, + }); + } }); + + await expect(getMtimes(['/foo/bar', '/foo/missing', '/foo/baz', '/foo/bar'])).resolves + .toMatchInlineSnapshot(` + Map { + "/foo/bar" => 1234, + "/foo/baz" => 1234, + } + `); }); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.js b/packages/kbn-optimizer/src/optimizer/get_mtimes.ts similarity index 51% rename from src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.js rename to packages/kbn-optimizer/src/optimizer/get_mtimes.ts index 2aa53db11c1d9..9ac156cb5b8de 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.js +++ b/packages/kbn-optimizer/src/optimizer/get_mtimes.ts @@ -17,22 +17,31 @@ * under the License. */ -import React from 'react'; +import Fs from 'fs'; -import { banners } from 'ui/notify'; -import { OptedInBanner } from '../../components/opted_in_notice_banner'; +import * as Rx from 'rxjs'; +import { mergeMap, toArray, map, catchError } from 'rxjs/operators'; + +const stat$ = Rx.bindNodeCallback(Fs.stat); /** - * Render the Telemetry Opt-in notice banner. - * - * @param {Object} telemetryOptInProvider The telemetry opt-in provider. - * @param {Object} _banners Banners singleton, which can be overridden for tests. + * get mtimes of referenced paths concurrently, limit concurrency to 100 */ -export function renderOptedInBanner(telemetryOptInProvider, { _banners = banners } = {}) { - const bannerId = _banners.add({ - component: , - priority: 10000, - }); - - telemetryOptInProvider.setOptInBannerNoticeId(bannerId); +export async function getMtimes(paths: Iterable) { + return await Rx.from(paths) + .pipe( + // map paths to [path, mtimeMs] entries with concurrency of + // 100 at a time, ignoring missing paths + mergeMap( + path => + stat$(path).pipe( + map(stat => [path, stat.mtimeMs] as const), + catchError((error: any) => (error?.code === 'ENOENT' ? Rx.EMPTY : Rx.throwError(error))) + ), + 100 + ), + toArray(), + map(entries => new Map(entries)) + ) + .toPromise(); } diff --git a/src/legacy/core_plugins/telemetry/public/components/index.ts b/packages/kbn-optimizer/src/optimizer/index.ts similarity index 75% rename from src/legacy/core_plugins/telemetry/public/components/index.ts rename to packages/kbn-optimizer/src/optimizer/index.ts index 1fc55eadd1e10..b7f14cf3c517f 100644 --- a/src/legacy/core_plugins/telemetry/public/components/index.ts +++ b/packages/kbn-optimizer/src/optimizer/index.ts @@ -17,8 +17,10 @@ * under the License. */ -// @ts-ignore -export { TelemetryForm } from './telemetry_form'; -export { OptInExampleFlyout } from './opt_in_details_component'; -export { OptInBanner } from './opt_in_banner_component'; -export { OptInMessage } from './opt_in_message'; +export * from './optimizer_config'; +export { WorkerStdio } from './observe_worker'; +export * from './optimizer_reducer'; +export * from './cache_keys'; +export * from './watch_bundles_for_changes'; +export * from './run_workers'; +export * from './bundle_cache'; diff --git a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.test.ts b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.test.ts new file mode 100644 index 0000000000000..e047b6d1e44cf --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.test.ts @@ -0,0 +1,60 @@ +/* + * 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 Path from 'path'; + +import { createAbsolutePathSerializer } from '@kbn/dev-utils'; + +import { findKibanaPlatformPlugins } from './kibana_platform_plugins'; + +expect.addSnapshotSerializer(createAbsolutePathSerializer()); + +const FIXTURES_PATH = Path.resolve(__dirname, '../__fixtures__'); + +it('parses kibana.json files of plugins found in pluginDirs', () => { + expect( + findKibanaPlatformPlugins( + [Path.resolve(FIXTURES_PATH, 'mock_repo/plugins')], + [Path.resolve(FIXTURES_PATH, 'mock_repo/test_plugins/test_baz')] + ) + ).toMatchInlineSnapshot(` + Array [ + Object { + "directory": /packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar, + "id": "bar", + "isUiPlugin": true, + }, + Object { + "directory": /packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz, + "id": "baz", + "isUiPlugin": false, + }, + Object { + "directory": /packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo, + "id": "foo", + "isUiPlugin": true, + }, + Object { + "directory": /packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz, + "id": "test_baz", + "isUiPlugin": false, + }, + ] + `); +}); diff --git a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts new file mode 100644 index 0000000000000..2165878e92ff4 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts @@ -0,0 +1,72 @@ +/* + * 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 Path from 'path'; + +import globby from 'globby'; +import loadJsonFile from 'load-json-file'; + +export interface KibanaPlatformPlugin { + readonly directory: string; + readonly id: string; + readonly isUiPlugin: boolean; +} + +/** + * Helper to find the new platform plugins. + */ +export function findKibanaPlatformPlugins(scanDirs: string[], paths: string[]) { + return globby + .sync( + Array.from( + new Set([ + ...scanDirs.map(dir => `${dir}/*/kibana.json`), + ...paths.map(path => `${path}/kibana.json`), + ]) + ), + { + absolute: true, + } + ) + .map(path => + // absolute paths returned from globby are using normalize or something so the path separators are `/` even on windows, Path.resolve solves this + readKibanaPlatformPlugin(Path.resolve(path)) + ); +} + +function readKibanaPlatformPlugin(manifestPath: string): KibanaPlatformPlugin { + if (!Path.isAbsolute(manifestPath)) { + throw new TypeError('expected new platform manifest path to be absolute'); + } + + const manifest = loadJsonFile.sync(manifestPath); + if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) { + throw new TypeError('expected new platform plugin manifest to be a JSON encoded object'); + } + + if (typeof manifest.id !== 'string') { + throw new TypeError('expected new platform plugin manifest to have a string id'); + } + + return { + directory: Path.dirname(manifestPath), + id: manifest.id, + isUiPlugin: !!manifest.ui, + }; +} diff --git a/packages/kbn-optimizer/src/optimizer/observe_worker.ts b/packages/kbn-optimizer/src/optimizer/observe_worker.ts new file mode 100644 index 0000000000000..bfc853e5a6b75 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/observe_worker.ts @@ -0,0 +1,199 @@ +/* + * 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 { fork, ChildProcess } from 'child_process'; +import { Readable } from 'stream'; +import { inspect } from 'util'; + +import * as Rx from 'rxjs'; +import { map, takeUntil } from 'rxjs/operators'; + +import { isWorkerMsg, WorkerConfig, WorkerMsg, Bundle } from '../common'; + +import { OptimizerConfig } from './optimizer_config'; + +export interface WorkerStdio { + type: 'worker stdio'; + stream: 'stdout' | 'stderr'; + chunk: Buffer; +} + +export interface WorkerStarted { + type: 'worker started'; + bundles: Bundle[]; +} + +export type WorkerStatus = WorkerStdio | WorkerStarted; + +interface ProcResource extends Rx.Unsubscribable { + proc: ChildProcess; +} +const isNumeric = (input: any) => String(input).match(/^[0-9]+$/); + +let inspectPortCounter = 9230; +const inspectFlagIndex = process.execArgv.findIndex(flag => flag.startsWith('--inspect')); +let inspectFlag: string | undefined; +if (inspectFlagIndex !== -1) { + const argv = process.execArgv[inspectFlagIndex]; + if (argv.includes('=')) { + // --inspect=port + const [flag, port] = argv.split('='); + inspectFlag = flag; + inspectPortCounter = Number.parseInt(port, 10) + 1; + } else { + // --inspect + inspectFlag = argv; + if (isNumeric(process.execArgv[inspectFlagIndex + 1])) { + // --inspect port + inspectPortCounter = Number.parseInt(process.execArgv[inspectFlagIndex + 1], 10) + 1; + } + } +} + +function usingWorkerProc( + config: OptimizerConfig, + workerConfig: WorkerConfig, + bundles: Bundle[], + fn: (proc: ChildProcess) => Rx.Observable +) { + return Rx.using( + (): ProcResource => { + const args = [JSON.stringify(workerConfig), JSON.stringify(bundles.map(b => b.toSpec()))]; + + const proc = fork(require.resolve('../worker/run_worker'), args, { + stdio: ['ignore', 'pipe', 'pipe', 'ipc'], + execArgv: [ + ...(inspectFlag && config.inspectWorkers + ? [`${inspectFlag}=${inspectPortCounter++}`] + : []), + ...(config.maxWorkerCount <= 3 ? ['--max-old-space-size=2048'] : []), + ], + }); + + return { + proc, + unsubscribe() { + proc.kill('SIGKILL'); + }, + }; + }, + + resource => { + const { proc } = resource as ProcResource; + return fn(proc); + } + ); +} + +function observeStdio$(stream: Readable, name: WorkerStdio['stream']) { + return Rx.fromEvent(stream, 'data').pipe( + takeUntil( + Rx.race( + Rx.fromEvent(stream, 'end'), + Rx.fromEvent(stream, 'error').pipe( + map(error => { + throw error; + }) + ) + ) + ), + map( + (chunk): WorkerStdio => ({ + type: 'worker stdio', + chunk, + stream: name, + }) + ) + ); +} + +/** + * Start a worker process with the specified `workerConfig` and + * `bundles` and return an observable of the events related to + * that worker, including the messages sent to us by that worker + * and the status of the process (stdio, started). + */ +export function observeWorker( + config: OptimizerConfig, + workerConfig: WorkerConfig, + bundles: Bundle[] +): Rx.Observable { + return usingWorkerProc(config, workerConfig, bundles, proc => { + let lastMsg: WorkerMsg; + + return Rx.merge( + Rx.of({ + type: 'worker started', + bundles, + }), + observeStdio$(proc.stdout, 'stdout'), + observeStdio$(proc.stderr, 'stderr'), + Rx.fromEvent<[unknown]>(proc, 'message') + .pipe( + // validate the messages from the process + map(([msg]) => { + if (!isWorkerMsg(msg)) { + throw new Error(`unexpected message from worker: ${JSON.stringify(msg)}`); + } + + lastMsg = msg; + return msg; + }) + ) + .pipe( + takeUntil( + Rx.race( + // throw into stream on error events + Rx.fromEvent(proc, 'error').pipe( + map(error => { + throw new Error(`worker failed to spawn: ${error.message}`); + }) + ), + + // throw into stream on unexpected exits, or emit to trigger the stream to close + Rx.fromEvent<[number | void]>(proc, 'exit').pipe( + map(([code]) => { + const terminalMsgTypes: Array = [ + 'compiler error', + 'worker error', + ]; + + if (!config.watch) { + terminalMsgTypes.push('compiler issue', 'compiler success'); + } + + // verify that this is an expected exit state + if (code === 0 && lastMsg && terminalMsgTypes.includes(lastMsg.type)) { + // emit undefined so that takeUntil completes the observable + return; + } + + throw new Error( + `worker exitted unexpectedly with code ${code} [last message: ${inspect( + lastMsg + )}]` + ); + }) + ) + ) + ) + ) + ); + }); +} diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts new file mode 100644 index 0000000000000..d67b957416753 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts @@ -0,0 +1,408 @@ +/* + * 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. + */ + +jest.mock('./assign_bundles_to_workers.ts'); +jest.mock('./kibana_platform_plugins.ts'); +jest.mock('./get_bundles.ts'); + +import Path from 'path'; +import Os from 'os'; + +import { REPO_ROOT, createAbsolutePathSerializer } from '@kbn/dev-utils'; + +import { OptimizerConfig } from './optimizer_config'; + +jest.spyOn(Os, 'cpus').mockReturnValue(['foo'] as any); + +expect.addSnapshotSerializer(createAbsolutePathSerializer()); + +beforeEach(() => { + delete process.env.KBN_OPTIMIZER_MAX_WORKERS; + delete process.env.KBN_OPTIMIZER_NO_CACHE; + jest.clearAllMocks(); +}); + +describe('OptimizerConfig::parseOptions()', () => { + it('validates that repoRoot is absolute', () => { + expect(() => + OptimizerConfig.parseOptions({ repoRoot: 'foo/bar' }) + ).toThrowErrorMatchingInlineSnapshot(`"repoRoot must be an absolute path"`); + }); + + it('validates that pluginScanDirs are absolute', () => { + expect(() => + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + pluginScanDirs: ['foo/bar'], + }) + ).toThrowErrorMatchingInlineSnapshot(`"pluginScanDirs must all be absolute paths"`); + }); + + it('validates that pluginPaths are absolute', () => { + expect(() => + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + pluginPaths: ['foo/bar'], + }) + ).toThrowErrorMatchingInlineSnapshot(`"pluginPaths must all be absolute paths"`); + }); + + it('validates that extraPluginScanDirs are absolute', () => { + expect(() => + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + extraPluginScanDirs: ['foo/bar'], + }) + ).toThrowErrorMatchingInlineSnapshot(`"extraPluginScanDirs must all be absolute paths"`); + }); + + it('validates that maxWorkerCount is a number', () => { + expect(() => { + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + maxWorkerCount: NaN, + }); + }).toThrowErrorMatchingInlineSnapshot(`"worker count must be a number"`); + }); + + it('applies defaults', () => { + expect( + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + }) + ).toMatchInlineSnapshot(` + Object { + "cache": true, + "dist": false, + "inspectWorkers": false, + "maxWorkerCount": 2, + "pluginPaths": Array [], + "pluginScanDirs": Array [ + /src/plugins, + /x-pack/plugins, + /plugins, + -extra, + ], + "profileWebpack": false, + "repoRoot": , + "watch": false, + } + `); + + expect( + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + cache: false, + }) + ).toMatchInlineSnapshot(` + Object { + "cache": false, + "dist": false, + "inspectWorkers": false, + "maxWorkerCount": 2, + "pluginPaths": Array [], + "pluginScanDirs": Array [ + /src/plugins, + /x-pack/plugins, + /plugins, + -extra, + ], + "profileWebpack": false, + "repoRoot": , + "watch": false, + } + `); + + expect( + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + examples: true, + }) + ).toMatchInlineSnapshot(` + Object { + "cache": true, + "dist": false, + "inspectWorkers": false, + "maxWorkerCount": 2, + "pluginPaths": Array [], + "pluginScanDirs": Array [ + /src/plugins, + /x-pack/plugins, + /plugins, + /examples, + -extra, + ], + "profileWebpack": false, + "repoRoot": , + "watch": false, + } + `); + + expect( + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + oss: true, + }) + ).toMatchInlineSnapshot(` + Object { + "cache": true, + "dist": false, + "inspectWorkers": false, + "maxWorkerCount": 2, + "pluginPaths": Array [], + "pluginScanDirs": Array [ + /src/plugins, + /plugins, + -extra, + ], + "profileWebpack": false, + "repoRoot": , + "watch": false, + } + `); + + expect( + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + pluginScanDirs: [Path.resolve(REPO_ROOT, 'x/y/z'), '/outside/of/repo'], + }) + ).toMatchInlineSnapshot(` + Object { + "cache": true, + "dist": false, + "inspectWorkers": false, + "maxWorkerCount": 2, + "pluginPaths": Array [], + "pluginScanDirs": Array [ + /x/y/z, + "/outside/of/repo", + ], + "profileWebpack": false, + "repoRoot": , + "watch": false, + } + `); + + process.env.KBN_OPTIMIZER_MAX_WORKERS = '100'; + expect( + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + pluginScanDirs: [], + }) + ).toMatchInlineSnapshot(` + Object { + "cache": true, + "dist": false, + "inspectWorkers": false, + "maxWorkerCount": 100, + "pluginPaths": Array [], + "pluginScanDirs": Array [], + "profileWebpack": false, + "repoRoot": , + "watch": false, + } + `); + + process.env.KBN_OPTIMIZER_NO_CACHE = '0'; + expect( + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + pluginScanDirs: [], + }) + ).toMatchInlineSnapshot(` + Object { + "cache": false, + "dist": false, + "inspectWorkers": false, + "maxWorkerCount": 100, + "pluginPaths": Array [], + "pluginScanDirs": Array [], + "profileWebpack": false, + "repoRoot": , + "watch": false, + } + `); + + process.env.KBN_OPTIMIZER_NO_CACHE = '1'; + expect( + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + pluginScanDirs: [], + }) + ).toMatchInlineSnapshot(` + Object { + "cache": false, + "dist": false, + "inspectWorkers": false, + "maxWorkerCount": 100, + "pluginPaths": Array [], + "pluginScanDirs": Array [], + "profileWebpack": false, + "repoRoot": , + "watch": false, + } + `); + + process.env.KBN_OPTIMIZER_NO_CACHE = '1'; + expect( + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + pluginScanDirs: [], + cache: true, + }) + ).toMatchInlineSnapshot(` + Object { + "cache": false, + "dist": false, + "inspectWorkers": false, + "maxWorkerCount": 100, + "pluginPaths": Array [], + "pluginScanDirs": Array [], + "profileWebpack": false, + "repoRoot": , + "watch": false, + } + `); + + delete process.env.KBN_OPTIMIZER_NO_CACHE; + expect( + OptimizerConfig.parseOptions({ + repoRoot: REPO_ROOT, + pluginScanDirs: [], + cache: true, + }) + ).toMatchInlineSnapshot(` + Object { + "cache": true, + "dist": false, + "inspectWorkers": false, + "maxWorkerCount": 100, + "pluginPaths": Array [], + "pluginScanDirs": Array [], + "profileWebpack": false, + "repoRoot": , + "watch": false, + } + `); + }); +}); + +/** + * NOTE: this method is basically just calling others, so we're mocking out the return values + * of each function with a Symbol, including the return values of OptimizerConfig.parseOptions + * and just making sure that the arguments are coming from where we expect + */ +describe('OptimizerConfig::create()', () => { + const assignBundlesToWorkers: jest.Mock = jest.requireMock('./assign_bundles_to_workers.ts') + .assignBundlesToWorkers; + const findKibanaPlatformPlugins: jest.Mock = jest.requireMock('./kibana_platform_plugins.ts') + .findKibanaPlatformPlugins; + const getBundles: jest.Mock = jest.requireMock('./get_bundles.ts').getBundles; + + beforeEach(() => { + if ('mock' in OptimizerConfig.parseOptions) { + (OptimizerConfig.parseOptions as jest.Mock).mockRestore(); + } + + assignBundlesToWorkers.mockReturnValue([ + { config: Symbol('worker config 1') }, + { config: Symbol('worker config 2') }, + ]); + findKibanaPlatformPlugins.mockReturnValue(Symbol('new platform plugins')); + getBundles.mockReturnValue(Symbol('bundles')); + + jest.spyOn(OptimizerConfig, 'parseOptions').mockImplementation((): any => ({ + cache: Symbol('parsed cache'), + dist: Symbol('parsed dist'), + maxWorkerCount: Symbol('parsed max worker count'), + pluginPaths: Symbol('parsed plugin paths'), + pluginScanDirs: Symbol('parsed plugin scan dirs'), + repoRoot: Symbol('parsed repo root'), + watch: Symbol('parsed watch'), + inspectWorkers: Symbol('parsed inspect workers'), + profileWebpack: Symbol('parsed profile webpack'), + })); + }); + + it('passes parsed options to findKibanaPlatformPlugins, getBundles, and assignBundlesToWorkers', () => { + const config = OptimizerConfig.create({ + repoRoot: REPO_ROOT, + }); + + expect(config).toMatchInlineSnapshot(` + OptimizerConfig { + "bundles": Symbol(bundles), + "cache": Symbol(parsed cache), + "dist": Symbol(parsed dist), + "inspectWorkers": Symbol(parsed inspect workers), + "maxWorkerCount": Symbol(parsed max worker count), + "plugins": Symbol(new platform plugins), + "profileWebpack": Symbol(parsed profile webpack), + "repoRoot": Symbol(parsed repo root), + "watch": Symbol(parsed watch), + } + `); + + expect(findKibanaPlatformPlugins.mock).toMatchInlineSnapshot(` + Object { + "calls": Array [ + Array [ + Symbol(parsed plugin scan dirs), + Symbol(parsed plugin paths), + ], + ], + "instances": Array [ + [Window], + ], + "invocationCallOrder": Array [ + 7, + ], + "results": Array [ + Object { + "type": "return", + "value": Symbol(new platform plugins), + }, + ], + } + `); + + expect(getBundles.mock).toMatchInlineSnapshot(` + Object { + "calls": Array [ + Array [ + Symbol(new platform plugins), + Symbol(parsed repo root), + ], + ], + "instances": Array [ + [Window], + ], + "invocationCallOrder": Array [ + 8, + ], + "results": Array [ + Object { + "type": "return", + "value": Symbol(bundles), + }, + ], + } + `); + }); +}); diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts new file mode 100644 index 0000000000000..a258e1010fce3 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -0,0 +1,172 @@ +/* + * 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 Path from 'path'; +import Os from 'os'; + +import { Bundle, WorkerConfig } from '../common'; + +import { findKibanaPlatformPlugins, KibanaPlatformPlugin } from './kibana_platform_plugins'; +import { getBundles } from './get_bundles'; + +interface Options { + /** absolute path to root of the repo/build */ + repoRoot: string; + /** enable to run the optimizer in watch mode */ + watch?: boolean; + /** the maximum number of workers that will be created */ + maxWorkerCount?: number; + /** set to false to disabling writing/reading of caches */ + cache?: boolean; + /** build assets suitable for use in the distributable */ + dist?: boolean; + /** enable webpack profiling, writes stats.json files to the root of each plugin's output dir */ + profileWebpack?: boolean; + /** set to true to inspecting workers when the parent process is being inspected */ + inspectWorkers?: boolean; + + /** include only oss plugins in default scan dirs */ + oss?: boolean; + /** include examples in default scan dirs */ + examples?: boolean; + /** absolute paths to specific plugins that should be built */ + pluginPaths?: string[]; + /** absolute paths to directories that should be built, overrides the default scan dirs */ + pluginScanDirs?: string[]; + /** absolute paths that should be added to the default scan dirs */ + extraPluginScanDirs?: string[]; +} + +interface ParsedOptions { + repoRoot: string; + watch: boolean; + maxWorkerCount: number; + profileWebpack: boolean; + cache: boolean; + dist: boolean; + pluginPaths: string[]; + pluginScanDirs: string[]; + inspectWorkers: boolean; +} + +export class OptimizerConfig { + static parseOptions(options: Options): ParsedOptions { + const watch = !!options.watch; + const oss = !!options.oss; + const dist = !!options.dist; + const examples = !!options.examples; + const profileWebpack = !!options.profileWebpack; + const inspectWorkers = !!options.inspectWorkers; + const cache = options.cache !== false && !process.env.KBN_OPTIMIZER_NO_CACHE; + + const repoRoot = options.repoRoot; + if (!Path.isAbsolute(repoRoot)) { + throw new TypeError('repoRoot must be an absolute path'); + } + + /** + * BEWARE: this needs to stay roughly synchronized with + * `src/core/server/config/env.ts` which determins which paths + * should be searched for plugins to load + */ + const pluginScanDirs = options.pluginScanDirs || [ + Path.resolve(repoRoot, 'src/plugins'), + ...(oss ? [] : [Path.resolve(repoRoot, 'x-pack/plugins')]), + Path.resolve(repoRoot, 'plugins'), + ...(examples ? [Path.resolve('examples')] : []), + Path.resolve(repoRoot, '../kibana-extra'), + ]; + if (!pluginScanDirs.every(p => Path.isAbsolute(p))) { + throw new TypeError('pluginScanDirs must all be absolute paths'); + } + + for (const extraPluginScanDir of options.extraPluginScanDirs || []) { + if (!Path.isAbsolute(extraPluginScanDir)) { + throw new TypeError('extraPluginScanDirs must all be absolute paths'); + } + pluginScanDirs.push(extraPluginScanDir); + } + + const pluginPaths = options.pluginPaths || []; + if (!pluginPaths.every(s => Path.isAbsolute(s))) { + throw new TypeError('pluginPaths must all be absolute paths'); + } + + const maxWorkerCount = process.env.KBN_OPTIMIZER_MAX_WORKERS + ? parseInt(process.env.KBN_OPTIMIZER_MAX_WORKERS, 10) + : options.maxWorkerCount ?? Math.max(Math.ceil(Math.max(Os.cpus()?.length, 1) / 3), 2); + if (typeof maxWorkerCount !== 'number' || !Number.isFinite(maxWorkerCount)) { + throw new TypeError('worker count must be a number'); + } + + return { + watch, + dist, + repoRoot, + maxWorkerCount, + profileWebpack, + cache, + pluginScanDirs, + pluginPaths, + inspectWorkers, + }; + } + + static create(inputOptions: Options) { + const options = OptimizerConfig.parseOptions(inputOptions); + const plugins = findKibanaPlatformPlugins(options.pluginScanDirs, options.pluginPaths); + const bundles = getBundles(plugins, options.repoRoot); + + return new OptimizerConfig( + bundles, + options.cache, + options.watch, + options.inspectWorkers, + plugins, + options.repoRoot, + options.maxWorkerCount, + options.dist, + options.profileWebpack + ); + } + + constructor( + public readonly bundles: Bundle[], + public readonly cache: boolean, + public readonly watch: boolean, + public readonly inspectWorkers: boolean, + public readonly plugins: KibanaPlatformPlugin[], + public readonly repoRoot: string, + public readonly maxWorkerCount: number, + public readonly dist: boolean, + public readonly profileWebpack: boolean + ) {} + + getWorkerConfig(optimizerCacheKey: unknown): WorkerConfig { + return { + cache: this.cache, + dist: this.dist, + profileWebpack: this.profileWebpack, + repoRoot: this.repoRoot, + watch: this.watch, + optimizerCacheKey, + browserslistEnv: this.dist ? 'production' : process.env.BROWSERSLIST_ENV || 'dev', + }; + } +} diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_reducer.ts b/packages/kbn-optimizer/src/optimizer/optimizer_reducer.ts new file mode 100644 index 0000000000000..c1e6572bd7e75 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/optimizer_reducer.ts @@ -0,0 +1,170 @@ +/* + * 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 { inspect } from 'util'; + +import { WorkerMsg, CompilerMsg, Bundle, Summarizer } from '../common'; + +import { ChangeEvent } from './watcher'; +import { WorkerStatus } from './observe_worker'; +import { BundleCacheEvent } from './bundle_cache'; +import { OptimizerConfig } from './optimizer_config'; + +export interface OptimizerInitializedEvent { + type: 'optimizer initialized'; +} + +export type OptimizerEvent = + | OptimizerInitializedEvent + | ChangeEvent + | WorkerMsg + | WorkerStatus + | BundleCacheEvent; + +export interface OptimizerState { + phase: 'initializing' | 'initialized' | 'running' | 'issue' | 'success' | 'reallocating'; + startTime: number; + durSec: number; + compilerStates: CompilerMsg[]; + onlineBundles: Bundle[]; + offlineBundles: Bundle[]; +} + +const msToSec = (ms: number) => Math.round(ms / 100) / 10; + +/** + * merge a state and some updates into a new optimizer state, apply some + * standard updates related to timing + */ +function createOptimizerState( + prevState: OptimizerState, + update?: Partial> +): OptimizerState { + // reset start time if we are transitioning into running + const startTime = + (prevState.phase === 'success' || prevState.phase === 'issue') && + (update?.phase === 'running' || update?.phase === 'reallocating') + ? Date.now() + : prevState.startTime; + + return { + ...prevState, + ...update, + startTime, + durSec: msToSec(Date.now() - startTime), + }; +} + +/** + * calculate the total state, given a set of compiler messages + */ +function getStatePhase(states: CompilerMsg[]) { + const types = states.map(s => s.type); + + if (types.includes('running')) { + return 'running'; + } + + if (types.includes('compiler issue')) { + return 'issue'; + } + + if (types.every(s => s === 'compiler success')) { + return 'success'; + } + + throw new Error(`unable to summarize bundle states: ${JSON.stringify(states)}`); +} + +export function createOptimizerReducer( + config: OptimizerConfig +): Summarizer { + return (state, event) => { + if (event.type === 'optimizer initialized') { + return createOptimizerState(state, { + phase: 'initialized', + }); + } + + if (event.type === 'worker error' || event.type === 'compiler error') { + // unrecoverable error states + const error = new Error(event.errorMsg); + error.stack = event.errorStack; + throw error; + } + + if (event.type === 'worker stdio' || event.type === 'worker started') { + // same state, but updated to the event is shared externally + return createOptimizerState(state); + } + + if (event.type === 'changes detected') { + // switch to running early, before workers are started, so that + // base path proxy can prevent requests in the delay between changes + // and workers started + return createOptimizerState(state, { + phase: 'reallocating', + }); + } + + if ( + event.type === 'changes' || + event.type === 'bundle cached' || + event.type === 'bundle not cached' + ) { + const onlineBundles: Bundle[] = [...state.onlineBundles]; + if (event.type === 'changes') { + onlineBundles.push(...event.bundles); + } + if (event.type === 'bundle not cached') { + onlineBundles.push(event.bundle); + } + + const offlineBundles: Bundle[] = []; + for (const bundle of config.bundles) { + if (!onlineBundles.includes(bundle)) { + offlineBundles.push(bundle); + } + } + + return createOptimizerState(state, { + phase: state.phase === 'initializing' ? 'initializing' : 'running', + onlineBundles, + offlineBundles, + }); + } + + if ( + event.type === 'compiler issue' || + event.type === 'compiler success' || + event.type === 'running' + ) { + const compilerStates: CompilerMsg[] = [ + ...state.compilerStates.filter(c => c.bundleId !== event.bundleId), + event, + ]; + return createOptimizerState(state, { + phase: getStatePhase(compilerStates), + compilerStates, + }); + } + + throw new Error(`unexpected optimizer event ${inspect(event)}`); + }; +} diff --git a/packages/kbn-optimizer/src/optimizer/run_workers.ts b/packages/kbn-optimizer/src/optimizer/run_workers.ts new file mode 100644 index 0000000000000..e91b0d25fd72b --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/run_workers.ts @@ -0,0 +1,67 @@ +/* + * 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 * as Rx from 'rxjs'; +import { mergeMap, toArray } from 'rxjs/operators'; + +import { maybeMap } from '../common'; + +import { OptimizerConfig } from './optimizer_config'; +import { BundleCacheEvent } from './bundle_cache'; +import { ChangeEvent } from './watcher'; +import { assignBundlesToWorkers } from './assign_bundles_to_workers'; +import { observeWorker } from './observe_worker'; + +/** + * Create a stream of all worker events, these include messages + * from workers and events about the status of workers. To get + * these events we assign the bundles to workers via + * `assignBundlesToWorkers()` and then start a worler for each + * assignment with `observeWorker()`. + * + * Subscribes to `changeEvent$` in order to determine when more + * bundles should be assigned to workers. + * + * Completes when all workers have exitted. If we are running in + * watch mode this observable will never exit. + */ +export function runWorkers( + config: OptimizerConfig, + optimizerCacheKey: unknown, + bundleCache$: Rx.Observable, + changeEvent$: Rx.Observable +) { + return Rx.concat( + // first batch of bundles are based on how up-to-date the cache is + bundleCache$.pipe( + maybeMap(event => (event.type === 'bundle not cached' ? event.bundle : undefined)), + toArray() + ), + // subsequent batches are defined by changeEvent$ + changeEvent$.pipe(maybeMap(c => (c.type === 'changes' ? c.bundles : undefined))) + ).pipe( + mergeMap(bundles => + Rx.from(assignBundlesToWorkers(bundles, config.maxWorkerCount)).pipe( + mergeMap(assignment => + observeWorker(config, config.getWorkerConfig(optimizerCacheKey), assignment.bundles) + ) + ) + ) + ); +} diff --git a/packages/kbn-optimizer/src/optimizer/watch_bundles_for_changes.ts b/packages/kbn-optimizer/src/optimizer/watch_bundles_for_changes.ts new file mode 100644 index 0000000000000..9149c483786fc --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/watch_bundles_for_changes.ts @@ -0,0 +1,85 @@ +/* + * 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 * as Rx from 'rxjs'; +import { mergeMap, toArray } from 'rxjs/operators'; + +import { Bundle, maybeMap } from '../common'; + +import { BundleCacheEvent } from './bundle_cache'; +import { Watcher } from './watcher'; + +/** + * Recursively call watcher.getNextChange$, passing it + * just the bundles that haven't been changed yet until + * all bundles have changed, then exit + */ +function recursiveGetNextChange$( + watcher: Watcher, + bundles: Bundle[], + startTime: number +): ReturnType { + return !bundles.length + ? Rx.EMPTY + : watcher.getNextChange$(bundles, startTime).pipe( + mergeMap(event => { + if (event.type === 'changes detected') { + return Rx.of(event); + } + + return Rx.concat( + Rx.of(event), + + recursiveGetNextChange$( + watcher, + bundles.filter(b => !event.bundles.includes(b)), + Date.now() + ) + ); + }) + ); +} + +/** + * Create an observable that emits change events for offline + * bundles. + * + * Once changes are seen in a bundle that bundles + * files will no longer be watched. + * + * Once changes have been seen in all bundles changeEvent$ + * will complete. + * + * If there are no bundles to watch or we config.watch === false + * the observable completes without sending any notifications. + */ +export function watchBundlesForChanges$( + bundleCacheEvent$: Rx.Observable, + initialStartTime: number +) { + return bundleCacheEvent$.pipe( + maybeMap(event => (event.type === 'bundle cached' ? event.bundle : undefined)), + toArray(), + mergeMap(bundles => + bundles.length + ? Watcher.using(watcher => recursiveGetNextChange$(watcher, bundles, initialStartTime)) + : Rx.EMPTY + ) + ); +} diff --git a/packages/kbn-optimizer/src/optimizer/watcher.ts b/packages/kbn-optimizer/src/optimizer/watcher.ts new file mode 100644 index 0000000000000..343f391921383 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/watcher.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 * as Rx from 'rxjs'; +import { take, map, share } from 'rxjs/operators'; +import Watchpack from 'watchpack'; + +import { debounceTimeBuffer, Bundle } from '../common'; + +export interface ChangesStarted { + type: 'changes detected'; +} + +export interface Changes { + type: 'changes'; + bundles: Bundle[]; +} + +export type ChangeEvent = ChangesStarted | Changes; + +export class Watcher { + /** + * Use watcher as an RxJS Resource, which is a special type of observable + * that calls unsubscribe on the resource (the Watcher instance in this case) + * when the observable is unsubscribed. + */ + static using(fn: (watcher: Watcher) => Rx.Observable) { + return Rx.using( + () => new Watcher(), + resource => fn(resource as Watcher) + ); + } + + private readonly watchpack = new Watchpack({ + aggregateTimeout: 0, + ignored: /node_modules\/([^\/]+[\/])*(?!package.json)([^\/]+)$/, + }); + + private readonly change$ = Rx.fromEvent<[string]>(this.watchpack, 'change').pipe(share()); + + public getNextChange$(bundles: Bundle[], startTime: number) { + return Rx.merge( + // emit ChangesStarted as soon as we have been triggered + this.change$.pipe( + take(1), + map( + (): ChangesStarted => ({ + type: 'changes detected', + }) + ) + ), + + // debounce and bufffer change events for 1 second to create + // final change notification + this.change$.pipe( + map(event => event[0]), + debounceTimeBuffer(1000), + map( + (changes): Changes => ({ + type: 'changes', + bundles: bundles.filter(bundle => { + const referencedFiles = bundle.cache.getReferencedFiles(); + return changes.some(change => referencedFiles?.includes(change)); + }), + }) + ), + take(1) + ), + + // call watchpack.watch after listerners are setup + Rx.defer(() => { + const watchPaths: string[] = []; + + for (const bundle of bundles) { + for (const path of bundle.cache.getReferencedFiles() || []) { + watchPaths.push(path); + } + } + + this.watchpack.watch(watchPaths, [], startTime); + return Rx.EMPTY; + }) + ); + } + + /** + * Called automatically by RxJS when Watcher instances + * are used as resources + */ + unsubscribe() { + this.watchpack.close(); + } +} diff --git a/packages/kbn-optimizer/src/run_optimizer.ts b/packages/kbn-optimizer/src/run_optimizer.ts new file mode 100644 index 0000000000000..e6cce8d306e35 --- /dev/null +++ b/packages/kbn-optimizer/src/run_optimizer.ts @@ -0,0 +1,82 @@ +/* + * 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 * as Rx from 'rxjs'; +import { mergeMap, share, observeOn } from 'rxjs/operators'; + +import { summarizeEvent$, Update } from './common'; + +import { + OptimizerConfig, + OptimizerEvent, + OptimizerState, + getBundleCacheEvent$, + getOptimizerCacheKey, + watchBundlesForChanges$, + runWorkers, + OptimizerInitializedEvent, + createOptimizerReducer, +} from './optimizer'; + +export type OptimizerUpdate = Update; +export type OptimizerUpdate$ = Rx.Observable; + +export function runOptimizer(config: OptimizerConfig) { + return Rx.defer(async () => ({ + startTime: Date.now(), + cacheKey: await getOptimizerCacheKey(config), + })).pipe( + mergeMap(({ startTime, cacheKey }) => { + const bundleCacheEvent$ = getBundleCacheEvent$(config, cacheKey).pipe( + observeOn(Rx.asyncScheduler), + share() + ); + + // initialization completes once all bundle caches have been resolved + const init$ = Rx.concat( + bundleCacheEvent$, + Rx.of({ + type: 'optimizer initialized', + }) + ); + + // watch the offline bundles for changes, turning them online... + const changeEvent$ = config.watch + ? watchBundlesForChanges$(bundleCacheEvent$, startTime).pipe(share()) + : Rx.EMPTY; + + // run workers to build all the online bundles, including the bundles turned online by changeEvent$ + const workerEvent$ = runWorkers(config, cacheKey, bundleCacheEvent$, changeEvent$); + + // create the stream that summarized all the events into specific states + return summarizeEvent$( + Rx.merge(init$, changeEvent$, workerEvent$), + { + phase: 'initializing', + compilerStates: [], + offlineBundles: [], + onlineBundles: [], + startTime, + durSec: 0, + }, + createOptimizerReducer(config) + ); + }) + ); +} diff --git a/packages/kbn-optimizer/src/worker/postcss.config.js b/packages/kbn-optimizer/src/worker/postcss.config.js new file mode 100644 index 0000000000000..571bae86dee37 --- /dev/null +++ b/packages/kbn-optimizer/src/worker/postcss.config.js @@ -0,0 +1,22 @@ +/* + * 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. + */ + +module.exports = { + plugins: [require('autoprefixer')()], +}; diff --git a/packages/kbn-optimizer/src/worker/run_compilers.ts b/packages/kbn-optimizer/src/worker/run_compilers.ts new file mode 100644 index 0000000000000..7dcce8a0fae8d --- /dev/null +++ b/packages/kbn-optimizer/src/worker/run_compilers.ts @@ -0,0 +1,210 @@ +/* + * 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 'source-map-support/register'; + +import Fs from 'fs'; +import Path from 'path'; +import { inspect } from 'util'; + +import webpack, { Stats } from 'webpack'; +import * as Rx from 'rxjs'; +import { mergeMap, map, mapTo, takeUntil } from 'rxjs/operators'; + +import { CompilerMsgs, CompilerMsg, maybeMap, Bundle, WorkerConfig } from '../common'; +import { getWebpackConfig } from './webpack.config'; +import { isFailureStats, failedStatsToErrorMessage } from './webpack_helpers'; +import { + isExternalModule, + isNormalModule, + isIgnoredModule, + isConcatenatedModule, + WebpackNormalModule, + getModulePath, +} from './webpack_helpers'; + +const PLUGIN_NAME = '@kbn/optimizer'; + +/** + * Create an Observable for a specific child compiler + bundle + */ +const observeCompiler = ( + workerConfig: WorkerConfig, + bundle: Bundle, + compiler: webpack.Compiler +): Rx.Observable => { + const compilerMsgs = new CompilerMsgs(bundle.id); + const done$ = new Rx.Subject(); + const { beforeRun, watchRun, done } = compiler.hooks; + + /** + * Called by webpack as a single run compilation is starting + */ + const started$ = Rx.merge( + Rx.fromEventPattern(cb => beforeRun.tap(PLUGIN_NAME, cb)), + Rx.fromEventPattern(cb => watchRun.tap(PLUGIN_NAME, cb)) + ).pipe(mapTo(compilerMsgs.running())); + + /** + * Called by webpack as any compilation is complete. If the + * needAdditionalPass property is set then another compilation + * is about to be started, so we shouldn't send complete quite yet + */ + const complete$ = Rx.fromEventPattern(cb => done.tap(PLUGIN_NAME, cb)).pipe( + maybeMap(stats => { + // @ts-ignore not included in types, but it is real https://github.com/webpack/webpack/blob/ab4fa8ddb3f433d286653cd6af7e3aad51168649/lib/Watching.js#L58 + if (stats.compilation.needAdditionalPass) { + return undefined; + } + + if (workerConfig.profileWebpack) { + Fs.writeFileSync( + Path.resolve(bundle.outputDir, 'stats.json'), + JSON.stringify(stats.toJson()) + ); + } + + if (!workerConfig.watch) { + process.nextTick(() => done$.next()); + } + + if (isFailureStats(stats)) { + return compilerMsgs.compilerFailure({ + failure: failedStatsToErrorMessage(stats), + }); + } + + const normalModules = stats.compilation.modules.filter( + (module): module is WebpackNormalModule => { + if (isNormalModule(module)) { + return true; + } + + if (isExternalModule(module) || isIgnoredModule(module) || isConcatenatedModule(module)) { + return false; + } + + throw new Error(`Unexpected module type: ${inspect(module)}`); + } + ); + + const referencedFiles = new Set(); + + for (const module of normalModules) { + const path = getModulePath(module); + + const parsedPath = Path.parse(path); + const dirSegments = parsedPath.dir.split(Path.sep); + if (!dirSegments.includes('node_modules')) { + referencedFiles.add(path); + continue; + } + + const nmIndex = dirSegments.lastIndexOf('node_modules'); + const isScoped = dirSegments[nmIndex + 1].startsWith('@'); + referencedFiles.add( + Path.join( + parsedPath.root, + ...dirSegments.slice(0, nmIndex + 1 + (isScoped ? 2 : 1)), + 'package.json' + ) + ); + } + + const files = Array.from(referencedFiles); + const mtimes = new Map( + files.map((path): [string, number | undefined] => { + try { + return [path, compiler.inputFileSystem.statSync(path)?.mtimeMs]; + } catch (error) { + if (error?.code === 'ENOENT') { + return [path, undefined]; + } + + throw error; + } + }) + ); + + bundle.cache.set({ + optimizerCacheKey: workerConfig.optimizerCacheKey, + cacheKey: bundle.createCacheKey(files, mtimes), + moduleCount: normalModules.length, + files, + }); + + return compilerMsgs.compilerSuccess({ + moduleCount: normalModules.length, + }); + }) + ); + + /** + * Called whenever the compilation results in an error that + * prevets assets from being emitted, and prevents watching + * from continuing. + */ + const error$ = Rx.fromEventPattern(cb => compiler.hooks.failed.tap(PLUGIN_NAME, cb)).pipe( + map(error => { + throw compilerMsgs.error(error); + }) + ); + + /** + * Merge events into a single stream, if we're not watching + * complete the stream after our first complete$ event + */ + return Rx.merge(started$, complete$, error$).pipe(takeUntil(done$)); +}; + +/** + * Run webpack compilers + */ +export const runCompilers = (workerConfig: WorkerConfig, bundles: Bundle[]) => { + const multiCompiler = webpack(bundles.map(def => getWebpackConfig(def, workerConfig))); + + return Rx.merge( + /** + * convert each compiler into an event stream that represents + * the status of each compiler, if we aren't watching the streams + * will complete after the compilers are complete. + * + * If a significant error occurs the stream will error + */ + Rx.from(multiCompiler.compilers.entries()).pipe( + mergeMap(([compilerIndex, compiler]) => { + const bundle = bundles[compilerIndex]; + return observeCompiler(workerConfig, bundle, compiler); + }) + ), + + /** + * compilers have been hooked up for their events, trigger run()/watch() + */ + Rx.defer(() => { + if (!workerConfig.watch) { + multiCompiler.run(() => {}); + } else { + multiCompiler.watch({}, () => {}); + } + + return []; + }) + ); +}; diff --git a/packages/kbn-optimizer/src/worker/run_worker.ts b/packages/kbn-optimizer/src/worker/run_worker.ts new file mode 100644 index 0000000000000..cbec4c3f44c7d --- /dev/null +++ b/packages/kbn-optimizer/src/worker/run_worker.ts @@ -0,0 +1,100 @@ +/* + * 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 * as Rx from 'rxjs'; + +import { parseBundles, parseWorkerConfig, WorkerMsg, isWorkerMsg, WorkerMsgs } from '../common'; + +import { runCompilers } from './run_compilers'; + +/** + ** + ** + ** Entry file for optimizer workers, this hooks into the process, handles + ** sending messages to the parent, makes sure the worker exits properly + ** and triggers all the compilers by calling runCompilers() + ** + ** + **/ + +const workerMsgs = new WorkerMsgs(); + +if (!process.send) { + throw new Error('worker process was not started with an IPC channel'); +} + +const send = (msg: WorkerMsg) => { + if (!process.send) { + // parent is gone + process.exit(0); + } else { + process.send(msg); + } +}; + +/** + * set the exitCode and wait for the process to exit, if it + * doesn't exit naturally do so forcibly and fail. + */ +const exit = (code: number) => { + process.exitCode = code; + setTimeout(() => { + send( + workerMsgs.error( + new Error('process did not automatically exit within 5 seconds, forcing exit') + ) + ); + process.exit(1); + }, 5000).unref(); +}; + +// check for connected parent on an unref'd timer rather than listening +// to "disconnect" since that listner prevents the process from exiting +setInterval(() => { + if (!process.connected) { + // parent is gone + process.exit(0); + } +}, 1000).unref(); + +Rx.defer(() => { + const workerConfig = parseWorkerConfig(process.argv[2]); + const bundles = parseBundles(process.argv[3]); + + // set BROWSERSLIST_ENV so that style/babel loaders see it before running compilers + process.env.BROWSERSLIST_ENV = workerConfig.browserslistEnv; + + return runCompilers(workerConfig, bundles); +}).subscribe( + msg => { + send(msg); + }, + error => { + if (isWorkerMsg(error)) { + send(error); + } else { + send(workerMsgs.error(error)); + } + + exit(1); + }, + () => { + exit(0); + } +); diff --git a/src/legacy/core_plugins/telemetry/public/services/path.ts b/packages/kbn-optimizer/src/worker/theme_loader.ts similarity index 67% rename from src/legacy/core_plugins/telemetry/public/services/path.ts rename to packages/kbn-optimizer/src/worker/theme_loader.ts index 4af545e982eaa..6d6686a5bde1b 100644 --- a/src/legacy/core_plugins/telemetry/public/services/path.ts +++ b/packages/kbn-optimizer/src/worker/theme_loader.ts @@ -17,9 +17,16 @@ * under the License. */ -import chrome from 'ui/chrome'; +import webpack from 'webpack'; +import { stringifyRequest } from 'loader-utils'; -export function isUnauthenticated() { - const path = (chrome as any).removeBasePath(window.location.pathname); - return path === '/login' || path === '/logout' || path === '/logged_out' || path === '/status'; +// eslint-disable-next-line import/no-default-export +export default function(this: webpack.loader.LoaderContext) { + return ` +if (window.__kbnDarkMode__) { + require(${stringifyRequest(this, `${this.resourcePath}?dark`)}) +} else { + require(${stringifyRequest(this, `${this.resourcePath}?light`)}); +} + `; } diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts new file mode 100644 index 0000000000000..1e87b8a5a7f7b --- /dev/null +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -0,0 +1,244 @@ +/* + * 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 Path from 'path'; + +import { stringifyRequest } from 'loader-utils'; +import webpack from 'webpack'; +// @ts-ignore +import TerserPlugin from 'terser-webpack-plugin'; +// @ts-ignore +import webpackMerge from 'webpack-merge'; +// @ts-ignore +import { CleanWebpackPlugin } from 'clean-webpack-plugin'; +import * as SharedDeps from '@kbn/ui-shared-deps'; + +import { Bundle, WorkerConfig } from '../common'; + +const IS_CODE_COVERAGE = !!process.env.CODE_COVERAGE; +const ISTANBUL_PRESET_PATH = require.resolve('@kbn/babel-preset/istanbul_preset'); +const PUBLIC_PATH_PLACEHOLDER = '__REPLACE_WITH_PUBLIC_PATH__'; +const BABEL_PRESET_PATH = require.resolve('@kbn/babel-preset/webpack_preset'); + +export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { + const commonConfig: webpack.Configuration = { + node: { fs: 'empty' }, + context: bundle.contextDir, + cache: true, + entry: { + [bundle.id]: bundle.entry, + }, + + devtool: worker.dist ? false : '#cheap-source-map', + profile: worker.profileWebpack, + + output: { + path: bundle.outputDir, + filename: '[name].plugin.js', + publicPath: PUBLIC_PATH_PLACEHOLDER, + devtoolModuleFilenameTemplate: info => + `/${bundle.type}:${bundle.id}/${Path.relative( + bundle.sourceRoot, + info.absoluteResourcePath + )}${info.query}`, + jsonpFunction: `${bundle.id}_bundle_jsonpfunction`, + ...(bundle.type === 'plugin' + ? { + // When the entry point is loaded, assign it's exported `plugin` + // value to a key on the global `__kbnBundles__` object. + library: ['__kbnBundles__', `plugin/${bundle.id}`], + libraryExport: 'plugin', + } + : {}), + }, + + optimization: { + noEmitOnErrors: true, + }, + + externals: { + ...SharedDeps.externals, + }, + + plugins: [new CleanWebpackPlugin()], + + module: { + // no parse rules for a few known large packages which have no require() statements + noParse: [ + /[\///]node_modules[\///]elasticsearch-browser[\///]/, + /[\///]node_modules[\///]lodash[\///]index\.js/, + ], + + rules: [ + { + test: /\.css$/, + include: /node_modules/, + use: [ + { + loader: 'style-loader', + }, + { + loader: 'css-loader', + options: { + sourceMap: !worker.dist, + }, + }, + ], + }, + { + test: /\.scss$/, + exclude: /node_modules/, + oneOf: [ + { + resourceQuery: /dark|light/, + use: [ + { + loader: 'style-loader', + }, + { + loader: 'css-loader', + options: { + sourceMap: !worker.dist, + }, + }, + { + loader: 'postcss-loader', + options: { + sourceMap: !worker.dist, + config: { + path: require.resolve('./postcss.config'), + }, + }, + }, + { + loader: 'sass-loader', + options: { + sourceMap: !worker.dist, + prependData(loaderContext: webpack.loader.LoaderContext) { + return `@import ${stringifyRequest( + loaderContext, + Path.resolve( + worker.repoRoot, + 'src/legacy/ui/public/styles/_styling_constants.scss' + ) + )};\n`; + }, + webpackImporter: false, + implementation: require('node-sass'), + sassOptions(loaderContext: webpack.loader.LoaderContext) { + const darkMode = loaderContext.resourceQuery === '?dark'; + + return { + outputStyle: 'nested', + includePaths: [Path.resolve(worker.repoRoot, 'node_modules')], + sourceMapRoot: `/${bundle.type}:${bundle.id}`, + importer: (url: string) => { + if (darkMode && url.includes('eui_colors_light')) { + return { file: url.replace('eui_colors_light', 'eui_colors_dark') }; + } + + return { file: url }; + }, + }; + }, + }, + }, + ], + }, + { + loader: require.resolve('./theme_loader'), + }, + ], + }, + { + test: /\.(woff|woff2|ttf|eot|svg|ico|png|jpg|gif|jpeg)(\?|$)/, + loader: 'url-loader', + options: { + limit: 8192, + }, + }, + { + test: /\.(js|tsx?)$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + options: { + babelrc: false, + presets: IS_CODE_COVERAGE + ? [ISTANBUL_PRESET_PATH, BABEL_PRESET_PATH] + : [BABEL_PRESET_PATH], + }, + }, + }, + { + test: /\.(html|md|txt|tmpl)$/, + use: { + loader: 'raw-loader', + }, + }, + ], + }, + + resolve: { + extensions: ['.js', '.ts', '.tsx', '.json'], + alias: { + tinymath: require.resolve('tinymath/lib/tinymath.es5.js'), + }, + }, + + performance: { + // NOTE: we are disabling this as those hints + // are more tailored for the final bundles result + // and not for the webpack compilations performance itself + hints: false, + }, + }; + + const nonDistributableConfig: webpack.Configuration = { + mode: 'development', + }; + + const distributableConfig: webpack.Configuration = { + mode: 'production', + + plugins: [ + new webpack.DefinePlugin({ + 'process.env': { + IS_KIBANA_DISTRIBUTABLE: `"true"`, + }, + }), + ], + + optimization: { + minimizer: [ + new TerserPlugin({ + cache: false, + sourceMap: false, + extractComments: false, + terserOptions: { + compress: false, + mangle: false, + }, + }), + ], + }, + }; + + return webpackMerge(commonConfig, worker.dist ? distributableConfig : nonDistributableConfig); +} diff --git a/packages/kbn-optimizer/src/worker/webpack_helpers.ts b/packages/kbn-optimizer/src/worker/webpack_helpers.ts new file mode 100644 index 0000000000000..a11c85c64198e --- /dev/null +++ b/packages/kbn-optimizer/src/worker/webpack_helpers.ts @@ -0,0 +1,166 @@ +/* + * 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 webpack from 'webpack'; +import { defaults } from 'lodash'; +// @ts-ignore +import Stats from 'webpack/lib/Stats'; + +export function isFailureStats(stats: webpack.Stats) { + if (stats.hasErrors()) { + return true; + } + + const { warnings } = stats.toJson({ all: false, warnings: true }); + + // 1 - when typescript doesn't do a full type check, as we have the ts-loader + // configured here, it does not have enough information to determine + // whether an imported name is a type or not, so when the name is then + // exported, typescript has no choice but to emit the export. Fortunately, + // the extraneous export should not be harmful, so we just suppress these warnings + // https://github.com/TypeStrong/ts-loader#transpileonly-boolean-defaultfalse + // + // 2 - Mini Css Extract plugin tracks the order for each css import we have + // through the project (and it's successive imports) since version 0.4.2. + // In case we have the same imports more than one time with different + // sequences, this plugin will throw a warning. This should not be harmful, + // but the an issue was opened and can be followed on: + // https://github.com/webpack-contrib/mini-css-extract-plugin/issues/250#issuecomment-415345126 + const filteredWarnings = Stats.filterWarnings(warnings, STATS_WARNINGS_FILTER); + + return filteredWarnings.length > 0; +} + +const STATS_WARNINGS_FILTER = new RegExp( + [ + '(export .* was not found in)', + '|(chunk .* \\[mini-css-extract-plugin\\]\\\nConflicting order between:)', + ].join('') +); + +export function failedStatsToErrorMessage(stats: webpack.Stats) { + const details = stats.toString( + defaults( + { colors: true, warningsFilter: STATS_WARNINGS_FILTER }, + Stats.presetToOptions('minimal') + ) + ); + + return `Optimizations failure.\n${details.split('\n').join('\n ')}`; +} + +export interface WebpackResolveData { + /** compilation context */ + context: string; + /** full request (with loaders) */ + request: string; + dependencies: [ + { + module: unknown; + weak: boolean; + optional: boolean; + loc: unknown; + request: string; + userRequest: string; + } + ]; + /** absolute path, but probably includes loaders in some cases */ + userRequest: string; + /** string from source code */ + rawRequest: string; + loaders: unknown; + /** absolute path to file, but probablt includes loaders in some cases */ + resource: string; + /** module type */ + type: string | 'javascript/auto'; + + resourceResolveData: { + context: { + /** absolute path to the file that issued the request */ + issuer: string; + }; + /** absolute path to the resolved file */ + path: string; + }; +} + +interface Dependency { + type: 'null' | 'cjs require'; + module: unknown; +} + +/** used for standard js/ts modules */ +export interface WebpackNormalModule { + type: string; + /** absolute path to file on disk */ + resource: string; + buildInfo: { + cacheable: boolean; + fileDependencies: Set; + }; + dependencies: Dependency[]; +} + +export function isNormalModule(module: any): module is WebpackNormalModule { + return module?.constructor?.name === 'NormalModule'; +} + +/** module used for ignored code */ +export interface WebpackIgnoredModule { + type: string; + /** unique string to identify this module with (starts with `ignored`) */ + identifierStr: string; + /** human readable identifier */ + readableIdentifierStr: string; +} + +export function isIgnoredModule(module: any): module is WebpackIgnoredModule { + return module?.constructor?.name === 'RawModule' && module.identifierStr?.startsWith('ignored '); +} + +/** module replacing imports for webpack externals */ +export interface WebpackExternalModule { + type: string; + id: string; + /** JS used to get instance of External */ + request: string; + /** module name that is handled by externals */ + userRequest: string; +} + +export function isExternalModule(module: any): module is WebpackExternalModule { + return module?.constructor?.name === 'ExternalModule'; +} + +/** module replacing imports for webpack externals */ +export interface WebpackConcatenatedModule { + type: string; + id: number; + dependencies: Dependency[]; + usedExports: string[]; +} + +export function isConcatenatedModule(module: any): module is WebpackConcatenatedModule { + return module?.constructor?.name === 'ConcatenatedModule'; +} + +export function getModulePath(module: WebpackNormalModule) { + const queryIndex = module.resource.indexOf('?'); + return queryIndex === -1 ? module.resource : module.resource.slice(0, queryIndex); +} diff --git a/packages/kbn-optimizer/tsconfig.json b/packages/kbn-optimizer/tsconfig.json new file mode 100644 index 0000000000000..e2994f4d02414 --- /dev/null +++ b/packages/kbn-optimizer/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "include": [ + "index.d.ts", + "src/**/*" + ] +} diff --git a/packages/kbn-optimizer/yarn.lock b/packages/kbn-optimizer/yarn.lock new file mode 120000 index 0000000000000..3f82ebc9cdbae --- /dev/null +++ b/packages/kbn-optimizer/yarn.lock @@ -0,0 +1 @@ +../../yarn.lock \ No newline at end of file diff --git a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js index 129125c4583d5..51a404379fedb 100644 --- a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js +++ b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js @@ -24,7 +24,7 @@ import util from 'util'; import { stat, readFileSync } from 'fs'; import { snakeCase } from 'lodash'; import del from 'del'; -import { withProcRunner, ToolingLog } from '@kbn/dev-utils'; +import { ProcRunner, ToolingLog } from '@kbn/dev-utils'; import { createLegacyEsTestCluster } from '@kbn/test'; import execa from 'execa'; @@ -84,27 +84,30 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug }); describe('with es instance', () => { - const log = new ToolingLog(); + const log = new ToolingLog({ + level: 'verbose', + writeTo: process.stdout, + }); + const pr = new ProcRunner(log); const es = createLegacyEsTestCluster({ license: 'basic', log }); beforeAll(es.start); afterAll(es.stop); + afterAll(() => pr.teardown()); it(`'yarn start' should result in the spec plugin being initialized on kibana's stdout`, async () => { - await withProcRunner(log, async proc => { - await proc.run('kibana', { - cmd: 'yarn', - args: [ - 'start', - '--optimize.enabled=false', - '--logging.json=false', - '--migrations.skip=true', - ], - cwd: generatedPath, - wait: new RegExp('\\[ispecPlugin\\]\\[plugins\\] Setting up plugin'), - }); - await proc.stop('kibana'); + await pr.run('kibana', { + cmd: 'yarn', + args: [ + 'start', + '--optimize.enabled=false', + '--logging.json=false', + '--migrations.skip=true', + ], + cwd: generatedPath, + wait: new RegExp('\\[ispecPlugin\\]\\[plugins\\] Setting up plugin'), }); + await pr.stop('kibana'); }); }); diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index e3df1ab585ee4..314bcf31e6d05 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -4490,14 +4490,10 @@ const tslib_1 = __webpack_require__(36); var proc_runner_1 = __webpack_require__(37); exports.withProcRunner = proc_runner_1.withProcRunner; exports.ProcRunner = proc_runner_1.ProcRunner; -var tooling_log_1 = __webpack_require__(415); -exports.ToolingLog = tooling_log_1.ToolingLog; -exports.ToolingLogTextWriter = tooling_log_1.ToolingLogTextWriter; -exports.pickLevelFromFlags = tooling_log_1.pickLevelFromFlags; -exports.ToolingLogCollectingWriter = tooling_log_1.ToolingLogCollectingWriter; +tslib_1.__exportStar(__webpack_require__(415), exports); var serializers_1 = __webpack_require__(420); exports.createAbsolutePathSerializer = serializers_1.createAbsolutePathSerializer; -var certs_1 = __webpack_require__(422); +var certs_1 = __webpack_require__(445); exports.CA_CERT_PATH = certs_1.CA_CERT_PATH; exports.ES_KEY_PATH = certs_1.ES_KEY_PATH; exports.ES_CERT_PATH = certs_1.ES_CERT_PATH; @@ -4509,13 +4505,13 @@ exports.KBN_KEY_PATH = certs_1.KBN_KEY_PATH; exports.KBN_CERT_PATH = certs_1.KBN_CERT_PATH; exports.KBN_P12_PATH = certs_1.KBN_P12_PATH; exports.KBN_P12_PASSWORD = certs_1.KBN_P12_PASSWORD; -var run_1 = __webpack_require__(423); +var run_1 = __webpack_require__(446); exports.run = run_1.run; exports.createFailError = run_1.createFailError; exports.createFlagError = run_1.createFlagError; exports.combineErrors = run_1.combineErrors; exports.isFailError = run_1.isFailError; -var repo_root_1 = __webpack_require__(428); +var repo_root_1 = __webpack_require__(422); exports.REPO_ROOT = repo_root_1.REPO_ROOT; var kbn_client_1 = __webpack_require__(451); exports.KbnClient = kbn_client_1.KbnClient; @@ -36634,6 +36630,7 @@ var tooling_log_text_writer_1 = __webpack_require__(417); exports.ToolingLogTextWriter = tooling_log_text_writer_1.ToolingLogTextWriter; var log_levels_1 = __webpack_require__(418); exports.pickLevelFromFlags = log_levels_1.pickLevelFromFlags; +exports.parseLogLevel = log_levels_1.parseLogLevel; var tooling_log_collecting_writer_1 = __webpack_require__(419); exports.ToolingLogCollectingWriter = tooling_log_collecting_writer_1.ToolingLogCollectingWriter; @@ -36789,17 +36786,23 @@ class ToolingLogTextWriter { throw new Error('ToolingLogTextWriter requires the `writeTo` option be set to a stream (like process.stdout)'); } } - write({ type, indent, args }) { - if (!shouldWriteType(this.level, type)) { + write(msg) { + if (!shouldWriteType(this.level, msg.type)) { return false; } - const txt = type === 'error' ? stringifyError(args[0]) : util_1.format(args[0], ...args.slice(1)); - const prefix = has(MSG_PREFIXES, type) ? MSG_PREFIXES[type] : ''; + const prefix = has(MSG_PREFIXES, msg.type) ? MSG_PREFIXES[msg.type] : ''; + ToolingLogTextWriter.write(this.writeTo, prefix, msg); + return true; + } + static write(writeTo, prefix, msg) { + const txt = msg.type === 'error' + ? stringifyError(msg.args[0]) + : util_1.format(msg.args[0], ...msg.args.slice(1)); (prefix + txt).split('\n').forEach((line, i) => { let lineIndent = ''; - if (indent > 0) { + if (msg.indent > 0) { // if we are indenting write some spaces followed by a symbol - lineIndent += ' '.repeat(indent - 1); + lineIndent += ' '.repeat(msg.indent - 1); lineIndent += line.startsWith('-') ? '└' : '│'; } if (line && prefix && i > 0) { @@ -36807,9 +36810,8 @@ class ToolingLogTextWriter { // the first if this message gets a prefix lineIndent += PREFIX_INDENT; } - this.writeTo.write(`${lineIndent}${line}\n`); + writeTo.write(`${lineIndent}${line}\n`); }); - return true; } } exports.ToolingLogTextWriter = ToolingLogTextWriter; @@ -36968,7 +36970,8 @@ exports.createAbsolutePathSerializer = absolute_path_serializer_1.createAbsolute * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -function createAbsolutePathSerializer(rootPath) { +const repo_root_1 = __webpack_require__(422); +function createAbsolutePathSerializer(rootPath = repo_root_1.REPO_ROOT) { return { print: (value) => value.replace(rootPath, '').replace(/\\/g, '/'), test: (value) => typeof value === 'string' && value.startsWith(rootPath), @@ -36983,79 +36986,6 @@ exports.createAbsolutePathSerializer = createAbsolutePathSerializer; "use strict"; -/* - * 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. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -const path_1 = __webpack_require__(16); -exports.CA_CERT_PATH = path_1.resolve(__dirname, '../certs/ca.crt'); -exports.ES_KEY_PATH = path_1.resolve(__dirname, '../certs/elasticsearch.key'); -exports.ES_CERT_PATH = path_1.resolve(__dirname, '../certs/elasticsearch.crt'); -exports.ES_P12_PATH = path_1.resolve(__dirname, '../certs/elasticsearch.p12'); -exports.ES_P12_PASSWORD = 'storepass'; -exports.ES_EMPTYPASSWORD_P12_PATH = path_1.resolve(__dirname, '../certs/elasticsearch_emptypassword.p12'); -exports.ES_NOPASSWORD_P12_PATH = path_1.resolve(__dirname, '../certs/elasticsearch_nopassword.p12'); -exports.KBN_KEY_PATH = path_1.resolve(__dirname, '../certs/kibana.key'); -exports.KBN_CERT_PATH = path_1.resolve(__dirname, '../certs/kibana.crt'); -exports.KBN_P12_PATH = path_1.resolve(__dirname, '../certs/kibana.p12'); -exports.KBN_P12_PASSWORD = 'storepass'; - - -/***/ }), -/* 423 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - -/* - * 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. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -var run_1 = __webpack_require__(424); -exports.run = run_1.run; -var fail_1 = __webpack_require__(425); -exports.createFailError = fail_1.createFailError; -exports.createFlagError = fail_1.createFlagError; -exports.combineErrors = fail_1.combineErrors; -exports.isFailError = fail_1.isFailError; - - -/***/ }), -/* 424 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -37076,688 +37006,176 @@ exports.isFailError = fail_1.isFailError; */ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = __webpack_require__(36); -// @ts-ignore @types are outdated and module is super simple -const exit_hook_1 = tslib_1.__importDefault(__webpack_require__(348)); -const tooling_log_1 = __webpack_require__(415); -const fail_1 = __webpack_require__(425); -const flags_1 = __webpack_require__(426); -const proc_runner_1 = __webpack_require__(37); -async function run(fn, options = {}) { - var _a; - const flags = flags_1.getFlags(process.argv.slice(2), options); - if (flags.help) { - process.stderr.write(flags_1.getHelp(options)); - process.exit(1); - } - const log = new tooling_log_1.ToolingLog({ - level: tooling_log_1.pickLevelFromFlags(flags), - writeTo: process.stdout, - }); - process.on('unhandledRejection', error => { - log.error('UNHANDLED PROMISE REJECTION'); - log.error(error); - process.exit(1); - }); - const handleErrorWithoutExit = (error) => { - if (fail_1.isFailError(error)) { - log.error(error.message); - if (error.showHelp) { - log.write(flags_1.getHelp(options)); - } - process.exitCode = error.exitCode; - } - else { - log.error('UNHANDLED ERROR'); - log.error(error); - process.exitCode = 1; - } - }; - const doCleanup = () => { - const tasks = cleanupTasks.slice(0); - cleanupTasks.length = 0; - for (const task of tasks) { - try { - task(); - } - catch (error) { - handleErrorWithoutExit(error); - } - } - }; - const unhookExit = exit_hook_1.default(doCleanup); - const cleanupTasks = [unhookExit]; +const path_1 = tslib_1.__importDefault(__webpack_require__(16)); +const fs_1 = tslib_1.__importDefault(__webpack_require__(23)); +const load_json_file_1 = tslib_1.__importDefault(__webpack_require__(423)); +const isKibanaDir = (dir) => { try { - if (!((_a = options.flags) === null || _a === void 0 ? void 0 : _a.allowUnexpected) && flags.unexpected.length) { - throw fail_1.createFlagError(`Unknown flag(s) "${flags.unexpected.join('", "')}"`); - } - try { - await proc_runner_1.withProcRunner(log, async (procRunner) => { - await fn({ - log, - flags, - procRunner, - addCleanupTask: (task) => cleanupTasks.push(task), - }); - }); - } - finally { - doCleanup(); + const path = path_1.default.resolve(dir, 'package.json'); + const json = load_json_file_1.default.sync(path); + if (json && typeof json === 'object' && 'name' in json && json.name === 'kibana') { + return true; } } catch (error) { - handleErrorWithoutExit(error); - process.exit(); + if (error && error.code === 'ENOENT') { + return false; + } + throw error; + } +}; +// search for the kibana directory, since this file is moved around it might +// not be where we think but should always be a relatively close parent +// of this directory +const startDir = fs_1.default.realpathSync(__dirname); +const { root: rootDir } = path_1.default.parse(startDir); +let cursor = startDir; +while (true) { + if (isKibanaDir(cursor)) { + break; + } + const parent = path_1.default.dirname(cursor); + if (parent === rootDir) { + throw new Error(`unable to find kibana directory from ${startDir}`); } + cursor = parent; } -exports.run = run; +exports.REPO_ROOT = cursor; /***/ }), -/* 425 */ +/* 423 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -/* - * 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. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -const util_1 = __webpack_require__(29); -const FAIL_TAG = Symbol('fail error'); -function createFailError(reason, options = {}) { - const { exitCode = 1, showHelp = false } = options; - return Object.assign(new Error(reason), { - exitCode, - showHelp, - [FAIL_TAG]: true, - }); -} -exports.createFailError = createFailError; -function createFlagError(reason) { - return createFailError(reason, { - showHelp: true, - }); -} -exports.createFlagError = createFlagError; -function isFailError(error) { - return Boolean(error && error[FAIL_TAG]); -} -exports.isFailError = isFailError; -function combineErrors(errors) { - if (errors.length === 1) { - return errors[0]; - } - const exitCode = errors - .filter(isFailError) - .reduce((acc, error) => Math.max(acc, error.exitCode), 1); - const showHelp = errors.some(error => isFailError(error) && error.showHelp); - const message = errors.reduce((acc, error) => { - if (isFailError(error)) { - return acc + '\n' + error.message; - } - return acc + `\nUNHANDLED ERROR\n${util_1.inspect(error)}`; - }, ''); - return createFailError(`${errors.length} errors:\n${message}`, { - exitCode, - showHelp, - }); -} -exports.combineErrors = combineErrors; - - -/***/ }), -/* 426 */ -/***/ (function(module, exports, __webpack_require__) { +const path = __webpack_require__(16); +const {promisify} = __webpack_require__(29); +const fs = __webpack_require__(424); +const stripBom = __webpack_require__(428); +const parseJson = __webpack_require__(429); -"use strict"; +const parse = (data, filePath, options = {}) => { + data = stripBom(data); -/* - * 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. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -const tslib_1 = __webpack_require__(36); -const path_1 = __webpack_require__(16); -const dedent_1 = tslib_1.__importDefault(__webpack_require__(14)); -const getopts_1 = tslib_1.__importDefault(__webpack_require__(427)); -function getFlags(argv, options) { - const unexpectedNames = new Set(); - const flagOpts = options.flags || {}; - const { verbose, quiet, silent, debug, help, _, ...others } = getopts_1.default(argv, { - string: flagOpts.string, - boolean: [...(flagOpts.boolean || []), 'verbose', 'quiet', 'silent', 'debug', 'help'], - alias: { - ...(flagOpts.alias || {}), - v: 'verbose', - }, - default: flagOpts.default, - unknown: (name) => { - unexpectedNames.add(name); - return flagOpts.guessTypesForUnexpectedFlags; - }, - }); - const unexpected = []; - for (const unexpectedName of unexpectedNames) { - const matchingArgv = []; - iterArgv: for (const [i, v] of argv.entries()) { - for (const prefix of ['--', '-']) { - if (v.startsWith(prefix)) { - // -/--name=value - if (v.startsWith(`${prefix}${unexpectedName}=`)) { - matchingArgv.push(v); - continue iterArgv; - } - // -/--name (value possibly follows) - if (v === `${prefix}${unexpectedName}`) { - matchingArgv.push(v); - // value follows -/--name - if (argv.length > i + 1 && !argv[i + 1].startsWith('-')) { - matchingArgv.push(argv[i + 1]); - } - continue iterArgv; - } - } - } - // special case for `--no-{flag}` disabling of boolean flags - if (v === `--no-${unexpectedName}`) { - matchingArgv.push(v); - continue iterArgv; - } - // special case for shortcut flags formatted as `-abc` where `a`, `b`, - // and `c` will be three separate unexpected flags - if (unexpectedName.length === 1 && - v[0] === '-' && - v[1] !== '-' && - !v.includes('=') && - v.includes(unexpectedName)) { - matchingArgv.push(`-${unexpectedName}`); - continue iterArgv; - } - } - if (matchingArgv.length) { - unexpected.push(...matchingArgv); - } - else { - throw new Error(`unable to find unexpected flag named "${unexpectedName}"`); - } - } - return { - verbose, - quiet, - silent, - debug, - help, - _, - unexpected, - ...others, - }; -} -exports.getFlags = getFlags; -function getHelp(options) { - var _a, _b; - const usage = options.usage || `node ${path_1.relative(process.cwd(), process.argv[1])}`; - const optionHelp = (dedent_1.default(((_b = (_a = options) === null || _a === void 0 ? void 0 : _a.flags) === null || _b === void 0 ? void 0 : _b.help) || '') + - '\n' + - dedent_1.default ` - --verbose, -v Log verbosely - --debug Log debug messages (less than verbose) - --quiet Only log errors - --silent Don't log anything - --help Show this message - `) - .split('\n') - .filter(Boolean) - .join('\n '); - return ` - ${usage} + if (typeof options.beforeParse === 'function') { + data = options.beforeParse(data); + } - ${dedent_1.default(options.description || 'Runs a dev task') - .split('\n') - .join('\n ')} + return parseJson(data, options.reviver, path.relative(process.cwd(), filePath)); +}; - Options: - ${optionHelp + '\n\n'}`; -} -exports.getHelp = getHelp; +module.exports = async (filePath, options) => parse(await promisify(fs.readFile)(filePath, 'utf8'), filePath, options); +module.exports.sync = (filePath, options) => parse(fs.readFileSync(filePath, 'utf8'), filePath, options); /***/ }), -/* 427 */ +/* 424 */ /***/ (function(module, exports, __webpack_require__) { -"use strict"; +var fs = __webpack_require__(23) +var polyfills = __webpack_require__(425) +var legacy = __webpack_require__(426) +var clone = __webpack_require__(427) +var queue = [] -const EMPTYARR = [] -const SHORTSPLIT = /$|[!-@[-`{-~][\s\S]*/g -const isArray = Array.isArray +var util = __webpack_require__(29) -const parseValue = function(any) { - if (any === "") return "" - if (any === "false") return false - const maybe = Number(any) - return maybe * 0 === 0 ? maybe : any +function noop () {} + +var debug = noop +if (util.debuglog) + debug = util.debuglog('gfs4') +else if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) + debug = function() { + var m = util.format.apply(util, arguments) + m = 'GFS4: ' + m.split(/\n/).join('\nGFS4: ') + console.error(m) + } + +if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) { + process.on('exit', function() { + debug(queue) + __webpack_require__(30).equal(queue.length, 0) + }) } -const parseAlias = function(aliases) { - let out = {}, - key, - alias, - prev, - len, - any, - i, - k +module.exports = patch(clone(fs)) +if (process.env.TEST_GRACEFUL_FS_GLOBAL_PATCH && !fs.__patched) { + module.exports = patch(fs) + fs.__patched = true; +} - for (key in aliases) { - any = aliases[key] - alias = out[key] = isArray(any) ? any : [any] +// Always patch fs.close/closeSync, because we want to +// retry() whenever a close happens *anywhere* in the program. +// This is essential when multiple graceful-fs instances are +// in play at the same time. +module.exports.close = (function (fs$close) { return function (fd, cb) { + return fs$close.call(fs, fd, function (err) { + if (!err) + retry() - for (i = 0, len = alias.length; i < len; i++) { - prev = out[alias[i]] = [key] + if (typeof cb === 'function') + cb.apply(this, arguments) + }) +}})(fs.close) - for (k = 0; k < len; k++) { - if (i !== k) prev.push(alias[k]) - } - } - } +module.exports.closeSync = (function (fs$closeSync) { return function (fd) { + // Note that graceful-fs also retries when fs.closeSync() fails. + // Looks like a bug to me, although it's probably a harmless one. + var rval = fs$closeSync.apply(fs, arguments) + retry() + return rval +}})(fs.closeSync) - return out +// Only patch fs once, otherwise we'll run into a memory leak if +// graceful-fs is loaded multiple times, such as in test environments that +// reset the loaded modules between tests. +// We look for the string `graceful-fs` from the comment above. This +// way we are not adding any extra properties and it will detect if older +// versions of graceful-fs are installed. +if (!/\bgraceful-fs\b/.test(fs.closeSync.toString())) { + fs.closeSync = module.exports.closeSync; + fs.close = module.exports.close; } -const parseDefault = function(aliases, defaults) { - let out = {}, - key, - alias, - value, - len, - i - - for (key in defaults) { - value = defaults[key] - alias = aliases[key] +function patch (fs) { + // Everything that references the open() function needs to be in here + polyfills(fs) + fs.gracefulify = patch + fs.FileReadStream = ReadStream; // Legacy name. + fs.FileWriteStream = WriteStream; // Legacy name. + fs.createReadStream = createReadStream + fs.createWriteStream = createWriteStream + var fs$readFile = fs.readFile + fs.readFile = readFile + function readFile (path, options, cb) { + if (typeof options === 'function') + cb = options, options = null - out[key] = value + return go$readFile(path, options, cb) - if (alias === undefined) { - aliases[key] = EMPTYARR - } else { - for (i = 0, len = alias.length; i < len; i++) { - out[alias[i]] = value - } + function go$readFile (path, options, cb) { + return fs$readFile(path, options, function (err) { + if (err && (err.code === 'EMFILE' || err.code === 'ENFILE')) + enqueue([go$readFile, [path, options, cb]]) + else { + if (typeof cb === 'function') + cb.apply(this, arguments) + retry() + } + }) } } - return out -} + var fs$writeFile = fs.writeFile + fs.writeFile = writeFile + function writeFile (path, data, options, cb) { + if (typeof options === 'function') + cb = options, options = null -const parseOptions = function(aliases, options, value) { - let out = {}, - key, - alias, - len, - end, - i, - k - - if (options !== undefined) { - for (i = 0, len = options.length; i < len; i++) { - key = options[i] - alias = aliases[key] - - out[key] = value - - if (alias === undefined) { - aliases[key] = EMPTYARR - } else { - for (k = 0, end = alias.length; k < end; k++) { - out[alias[k]] = value - } - } - } - } - - return out -} - -const write = function(out, key, value, aliases, unknown) { - let i, - prev, - alias = aliases[key], - len = alias === undefined ? -1 : alias.length - - if (len >= 0 || unknown === undefined || unknown(key)) { - prev = out[key] - - if (prev === undefined) { - out[key] = value - } else { - if (isArray(prev)) { - prev.push(value) - } else { - out[key] = [prev, value] - } - } - - for (i = 0; i < len; i++) { - out[alias[i]] = out[key] - } - } -} - -const getopts = function(argv, opts) { - let unknown = (opts = opts || {}).unknown, - aliases = parseAlias(opts.alias), - strings = parseOptions(aliases, opts.string, ""), - values = parseDefault(aliases, opts.default), - bools = parseOptions(aliases, opts.boolean, false), - stopEarly = opts.stopEarly, - _ = [], - out = { _ }, - i = 0, - k = 0, - len = argv.length, - key, - arg, - end, - match, - value - - for (; i < len; i++) { - arg = argv[i] - - if (arg[0] !== "-" || arg === "-") { - if (stopEarly) while (i < len) _.push(argv[i++]) - else _.push(arg) - } else if (arg === "--") { - while (++i < len) _.push(argv[i]) - } else if (arg[1] === "-") { - end = arg.indexOf("=", 2) - if (arg[2] === "n" && arg[3] === "o" && arg[4] === "-") { - key = arg.slice(5, end >= 0 ? end : undefined) - value = false - } else if (end >= 0) { - key = arg.slice(2, end) - value = - bools[key] !== undefined || - (strings[key] === undefined - ? parseValue(arg.slice(end + 1)) - : arg.slice(end + 1)) - } else { - key = arg.slice(2) - value = - bools[key] !== undefined || - (len === i + 1 || argv[i + 1][0] === "-" - ? strings[key] === undefined - ? true - : "" - : strings[key] === undefined - ? parseValue(argv[++i]) - : argv[++i]) - } - write(out, key, value, aliases, unknown) - } else { - SHORTSPLIT.lastIndex = 2 - match = SHORTSPLIT.exec(arg) - end = match.index - value = match[0] - - for (k = 1; k < end; k++) { - write( - out, - (key = arg[k]), - k + 1 < end - ? strings[key] === undefined || - arg.substring(k + 1, (k = end)) + value - : value === "" - ? len === i + 1 || argv[i + 1][0] === "-" - ? strings[key] === undefined || "" - : bools[key] !== undefined || - (strings[key] === undefined ? parseValue(argv[++i]) : argv[++i]) - : bools[key] !== undefined || - (strings[key] === undefined ? parseValue(value) : value), - aliases, - unknown - ) - } - } - } - - for (key in values) if (out[key] === undefined) out[key] = values[key] - for (key in bools) if (out[key] === undefined) out[key] = false - for (key in strings) if (out[key] === undefined) out[key] = "" - - return out -} - -module.exports = getopts - - -/***/ }), -/* 428 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - -/* - * 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. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -const tslib_1 = __webpack_require__(36); -const path_1 = tslib_1.__importDefault(__webpack_require__(16)); -const fs_1 = tslib_1.__importDefault(__webpack_require__(23)); -const load_json_file_1 = tslib_1.__importDefault(__webpack_require__(429)); -const isKibanaDir = (dir) => { - try { - const path = path_1.default.resolve(dir, 'package.json'); - const json = load_json_file_1.default.sync(path); - if (json && typeof json === 'object' && 'name' in json && json.name === 'kibana') { - return true; - } - } - catch (error) { - if (error && error.code === 'ENOENT') { - return false; - } - throw error; - } -}; -// search for the kibana directory, since this file is moved around it might -// not be where we think but should always be a relatively close parent -// of this directory -const startDir = fs_1.default.realpathSync(__dirname); -const { root: rootDir } = path_1.default.parse(startDir); -let cursor = startDir; -while (true) { - if (isKibanaDir(cursor)) { - break; - } - const parent = path_1.default.dirname(cursor); - if (parent === rootDir) { - throw new Error(`unable to find kibana directory from ${startDir}`); - } - cursor = parent; -} -exports.REPO_ROOT = cursor; - - -/***/ }), -/* 429 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - -const path = __webpack_require__(16); -const {promisify} = __webpack_require__(29); -const fs = __webpack_require__(430); -const stripBom = __webpack_require__(434); -const parseJson = __webpack_require__(435); - -const parse = (data, filePath, options = {}) => { - data = stripBom(data); - - if (typeof options.beforeParse === 'function') { - data = options.beforeParse(data); - } - - return parseJson(data, options.reviver, path.relative(process.cwd(), filePath)); -}; - -module.exports = async (filePath, options) => parse(await promisify(fs.readFile)(filePath, 'utf8'), filePath, options); -module.exports.sync = (filePath, options) => parse(fs.readFileSync(filePath, 'utf8'), filePath, options); - - -/***/ }), -/* 430 */ -/***/ (function(module, exports, __webpack_require__) { - -var fs = __webpack_require__(23) -var polyfills = __webpack_require__(431) -var legacy = __webpack_require__(432) -var clone = __webpack_require__(433) - -var queue = [] - -var util = __webpack_require__(29) - -function noop () {} - -var debug = noop -if (util.debuglog) - debug = util.debuglog('gfs4') -else if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) - debug = function() { - var m = util.format.apply(util, arguments) - m = 'GFS4: ' + m.split(/\n/).join('\nGFS4: ') - console.error(m) - } - -if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) { - process.on('exit', function() { - debug(queue) - __webpack_require__(30).equal(queue.length, 0) - }) -} - -module.exports = patch(clone(fs)) -if (process.env.TEST_GRACEFUL_FS_GLOBAL_PATCH && !fs.__patched) { - module.exports = patch(fs) - fs.__patched = true; -} - -// Always patch fs.close/closeSync, because we want to -// retry() whenever a close happens *anywhere* in the program. -// This is essential when multiple graceful-fs instances are -// in play at the same time. -module.exports.close = (function (fs$close) { return function (fd, cb) { - return fs$close.call(fs, fd, function (err) { - if (!err) - retry() - - if (typeof cb === 'function') - cb.apply(this, arguments) - }) -}})(fs.close) - -module.exports.closeSync = (function (fs$closeSync) { return function (fd) { - // Note that graceful-fs also retries when fs.closeSync() fails. - // Looks like a bug to me, although it's probably a harmless one. - var rval = fs$closeSync.apply(fs, arguments) - retry() - return rval -}})(fs.closeSync) - -// Only patch fs once, otherwise we'll run into a memory leak if -// graceful-fs is loaded multiple times, such as in test environments that -// reset the loaded modules between tests. -// We look for the string `graceful-fs` from the comment above. This -// way we are not adding any extra properties and it will detect if older -// versions of graceful-fs are installed. -if (!/\bgraceful-fs\b/.test(fs.closeSync.toString())) { - fs.closeSync = module.exports.closeSync; - fs.close = module.exports.close; -} - -function patch (fs) { - // Everything that references the open() function needs to be in here - polyfills(fs) - fs.gracefulify = patch - fs.FileReadStream = ReadStream; // Legacy name. - fs.FileWriteStream = WriteStream; // Legacy name. - fs.createReadStream = createReadStream - fs.createWriteStream = createWriteStream - var fs$readFile = fs.readFile - fs.readFile = readFile - function readFile (path, options, cb) { - if (typeof options === 'function') - cb = options, options = null - - return go$readFile(path, options, cb) - - function go$readFile (path, options, cb) { - return fs$readFile(path, options, function (err) { - if (err && (err.code === 'EMFILE' || err.code === 'ENFILE')) - enqueue([go$readFile, [path, options, cb]]) - else { - if (typeof cb === 'function') - cb.apply(this, arguments) - retry() - } - }) - } - } - - var fs$writeFile = fs.writeFile - fs.writeFile = writeFile - function writeFile (path, data, options, cb) { - if (typeof options === 'function') - cb = options, options = null - - return go$writeFile(path, data, options, cb) + return go$writeFile(path, data, options, cb) function go$writeFile (path, data, options, cb) { return fs$writeFile(path, data, options, function (err) { @@ -37937,7 +37355,7 @@ function retry () { /***/ }), -/* 431 */ +/* 425 */ /***/ (function(module, exports, __webpack_require__) { var constants = __webpack_require__(25) @@ -38272,7 +37690,7 @@ function patch (fs) { /***/ }), -/* 432 */ +/* 426 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(27).Stream @@ -38396,7 +37814,7 @@ function legacy (fs) { /***/ }), -/* 433 */ +/* 427 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38422,7 +37840,7 @@ function clone (obj) { /***/ }), -/* 434 */ +/* 428 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38444,15 +37862,15 @@ module.exports = string => { /***/ }), -/* 435 */ +/* 429 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const errorEx = __webpack_require__(436); -const fallback = __webpack_require__(438); -const {default: LinesAndColumns} = __webpack_require__(439); -const {codeFrameColumns} = __webpack_require__(440); +const errorEx = __webpack_require__(430); +const fallback = __webpack_require__(432); +const {default: LinesAndColumns} = __webpack_require__(433); +const {codeFrameColumns} = __webpack_require__(434); const JSONError = errorEx('JSONError', { fileName: errorEx.append('in %s'), @@ -38501,14 +37919,14 @@ module.exports = (string, reviver, filename) => { /***/ }), -/* 436 */ +/* 430 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var isArrayish = __webpack_require__(437); +var isArrayish = __webpack_require__(431); var errorEx = function errorEx(name, properties) { if (!name || name.constructor !== String) { @@ -38641,7 +38059,7 @@ module.exports = errorEx; /***/ }), -/* 437 */ +/* 431 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38658,7 +38076,7 @@ module.exports = function isArrayish(obj) { /***/ }), -/* 438 */ +/* 432 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38697,7 +38115,7 @@ function parseJson (txt, reviver, context) { /***/ }), -/* 439 */ +/* 433 */ /***/ (function(__webpack_module__, __webpack_exports__, __webpack_require__) { "use strict"; @@ -38761,7 +38179,7 @@ var LinesAndColumns = (function () { /***/ }), -/* 440 */ +/* 434 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38774,7 +38192,7 @@ exports.codeFrameColumns = codeFrameColumns; exports.default = _default; function _highlight() { - const data = _interopRequireWildcard(__webpack_require__(441)); + const data = _interopRequireWildcard(__webpack_require__(435)); _highlight = function () { return data; @@ -38940,7 +38358,7 @@ function _default(rawLines, lineNumber, colNumber, opts = {}) { } /***/ }), -/* 441 */ +/* 435 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38954,7 +38372,7 @@ exports.getChalk = getChalk; exports.default = highlight; function _jsTokens() { - const data = _interopRequireWildcard(__webpack_require__(442)); + const data = _interopRequireWildcard(__webpack_require__(436)); _jsTokens = function () { return data; @@ -38964,7 +38382,7 @@ function _jsTokens() { } function _esutils() { - const data = _interopRequireDefault(__webpack_require__(443)); + const data = _interopRequireDefault(__webpack_require__(437)); _esutils = function () { return data; @@ -38974,7 +38392,7 @@ function _esutils() { } function _chalk() { - const data = _interopRequireDefault(__webpack_require__(447)); + const data = _interopRequireDefault(__webpack_require__(441)); _chalk = function () { return data; @@ -39075,7 +38493,7 @@ function highlight(code, options = {}) { } /***/ }), -/* 442 */ +/* 436 */ /***/ (function(module, exports) { // Copyright 2014, 2015, 2016, 2017, 2018 Simon Lydell @@ -39104,7 +38522,7 @@ exports.matchToToken = function(match) { /***/ }), -/* 443 */ +/* 437 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -39135,15 +38553,15 @@ exports.matchToToken = function(match) { (function () { 'use strict'; - exports.ast = __webpack_require__(444); - exports.code = __webpack_require__(445); - exports.keyword = __webpack_require__(446); + exports.ast = __webpack_require__(438); + exports.code = __webpack_require__(439); + exports.keyword = __webpack_require__(440); }()); /* vim: set sw=4 ts=4 et tw=80 : */ /***/ }), -/* 444 */ +/* 438 */ /***/ (function(module, exports) { /* @@ -39293,7 +38711,7 @@ exports.matchToToken = function(match) { /***/ }), -/* 445 */ +/* 439 */ /***/ (function(module, exports) { /* @@ -39434,7 +38852,7 @@ exports.matchToToken = function(match) { /***/ }), -/* 446 */ +/* 440 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -39464,7 +38882,7 @@ exports.matchToToken = function(match) { (function () { 'use strict'; - var code = __webpack_require__(445); + var code = __webpack_require__(439); function isStrictModeReservedWordES6(id) { switch (id) { @@ -39605,16 +39023,16 @@ exports.matchToToken = function(match) { /***/ }), -/* 447 */ +/* 441 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const escapeStringRegexp = __webpack_require__(3); -const ansiStyles = __webpack_require__(448); -const stdoutColor = __webpack_require__(449).stdout; +const ansiStyles = __webpack_require__(442); +const stdoutColor = __webpack_require__(443).stdout; -const template = __webpack_require__(450); +const template = __webpack_require__(444); const isSimpleWindowsTerm = process.platform === 'win32' && !(process.env.TERM || '').toLowerCase().startsWith('xterm'); @@ -39840,7 +39258,7 @@ module.exports.default = module.exports; // For TypeScript /***/ }), -/* 448 */ +/* 442 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40013,7 +39431,7 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(5)(module))) /***/ }), -/* 449 */ +/* 443 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40155,7 +39573,7 @@ module.exports = { /***/ }), -/* 450 */ +/* 444 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40290,7 +39708,7 @@ module.exports = (chalk, tmp) => { /***/ }), -/* 451 */ +/* 445 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40314,14 +39732,22 @@ module.exports = (chalk, tmp) => { * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -var kbn_client_1 = __webpack_require__(452); -exports.KbnClient = kbn_client_1.KbnClient; -var kbn_client_requester_1 = __webpack_require__(453); -exports.uriencode = kbn_client_requester_1.uriencode; +const path_1 = __webpack_require__(16); +exports.CA_CERT_PATH = path_1.resolve(__dirname, '../certs/ca.crt'); +exports.ES_KEY_PATH = path_1.resolve(__dirname, '../certs/elasticsearch.key'); +exports.ES_CERT_PATH = path_1.resolve(__dirname, '../certs/elasticsearch.crt'); +exports.ES_P12_PATH = path_1.resolve(__dirname, '../certs/elasticsearch.p12'); +exports.ES_P12_PASSWORD = 'storepass'; +exports.ES_EMPTYPASSWORD_P12_PATH = path_1.resolve(__dirname, '../certs/elasticsearch_emptypassword.p12'); +exports.ES_NOPASSWORD_P12_PATH = path_1.resolve(__dirname, '../certs/elasticsearch_nopassword.p12'); +exports.KBN_KEY_PATH = path_1.resolve(__dirname, '../certs/kibana.key'); +exports.KBN_CERT_PATH = path_1.resolve(__dirname, '../certs/kibana.crt'); +exports.KBN_P12_PATH = path_1.resolve(__dirname, '../certs/kibana.p12'); +exports.KBN_P12_PASSWORD = 'storepass'; /***/ }), -/* 452 */ +/* 446 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40345,50 +39771,627 @@ exports.uriencode = kbn_client_requester_1.uriencode; * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -const kbn_client_requester_1 = __webpack_require__(453); -const kbn_client_status_1 = __webpack_require__(495); -const kbn_client_plugins_1 = __webpack_require__(496); -const kbn_client_version_1 = __webpack_require__(497); -const kbn_client_saved_objects_1 = __webpack_require__(498); -const kbn_client_ui_settings_1 = __webpack_require__(499); -class KbnClient { - /** - * Basic Kibana server client that implements common behaviors for talking - * to the Kibana server from dev tooling. - * - * @param log ToolingLog - * @param kibanaUrls Array of kibana server urls to send requests to - * @param uiSettingDefaults Map of uiSetting values that will be merged with all uiSetting resets - */ - constructor(log, kibanaUrls, uiSettingDefaults) { - this.log = log; - this.kibanaUrls = kibanaUrls; - this.uiSettingDefaults = uiSettingDefaults; - this.requester = new kbn_client_requester_1.KbnClientRequester(this.log, this.kibanaUrls); - this.status = new kbn_client_status_1.KbnClientStatus(this.requester); - this.plugins = new kbn_client_plugins_1.KbnClientPlugins(this.status); - this.version = new kbn_client_version_1.KbnClientVersion(this.status); - this.savedObjects = new kbn_client_saved_objects_1.KbnClientSavedObjects(this.log, this.requester); - this.uiSettings = new kbn_client_ui_settings_1.KbnClientUiSettings(this.log, this.requester, this.uiSettingDefaults); - if (!kibanaUrls.length) { - throw new Error('missing Kibana urls'); +var run_1 = __webpack_require__(447); +exports.run = run_1.run; +var fail_1 = __webpack_require__(448); +exports.createFailError = fail_1.createFailError; +exports.createFlagError = fail_1.createFlagError; +exports.combineErrors = fail_1.combineErrors; +exports.isFailError = fail_1.isFailError; + + +/***/ }), +/* 447 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +/* + * 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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const tslib_1 = __webpack_require__(36); +// @ts-ignore @types are outdated and module is super simple +const exit_hook_1 = tslib_1.__importDefault(__webpack_require__(348)); +const tooling_log_1 = __webpack_require__(415); +const fail_1 = __webpack_require__(448); +const flags_1 = __webpack_require__(449); +const proc_runner_1 = __webpack_require__(37); +async function run(fn, options = {}) { + var _a; + const flags = flags_1.getFlags(process.argv.slice(2), options); + if (flags.help) { + process.stderr.write(flags_1.getHelp(options)); + process.exit(1); + } + const log = new tooling_log_1.ToolingLog({ + level: tooling_log_1.pickLevelFromFlags(flags), + writeTo: process.stdout, + }); + process.on('unhandledRejection', error => { + log.error('UNHANDLED PROMISE REJECTION'); + log.error(error); + process.exit(1); + }); + const handleErrorWithoutExit = (error) => { + if (fail_1.isFailError(error)) { + log.error(error.message); + if (error.showHelp) { + log.write(flags_1.getHelp(options)); + } + process.exitCode = error.exitCode; + } + else { + log.error('UNHANDLED ERROR'); + log.error(error); + process.exitCode = 1; + } + }; + const doCleanup = () => { + const tasks = cleanupTasks.slice(0); + cleanupTasks.length = 0; + for (const task of tasks) { + try { + task(); + } + catch (error) { + handleErrorWithoutExit(error); + } + } + }; + const unhookExit = exit_hook_1.default(doCleanup); + const cleanupTasks = [unhookExit]; + try { + if (!((_a = options.flags) === null || _a === void 0 ? void 0 : _a.allowUnexpected) && flags.unexpected.length) { + throw fail_1.createFlagError(`Unknown flag(s) "${flags.unexpected.join('", "')}"`); + } + try { + await proc_runner_1.withProcRunner(log, async (procRunner) => { + await fn({ + log, + flags, + procRunner, + addCleanupTask: (task) => cleanupTasks.push(task), + }); + }); + } + finally { + doCleanup(); } } - /** - * Make a direct request to the Kibana server - */ - async request(options) { - return await this.requester.request(options); + catch (error) { + handleErrorWithoutExit(error); + process.exit(); } - resolveUrl(relativeUrl) { - return this.requester.resolveUrl(relativeUrl); +} +exports.run = run; + + +/***/ }), +/* 448 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +/* + * 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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const util_1 = __webpack_require__(29); +const FAIL_TAG = Symbol('fail error'); +function createFailError(reason, options = {}) { + const { exitCode = 1, showHelp = false } = options; + return Object.assign(new Error(reason), { + exitCode, + showHelp, + [FAIL_TAG]: true, + }); +} +exports.createFailError = createFailError; +function createFlagError(reason) { + return createFailError(reason, { + showHelp: true, + }); +} +exports.createFlagError = createFlagError; +function isFailError(error) { + return Boolean(error && error[FAIL_TAG]); +} +exports.isFailError = isFailError; +function combineErrors(errors) { + if (errors.length === 1) { + return errors[0]; } + const exitCode = errors + .filter(isFailError) + .reduce((acc, error) => Math.max(acc, error.exitCode), 1); + const showHelp = errors.some(error => isFailError(error) && error.showHelp); + const message = errors.reduce((acc, error) => { + if (isFailError(error)) { + return acc + '\n' + error.message; + } + return acc + `\nUNHANDLED ERROR\n${util_1.inspect(error)}`; + }, ''); + return createFailError(`${errors.length} errors:\n${message}`, { + exitCode, + showHelp, + }); } -exports.KbnClient = KbnClient; +exports.combineErrors = combineErrors; /***/ }), -/* 453 */ +/* 449 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +/* + * 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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const tslib_1 = __webpack_require__(36); +const path_1 = __webpack_require__(16); +const dedent_1 = tslib_1.__importDefault(__webpack_require__(14)); +const getopts_1 = tslib_1.__importDefault(__webpack_require__(450)); +function getFlags(argv, options) { + const unexpectedNames = new Set(); + const flagOpts = options.flags || {}; + const { verbose, quiet, silent, debug, help, _, ...others } = getopts_1.default(argv, { + string: flagOpts.string, + boolean: [...(flagOpts.boolean || []), 'verbose', 'quiet', 'silent', 'debug', 'help'], + alias: { + ...(flagOpts.alias || {}), + v: 'verbose', + }, + default: flagOpts.default, + unknown: (name) => { + unexpectedNames.add(name); + return flagOpts.guessTypesForUnexpectedFlags; + }, + }); + const unexpected = []; + for (const unexpectedName of unexpectedNames) { + const matchingArgv = []; + iterArgv: for (const [i, v] of argv.entries()) { + for (const prefix of ['--', '-']) { + if (v.startsWith(prefix)) { + // -/--name=value + if (v.startsWith(`${prefix}${unexpectedName}=`)) { + matchingArgv.push(v); + continue iterArgv; + } + // -/--name (value possibly follows) + if (v === `${prefix}${unexpectedName}`) { + matchingArgv.push(v); + // value follows -/--name + if (argv.length > i + 1 && !argv[i + 1].startsWith('-')) { + matchingArgv.push(argv[i + 1]); + } + continue iterArgv; + } + } + } + // special case for `--no-{flag}` disabling of boolean flags + if (v === `--no-${unexpectedName}`) { + matchingArgv.push(v); + continue iterArgv; + } + // special case for shortcut flags formatted as `-abc` where `a`, `b`, + // and `c` will be three separate unexpected flags + if (unexpectedName.length === 1 && + v[0] === '-' && + v[1] !== '-' && + !v.includes('=') && + v.includes(unexpectedName)) { + matchingArgv.push(`-${unexpectedName}`); + continue iterArgv; + } + } + if (matchingArgv.length) { + unexpected.push(...matchingArgv); + } + else { + throw new Error(`unable to find unexpected flag named "${unexpectedName}"`); + } + } + return { + verbose, + quiet, + silent, + debug, + help, + _, + unexpected, + ...others, + }; +} +exports.getFlags = getFlags; +function getHelp(options) { + var _a, _b; + const usage = options.usage || `node ${path_1.relative(process.cwd(), process.argv[1])}`; + const optionHelp = (dedent_1.default(((_b = (_a = options) === null || _a === void 0 ? void 0 : _a.flags) === null || _b === void 0 ? void 0 : _b.help) || '') + + '\n' + + dedent_1.default ` + --verbose, -v Log verbosely + --debug Log debug messages (less than verbose) + --quiet Only log errors + --silent Don't log anything + --help Show this message + `) + .split('\n') + .filter(Boolean) + .join('\n '); + return ` + ${usage} + + ${dedent_1.default(options.description || 'Runs a dev task') + .split('\n') + .join('\n ')} + + Options: + ${optionHelp + '\n\n'}`; +} +exports.getHelp = getHelp; + + +/***/ }), +/* 450 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const EMPTYARR = [] +const SHORTSPLIT = /$|[!-@[-`{-~][\s\S]*/g +const isArray = Array.isArray + +const parseValue = function(any) { + if (any === "") return "" + if (any === "false") return false + const maybe = Number(any) + return maybe * 0 === 0 ? maybe : any +} + +const parseAlias = function(aliases) { + let out = {}, + key, + alias, + prev, + len, + any, + i, + k + + for (key in aliases) { + any = aliases[key] + alias = out[key] = isArray(any) ? any : [any] + + for (i = 0, len = alias.length; i < len; i++) { + prev = out[alias[i]] = [key] + + for (k = 0; k < len; k++) { + if (i !== k) prev.push(alias[k]) + } + } + } + + return out +} + +const parseDefault = function(aliases, defaults) { + let out = {}, + key, + alias, + value, + len, + i + + for (key in defaults) { + value = defaults[key] + alias = aliases[key] + + out[key] = value + + if (alias === undefined) { + aliases[key] = EMPTYARR + } else { + for (i = 0, len = alias.length; i < len; i++) { + out[alias[i]] = value + } + } + } + + return out +} + +const parseOptions = function(aliases, options, value) { + let out = {}, + key, + alias, + len, + end, + i, + k + + if (options !== undefined) { + for (i = 0, len = options.length; i < len; i++) { + key = options[i] + alias = aliases[key] + + out[key] = value + + if (alias === undefined) { + aliases[key] = EMPTYARR + } else { + for (k = 0, end = alias.length; k < end; k++) { + out[alias[k]] = value + } + } + } + } + + return out +} + +const write = function(out, key, value, aliases, unknown) { + let i, + prev, + alias = aliases[key], + len = alias === undefined ? -1 : alias.length + + if (len >= 0 || unknown === undefined || unknown(key)) { + prev = out[key] + + if (prev === undefined) { + out[key] = value + } else { + if (isArray(prev)) { + prev.push(value) + } else { + out[key] = [prev, value] + } + } + + for (i = 0; i < len; i++) { + out[alias[i]] = out[key] + } + } +} + +const getopts = function(argv, opts) { + let unknown = (opts = opts || {}).unknown, + aliases = parseAlias(opts.alias), + strings = parseOptions(aliases, opts.string, ""), + values = parseDefault(aliases, opts.default), + bools = parseOptions(aliases, opts.boolean, false), + stopEarly = opts.stopEarly, + _ = [], + out = { _ }, + i = 0, + k = 0, + len = argv.length, + key, + arg, + end, + match, + value + + for (; i < len; i++) { + arg = argv[i] + + if (arg[0] !== "-" || arg === "-") { + if (stopEarly) while (i < len) _.push(argv[i++]) + else _.push(arg) + } else if (arg === "--") { + while (++i < len) _.push(argv[i]) + } else if (arg[1] === "-") { + end = arg.indexOf("=", 2) + if (arg[2] === "n" && arg[3] === "o" && arg[4] === "-") { + key = arg.slice(5, end >= 0 ? end : undefined) + value = false + } else if (end >= 0) { + key = arg.slice(2, end) + value = + bools[key] !== undefined || + (strings[key] === undefined + ? parseValue(arg.slice(end + 1)) + : arg.slice(end + 1)) + } else { + key = arg.slice(2) + value = + bools[key] !== undefined || + (len === i + 1 || argv[i + 1][0] === "-" + ? strings[key] === undefined + ? true + : "" + : strings[key] === undefined + ? parseValue(argv[++i]) + : argv[++i]) + } + write(out, key, value, aliases, unknown) + } else { + SHORTSPLIT.lastIndex = 2 + match = SHORTSPLIT.exec(arg) + end = match.index + value = match[0] + + for (k = 1; k < end; k++) { + write( + out, + (key = arg[k]), + k + 1 < end + ? strings[key] === undefined || + arg.substring(k + 1, (k = end)) + value + : value === "" + ? len === i + 1 || argv[i + 1][0] === "-" + ? strings[key] === undefined || "" + : bools[key] !== undefined || + (strings[key] === undefined ? parseValue(argv[++i]) : argv[++i]) + : bools[key] !== undefined || + (strings[key] === undefined ? parseValue(value) : value), + aliases, + unknown + ) + } + } + } + + for (key in values) if (out[key] === undefined) out[key] = values[key] + for (key in bools) if (out[key] === undefined) out[key] = false + for (key in strings) if (out[key] === undefined) out[key] = "" + + return out +} + +module.exports = getopts + + +/***/ }), +/* 451 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +/* + * 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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +var kbn_client_1 = __webpack_require__(452); +exports.KbnClient = kbn_client_1.KbnClient; +var kbn_client_requester_1 = __webpack_require__(453); +exports.uriencode = kbn_client_requester_1.uriencode; + + +/***/ }), +/* 452 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +/* + * 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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const kbn_client_requester_1 = __webpack_require__(453); +const kbn_client_status_1 = __webpack_require__(495); +const kbn_client_plugins_1 = __webpack_require__(496); +const kbn_client_version_1 = __webpack_require__(497); +const kbn_client_saved_objects_1 = __webpack_require__(498); +const kbn_client_ui_settings_1 = __webpack_require__(499); +class KbnClient { + /** + * Basic Kibana server client that implements common behaviors for talking + * to the Kibana server from dev tooling. + * + * @param log ToolingLog + * @param kibanaUrls Array of kibana server urls to send requests to + * @param uiSettingDefaults Map of uiSetting values that will be merged with all uiSetting resets + */ + constructor(log, kibanaUrls, uiSettingDefaults) { + this.log = log; + this.kibanaUrls = kibanaUrls; + this.uiSettingDefaults = uiSettingDefaults; + this.requester = new kbn_client_requester_1.KbnClientRequester(this.log, this.kibanaUrls); + this.status = new kbn_client_status_1.KbnClientStatus(this.requester); + this.plugins = new kbn_client_plugins_1.KbnClientPlugins(this.status); + this.version = new kbn_client_version_1.KbnClientVersion(this.status); + this.savedObjects = new kbn_client_saved_objects_1.KbnClientSavedObjects(this.log, this.requester); + this.uiSettings = new kbn_client_ui_settings_1.KbnClientUiSettings(this.log, this.requester, this.uiSettingDefaults); + if (!kibanaUrls.length) { + throw new Error('missing Kibana urls'); + } + } + /** + * Make a direct request to the Kibana server + */ + async request(options) { + return await this.requester.request(options); + } + resolveUrl(relativeUrl) { + return this.requester.resolveUrl(relativeUrl); + } +} +exports.KbnClient = KbnClient; + + +/***/ }), +/* 453 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43043,28 +43046,21 @@ module.exports = require("tty"); const os = __webpack_require__(11); const hasFlag = __webpack_require__(12); -const {env} = process; +const env = process.env; let forceColor; if (hasFlag('no-color') || hasFlag('no-colors') || - hasFlag('color=false') || - hasFlag('color=never')) { - forceColor = 0; + hasFlag('color=false')) { + forceColor = false; } else if (hasFlag('color') || hasFlag('colors') || hasFlag('color=true') || hasFlag('color=always')) { - forceColor = 1; + forceColor = true; } if ('FORCE_COLOR' in env) { - if (env.FORCE_COLOR === true || env.FORCE_COLOR === 'true') { - forceColor = 1; - } else if (env.FORCE_COLOR === false || env.FORCE_COLOR === 'false') { - forceColor = 0; - } else { - forceColor = env.FORCE_COLOR.length === 0 ? 1 : Math.min(parseInt(env.FORCE_COLOR, 10), 3); - } + forceColor = env.FORCE_COLOR.length === 0 || parseInt(env.FORCE_COLOR, 10) !== 0; } function translateLevel(level) { @@ -43081,7 +43077,7 @@ function translateLevel(level) { } function supportsColor(stream) { - if (forceColor === 0) { + if (forceColor === false) { return 0; } @@ -43095,15 +43091,11 @@ function supportsColor(stream) { return 2; } - if (stream && !stream.isTTY && forceColor === undefined) { + if (stream && !stream.isTTY && forceColor !== true) { return 0; } - const min = forceColor || 0; - - if (env.TERM === 'dumb') { - return min; - } + const min = forceColor ? 1 : 0; if (process.platform === 'win32') { // Node.js 7.5.0 is the first version of Node.js to include a patch to @@ -43164,6 +43156,10 @@ function supportsColor(stream) { return 1; } + if (env.TERM === 'dumb') { + return min; + } + return min; } @@ -47879,10 +47875,10 @@ module.exports.sync = options => { "use strict"; -const errorEx = __webpack_require__(436); -const fallback = __webpack_require__(438); -const {default: LinesAndColumns} = __webpack_require__(439); -const {codeFrameColumns} = __webpack_require__(440); +const errorEx = __webpack_require__(430); +const fallback = __webpack_require__(432); +const {default: LinesAndColumns} = __webpack_require__(433); +const {codeFrameColumns} = __webpack_require__(434); const JSONError = errorEx('JSONError', { fileName: errorEx.append('in %s'), @@ -80679,7 +80675,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(706); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _build_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildProductionProjects"]; }); -/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(923); +/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(929); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); /* @@ -80859,16 +80855,24 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { const EventEmitter = __webpack_require__(379); const path = __webpack_require__(16); -const arrify = __webpack_require__(708); -const globby = __webpack_require__(709); -const cpFile = __webpack_require__(912); -const CpyError = __webpack_require__(921); +const os = __webpack_require__(11); +const pAll = __webpack_require__(708); +const arrify = __webpack_require__(710); +const globby = __webpack_require__(711); +const isGlob = __webpack_require__(606); +const cpFile = __webpack_require__(914); +const junk = __webpack_require__(926); +const CpyError = __webpack_require__(927); + +const defaultOptions = { + ignoreJunk: true +}; -const preprocessSrcPath = (srcPath, options) => options.cwd ? path.resolve(options.cwd, srcPath) : srcPath; +const preprocessSourcePath = (source, options) => options.cwd ? path.resolve(options.cwd, source) : source; -const preprocessDestPath = (srcPath, dest, options) => { - let basename = path.basename(srcPath); - const dirname = path.dirname(srcPath); +const preprocessDestinationPath = (source, destination, options) => { + let basename = path.basename(source); + const dirname = path.dirname(source); if (typeof options.rename === 'string') { basename = options.rename; @@ -80877,122 +80881,239 @@ const preprocessDestPath = (srcPath, dest, options) => { } if (options.cwd) { - dest = path.resolve(options.cwd, dest); + destination = path.resolve(options.cwd, destination); } if (options.parents) { - return path.join(dest, dirname, basename); + return path.join(destination, dirname, basename); } - return path.join(dest, basename); + return path.join(destination, basename); }; -const cpy = (src, dest, options = {}) => { - src = arrify(src); - +module.exports = (source, destination, { + concurrency = (os.cpus().length || 1) * 2, + ...options +} = {}) => { const progressEmitter = new EventEmitter(); - if (src.length === 0 || !dest) { - const promise = Promise.reject(new CpyError('`files` and `destination` required')); - promise.on = (...args) => { - progressEmitter.on(...args); - return promise; - }; + options = { + ...defaultOptions, + ...options + }; - return promise; - } + const promise = (async () => { + source = arrify(source); - const copyStatus = new Map(); - let completedFiles = 0; - let completedSize = 0; + if (source.length === 0 || !destination) { + throw new CpyError('`source` and `destination` required'); + } - const promise = globby(src, options) - .catch(error => { - throw new CpyError(`Cannot glob \`${src}\`: ${error.message}`, error); - }) - .then(files => { - if (files.length === 0) { - progressEmitter.emit('progress', { - totalFiles: 0, - percent: 1, - completedFiles: 0, - completedSize: 0 - }); + const copyStatus = new Map(); + let completedFiles = 0; + let completedSize = 0; + + let files; + try { + files = await globby(source, options); + + if (options.ignoreJunk) { + files = files.filter(file => junk.not(path.basename(file))); } + } catch (error) { + throw new CpyError(`Cannot glob \`${source}\`: ${error.message}`, error); + } - return Promise.all(files.map(srcPath => { - const from = preprocessSrcPath(srcPath, options); - const to = preprocessDestPath(srcPath, dest, options); + const sourcePaths = source.filter(value => !isGlob(value)); - return cpFile(from, to, options) - .on('progress', event => { - const fileStatus = copyStatus.get(event.src) || {written: 0, percent: 0}; + if (files.length === 0 || (sourcePaths.length > 0 && !sourcePaths.every(value => files.includes(value)))) { + throw new CpyError(`Cannot copy \`${source}\`: the file doesn't exist`); + } - if (fileStatus.written !== event.written || fileStatus.percent !== event.percent) { - completedSize -= fileStatus.written; - completedSize += event.written; + const fileProgressHandler = event => { + const fileStatus = copyStatus.get(event.src) || {written: 0, percent: 0}; - if (event.percent === 1 && fileStatus.percent !== 1) { - completedFiles++; - } + if (fileStatus.written !== event.written || fileStatus.percent !== event.percent) { + completedSize -= fileStatus.written; + completedSize += event.written; - copyStatus.set(event.src, {written: event.written, percent: event.percent}); + if (event.percent === 1 && fileStatus.percent !== 1) { + completedFiles++; + } - progressEmitter.emit('progress', { - totalFiles: files.length, - percent: completedFiles / files.length, - completedFiles, - completedSize - }); - } - }) - .then(() => to) - .catch(error => { - throw new CpyError(`Cannot copy from \`${from}\` to \`${to}\`: ${error.message}`, error); - }); - })); - }); + copyStatus.set(event.src, { + written: event.written, + percent: event.percent + }); - promise.on = (...args) => { - progressEmitter.on(...args); + progressEmitter.emit('progress', { + totalFiles: files.length, + percent: completedFiles / files.length, + completedFiles, + completedSize + }); + } + }; + + return pAll(files.map(sourcePath => { + return async () => { + const from = preprocessSourcePath(sourcePath, options); + const to = preprocessDestinationPath(sourcePath, destination, options); + + try { + await cpFile(from, to, options).on('progress', fileProgressHandler); + } catch (error) { + throw new CpyError(`Cannot copy from \`${from}\` to \`${to}\`: ${error.message}`, error); + } + + return to; + }; + }), {concurrency}); + })(); + + promise.on = (...arguments_) => { + progressEmitter.on(...arguments_); return promise; }; return promise; }; -module.exports = cpy; + +/***/ }), +/* 708 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +const pMap = __webpack_require__(709); + +module.exports = (iterable, options) => pMap(iterable, element => element(), options); // TODO: Remove this for the next major release -module.exports.default = cpy; +module.exports.default = module.exports; /***/ }), -/* 708 */ +/* 709 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const pMap = (iterable, mapper, options) => new Promise((resolve, reject) => { + options = Object.assign({ + concurrency: Infinity + }, options); + + if (typeof mapper !== 'function') { + throw new TypeError('Mapper function is required'); + } + + const {concurrency} = options; + + if (!(typeof concurrency === 'number' && concurrency >= 1)) { + throw new TypeError(`Expected \`concurrency\` to be a number from 1 and up, got \`${concurrency}\` (${typeof concurrency})`); + } + + const ret = []; + const iterator = iterable[Symbol.iterator](); + let isRejected = false; + let isIterableDone = false; + let resolvingCount = 0; + let currentIndex = 0; + + const next = () => { + if (isRejected) { + return; + } + + const nextItem = iterator.next(); + const i = currentIndex; + currentIndex++; + + if (nextItem.done) { + isIterableDone = true; + + if (resolvingCount === 0) { + resolve(ret); + } + + return; + } + + resolvingCount++; + + Promise.resolve(nextItem.value) + .then(element => mapper(element, i)) + .then( + value => { + ret[i] = value; + resolvingCount--; + next(); + }, + error => { + isRejected = true; + reject(error); + } + ); + }; + + for (let i = 0; i < concurrency; i++) { + next(); + + if (isIterableDone) { + break; + } + } +}); + +module.exports = pMap; +// TODO: Remove this for the next major release +module.exports.default = pMap; + + +/***/ }), +/* 710 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -module.exports = function (val) { - if (val === null || val === undefined) { + +const arrify = value => { + if (value === null || value === undefined) { return []; } - return Array.isArray(val) ? val : [val]; + if (Array.isArray(value)) { + return value; + } + + if (typeof value === 'string') { + return [value]; + } + + if (typeof value[Symbol.iterator] === 'function') { + return [...value]; + } + + return [value]; }; +module.exports = arrify; + /***/ }), -/* 709 */ +/* 711 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const arrayUnion = __webpack_require__(710); -const glob = __webpack_require__(712); -const fastGlob = __webpack_require__(717); -const dirGlob = __webpack_require__(905); -const gitignore = __webpack_require__(908); +const arrayUnion = __webpack_require__(712); +const glob = __webpack_require__(714); +const fastGlob = __webpack_require__(719); +const dirGlob = __webpack_require__(907); +const gitignore = __webpack_require__(910); const DEFAULT_FILTER = () => false; @@ -81137,12 +81258,12 @@ module.exports.gitignore = gitignore; /***/ }), -/* 710 */ +/* 712 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var arrayUniq = __webpack_require__(711); +var arrayUniq = __webpack_require__(713); module.exports = function () { return arrayUniq([].concat.apply([], arguments)); @@ -81150,7 +81271,7 @@ module.exports = function () { /***/ }), -/* 711 */ +/* 713 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81219,7 +81340,7 @@ if ('Set' in global) { /***/ }), -/* 712 */ +/* 714 */ /***/ (function(module, exports, __webpack_require__) { // Approach: @@ -81268,13 +81389,13 @@ var fs = __webpack_require__(23) var rp = __webpack_require__(503) var minimatch = __webpack_require__(505) var Minimatch = minimatch.Minimatch -var inherits = __webpack_require__(713) +var inherits = __webpack_require__(715) var EE = __webpack_require__(379).EventEmitter var path = __webpack_require__(16) var assert = __webpack_require__(30) var isAbsolute = __webpack_require__(511) -var globSync = __webpack_require__(715) -var common = __webpack_require__(716) +var globSync = __webpack_require__(717) +var common = __webpack_require__(718) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -82015,7 +82136,7 @@ Glob.prototype._stat2 = function (f, abs, er, stat, cb) { /***/ }), -/* 713 */ +/* 715 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -82025,12 +82146,12 @@ try { module.exports = util.inherits; } catch (e) { /* istanbul ignore next */ - module.exports = __webpack_require__(714); + module.exports = __webpack_require__(716); } /***/ }), -/* 714 */ +/* 716 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -82063,7 +82184,7 @@ if (typeof Object.create === 'function') { /***/ }), -/* 715 */ +/* 717 */ /***/ (function(module, exports, __webpack_require__) { module.exports = globSync @@ -82073,12 +82194,12 @@ var fs = __webpack_require__(23) var rp = __webpack_require__(503) var minimatch = __webpack_require__(505) var Minimatch = minimatch.Minimatch -var Glob = __webpack_require__(712).Glob +var Glob = __webpack_require__(714).Glob var util = __webpack_require__(29) var path = __webpack_require__(16) var assert = __webpack_require__(30) var isAbsolute = __webpack_require__(511) -var common = __webpack_require__(716) +var common = __webpack_require__(718) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -82555,7 +82676,7 @@ GlobSync.prototype._makeAbs = function (f) { /***/ }), -/* 716 */ +/* 718 */ /***/ (function(module, exports, __webpack_require__) { exports.alphasort = alphasort @@ -82801,10 +82922,10 @@ function childrenIgnored (self, path) { /***/ }), -/* 717 */ +/* 719 */ /***/ (function(module, exports, __webpack_require__) { -const pkg = __webpack_require__(718); +const pkg = __webpack_require__(720); module.exports = pkg.async; module.exports.default = pkg.async; @@ -82817,19 +82938,19 @@ module.exports.generateTasks = pkg.generateTasks; /***/ }), -/* 718 */ +/* 720 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var optionsManager = __webpack_require__(719); -var taskManager = __webpack_require__(720); -var reader_async_1 = __webpack_require__(876); -var reader_stream_1 = __webpack_require__(900); -var reader_sync_1 = __webpack_require__(901); -var arrayUtils = __webpack_require__(903); -var streamUtils = __webpack_require__(904); +var optionsManager = __webpack_require__(721); +var taskManager = __webpack_require__(722); +var reader_async_1 = __webpack_require__(878); +var reader_stream_1 = __webpack_require__(902); +var reader_sync_1 = __webpack_require__(903); +var arrayUtils = __webpack_require__(905); +var streamUtils = __webpack_require__(906); /** * Synchronous API. */ @@ -82895,7 +83016,7 @@ function isString(source) { /***/ }), -/* 719 */ +/* 721 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82933,13 +83054,13 @@ exports.prepare = prepare; /***/ }), -/* 720 */ +/* 722 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var patternUtils = __webpack_require__(721); +var patternUtils = __webpack_require__(723); /** * Generate tasks based on parent directory of each pattern. */ @@ -83030,16 +83151,16 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 721 */ +/* 723 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(16); -var globParent = __webpack_require__(722); -var isGlob = __webpack_require__(725); -var micromatch = __webpack_require__(726); +var globParent = __webpack_require__(724); +var isGlob = __webpack_require__(727); +var micromatch = __webpack_require__(728); var GLOBSTAR = '**'; /** * Return true for static pattern. @@ -83185,15 +83306,15 @@ exports.matchAny = matchAny; /***/ }), -/* 722 */ +/* 724 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var path = __webpack_require__(16); -var isglob = __webpack_require__(723); -var pathDirname = __webpack_require__(724); +var isglob = __webpack_require__(725); +var pathDirname = __webpack_require__(726); var isWin32 = __webpack_require__(11).platform() === 'win32'; module.exports = function globParent(str) { @@ -83216,7 +83337,7 @@ module.exports = function globParent(str) { /***/ }), -/* 723 */ +/* 725 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -83247,7 +83368,7 @@ module.exports = function isGlob(str) { /***/ }), -/* 724 */ +/* 726 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83397,7 +83518,7 @@ module.exports.win32 = win32; /***/ }), -/* 725 */ +/* 727 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -83449,7 +83570,7 @@ module.exports = function isGlob(str, options) { /***/ }), -/* 726 */ +/* 728 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83460,18 +83581,18 @@ module.exports = function isGlob(str, options) { */ var util = __webpack_require__(29); -var braces = __webpack_require__(727); -var toRegex = __webpack_require__(829); -var extend = __webpack_require__(837); +var braces = __webpack_require__(729); +var toRegex = __webpack_require__(831); +var extend = __webpack_require__(839); /** * Local dependencies */ -var compilers = __webpack_require__(840); -var parsers = __webpack_require__(872); -var cache = __webpack_require__(873); -var utils = __webpack_require__(874); +var compilers = __webpack_require__(842); +var parsers = __webpack_require__(874); +var cache = __webpack_require__(875); +var utils = __webpack_require__(876); var MAX_LENGTH = 1024 * 64; /** @@ -84333,7 +84454,7 @@ module.exports = micromatch; /***/ }), -/* 727 */ +/* 729 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84343,18 +84464,18 @@ module.exports = micromatch; * Module dependencies */ -var toRegex = __webpack_require__(728); -var unique = __webpack_require__(740); -var extend = __webpack_require__(737); +var toRegex = __webpack_require__(730); +var unique = __webpack_require__(742); +var extend = __webpack_require__(739); /** * Local dependencies */ -var compilers = __webpack_require__(741); -var parsers = __webpack_require__(756); -var Braces = __webpack_require__(766); -var utils = __webpack_require__(742); +var compilers = __webpack_require__(743); +var parsers = __webpack_require__(758); +var Braces = __webpack_require__(768); +var utils = __webpack_require__(744); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -84658,15 +84779,15 @@ module.exports = braces; /***/ }), -/* 728 */ +/* 730 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(729); -var extend = __webpack_require__(737); -var not = __webpack_require__(739); +var define = __webpack_require__(731); +var extend = __webpack_require__(739); +var not = __webpack_require__(741); var MAX_LENGTH = 1024 * 64; /** @@ -84813,7 +84934,7 @@ module.exports.makeRe = makeRe; /***/ }), -/* 729 */ +/* 731 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84826,7 +84947,7 @@ module.exports.makeRe = makeRe; -var isDescriptor = __webpack_require__(730); +var isDescriptor = __webpack_require__(732); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -84851,7 +84972,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 730 */ +/* 732 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84864,9 +84985,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(731); -var isAccessor = __webpack_require__(732); -var isData = __webpack_require__(735); +var typeOf = __webpack_require__(733); +var isAccessor = __webpack_require__(734); +var isData = __webpack_require__(737); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -84880,7 +85001,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 731 */ +/* 733 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -85033,7 +85154,7 @@ function isBuffer(val) { /***/ }), -/* 732 */ +/* 734 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85046,7 +85167,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(733); +var typeOf = __webpack_require__(735); // accessor descriptor properties var accessor = { @@ -85109,10 +85230,10 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 733 */ +/* 735 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(734); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -85231,7 +85352,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 734 */ +/* 736 */ /***/ (function(module, exports) { /*! @@ -85258,7 +85379,7 @@ function isSlowBuffer (obj) { /***/ }), -/* 735 */ +/* 737 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85271,7 +85392,7 @@ function isSlowBuffer (obj) { -var typeOf = __webpack_require__(736); +var typeOf = __webpack_require__(738); // data descriptor properties var data = { @@ -85320,10 +85441,10 @@ module.exports = isDataDescriptor; /***/ }), -/* 736 */ +/* 738 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(734); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -85442,13 +85563,13 @@ module.exports = function kindOf(val) { /***/ }), -/* 737 */ +/* 739 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(738); +var isObject = __webpack_require__(740); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -85482,7 +85603,7 @@ function hasOwn(obj, key) { /***/ }), -/* 738 */ +/* 740 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85502,13 +85623,13 @@ module.exports = function isExtendable(val) { /***/ }), -/* 739 */ +/* 741 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(737); +var extend = __webpack_require__(739); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -85575,7 +85696,7 @@ module.exports = toRegex; /***/ }), -/* 740 */ +/* 742 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85625,13 +85746,13 @@ module.exports.immutable = function uniqueImmutable(arr) { /***/ }), -/* 741 */ +/* 743 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(742); +var utils = __webpack_require__(744); module.exports = function(braces, options) { braces.compiler @@ -85914,25 +86035,25 @@ function hasQueue(node) { /***/ }), -/* 742 */ +/* 744 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var splitString = __webpack_require__(743); +var splitString = __webpack_require__(745); var utils = module.exports; /** * Module dependencies */ -utils.extend = __webpack_require__(737); -utils.flatten = __webpack_require__(749); -utils.isObject = __webpack_require__(747); -utils.fillRange = __webpack_require__(750); -utils.repeat = __webpack_require__(755); -utils.unique = __webpack_require__(740); +utils.extend = __webpack_require__(739); +utils.flatten = __webpack_require__(751); +utils.isObject = __webpack_require__(749); +utils.fillRange = __webpack_require__(752); +utils.repeat = __webpack_require__(757); +utils.unique = __webpack_require__(742); utils.define = function(obj, key, val) { Object.defineProperty(obj, key, { @@ -86264,7 +86385,7 @@ utils.escapeRegex = function(str) { /***/ }), -/* 743 */ +/* 745 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86277,7 +86398,7 @@ utils.escapeRegex = function(str) { -var extend = __webpack_require__(744); +var extend = __webpack_require__(746); module.exports = function(str, options, fn) { if (typeof str !== 'string') { @@ -86442,14 +86563,14 @@ function keepEscaping(opts, str, idx) { /***/ }), -/* 744 */ +/* 746 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(745); -var assignSymbols = __webpack_require__(748); +var isExtendable = __webpack_require__(747); +var assignSymbols = __webpack_require__(750); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -86509,7 +86630,7 @@ function isEnum(obj, key) { /***/ }), -/* 745 */ +/* 747 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86522,7 +86643,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(746); +var isPlainObject = __webpack_require__(748); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -86530,7 +86651,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 746 */ +/* 748 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86543,7 +86664,7 @@ module.exports = function isExtendable(val) { -var isObject = __webpack_require__(747); +var isObject = __webpack_require__(749); function isObjectObject(o) { return isObject(o) === true @@ -86574,7 +86695,7 @@ module.exports = function isPlainObject(o) { /***/ }), -/* 747 */ +/* 749 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86593,7 +86714,7 @@ module.exports = function isObject(val) { /***/ }), -/* 748 */ +/* 750 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86640,7 +86761,7 @@ module.exports = function(receiver, objects) { /***/ }), -/* 749 */ +/* 751 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86669,7 +86790,7 @@ function flat(arr, res) { /***/ }), -/* 750 */ +/* 752 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86683,10 +86804,10 @@ function flat(arr, res) { var util = __webpack_require__(29); -var isNumber = __webpack_require__(751); -var extend = __webpack_require__(737); -var repeat = __webpack_require__(753); -var toRegex = __webpack_require__(754); +var isNumber = __webpack_require__(753); +var extend = __webpack_require__(739); +var repeat = __webpack_require__(755); +var toRegex = __webpack_require__(756); /** * Return a range of numbers or letters. @@ -86884,7 +87005,7 @@ module.exports = fillRange; /***/ }), -/* 751 */ +/* 753 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86897,7 +87018,7 @@ module.exports = fillRange; -var typeOf = __webpack_require__(752); +var typeOf = __webpack_require__(754); module.exports = function isNumber(num) { var type = typeOf(num); @@ -86913,10 +87034,10 @@ module.exports = function isNumber(num) { /***/ }), -/* 752 */ +/* 754 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(734); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -87035,7 +87156,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 753 */ +/* 755 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87112,7 +87233,7 @@ function repeat(str, num) { /***/ }), -/* 754 */ +/* 756 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87125,8 +87246,8 @@ function repeat(str, num) { -var repeat = __webpack_require__(753); -var isNumber = __webpack_require__(751); +var repeat = __webpack_require__(755); +var isNumber = __webpack_require__(753); var cache = {}; function toRegexRange(min, max, options) { @@ -87413,7 +87534,7 @@ module.exports = toRegexRange; /***/ }), -/* 755 */ +/* 757 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87438,14 +87559,14 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 756 */ +/* 758 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(757); -var utils = __webpack_require__(742); +var Node = __webpack_require__(759); +var utils = __webpack_require__(744); /** * Braces parsers @@ -87805,15 +87926,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 757 */ +/* 759 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(747); -var define = __webpack_require__(758); -var utils = __webpack_require__(765); +var isObject = __webpack_require__(749); +var define = __webpack_require__(760); +var utils = __webpack_require__(767); var ownNames; /** @@ -88304,7 +88425,7 @@ exports = module.exports = Node; /***/ }), -/* 758 */ +/* 760 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -88317,7 +88438,7 @@ exports = module.exports = Node; -var isDescriptor = __webpack_require__(759); +var isDescriptor = __webpack_require__(761); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -88342,7 +88463,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 759 */ +/* 761 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -88355,9 +88476,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(760); -var isAccessor = __webpack_require__(761); -var isData = __webpack_require__(763); +var typeOf = __webpack_require__(762); +var isAccessor = __webpack_require__(763); +var isData = __webpack_require__(765); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -88371,7 +88492,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 760 */ +/* 762 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -88506,7 +88627,7 @@ function isBuffer(val) { /***/ }), -/* 761 */ +/* 763 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -88519,7 +88640,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(762); +var typeOf = __webpack_require__(764); // accessor descriptor properties var accessor = { @@ -88582,7 +88703,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 762 */ +/* 764 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -88717,7 +88838,7 @@ function isBuffer(val) { /***/ }), -/* 763 */ +/* 765 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -88730,7 +88851,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(764); +var typeOf = __webpack_require__(766); module.exports = function isDataDescriptor(obj, prop) { // data descriptor properties @@ -88773,7 +88894,7 @@ module.exports = function isDataDescriptor(obj, prop) { /***/ }), -/* 764 */ +/* 766 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -88908,13 +89029,13 @@ function isBuffer(val) { /***/ }), -/* 765 */ +/* 767 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(752); +var typeOf = __webpack_require__(754); var utils = module.exports; /** @@ -89934,17 +90055,17 @@ function assert(val, message) { /***/ }), -/* 766 */ +/* 768 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(737); -var Snapdragon = __webpack_require__(767); -var compilers = __webpack_require__(741); -var parsers = __webpack_require__(756); -var utils = __webpack_require__(742); +var extend = __webpack_require__(739); +var Snapdragon = __webpack_require__(769); +var compilers = __webpack_require__(743); +var parsers = __webpack_require__(758); +var utils = __webpack_require__(744); /** * Customize Snapdragon parser and renderer @@ -90045,17 +90166,17 @@ module.exports = Braces; /***/ }), -/* 767 */ +/* 769 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Base = __webpack_require__(768); -var define = __webpack_require__(729); -var Compiler = __webpack_require__(797); -var Parser = __webpack_require__(826); -var utils = __webpack_require__(806); +var Base = __webpack_require__(770); +var define = __webpack_require__(731); +var Compiler = __webpack_require__(799); +var Parser = __webpack_require__(828); +var utils = __webpack_require__(808); var regexCache = {}; var cache = {}; @@ -90226,20 +90347,20 @@ module.exports.Parser = Parser; /***/ }), -/* 768 */ +/* 770 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var define = __webpack_require__(769); -var CacheBase = __webpack_require__(770); -var Emitter = __webpack_require__(771); -var isObject = __webpack_require__(747); -var merge = __webpack_require__(788); -var pascal = __webpack_require__(791); -var cu = __webpack_require__(792); +var define = __webpack_require__(771); +var CacheBase = __webpack_require__(772); +var Emitter = __webpack_require__(773); +var isObject = __webpack_require__(749); +var merge = __webpack_require__(790); +var pascal = __webpack_require__(793); +var cu = __webpack_require__(794); /** * Optionally define a custom `cache` namespace to use. @@ -90668,7 +90789,7 @@ module.exports.namespace = namespace; /***/ }), -/* 769 */ +/* 771 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90681,7 +90802,7 @@ module.exports.namespace = namespace; -var isDescriptor = __webpack_require__(759); +var isDescriptor = __webpack_require__(761); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -90706,21 +90827,21 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 770 */ +/* 772 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(747); -var Emitter = __webpack_require__(771); -var visit = __webpack_require__(772); -var toPath = __webpack_require__(775); -var union = __webpack_require__(776); -var del = __webpack_require__(780); -var get = __webpack_require__(778); -var has = __webpack_require__(785); -var set = __webpack_require__(779); +var isObject = __webpack_require__(749); +var Emitter = __webpack_require__(773); +var visit = __webpack_require__(774); +var toPath = __webpack_require__(777); +var union = __webpack_require__(778); +var del = __webpack_require__(782); +var get = __webpack_require__(780); +var has = __webpack_require__(787); +var set = __webpack_require__(781); /** * Create a `Cache` constructor that when instantiated will @@ -90974,7 +91095,7 @@ module.exports.namespace = namespace; /***/ }), -/* 771 */ +/* 773 */ /***/ (function(module, exports, __webpack_require__) { @@ -91143,7 +91264,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 772 */ +/* 774 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91156,8 +91277,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(773); -var mapVisit = __webpack_require__(774); +var visit = __webpack_require__(775); +var mapVisit = __webpack_require__(776); module.exports = function(collection, method, val) { var result; @@ -91180,7 +91301,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 773 */ +/* 775 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91193,7 +91314,7 @@ module.exports = function(collection, method, val) { -var isObject = __webpack_require__(747); +var isObject = __webpack_require__(749); module.exports = function visit(thisArg, method, target, val) { if (!isObject(thisArg) && typeof thisArg !== 'function') { @@ -91220,14 +91341,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 774 */ +/* 776 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var visit = __webpack_require__(773); +var visit = __webpack_require__(775); /** * Map `visit` over an array of objects. @@ -91264,7 +91385,7 @@ function isObject(val) { /***/ }), -/* 775 */ +/* 777 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91277,7 +91398,7 @@ function isObject(val) { -var typeOf = __webpack_require__(752); +var typeOf = __webpack_require__(754); module.exports = function toPath(args) { if (typeOf(args) !== 'arguments') { @@ -91304,16 +91425,16 @@ function filter(arr) { /***/ }), -/* 776 */ +/* 778 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(738); -var union = __webpack_require__(777); -var get = __webpack_require__(778); -var set = __webpack_require__(779); +var isObject = __webpack_require__(740); +var union = __webpack_require__(779); +var get = __webpack_require__(780); +var set = __webpack_require__(781); module.exports = function unionValue(obj, prop, value) { if (!isObject(obj)) { @@ -91341,7 +91462,7 @@ function arrayify(val) { /***/ }), -/* 777 */ +/* 779 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91377,7 +91498,7 @@ module.exports = function union(init) { /***/ }), -/* 778 */ +/* 780 */ /***/ (function(module, exports) { /*! @@ -91433,7 +91554,7 @@ function toString(val) { /***/ }), -/* 779 */ +/* 781 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91446,10 +91567,10 @@ function toString(val) { -var split = __webpack_require__(743); -var extend = __webpack_require__(737); -var isPlainObject = __webpack_require__(746); -var isObject = __webpack_require__(738); +var split = __webpack_require__(745); +var extend = __webpack_require__(739); +var isPlainObject = __webpack_require__(748); +var isObject = __webpack_require__(740); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -91495,7 +91616,7 @@ function isValidKey(key) { /***/ }), -/* 780 */ +/* 782 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91508,8 +91629,8 @@ function isValidKey(key) { -var isObject = __webpack_require__(747); -var has = __webpack_require__(781); +var isObject = __webpack_require__(749); +var has = __webpack_require__(783); module.exports = function unset(obj, prop) { if (!isObject(obj)) { @@ -91534,7 +91655,7 @@ module.exports = function unset(obj, prop) { /***/ }), -/* 781 */ +/* 783 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91547,9 +91668,9 @@ module.exports = function unset(obj, prop) { -var isObject = __webpack_require__(782); -var hasValues = __webpack_require__(784); -var get = __webpack_require__(778); +var isObject = __webpack_require__(784); +var hasValues = __webpack_require__(786); +var get = __webpack_require__(780); module.exports = function(obj, prop, noZero) { if (isObject(obj)) { @@ -91560,7 +91681,7 @@ module.exports = function(obj, prop, noZero) { /***/ }), -/* 782 */ +/* 784 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91573,7 +91694,7 @@ module.exports = function(obj, prop, noZero) { -var isArray = __webpack_require__(783); +var isArray = __webpack_require__(785); module.exports = function isObject(val) { return val != null && typeof val === 'object' && isArray(val) === false; @@ -91581,7 +91702,7 @@ module.exports = function isObject(val) { /***/ }), -/* 783 */ +/* 785 */ /***/ (function(module, exports) { var toString = {}.toString; @@ -91592,7 +91713,7 @@ module.exports = Array.isArray || function (arr) { /***/ }), -/* 784 */ +/* 786 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91635,7 +91756,7 @@ module.exports = function hasValue(o, noZero) { /***/ }), -/* 785 */ +/* 787 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91648,9 +91769,9 @@ module.exports = function hasValue(o, noZero) { -var isObject = __webpack_require__(747); -var hasValues = __webpack_require__(786); -var get = __webpack_require__(778); +var isObject = __webpack_require__(749); +var hasValues = __webpack_require__(788); +var get = __webpack_require__(780); module.exports = function(val, prop) { return hasValues(isObject(val) && prop ? get(val, prop) : val); @@ -91658,7 +91779,7 @@ module.exports = function(val, prop) { /***/ }), -/* 786 */ +/* 788 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91671,8 +91792,8 @@ module.exports = function(val, prop) { -var typeOf = __webpack_require__(787); -var isNumber = __webpack_require__(751); +var typeOf = __webpack_require__(789); +var isNumber = __webpack_require__(753); module.exports = function hasValue(val) { // is-number checks for NaN and other edge cases @@ -91725,10 +91846,10 @@ module.exports = function hasValue(val) { /***/ }), -/* 787 */ +/* 789 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(734); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -91850,14 +91971,14 @@ module.exports = function kindOf(val) { /***/ }), -/* 788 */ +/* 790 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(789); -var forIn = __webpack_require__(790); +var isExtendable = __webpack_require__(791); +var forIn = __webpack_require__(792); function mixinDeep(target, objects) { var len = arguments.length, i = 0; @@ -91921,7 +92042,7 @@ module.exports = mixinDeep; /***/ }), -/* 789 */ +/* 791 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91934,7 +92055,7 @@ module.exports = mixinDeep; -var isPlainObject = __webpack_require__(746); +var isPlainObject = __webpack_require__(748); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -91942,7 +92063,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 790 */ +/* 792 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91965,7 +92086,7 @@ module.exports = function forIn(obj, fn, thisArg) { /***/ }), -/* 791 */ +/* 793 */ /***/ (function(module, exports) { /*! @@ -91992,14 +92113,14 @@ module.exports = pascalcase; /***/ }), -/* 792 */ +/* 794 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var utils = __webpack_require__(793); +var utils = __webpack_require__(795); /** * Expose class utils @@ -92364,7 +92485,7 @@ cu.bubble = function(Parent, events) { /***/ }), -/* 793 */ +/* 795 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92378,10 +92499,10 @@ var utils = {}; * Lazily required module dependencies */ -utils.union = __webpack_require__(777); -utils.define = __webpack_require__(729); -utils.isObj = __webpack_require__(747); -utils.staticExtend = __webpack_require__(794); +utils.union = __webpack_require__(779); +utils.define = __webpack_require__(731); +utils.isObj = __webpack_require__(749); +utils.staticExtend = __webpack_require__(796); /** @@ -92392,7 +92513,7 @@ module.exports = utils; /***/ }), -/* 794 */ +/* 796 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92405,8 +92526,8 @@ module.exports = utils; -var copy = __webpack_require__(795); -var define = __webpack_require__(729); +var copy = __webpack_require__(797); +var define = __webpack_require__(731); var util = __webpack_require__(29); /** @@ -92489,15 +92610,15 @@ module.exports = extend; /***/ }), -/* 795 */ +/* 797 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(752); -var copyDescriptor = __webpack_require__(796); -var define = __webpack_require__(729); +var typeOf = __webpack_require__(754); +var copyDescriptor = __webpack_require__(798); +var define = __webpack_require__(731); /** * Copy static properties, prototype properties, and descriptors from one object to another. @@ -92670,7 +92791,7 @@ module.exports.has = has; /***/ }), -/* 796 */ +/* 798 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92758,16 +92879,16 @@ function isObject(val) { /***/ }), -/* 797 */ +/* 799 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(798); -var define = __webpack_require__(729); -var debug = __webpack_require__(800)('snapdragon:compiler'); -var utils = __webpack_require__(806); +var use = __webpack_require__(800); +var define = __webpack_require__(731); +var debug = __webpack_require__(802)('snapdragon:compiler'); +var utils = __webpack_require__(808); /** * Create a new `Compiler` with the given `options`. @@ -92921,7 +93042,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(825); + var sourcemaps = __webpack_require__(827); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -92942,7 +93063,7 @@ module.exports = Compiler; /***/ }), -/* 798 */ +/* 800 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92955,7 +93076,7 @@ module.exports = Compiler; -var utils = __webpack_require__(799); +var utils = __webpack_require__(801); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -93070,7 +93191,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 799 */ +/* 801 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93084,8 +93205,8 @@ var utils = {}; * Lazily required module dependencies */ -utils.define = __webpack_require__(729); -utils.isObject = __webpack_require__(747); +utils.define = __webpack_require__(731); +utils.isObject = __webpack_require__(749); utils.isString = function(val) { @@ -93100,7 +93221,7 @@ module.exports = utils; /***/ }), -/* 800 */ +/* 802 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -93109,14 +93230,14 @@ module.exports = utils; */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(801); + module.exports = __webpack_require__(803); } else { - module.exports = __webpack_require__(804); + module.exports = __webpack_require__(806); } /***/ }), -/* 801 */ +/* 803 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -93125,7 +93246,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(802); +exports = module.exports = __webpack_require__(804); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -93307,7 +93428,7 @@ function localstorage() { /***/ }), -/* 802 */ +/* 804 */ /***/ (function(module, exports, __webpack_require__) { @@ -93323,7 +93444,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(803); +exports.humanize = __webpack_require__(805); /** * The currently active debug mode names, and names to skip. @@ -93515,7 +93636,7 @@ function coerce(val) { /***/ }), -/* 803 */ +/* 805 */ /***/ (function(module, exports) { /** @@ -93673,7 +93794,7 @@ function plural(ms, n, name) { /***/ }), -/* 804 */ +/* 806 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -93689,7 +93810,7 @@ var util = __webpack_require__(29); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(802); +exports = module.exports = __webpack_require__(804); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -93868,7 +93989,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(805); + var net = __webpack_require__(807); stream = new net.Socket({ fd: fd, readable: false, @@ -93927,13 +94048,13 @@ exports.enable(load()); /***/ }), -/* 805 */ +/* 807 */ /***/ (function(module, exports) { module.exports = require("net"); /***/ }), -/* 806 */ +/* 808 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93943,9 +94064,9 @@ module.exports = require("net"); * Module dependencies */ -exports.extend = __webpack_require__(737); -exports.SourceMap = __webpack_require__(807); -exports.sourceMapResolve = __webpack_require__(818); +exports.extend = __webpack_require__(739); +exports.SourceMap = __webpack_require__(809); +exports.sourceMapResolve = __webpack_require__(820); /** * Convert backslash in the given string to forward slashes @@ -93988,7 +94109,7 @@ exports.last = function(arr, n) { /***/ }), -/* 807 */ +/* 809 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -93996,13 +94117,13 @@ exports.last = function(arr, n) { * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ -exports.SourceMapGenerator = __webpack_require__(808).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(814).SourceMapConsumer; -exports.SourceNode = __webpack_require__(817).SourceNode; +exports.SourceMapGenerator = __webpack_require__(810).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(816).SourceMapConsumer; +exports.SourceNode = __webpack_require__(819).SourceNode; /***/ }), -/* 808 */ +/* 810 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -94012,10 +94133,10 @@ exports.SourceNode = __webpack_require__(817).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(809); -var util = __webpack_require__(811); -var ArraySet = __webpack_require__(812).ArraySet; -var MappingList = __webpack_require__(813).MappingList; +var base64VLQ = __webpack_require__(811); +var util = __webpack_require__(813); +var ArraySet = __webpack_require__(814).ArraySet; +var MappingList = __webpack_require__(815).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -94424,7 +94545,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 809 */ +/* 811 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -94464,7 +94585,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(810); +var base64 = __webpack_require__(812); // A single base 64 digit can contain 6 bits of data. For the base 64 variable // length quantities we use in the source map spec, the first bit is the sign, @@ -94570,7 +94691,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 810 */ +/* 812 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -94643,7 +94764,7 @@ exports.decode = function (charCode) { /***/ }), -/* 811 */ +/* 813 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95066,7 +95187,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 812 */ +/* 814 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95076,7 +95197,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(811); +var util = __webpack_require__(813); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -95193,7 +95314,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 813 */ +/* 815 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95203,7 +95324,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(811); +var util = __webpack_require__(813); /** * Determine whether mappingB is after mappingA with respect to generated @@ -95278,7 +95399,7 @@ exports.MappingList = MappingList; /***/ }), -/* 814 */ +/* 816 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95288,11 +95409,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(811); -var binarySearch = __webpack_require__(815); -var ArraySet = __webpack_require__(812).ArraySet; -var base64VLQ = __webpack_require__(809); -var quickSort = __webpack_require__(816).quickSort; +var util = __webpack_require__(813); +var binarySearch = __webpack_require__(817); +var ArraySet = __webpack_require__(814).ArraySet; +var base64VLQ = __webpack_require__(811); +var quickSort = __webpack_require__(818).quickSort; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -96366,7 +96487,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 815 */ +/* 817 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -96483,7 +96604,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 816 */ +/* 818 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -96603,7 +96724,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 817 */ +/* 819 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -96613,8 +96734,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(808).SourceMapGenerator; -var util = __webpack_require__(811); +var SourceMapGenerator = __webpack_require__(810).SourceMapGenerator; +var util = __webpack_require__(813); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -97022,17 +97143,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 818 */ +/* 820 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(819) -var resolveUrl = __webpack_require__(820) -var decodeUriComponent = __webpack_require__(821) -var urix = __webpack_require__(823) -var atob = __webpack_require__(824) +var sourceMappingURL = __webpack_require__(821) +var resolveUrl = __webpack_require__(822) +var decodeUriComponent = __webpack_require__(823) +var urix = __webpack_require__(825) +var atob = __webpack_require__(826) @@ -97330,7 +97451,7 @@ module.exports = { /***/ }), -/* 819 */ +/* 821 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -97393,7 +97514,7 @@ void (function(root, factory) { /***/ }), -/* 820 */ +/* 822 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -97411,13 +97532,13 @@ module.exports = resolveUrl /***/ }), -/* 821 */ +/* 823 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(822) +var decodeUriComponent = __webpack_require__(824) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -97428,7 +97549,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 822 */ +/* 824 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -97529,7 +97650,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 823 */ +/* 825 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -97552,7 +97673,7 @@ module.exports = urix /***/ }), -/* 824 */ +/* 826 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -97566,7 +97687,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 825 */ +/* 827 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -97574,8 +97695,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(23); var path = __webpack_require__(16); -var define = __webpack_require__(729); -var utils = __webpack_require__(806); +var define = __webpack_require__(731); +var utils = __webpack_require__(808); /** * Expose `mixin()`. @@ -97718,19 +97839,19 @@ exports.comment = function(node) { /***/ }), -/* 826 */ +/* 828 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(798); +var use = __webpack_require__(800); var util = __webpack_require__(29); -var Cache = __webpack_require__(827); -var define = __webpack_require__(729); -var debug = __webpack_require__(800)('snapdragon:parser'); -var Position = __webpack_require__(828); -var utils = __webpack_require__(806); +var Cache = __webpack_require__(829); +var define = __webpack_require__(731); +var debug = __webpack_require__(802)('snapdragon:parser'); +var Position = __webpack_require__(830); +var utils = __webpack_require__(808); /** * Create a new `Parser` with the given `input` and `options`. @@ -98258,7 +98379,7 @@ module.exports = Parser; /***/ }), -/* 827 */ +/* 829 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -98365,13 +98486,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 828 */ +/* 830 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(729); +var define = __webpack_require__(731); /** * Store position for a node @@ -98386,16 +98507,16 @@ module.exports = function Position(start, parser) { /***/ }), -/* 829 */ +/* 831 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var safe = __webpack_require__(830); -var define = __webpack_require__(836); -var extend = __webpack_require__(837); -var not = __webpack_require__(839); +var safe = __webpack_require__(832); +var define = __webpack_require__(838); +var extend = __webpack_require__(839); +var not = __webpack_require__(841); var MAX_LENGTH = 1024 * 64; /** @@ -98548,10 +98669,10 @@ module.exports.makeRe = makeRe; /***/ }), -/* 830 */ +/* 832 */ /***/ (function(module, exports, __webpack_require__) { -var parse = __webpack_require__(831); +var parse = __webpack_require__(833); var types = parse.types; module.exports = function (re, opts) { @@ -98597,13 +98718,13 @@ function isRegExp (x) { /***/ }), -/* 831 */ +/* 833 */ /***/ (function(module, exports, __webpack_require__) { -var util = __webpack_require__(832); -var types = __webpack_require__(833); -var sets = __webpack_require__(834); -var positions = __webpack_require__(835); +var util = __webpack_require__(834); +var types = __webpack_require__(835); +var sets = __webpack_require__(836); +var positions = __webpack_require__(837); module.exports = function(regexpStr) { @@ -98885,11 +99006,11 @@ module.exports.types = types; /***/ }), -/* 832 */ +/* 834 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(833); -var sets = __webpack_require__(834); +var types = __webpack_require__(835); +var sets = __webpack_require__(836); // All of these are private and only used by randexp. @@ -99002,7 +99123,7 @@ exports.error = function(regexp, msg) { /***/ }), -/* 833 */ +/* 835 */ /***/ (function(module, exports) { module.exports = { @@ -99018,10 +99139,10 @@ module.exports = { /***/ }), -/* 834 */ +/* 836 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(833); +var types = __webpack_require__(835); var INTS = function() { return [{ type: types.RANGE , from: 48, to: 57 }]; @@ -99106,10 +99227,10 @@ exports.anyChar = function() { /***/ }), -/* 835 */ +/* 837 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(833); +var types = __webpack_require__(835); exports.wordBoundary = function() { return { type: types.POSITION, value: 'b' }; @@ -99129,7 +99250,7 @@ exports.end = function() { /***/ }), -/* 836 */ +/* 838 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99142,8 +99263,8 @@ exports.end = function() { -var isobject = __webpack_require__(747); -var isDescriptor = __webpack_require__(759); +var isobject = __webpack_require__(749); +var isDescriptor = __webpack_require__(761); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -99174,14 +99295,14 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 837 */ +/* 839 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(838); -var assignSymbols = __webpack_require__(748); +var isExtendable = __webpack_require__(840); +var assignSymbols = __webpack_require__(750); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -99241,7 +99362,7 @@ function isEnum(obj, key) { /***/ }), -/* 838 */ +/* 840 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99254,7 +99375,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(746); +var isPlainObject = __webpack_require__(748); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -99262,14 +99383,14 @@ module.exports = function isExtendable(val) { /***/ }), -/* 839 */ +/* 841 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(837); -var safe = __webpack_require__(830); +var extend = __webpack_require__(839); +var safe = __webpack_require__(832); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -99341,14 +99462,14 @@ module.exports = toRegex; /***/ }), -/* 840 */ +/* 842 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(841); -var extglob = __webpack_require__(856); +var nanomatch = __webpack_require__(843); +var extglob = __webpack_require__(858); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -99425,7 +99546,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 841 */ +/* 843 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99436,17 +99557,17 @@ function escapeExtglobs(compiler) { */ var util = __webpack_require__(29); -var toRegex = __webpack_require__(728); -var extend = __webpack_require__(842); +var toRegex = __webpack_require__(730); +var extend = __webpack_require__(844); /** * Local dependencies */ -var compilers = __webpack_require__(844); -var parsers = __webpack_require__(845); -var cache = __webpack_require__(848); -var utils = __webpack_require__(850); +var compilers = __webpack_require__(846); +var parsers = __webpack_require__(847); +var cache = __webpack_require__(850); +var utils = __webpack_require__(852); var MAX_LENGTH = 1024 * 64; /** @@ -100270,14 +100391,14 @@ module.exports = nanomatch; /***/ }), -/* 842 */ +/* 844 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(843); -var assignSymbols = __webpack_require__(748); +var isExtendable = __webpack_require__(845); +var assignSymbols = __webpack_require__(750); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -100337,7 +100458,7 @@ function isEnum(obj, key) { /***/ }), -/* 843 */ +/* 845 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100350,7 +100471,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(746); +var isPlainObject = __webpack_require__(748); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -100358,7 +100479,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 844 */ +/* 846 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100704,15 +100825,15 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 845 */ +/* 847 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regexNot = __webpack_require__(739); -var toRegex = __webpack_require__(728); -var isOdd = __webpack_require__(846); +var regexNot = __webpack_require__(741); +var toRegex = __webpack_require__(730); +var isOdd = __webpack_require__(848); /** * Characters to use in negation regex (we want to "not" match @@ -101098,7 +101219,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 846 */ +/* 848 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101111,7 +101232,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(847); +var isNumber = __webpack_require__(849); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -101125,7 +101246,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 847 */ +/* 849 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101153,14 +101274,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 848 */ +/* 850 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(849))(); +module.exports = new (__webpack_require__(851))(); /***/ }), -/* 849 */ +/* 851 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101173,7 +101294,7 @@ module.exports = new (__webpack_require__(849))(); -var MapCache = __webpack_require__(827); +var MapCache = __webpack_require__(829); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -101295,7 +101416,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 850 */ +/* 852 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101308,14 +101429,14 @@ var path = __webpack_require__(16); * Module dependencies */ -var isWindows = __webpack_require__(851)(); -var Snapdragon = __webpack_require__(767); -utils.define = __webpack_require__(852); -utils.diff = __webpack_require__(853); -utils.extend = __webpack_require__(842); -utils.pick = __webpack_require__(854); -utils.typeOf = __webpack_require__(855); -utils.unique = __webpack_require__(740); +var isWindows = __webpack_require__(853)(); +var Snapdragon = __webpack_require__(769); +utils.define = __webpack_require__(854); +utils.diff = __webpack_require__(855); +utils.extend = __webpack_require__(844); +utils.pick = __webpack_require__(856); +utils.typeOf = __webpack_require__(857); +utils.unique = __webpack_require__(742); /** * Returns true if the given value is effectively an empty string @@ -101681,7 +101802,7 @@ utils.unixify = function(options) { /***/ }), -/* 851 */ +/* 853 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -101709,7 +101830,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 852 */ +/* 854 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101722,8 +101843,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ -var isobject = __webpack_require__(747); -var isDescriptor = __webpack_require__(759); +var isobject = __webpack_require__(749); +var isDescriptor = __webpack_require__(761); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -101754,7 +101875,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 853 */ +/* 855 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101808,7 +101929,7 @@ function diffArray(one, two) { /***/ }), -/* 854 */ +/* 856 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101821,7 +101942,7 @@ function diffArray(one, two) { -var isObject = __webpack_require__(747); +var isObject = __webpack_require__(749); module.exports = function pick(obj, keys) { if (!isObject(obj) && typeof obj !== 'function') { @@ -101850,7 +101971,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 855 */ +/* 857 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -101985,7 +102106,7 @@ function isBuffer(val) { /***/ }), -/* 856 */ +/* 858 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101995,18 +102116,18 @@ function isBuffer(val) { * Module dependencies */ -var extend = __webpack_require__(737); -var unique = __webpack_require__(740); -var toRegex = __webpack_require__(728); +var extend = __webpack_require__(739); +var unique = __webpack_require__(742); +var toRegex = __webpack_require__(730); /** * Local dependencies */ -var compilers = __webpack_require__(857); -var parsers = __webpack_require__(868); -var Extglob = __webpack_require__(871); -var utils = __webpack_require__(870); +var compilers = __webpack_require__(859); +var parsers = __webpack_require__(870); +var Extglob = __webpack_require__(873); +var utils = __webpack_require__(872); var MAX_LENGTH = 1024 * 64; /** @@ -102323,13 +102444,13 @@ module.exports = extglob; /***/ }), -/* 857 */ +/* 859 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(858); +var brackets = __webpack_require__(860); /** * Extglob compilers @@ -102499,7 +102620,7 @@ module.exports = function(extglob) { /***/ }), -/* 858 */ +/* 860 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102509,17 +102630,17 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(859); -var parsers = __webpack_require__(861); +var compilers = __webpack_require__(861); +var parsers = __webpack_require__(863); /** * Module dependencies */ -var debug = __webpack_require__(863)('expand-brackets'); -var extend = __webpack_require__(737); -var Snapdragon = __webpack_require__(767); -var toRegex = __webpack_require__(728); +var debug = __webpack_require__(865)('expand-brackets'); +var extend = __webpack_require__(739); +var Snapdragon = __webpack_require__(769); +var toRegex = __webpack_require__(730); /** * Parses the given POSIX character class `pattern` and returns a @@ -102717,13 +102838,13 @@ module.exports = brackets; /***/ }), -/* 859 */ +/* 861 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(860); +var posix = __webpack_require__(862); module.exports = function(brackets) { brackets.compiler @@ -102811,7 +102932,7 @@ module.exports = function(brackets) { /***/ }), -/* 860 */ +/* 862 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102840,14 +102961,14 @@ module.exports = { /***/ }), -/* 861 */ +/* 863 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(862); -var define = __webpack_require__(729); +var utils = __webpack_require__(864); +var define = __webpack_require__(731); /** * Text regex @@ -103066,14 +103187,14 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 862 */ +/* 864 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var toRegex = __webpack_require__(728); -var regexNot = __webpack_require__(739); +var toRegex = __webpack_require__(730); +var regexNot = __webpack_require__(741); var cached; /** @@ -103107,7 +103228,7 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 863 */ +/* 865 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -103116,14 +103237,14 @@ exports.createRegex = function(pattern, include) { */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(864); + module.exports = __webpack_require__(866); } else { - module.exports = __webpack_require__(867); + module.exports = __webpack_require__(869); } /***/ }), -/* 864 */ +/* 866 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -103132,7 +103253,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(865); +exports = module.exports = __webpack_require__(867); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -103314,7 +103435,7 @@ function localstorage() { /***/ }), -/* 865 */ +/* 867 */ /***/ (function(module, exports, __webpack_require__) { @@ -103330,7 +103451,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(866); +exports.humanize = __webpack_require__(868); /** * The currently active debug mode names, and names to skip. @@ -103522,7 +103643,7 @@ function coerce(val) { /***/ }), -/* 866 */ +/* 868 */ /***/ (function(module, exports) { /** @@ -103680,7 +103801,7 @@ function plural(ms, n, name) { /***/ }), -/* 867 */ +/* 869 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -103696,7 +103817,7 @@ var util = __webpack_require__(29); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(865); +exports = module.exports = __webpack_require__(867); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -103875,7 +103996,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(805); + var net = __webpack_require__(807); stream = new net.Socket({ fd: fd, readable: false, @@ -103934,15 +104055,15 @@ exports.enable(load()); /***/ }), -/* 868 */ +/* 870 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(858); -var define = __webpack_require__(869); -var utils = __webpack_require__(870); +var brackets = __webpack_require__(860); +var define = __webpack_require__(871); +var utils = __webpack_require__(872); /** * Characters to use in text regex (we want to "not" match @@ -104097,7 +104218,7 @@ module.exports = parsers; /***/ }), -/* 869 */ +/* 871 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104110,7 +104231,7 @@ module.exports = parsers; -var isDescriptor = __webpack_require__(759); +var isDescriptor = __webpack_require__(761); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -104135,14 +104256,14 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 870 */ +/* 872 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regex = __webpack_require__(739); -var Cache = __webpack_require__(849); +var regex = __webpack_require__(741); +var Cache = __webpack_require__(851); /** * Utils @@ -104211,7 +104332,7 @@ utils.createRegex = function(str) { /***/ }), -/* 871 */ +/* 873 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104221,16 +104342,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(767); -var define = __webpack_require__(869); -var extend = __webpack_require__(737); +var Snapdragon = __webpack_require__(769); +var define = __webpack_require__(871); +var extend = __webpack_require__(739); /** * Local dependencies */ -var compilers = __webpack_require__(857); -var parsers = __webpack_require__(868); +var compilers = __webpack_require__(859); +var parsers = __webpack_require__(870); /** * Customize Snapdragon parser and renderer @@ -104296,16 +104417,16 @@ module.exports = Extglob; /***/ }), -/* 872 */ +/* 874 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(856); -var nanomatch = __webpack_require__(841); -var regexNot = __webpack_require__(739); -var toRegex = __webpack_require__(829); +var extglob = __webpack_require__(858); +var nanomatch = __webpack_require__(843); +var regexNot = __webpack_require__(741); +var toRegex = __webpack_require__(831); var not; /** @@ -104386,14 +104507,14 @@ function textRegex(pattern) { /***/ }), -/* 873 */ +/* 875 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(849))(); +module.exports = new (__webpack_require__(851))(); /***/ }), -/* 874 */ +/* 876 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104406,13 +104527,13 @@ var path = __webpack_require__(16); * Module dependencies */ -var Snapdragon = __webpack_require__(767); -utils.define = __webpack_require__(836); -utils.diff = __webpack_require__(853); -utils.extend = __webpack_require__(837); -utils.pick = __webpack_require__(854); -utils.typeOf = __webpack_require__(875); -utils.unique = __webpack_require__(740); +var Snapdragon = __webpack_require__(769); +utils.define = __webpack_require__(838); +utils.diff = __webpack_require__(855); +utils.extend = __webpack_require__(839); +utils.pick = __webpack_require__(856); +utils.typeOf = __webpack_require__(877); +utils.unique = __webpack_require__(742); /** * Returns true if the platform is windows, or `path.sep` is `\\`. @@ -104709,7 +104830,7 @@ utils.unixify = function(options) { /***/ }), -/* 875 */ +/* 877 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -104844,7 +104965,7 @@ function isBuffer(val) { /***/ }), -/* 876 */ +/* 878 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104863,9 +104984,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(877); -var reader_1 = __webpack_require__(890); -var fs_stream_1 = __webpack_require__(894); +var readdir = __webpack_require__(879); +var reader_1 = __webpack_require__(892); +var fs_stream_1 = __webpack_require__(896); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -104926,15 +105047,15 @@ exports.default = ReaderAsync; /***/ }), -/* 877 */ +/* 879 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(878); -const readdirAsync = __webpack_require__(886); -const readdirStream = __webpack_require__(889); +const readdirSync = __webpack_require__(880); +const readdirAsync = __webpack_require__(888); +const readdirStream = __webpack_require__(891); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -105018,7 +105139,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 878 */ +/* 880 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105026,11 +105147,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(879); +const DirectoryReader = __webpack_require__(881); let syncFacade = { - fs: __webpack_require__(884), - forEach: __webpack_require__(885), + fs: __webpack_require__(886), + forEach: __webpack_require__(887), sync: true }; @@ -105059,7 +105180,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 879 */ +/* 881 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105068,9 +105189,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(27).Readable; const EventEmitter = __webpack_require__(379).EventEmitter; const path = __webpack_require__(16); -const normalizeOptions = __webpack_require__(880); -const stat = __webpack_require__(882); -const call = __webpack_require__(883); +const normalizeOptions = __webpack_require__(882); +const stat = __webpack_require__(884); +const call = __webpack_require__(885); /** * Asynchronously reads the contents of a directory and streams the results @@ -105446,14 +105567,14 @@ module.exports = DirectoryReader; /***/ }), -/* 880 */ +/* 882 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const globToRegExp = __webpack_require__(881); +const globToRegExp = __webpack_require__(883); module.exports = normalizeOptions; @@ -105630,7 +105751,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 881 */ +/* 883 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -105767,13 +105888,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 882 */ +/* 884 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(883); +const call = __webpack_require__(885); module.exports = stat; @@ -105848,7 +105969,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 883 */ +/* 885 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105909,14 +106030,14 @@ function callOnce (fn) { /***/ }), -/* 884 */ +/* 886 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const call = __webpack_require__(883); +const call = __webpack_require__(885); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -105980,7 +106101,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 885 */ +/* 887 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106009,7 +106130,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 886 */ +/* 888 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106017,12 +106138,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(887); -const DirectoryReader = __webpack_require__(879); +const maybe = __webpack_require__(889); +const DirectoryReader = __webpack_require__(881); let asyncFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(888), + forEach: __webpack_require__(890), async: true }; @@ -106064,7 +106185,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 887 */ +/* 889 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106091,7 +106212,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 888 */ +/* 890 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106127,7 +106248,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 889 */ +/* 891 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106135,11 +106256,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(879); +const DirectoryReader = __webpack_require__(881); let streamFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(888), + forEach: __webpack_require__(890), async: true }; @@ -106159,16 +106280,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 890 */ +/* 892 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(16); -var deep_1 = __webpack_require__(891); -var entry_1 = __webpack_require__(893); -var pathUtil = __webpack_require__(892); +var deep_1 = __webpack_require__(893); +var entry_1 = __webpack_require__(895); +var pathUtil = __webpack_require__(894); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -106234,14 +106355,14 @@ exports.default = Reader; /***/ }), -/* 891 */ +/* 893 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(892); -var patternUtils = __webpack_require__(721); +var pathUtils = __webpack_require__(894); +var patternUtils = __webpack_require__(723); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { this.options = options; @@ -106324,7 +106445,7 @@ exports.default = DeepFilter; /***/ }), -/* 892 */ +/* 894 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106355,14 +106476,14 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 893 */ +/* 895 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(892); -var patternUtils = __webpack_require__(721); +var pathUtils = __webpack_require__(894); +var patternUtils = __webpack_require__(723); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { this.options = options; @@ -106447,7 +106568,7 @@ exports.default = EntryFilter; /***/ }), -/* 894 */ +/* 896 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106467,8 +106588,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var fsStat = __webpack_require__(895); -var fs_1 = __webpack_require__(899); +var fsStat = __webpack_require__(897); +var fs_1 = __webpack_require__(901); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -106518,14 +106639,14 @@ exports.default = FileSystemStream; /***/ }), -/* 895 */ +/* 897 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(896); -const statProvider = __webpack_require__(898); +const optionsManager = __webpack_require__(898); +const statProvider = __webpack_require__(900); /** * Asynchronous API. */ @@ -106556,13 +106677,13 @@ exports.statSync = statSync; /***/ }), -/* 896 */ +/* 898 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(897); +const fsAdapter = __webpack_require__(899); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -106575,7 +106696,7 @@ exports.prepare = prepare; /***/ }), -/* 897 */ +/* 899 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106598,7 +106719,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 898 */ +/* 900 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106650,7 +106771,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 899 */ +/* 901 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106681,7 +106802,7 @@ exports.default = FileSystem; /***/ }), -/* 900 */ +/* 902 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106701,9 +106822,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var readdir = __webpack_require__(877); -var reader_1 = __webpack_require__(890); -var fs_stream_1 = __webpack_require__(894); +var readdir = __webpack_require__(879); +var reader_1 = __webpack_require__(892); +var fs_stream_1 = __webpack_require__(896); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -106771,7 +106892,7 @@ exports.default = ReaderStream; /***/ }), -/* 901 */ +/* 903 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106790,9 +106911,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(877); -var reader_1 = __webpack_require__(890); -var fs_sync_1 = __webpack_require__(902); +var readdir = __webpack_require__(879); +var reader_1 = __webpack_require__(892); +var fs_sync_1 = __webpack_require__(904); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -106852,7 +106973,7 @@ exports.default = ReaderSync; /***/ }), -/* 902 */ +/* 904 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106871,8 +106992,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(895); -var fs_1 = __webpack_require__(899); +var fsStat = __webpack_require__(897); +var fs_1 = __webpack_require__(901); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -106918,7 +107039,7 @@ exports.default = FileSystemSync; /***/ }), -/* 903 */ +/* 905 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106934,7 +107055,7 @@ exports.flatten = flatten; /***/ }), -/* 904 */ +/* 906 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106955,13 +107076,13 @@ exports.merge = merge; /***/ }), -/* 905 */ +/* 907 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const pathType = __webpack_require__(906); +const pathType = __webpack_require__(908); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -107027,13 +107148,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 906 */ +/* 908 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const pify = __webpack_require__(907); +const pify = __webpack_require__(909); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -107076,7 +107197,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 907 */ +/* 909 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -107167,17 +107288,17 @@ module.exports = (obj, opts) => { /***/ }), -/* 908 */ +/* 910 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); const path = __webpack_require__(16); -const fastGlob = __webpack_require__(717); -const gitIgnore = __webpack_require__(909); -const pify = __webpack_require__(910); -const slash = __webpack_require__(911); +const fastGlob = __webpack_require__(719); +const gitIgnore = __webpack_require__(911); +const pify = __webpack_require__(912); +const slash = __webpack_require__(913); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -107275,7 +107396,7 @@ module.exports.sync = options => { /***/ }), -/* 909 */ +/* 911 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -107744,7 +107865,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 910 */ +/* 912 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -107819,7 +107940,7 @@ module.exports = (input, options) => { /***/ }), -/* 911 */ +/* 913 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -107837,67 +107958,74 @@ module.exports = input => { /***/ }), -/* 912 */ +/* 914 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); const {constants: fsConstants} = __webpack_require__(23); -const {Buffer} = __webpack_require__(913); -const CpFileError = __webpack_require__(914); -const fs = __webpack_require__(918); -const ProgressEmitter = __webpack_require__(920); +const pEvent = __webpack_require__(915); +const CpFileError = __webpack_require__(918); +const fs = __webpack_require__(922); +const ProgressEmitter = __webpack_require__(925); + +const cpFileAsync = async (source, destination, options, progressEmitter) => { + let readError; + const stat = await fs.stat(source); + progressEmitter.size = stat.size; + + const read = await fs.createReadStream(source); + await fs.makeDir(path.dirname(destination)); + const write = fs.createWriteStream(destination, {flags: options.overwrite ? 'w' : 'wx'}); + read.on('data', () => { + progressEmitter.written = write.bytesWritten; + }); + read.once('error', error => { + readError = new CpFileError(`Cannot read from \`${source}\`: ${error.message}`, error); + write.end(); + }); -const cpFile = (source, destination, options) => { - if (!source || !destination) { - return Promise.reject(new CpFileError('`source` and `destination` required')); + let updateStats = false; + try { + const writePromise = pEvent(write, 'close'); + read.pipe(write); + await writePromise; + progressEmitter.written = progressEmitter.size; + updateStats = true; + } catch (error) { + if (options.overwrite || error.code !== 'EEXIST') { + throw new CpFileError(`Cannot write to \`${destination}\`: ${error.message}`, error); + } } - options = Object.assign({overwrite: true}, options); - - const progressEmitter = new ProgressEmitter(path.resolve(source), path.resolve(destination)); - - const promise = fs - .stat(source) - .then(stat => { - progressEmitter.size = stat.size; - }) - .then(() => fs.createReadStream(source)) - .then(read => fs.makeDir(path.dirname(destination)).then(() => read)) - .then(read => new Promise((resolve, reject) => { - const write = fs.createWriteStream(destination, {flags: options.overwrite ? 'w' : 'wx'}); - - read.on('data', () => { - progressEmitter.written = write.bytesWritten; - }); + if (readError) { + throw readError; + } - write.on('error', error => { - if (!options.overwrite && error.code === 'EEXIST') { - resolve(false); - return; - } + if (updateStats) { + const stats = await fs.lstat(source); - reject(new CpFileError(`Cannot write to \`${destination}\`: ${error.message}`, error)); - }); + return Promise.all([ + fs.utimes(destination, stats.atime, stats.mtime), + fs.chmod(destination, stats.mode), + fs.chown(destination, stats.uid, stats.gid) + ]); + } +}; - write.on('close', () => { - progressEmitter.written = progressEmitter.size; - resolve(true); - }); +const cpFile = (source, destination, options) => { + if (!source || !destination) { + return Promise.reject(new CpFileError('`source` and `destination` required')); + } - read.pipe(write); - })) - .then(updateStats => { - if (updateStats) { - return fs.lstat(source).then(stats => Promise.all([ - fs.utimes(destination, stats.atime, stats.mtime), - fs.chmod(destination, stats.mode), - fs.chown(destination, stats.uid, stats.gid) - ])); - } - }); + options = { + overwrite: true, + ...options + }; + const progressEmitter = new ProgressEmitter(path.resolve(source), path.resolve(destination)); + const promise = cpFileAsync(source, destination, options, progressEmitter); promise.on = (...args) => { progressEmitter.on(...args); return promise; @@ -107907,8 +108035,6 @@ const cpFile = (source, destination, options) => { }; module.exports = cpFile; -// TODO: Remove this for the next major release -module.exports.default = cpFile; const checkSourceIsFile = (stat, source) => { if (stat.isDirectory()) { @@ -107925,7 +108051,16 @@ const fixupAttributes = (destination, stat) => { fs.chownSync(destination, stat.uid, stat.gid); }; -const copySyncNative = (source, destination, options) => { +module.exports.sync = (source, destination, options) => { + if (!source || !destination) { + throw new CpFileError('`source` and `destination` required'); + } + + options = { + overwrite: true, + ...options + }; + const stat = fs.statSync(source); checkSourceIsFile(stat, source); fs.makeDirSync(path.dirname(destination)); @@ -107945,136 +108080,383 @@ const copySyncNative = (source, destination, options) => { fixupAttributes(destination, stat); }; -const copySyncFallback = (source, destination, options) => { - let bytesRead; - let position; - let read; // eslint-disable-line prefer-const - let write; - const BUF_LENGTH = 100 * 1024; - const buffer = Buffer.alloc(BUF_LENGTH); - const readSync = position => fs.readSync(read, buffer, 0, BUF_LENGTH, position, source); - const writeSync = () => fs.writeSync(write, buffer, 0, bytesRead, undefined, destination); - read = fs.openSync(source, 'r'); - bytesRead = readSync(0); - position = bytesRead; - fs.makeDirSync(path.dirname(destination)); +/***/ }), +/* 915 */ +/***/ (function(module, exports, __webpack_require__) { - try { - write = fs.openSync(destination, options.overwrite ? 'w' : 'wx'); - } catch (error) { - if (!options.overwrite && error.code === 'EEXIST') { - return; +"use strict"; + +const pTimeout = __webpack_require__(916); + +const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; + +const normalizeEmitter = emitter => { + const addListener = emitter.on || emitter.addListener || emitter.addEventListener; + const removeListener = emitter.off || emitter.removeListener || emitter.removeEventListener; + + if (!addListener || !removeListener) { + throw new TypeError('Emitter is not compatible'); + } + + return { + addListener: addListener.bind(emitter), + removeListener: removeListener.bind(emitter) + }; +}; + +const normalizeEvents = event => Array.isArray(event) ? event : [event]; + +const multiple = (emitter, event, options) => { + let cancel; + const ret = new Promise((resolve, reject) => { + options = { + rejectionEvents: ['error'], + multiArgs: false, + resolveImmediately: false, + ...options + }; + + if (!(options.count >= 0 && (options.count === Infinity || Number.isInteger(options.count)))) { + throw new TypeError('The `count` option should be at least 0 or more'); } - throw error; + // Allow multiple events + const events = normalizeEvents(event); + + const items = []; + const {addListener, removeListener} = normalizeEmitter(emitter); + + const onItem = (...args) => { + const value = options.multiArgs ? args : args[0]; + + if (options.filter && !options.filter(value)) { + return; + } + + items.push(value); + + if (options.count === items.length) { + cancel(); + resolve(items); + } + }; + + const rejectHandler = error => { + cancel(); + reject(error); + }; + + cancel = () => { + for (const event of events) { + removeListener(event, onItem); + } + + for (const rejectionEvent of options.rejectionEvents) { + removeListener(rejectionEvent, rejectHandler); + } + }; + + for (const event of events) { + addListener(event, onItem); + } + + for (const rejectionEvent of options.rejectionEvents) { + addListener(rejectionEvent, rejectHandler); + } + + if (options.resolveImmediately) { + resolve(items); + } + }); + + ret.cancel = cancel; + + if (typeof options.timeout === 'number') { + const timeout = pTimeout(ret, options.timeout); + timeout.cancel = cancel; + return timeout; } - writeSync(); + return ret; +}; - while (bytesRead === BUF_LENGTH) { - bytesRead = readSync(position); - writeSync(); - position += bytesRead; +const pEvent = (emitter, event, options) => { + if (typeof options === 'function') { + options = {filter: options}; } - const stat = fs.fstatSync(read, source); - fs.futimesSync(write, stat.atime, stat.mtime, destination); - fs.closeSync(read); - fs.closeSync(write); - fixupAttributes(destination, stat); + options = { + ...options, + count: 1, + resolveImmediately: false + }; + + const arrayPromise = multiple(emitter, event, options); + const promise = arrayPromise.then(array => array[0]); // eslint-disable-line promise/prefer-await-to-then + promise.cancel = arrayPromise.cancel; + + return promise; }; -module.exports.sync = (source, destination, options) => { - if (!source || !destination) { - throw new CpFileError('`source` and `destination` required'); +module.exports = pEvent; +// TODO: Remove this for the next major release +module.exports.default = pEvent; + +module.exports.multiple = multiple; + +module.exports.iterator = (emitter, event, options) => { + if (typeof options === 'function') { + options = {filter: options}; } - options = Object.assign({overwrite: true}, options); + // Allow multiple events + const events = normalizeEvents(event); - if (fs.copyFileSync) { - copySyncNative(source, destination, options); - } else { - copySyncFallback(source, destination, options); + options = { + rejectionEvents: ['error'], + resolutionEvents: [], + limit: Infinity, + multiArgs: false, + ...options + }; + + const {limit} = options; + const isValidLimit = limit >= 0 && (limit === Infinity || Number.isInteger(limit)); + if (!isValidLimit) { + throw new TypeError('The `limit` option should be a non-negative integer or Infinity'); + } + + if (limit === 0) { + // Return an empty async iterator to avoid any further cost + return { + [Symbol.asyncIterator]() { + return this; + }, + async next() { + return { + done: true, + value: undefined + }; + } + }; + } + + const {addListener, removeListener} = normalizeEmitter(emitter); + + let isDone = false; + let error; + let hasPendingError = false; + const nextQueue = []; + const valueQueue = []; + let eventCount = 0; + let isLimitReached = false; + + const valueHandler = (...args) => { + eventCount++; + isLimitReached = eventCount === limit; + + const value = options.multiArgs ? args : args[0]; + + if (nextQueue.length > 0) { + const {resolve} = nextQueue.shift(); + + resolve({done: false, value}); + + if (isLimitReached) { + cancel(); + } + + return; + } + + valueQueue.push(value); + + if (isLimitReached) { + cancel(); + } + }; + + const cancel = () => { + isDone = true; + for (const event of events) { + removeListener(event, valueHandler); + } + + for (const rejectionEvent of options.rejectionEvents) { + removeListener(rejectionEvent, rejectHandler); + } + + for (const resolutionEvent of options.resolutionEvents) { + removeListener(resolutionEvent, resolveHandler); + } + + while (nextQueue.length > 0) { + const {resolve} = nextQueue.shift(); + resolve({done: true, value: undefined}); + } + }; + + const rejectHandler = (...args) => { + error = options.multiArgs ? args : args[0]; + + if (nextQueue.length > 0) { + const {reject} = nextQueue.shift(); + reject(error); + } else { + hasPendingError = true; + } + + cancel(); + }; + + const resolveHandler = (...args) => { + const value = options.multiArgs ? args : args[0]; + + if (options.filter && !options.filter(value)) { + return; + } + + if (nextQueue.length > 0) { + const {resolve} = nextQueue.shift(); + resolve({done: true, value}); + } else { + valueQueue.push(value); + } + + cancel(); + }; + + for (const event of events) { + addListener(event, valueHandler); + } + + for (const rejectionEvent of options.rejectionEvents) { + addListener(rejectionEvent, rejectHandler); + } + + for (const resolutionEvent of options.resolutionEvents) { + addListener(resolutionEvent, resolveHandler); } + + return { + [symbolAsyncIterator]() { + return this; + }, + async next() { + if (valueQueue.length > 0) { + const value = valueQueue.shift(); + return { + done: isDone && valueQueue.length === 0 && !isLimitReached, + value + }; + } + + if (hasPendingError) { + hasPendingError = false; + throw error; + } + + if (isDone) { + return { + done: true, + value: undefined + }; + } + + return new Promise((resolve, reject) => nextQueue.push({resolve, reject})); + }, + async return(value) { + cancel(); + return { + done: isDone, + value + }; + } + }; }; /***/ }), -/* 913 */ +/* 916 */ /***/ (function(module, exports, __webpack_require__) { -/* eslint-disable node/no-deprecated-api */ -var buffer = __webpack_require__(585) -var Buffer = buffer.Buffer +"use strict"; -// alternative to using Object.keys for old browsers -function copyProps (src, dst) { - for (var key in src) { - dst[key] = src[key] - } -} -if (Buffer.from && Buffer.alloc && Buffer.allocUnsafe && Buffer.allocUnsafeSlow) { - module.exports = buffer -} else { - // Copy properties from require('buffer') - copyProps(buffer, exports) - exports.Buffer = SafeBuffer -} +const pFinally = __webpack_require__(917); -function SafeBuffer (arg, encodingOrOffset, length) { - return Buffer(arg, encodingOrOffset, length) +class TimeoutError extends Error { + constructor(message) { + super(message); + this.name = 'TimeoutError'; + } } -// Copy static methods from Buffer -copyProps(Buffer, SafeBuffer) +module.exports = (promise, ms, fallback) => new Promise((resolve, reject) => { + if (typeof ms !== 'number' || ms < 0) { + throw new TypeError('Expected `ms` to be a positive number'); + } -SafeBuffer.from = function (arg, encodingOrOffset, length) { - if (typeof arg === 'number') { - throw new TypeError('Argument must not be a number') - } - return Buffer(arg, encodingOrOffset, length) -} + const timer = setTimeout(() => { + if (typeof fallback === 'function') { + try { + resolve(fallback()); + } catch (err) { + reject(err); + } + return; + } -SafeBuffer.alloc = function (size, fill, encoding) { - if (typeof size !== 'number') { - throw new TypeError('Argument must be a number') - } - var buf = Buffer(size) - if (fill !== undefined) { - if (typeof encoding === 'string') { - buf.fill(fill, encoding) - } else { - buf.fill(fill) - } - } else { - buf.fill(0) - } - return buf -} + const message = typeof fallback === 'string' ? fallback : `Promise timed out after ${ms} milliseconds`; + const err = fallback instanceof Error ? fallback : new TimeoutError(message); -SafeBuffer.allocUnsafe = function (size) { - if (typeof size !== 'number') { - throw new TypeError('Argument must be a number') - } - return Buffer(size) -} + if (typeof promise.cancel === 'function') { + promise.cancel(); + } -SafeBuffer.allocUnsafeSlow = function (size) { - if (typeof size !== 'number') { - throw new TypeError('Argument must be a number') - } - return buffer.SlowBuffer(size) -} + reject(err); + }, ms); + + pFinally( + promise.then(resolve, reject), + () => { + clearTimeout(timer); + } + ); +}); + +module.exports.TimeoutError = TimeoutError; /***/ }), -/* 914 */ +/* 917 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(915); +module.exports = (promise, onFinally) => { + onFinally = onFinally || (() => {}); + + return promise.then( + val => new Promise(resolve => { + resolve(onFinally()); + }).then(() => val), + err => new Promise(resolve => { + resolve(onFinally()); + }).then(() => { + throw err; + }) + ); +}; + + +/***/ }), +/* 918 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +const NestedError = __webpack_require__(919); class CpFileError extends NestedError { constructor(message, nested) { @@ -108088,10 +108470,10 @@ module.exports = CpFileError; /***/ }), -/* 915 */ +/* 919 */ /***/ (function(module, exports, __webpack_require__) { -var inherits = __webpack_require__(916); +var inherits = __webpack_require__(920); var NestedError = function (message, nested) { this.nested = nested; @@ -108142,7 +108524,7 @@ module.exports = NestedError; /***/ }), -/* 916 */ +/* 920 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -108150,12 +108532,12 @@ try { if (typeof util.inherits !== 'function') throw ''; module.exports = util.inherits; } catch (e) { - module.exports = __webpack_require__(917); + module.exports = __webpack_require__(921); } /***/ }), -/* 917 */ +/* 921 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -108184,87 +108566,58 @@ if (typeof Object.create === 'function') { /***/ }), -/* 918 */ +/* 922 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; +const {promisify} = __webpack_require__(29); const fs = __webpack_require__(22); -const makeDir = __webpack_require__(559); -const pify = __webpack_require__(919); -const CpFileError = __webpack_require__(914); +const makeDir = __webpack_require__(923); +const pEvent = __webpack_require__(915); +const CpFileError = __webpack_require__(918); -const fsP = pify(fs); +const stat = promisify(fs.stat); +const lstat = promisify(fs.lstat); +const utimes = promisify(fs.utimes); +const chmod = promisify(fs.chmod); +const chown = promisify(fs.chown); exports.closeSync = fs.closeSync.bind(fs); exports.createWriteStream = fs.createWriteStream.bind(fs); -exports.createReadStream = (path, options) => new Promise((resolve, reject) => { +exports.createReadStream = async (path, options) => { const read = fs.createReadStream(path, options); - read.once('error', error => { - reject(new CpFileError(`Cannot read from \`${path}\`: ${error.message}`, error)); - }); - - read.once('readable', () => { - resolve(read); - }); + try { + await pEvent(read, ['readable', 'end']); + } catch (error) { + throw new CpFileError(`Cannot read from \`${path}\`: ${error.message}`, error); + } - read.once('end', () => { - resolve(read); - }); -}); + return read; +}; -exports.stat = path => fsP.stat(path).catch(error => { +exports.stat = path => stat(path).catch(error => { throw new CpFileError(`Cannot stat path \`${path}\`: ${error.message}`, error); }); -exports.lstat = path => fsP.lstat(path).catch(error => { +exports.lstat = path => lstat(path).catch(error => { throw new CpFileError(`lstat \`${path}\` failed: ${error.message}`, error); }); -exports.utimes = (path, atime, mtime) => fsP.utimes(path, atime, mtime).catch(error => { +exports.utimes = (path, atime, mtime) => utimes(path, atime, mtime).catch(error => { throw new CpFileError(`utimes \`${path}\` failed: ${error.message}`, error); }); -exports.chmod = (path, mode) => fsP.chmod(path, mode).catch(error => { +exports.chmod = (path, mode) => chmod(path, mode).catch(error => { throw new CpFileError(`chmod \`${path}\` failed: ${error.message}`, error); }); -exports.chown = (path, uid, gid) => fsP.chown(path, uid, gid).catch(error => { +exports.chown = (path, uid, gid) => chown(path, uid, gid).catch(error => { throw new CpFileError(`chown \`${path}\` failed: ${error.message}`, error); }); -exports.openSync = (path, flags, mode) => { - try { - return fs.openSync(path, flags, mode); - } catch (error) { - if (flags.includes('w')) { - throw new CpFileError(`Cannot write to \`${path}\`: ${error.message}`, error); - } - - throw new CpFileError(`Cannot open \`${path}\`: ${error.message}`, error); - } -}; - -// eslint-disable-next-line max-params -exports.readSync = (fileDescriptor, buffer, offset, length, position, path) => { - try { - return fs.readSync(fileDescriptor, buffer, offset, length, position); - } catch (error) { - throw new CpFileError(`Cannot read from \`${path}\`: ${error.message}`, error); - } -}; - -// eslint-disable-next-line max-params -exports.writeSync = (fileDescriptor, buffer, offset, length, position, path) => { - try { - return fs.writeSync(fileDescriptor, buffer, offset, length, position); - } catch (error) { - throw new CpFileError(`Cannot write to \`${path}\`: ${error.message}`, error); - } -}; - exports.statSync = path => { try { return fs.statSync(path); @@ -108273,22 +108626,6 @@ exports.statSync = path => { } }; -exports.fstatSync = (fileDescriptor, path) => { - try { - return fs.fstatSync(fileDescriptor); - } catch (error) { - throw new CpFileError(`fstat \`${path}\` failed: ${error.message}`, error); - } -}; - -exports.futimesSync = (fileDescriptor, atime, mtime, path) => { - try { - return fs.futimesSync(fileDescriptor, atime, mtime, path); - } catch (error) { - throw new CpFileError(`futimes \`${path}\` failed: ${error.message}`, error); - } -}; - exports.utimesSync = (path, atime, mtime) => { try { return fs.utimesSync(path, atime, mtime); @@ -108325,210 +108662,1938 @@ exports.makeDirSync = path => { } }; -if (fs.copyFileSync) { - exports.copyFileSync = (source, destination, flags) => { - try { - fs.copyFileSync(source, destination, flags); - } catch (error) { - throw new CpFileError(`Cannot copy from \`${source}\` to \`${destination}\`: ${error.message}`, error); - } - }; -} +exports.copyFileSync = (source, destination, flags) => { + try { + fs.copyFileSync(source, destination, flags); + } catch (error) { + throw new CpFileError(`Cannot copy from \`${source}\` to \`${destination}\`: ${error.message}`, error); + } +}; /***/ }), -/* 919 */ +/* 923 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; +const fs = __webpack_require__(23); +const path = __webpack_require__(16); +const {promisify} = __webpack_require__(29); +const semver = __webpack_require__(924); -const processFn = (fn, options) => function (...args) { - const P = options.promiseModule; +const defaults = { + mode: 0o777 & (~process.umask()), + fs +}; - return new P((resolve, reject) => { - if (options.multiArgs) { - args.push((...result) => { - if (options.errorFirst) { - if (result[0]) { - reject(result); - } else { - result.shift(); - resolve(result); - } - } else { - resolve(result); - } - }); - } else if (options.errorFirst) { - args.push((error, result) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }); - } else { - args.push(resolve); +const useNativeRecursiveOption = semver.satisfies(process.version, '>=10.12.0'); + +// https://github.com/nodejs/node/issues/8987 +// https://github.com/libuv/libuv/pull/1088 +const checkPath = pth => { + if (process.platform === 'win32') { + const pathHasInvalidWinCharacters = /[<>:"|?*]/.test(pth.replace(path.parse(pth).root, '')); + + if (pathHasInvalidWinCharacters) { + const error = new Error(`Path contains invalid characters: ${pth}`); + error.code = 'EINVAL'; + throw error; } + } +}; - fn.apply(this, args); - }); +const permissionError = pth => { + // This replicates the exception of `fs.mkdir` with native the + // `recusive` option when run on an invalid drive under Windows. + const error = new Error(`operation not permitted, mkdir '${pth}'`); + error.code = 'EPERM'; + error.errno = -4048; + error.path = pth; + error.syscall = 'mkdir'; + return error; }; -module.exports = (input, options) => { - options = Object.assign({ - exclude: [/.+(Sync|Stream)$/], - errorFirst: true, - promiseModule: Promise - }, options); +const makeDir = async (input, options) => { + checkPath(input); + options = { + ...defaults, + ...options + }; - const objType = typeof input; - if (!(input !== null && (objType === 'object' || objType === 'function'))) { - throw new TypeError(`Expected \`input\` to be a \`Function\` or \`Object\`, got \`${input === null ? 'null' : objType}\``); + const mkdir = promisify(options.fs.mkdir); + const stat = promisify(options.fs.stat); + + if (useNativeRecursiveOption && options.fs.mkdir === fs.mkdir) { + const pth = path.resolve(input); + + await mkdir(pth, { + mode: options.mode, + recursive: true + }); + + return pth; } - const filter = key => { - const match = pattern => typeof pattern === 'string' ? key === pattern : pattern.test(key); - return options.include ? options.include.some(match) : !options.exclude.some(match); + const make = async pth => { + try { + await mkdir(pth, options.mode); + + return pth; + } catch (error) { + if (error.code === 'EPERM') { + throw error; + } + + if (error.code === 'ENOENT') { + if (path.dirname(pth) === pth) { + throw permissionError(pth); + } + + if (error.message.includes('null bytes')) { + throw error; + } + + await make(path.dirname(pth)); + + return make(pth); + } + + const stats = await stat(pth); + if (!stats.isDirectory()) { + throw error; + } + + return pth; + } }; - let ret; - if (objType === 'function') { - ret = function (...args) { - return options.excludeMain ? input(...args) : processFn(input, options).apply(this, args); - }; - } else { - ret = Object.create(Object.getPrototypeOf(input)); - } + return make(path.resolve(input)); +}; - for (const key in input) { // eslint-disable-line guard-for-in - const property = input[key]; - ret[key] = typeof property === 'function' && filter(key) ? processFn(property, options) : property; +module.exports = makeDir; + +module.exports.sync = (input, options) => { + checkPath(input); + options = { + ...defaults, + ...options + }; + + if (useNativeRecursiveOption && options.fs.mkdirSync === fs.mkdirSync) { + const pth = path.resolve(input); + + fs.mkdirSync(pth, { + mode: options.mode, + recursive: true + }); + + return pth; } - return ret; + const make = pth => { + try { + options.fs.mkdirSync(pth, options.mode); + } catch (error) { + if (error.code === 'EPERM') { + throw error; + } + + if (error.code === 'ENOENT') { + if (path.dirname(pth) === pth) { + throw permissionError(pth); + } + + if (error.message.includes('null bytes')) { + throw error; + } + + make(path.dirname(pth)); + return make(pth); + } + + try { + if (!options.fs.statSync(pth).isDirectory()) { + throw new Error('The path is not a directory'); + } + } catch (_) { + throw error; + } + } + + return pth; + }; + + return make(path.resolve(input)); }; /***/ }), -/* 920 */ -/***/ (function(module, exports, __webpack_require__) { +/* 924 */ +/***/ (function(module, exports) { -"use strict"; +exports = module.exports = SemVer -const EventEmitter = __webpack_require__(379); +var debug +/* istanbul ignore next */ +if (typeof process === 'object' && + process.env && + process.env.NODE_DEBUG && + /\bsemver\b/i.test(process.env.NODE_DEBUG)) { + debug = function () { + var args = Array.prototype.slice.call(arguments, 0) + args.unshift('SEMVER') + console.log.apply(console, args) + } +} else { + debug = function () {} +} -const written = new WeakMap(); +// Note: this is the semver.org version of the spec that it implements +// Not necessarily the package version of this code. +exports.SEMVER_SPEC_VERSION = '2.0.0' -class ProgressEmitter extends EventEmitter { - constructor(source, destination) { - super(); - this._source = source; - this._destination = destination; - } +var MAX_LENGTH = 256 +var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || + /* istanbul ignore next */ 9007199254740991 - set written(value) { - written.set(this, value); - this.emitProgress(); - } +// Max safe segment length for coercion. +var MAX_SAFE_COMPONENT_LENGTH = 16 - get written() { - return written.get(this); - } +// The actual regexps go on exports.re +var re = exports.re = [] +var src = exports.src = [] +var t = exports.tokens = {} +var R = 0 - emitProgress() { - const {size, written} = this; - this.emit('progress', { - src: this._source, - dest: this._destination, - size, - written, - percent: written === size ? 1 : written / size - }); - } +function tok (n) { + t[n] = R++ } -module.exports = ProgressEmitter; +// The following Regular Expressions can be used for tokenizing, +// validating, and parsing SemVer version strings. +// ## Numeric Identifier +// A single `0`, or a non-zero digit followed by zero or more digits. -/***/ }), -/* 921 */ -/***/ (function(module, exports, __webpack_require__) { +tok('NUMERICIDENTIFIER') +src[t.NUMERICIDENTIFIER] = '0|[1-9]\\d*' +tok('NUMERICIDENTIFIERLOOSE') +src[t.NUMERICIDENTIFIERLOOSE] = '[0-9]+' -"use strict"; +// ## Non-numeric Identifier +// Zero or more digits, followed by a letter or hyphen, and then zero or +// more letters, digits, or hyphens. -const NestedError = __webpack_require__(922); +tok('NONNUMERICIDENTIFIER') +src[t.NONNUMERICIDENTIFIER] = '\\d*[a-zA-Z-][a-zA-Z0-9-]*' -class CpyError extends NestedError { - constructor(message, nested) { - super(message, nested); - Object.assign(this, nested); - this.name = 'CpyError'; - } -} +// ## Main Version +// Three dot-separated numeric identifiers. -module.exports = CpyError; +tok('MAINVERSION') +src[t.MAINVERSION] = '(' + src[t.NUMERICIDENTIFIER] + ')\\.' + + '(' + src[t.NUMERICIDENTIFIER] + ')\\.' + + '(' + src[t.NUMERICIDENTIFIER] + ')' +tok('MAINVERSIONLOOSE') +src[t.MAINVERSIONLOOSE] = '(' + src[t.NUMERICIDENTIFIERLOOSE] + ')\\.' + + '(' + src[t.NUMERICIDENTIFIERLOOSE] + ')\\.' + + '(' + src[t.NUMERICIDENTIFIERLOOSE] + ')' -/***/ }), -/* 922 */ -/***/ (function(module, exports, __webpack_require__) { +// ## Pre-release Version Identifier +// A numeric identifier, or a non-numeric identifier. -var inherits = __webpack_require__(29).inherits; +tok('PRERELEASEIDENTIFIER') +src[t.PRERELEASEIDENTIFIER] = '(?:' + src[t.NUMERICIDENTIFIER] + + '|' + src[t.NONNUMERICIDENTIFIER] + ')' -var NestedError = function (message, nested) { - this.nested = nested; +tok('PRERELEASEIDENTIFIERLOOSE') +src[t.PRERELEASEIDENTIFIERLOOSE] = '(?:' + src[t.NUMERICIDENTIFIERLOOSE] + + '|' + src[t.NONNUMERICIDENTIFIER] + ')' - if (message instanceof Error) { - nested = message; - } else if (typeof message !== 'undefined') { - Object.defineProperty(this, 'message', { - value: message, - writable: true, - enumerable: false, - configurable: true - }); - } +// ## Pre-release Version +// Hyphen, followed by one or more dot-separated pre-release version +// identifiers. - Error.captureStackTrace(this, this.constructor); - var oldStackDescriptor = Object.getOwnPropertyDescriptor(this, 'stack'); - var stackDescriptor = buildStackDescriptor(oldStackDescriptor, nested); - Object.defineProperty(this, 'stack', stackDescriptor); -}; +tok('PRERELEASE') +src[t.PRERELEASE] = '(?:-(' + src[t.PRERELEASEIDENTIFIER] + + '(?:\\.' + src[t.PRERELEASEIDENTIFIER] + ')*))' -function buildStackDescriptor(oldStackDescriptor, nested) { - if (oldStackDescriptor.get) { - return { - get: function () { - var stack = oldStackDescriptor.get.call(this); - return buildCombinedStacks(stack, this.nested); - } - }; - } else { - var stack = oldStackDescriptor.value; - return { - value: buildCombinedStacks(stack, nested) - }; - } +tok('PRERELEASELOOSE') +src[t.PRERELEASELOOSE] = '(?:-?(' + src[t.PRERELEASEIDENTIFIERLOOSE] + + '(?:\\.' + src[t.PRERELEASEIDENTIFIERLOOSE] + ')*))' + +// ## Build Metadata Identifier +// Any combination of digits, letters, or hyphens. + +tok('BUILDIDENTIFIER') +src[t.BUILDIDENTIFIER] = '[0-9A-Za-z-]+' + +// ## Build Metadata +// Plus sign, followed by one or more period-separated build metadata +// identifiers. + +tok('BUILD') +src[t.BUILD] = '(?:\\+(' + src[t.BUILDIDENTIFIER] + + '(?:\\.' + src[t.BUILDIDENTIFIER] + ')*))' + +// ## Full Version String +// A main version, followed optionally by a pre-release version and +// build metadata. + +// Note that the only major, minor, patch, and pre-release sections of +// the version string are capturing groups. The build metadata is not a +// capturing group, because it should not ever be used in version +// comparison. + +tok('FULL') +tok('FULLPLAIN') +src[t.FULLPLAIN] = 'v?' + src[t.MAINVERSION] + + src[t.PRERELEASE] + '?' + + src[t.BUILD] + '?' + +src[t.FULL] = '^' + src[t.FULLPLAIN] + '$' + +// like full, but allows v1.2.3 and =1.2.3, which people do sometimes. +// also, 1.0.0alpha1 (prerelease without the hyphen) which is pretty +// common in the npm registry. +tok('LOOSEPLAIN') +src[t.LOOSEPLAIN] = '[v=\\s]*' + src[t.MAINVERSIONLOOSE] + + src[t.PRERELEASELOOSE] + '?' + + src[t.BUILD] + '?' + +tok('LOOSE') +src[t.LOOSE] = '^' + src[t.LOOSEPLAIN] + '$' + +tok('GTLT') +src[t.GTLT] = '((?:<|>)?=?)' + +// Something like "2.*" or "1.2.x". +// Note that "x.x" is a valid xRange identifer, meaning "any version" +// Only the first item is strictly required. +tok('XRANGEIDENTIFIERLOOSE') +src[t.XRANGEIDENTIFIERLOOSE] = src[t.NUMERICIDENTIFIERLOOSE] + '|x|X|\\*' +tok('XRANGEIDENTIFIER') +src[t.XRANGEIDENTIFIER] = src[t.NUMERICIDENTIFIER] + '|x|X|\\*' + +tok('XRANGEPLAIN') +src[t.XRANGEPLAIN] = '[v=\\s]*(' + src[t.XRANGEIDENTIFIER] + ')' + + '(?:\\.(' + src[t.XRANGEIDENTIFIER] + ')' + + '(?:\\.(' + src[t.XRANGEIDENTIFIER] + ')' + + '(?:' + src[t.PRERELEASE] + ')?' + + src[t.BUILD] + '?' + + ')?)?' + +tok('XRANGEPLAINLOOSE') +src[t.XRANGEPLAINLOOSE] = '[v=\\s]*(' + src[t.XRANGEIDENTIFIERLOOSE] + ')' + + '(?:\\.(' + src[t.XRANGEIDENTIFIERLOOSE] + ')' + + '(?:\\.(' + src[t.XRANGEIDENTIFIERLOOSE] + ')' + + '(?:' + src[t.PRERELEASELOOSE] + ')?' + + src[t.BUILD] + '?' + + ')?)?' + +tok('XRANGE') +src[t.XRANGE] = '^' + src[t.GTLT] + '\\s*' + src[t.XRANGEPLAIN] + '$' +tok('XRANGELOOSE') +src[t.XRANGELOOSE] = '^' + src[t.GTLT] + '\\s*' + src[t.XRANGEPLAINLOOSE] + '$' + +// Coercion. +// Extract anything that could conceivably be a part of a valid semver +tok('COERCE') +src[t.COERCE] = '(^|[^\\d])' + + '(\\d{1,' + MAX_SAFE_COMPONENT_LENGTH + '})' + + '(?:\\.(\\d{1,' + MAX_SAFE_COMPONENT_LENGTH + '}))?' + + '(?:\\.(\\d{1,' + MAX_SAFE_COMPONENT_LENGTH + '}))?' + + '(?:$|[^\\d])' +tok('COERCERTL') +re[t.COERCERTL] = new RegExp(src[t.COERCE], 'g') + +// Tilde ranges. +// Meaning is "reasonably at or greater than" +tok('LONETILDE') +src[t.LONETILDE] = '(?:~>?)' + +tok('TILDETRIM') +src[t.TILDETRIM] = '(\\s*)' + src[t.LONETILDE] + '\\s+' +re[t.TILDETRIM] = new RegExp(src[t.TILDETRIM], 'g') +var tildeTrimReplace = '$1~' + +tok('TILDE') +src[t.TILDE] = '^' + src[t.LONETILDE] + src[t.XRANGEPLAIN] + '$' +tok('TILDELOOSE') +src[t.TILDELOOSE] = '^' + src[t.LONETILDE] + src[t.XRANGEPLAINLOOSE] + '$' + +// Caret ranges. +// Meaning is "at least and backwards compatible with" +tok('LONECARET') +src[t.LONECARET] = '(?:\\^)' + +tok('CARETTRIM') +src[t.CARETTRIM] = '(\\s*)' + src[t.LONECARET] + '\\s+' +re[t.CARETTRIM] = new RegExp(src[t.CARETTRIM], 'g') +var caretTrimReplace = '$1^' + +tok('CARET') +src[t.CARET] = '^' + src[t.LONECARET] + src[t.XRANGEPLAIN] + '$' +tok('CARETLOOSE') +src[t.CARETLOOSE] = '^' + src[t.LONECARET] + src[t.XRANGEPLAINLOOSE] + '$' + +// A simple gt/lt/eq thing, or just "" to indicate "any version" +tok('COMPARATORLOOSE') +src[t.COMPARATORLOOSE] = '^' + src[t.GTLT] + '\\s*(' + src[t.LOOSEPLAIN] + ')$|^$' +tok('COMPARATOR') +src[t.COMPARATOR] = '^' + src[t.GTLT] + '\\s*(' + src[t.FULLPLAIN] + ')$|^$' + +// An expression to strip any whitespace between the gtlt and the thing +// it modifies, so that `> 1.2.3` ==> `>1.2.3` +tok('COMPARATORTRIM') +src[t.COMPARATORTRIM] = '(\\s*)' + src[t.GTLT] + + '\\s*(' + src[t.LOOSEPLAIN] + '|' + src[t.XRANGEPLAIN] + ')' + +// this one has to use the /g flag +re[t.COMPARATORTRIM] = new RegExp(src[t.COMPARATORTRIM], 'g') +var comparatorTrimReplace = '$1$2$3' + +// Something like `1.2.3 - 1.2.4` +// Note that these all use the loose form, because they'll be +// checked against either the strict or loose comparator form +// later. +tok('HYPHENRANGE') +src[t.HYPHENRANGE] = '^\\s*(' + src[t.XRANGEPLAIN] + ')' + + '\\s+-\\s+' + + '(' + src[t.XRANGEPLAIN] + ')' + + '\\s*$' + +tok('HYPHENRANGELOOSE') +src[t.HYPHENRANGELOOSE] = '^\\s*(' + src[t.XRANGEPLAINLOOSE] + ')' + + '\\s+-\\s+' + + '(' + src[t.XRANGEPLAINLOOSE] + ')' + + '\\s*$' + +// Star ranges basically just allow anything at all. +tok('STAR') +src[t.STAR] = '(<|>)?=?\\s*\\*' + +// Compile to actual regexp objects. +// All are flag-free, unless they were created above with a flag. +for (var i = 0; i < R; i++) { + debug(i, src[i]) + if (!re[i]) { + re[i] = new RegExp(src[i]) + } } -function buildCombinedStacks(stack, nested) { - if (nested) { - stack += '\nCaused By: ' + nested.stack; +exports.parse = parse +function parse (version, options) { + if (!options || typeof options !== 'object') { + options = { + loose: !!options, + includePrerelease: false } - return stack; + } + + if (version instanceof SemVer) { + return version + } + + if (typeof version !== 'string') { + return null + } + + if (version.length > MAX_LENGTH) { + return null + } + + var r = options.loose ? re[t.LOOSE] : re[t.FULL] + if (!r.test(version)) { + return null + } + + try { + return new SemVer(version, options) + } catch (er) { + return null + } } -inherits(NestedError, Error); -NestedError.prototype.name = 'NestedError'; +exports.valid = valid +function valid (version, options) { + var v = parse(version, options) + return v ? v.version : null +} + +exports.clean = clean +function clean (version, options) { + var s = parse(version.trim().replace(/^[=v]+/, ''), options) + return s ? s.version : null +} +exports.SemVer = SemVer -module.exports = NestedError; +function SemVer (version, options) { + if (!options || typeof options !== 'object') { + options = { + loose: !!options, + includePrerelease: false + } + } + if (version instanceof SemVer) { + if (version.loose === options.loose) { + return version + } else { + version = version.version + } + } else if (typeof version !== 'string') { + throw new TypeError('Invalid Version: ' + version) + } + + if (version.length > MAX_LENGTH) { + throw new TypeError('version is longer than ' + MAX_LENGTH + ' characters') + } + if (!(this instanceof SemVer)) { + return new SemVer(version, options) + } -/***/ }), -/* 923 */ + debug('SemVer', version, options) + this.options = options + this.loose = !!options.loose + + var m = version.trim().match(options.loose ? re[t.LOOSE] : re[t.FULL]) + + if (!m) { + throw new TypeError('Invalid Version: ' + version) + } + + this.raw = version + + // these are actually numbers + this.major = +m[1] + this.minor = +m[2] + this.patch = +m[3] + + if (this.major > MAX_SAFE_INTEGER || this.major < 0) { + throw new TypeError('Invalid major version') + } + + if (this.minor > MAX_SAFE_INTEGER || this.minor < 0) { + throw new TypeError('Invalid minor version') + } + + if (this.patch > MAX_SAFE_INTEGER || this.patch < 0) { + throw new TypeError('Invalid patch version') + } + + // numberify any prerelease numeric ids + if (!m[4]) { + this.prerelease = [] + } else { + this.prerelease = m[4].split('.').map(function (id) { + if (/^[0-9]+$/.test(id)) { + var num = +id + if (num >= 0 && num < MAX_SAFE_INTEGER) { + return num + } + } + return id + }) + } + + this.build = m[5] ? m[5].split('.') : [] + this.format() +} + +SemVer.prototype.format = function () { + this.version = this.major + '.' + this.minor + '.' + this.patch + if (this.prerelease.length) { + this.version += '-' + this.prerelease.join('.') + } + return this.version +} + +SemVer.prototype.toString = function () { + return this.version +} + +SemVer.prototype.compare = function (other) { + debug('SemVer.compare', this.version, this.options, other) + if (!(other instanceof SemVer)) { + other = new SemVer(other, this.options) + } + + return this.compareMain(other) || this.comparePre(other) +} + +SemVer.prototype.compareMain = function (other) { + if (!(other instanceof SemVer)) { + other = new SemVer(other, this.options) + } + + return compareIdentifiers(this.major, other.major) || + compareIdentifiers(this.minor, other.minor) || + compareIdentifiers(this.patch, other.patch) +} + +SemVer.prototype.comparePre = function (other) { + if (!(other instanceof SemVer)) { + other = new SemVer(other, this.options) + } + + // NOT having a prerelease is > having one + if (this.prerelease.length && !other.prerelease.length) { + return -1 + } else if (!this.prerelease.length && other.prerelease.length) { + return 1 + } else if (!this.prerelease.length && !other.prerelease.length) { + return 0 + } + + var i = 0 + do { + var a = this.prerelease[i] + var b = other.prerelease[i] + debug('prerelease compare', i, a, b) + if (a === undefined && b === undefined) { + return 0 + } else if (b === undefined) { + return 1 + } else if (a === undefined) { + return -1 + } else if (a === b) { + continue + } else { + return compareIdentifiers(a, b) + } + } while (++i) +} + +SemVer.prototype.compareBuild = function (other) { + if (!(other instanceof SemVer)) { + other = new SemVer(other, this.options) + } + + var i = 0 + do { + var a = this.build[i] + var b = other.build[i] + debug('prerelease compare', i, a, b) + if (a === undefined && b === undefined) { + return 0 + } else if (b === undefined) { + return 1 + } else if (a === undefined) { + return -1 + } else if (a === b) { + continue + } else { + return compareIdentifiers(a, b) + } + } while (++i) +} + +// preminor will bump the version up to the next minor release, and immediately +// down to pre-release. premajor and prepatch work the same way. +SemVer.prototype.inc = function (release, identifier) { + switch (release) { + case 'premajor': + this.prerelease.length = 0 + this.patch = 0 + this.minor = 0 + this.major++ + this.inc('pre', identifier) + break + case 'preminor': + this.prerelease.length = 0 + this.patch = 0 + this.minor++ + this.inc('pre', identifier) + break + case 'prepatch': + // If this is already a prerelease, it will bump to the next version + // drop any prereleases that might already exist, since they are not + // relevant at this point. + this.prerelease.length = 0 + this.inc('patch', identifier) + this.inc('pre', identifier) + break + // If the input is a non-prerelease version, this acts the same as + // prepatch. + case 'prerelease': + if (this.prerelease.length === 0) { + this.inc('patch', identifier) + } + this.inc('pre', identifier) + break + + case 'major': + // If this is a pre-major version, bump up to the same major version. + // Otherwise increment major. + // 1.0.0-5 bumps to 1.0.0 + // 1.1.0 bumps to 2.0.0 + if (this.minor !== 0 || + this.patch !== 0 || + this.prerelease.length === 0) { + this.major++ + } + this.minor = 0 + this.patch = 0 + this.prerelease = [] + break + case 'minor': + // If this is a pre-minor version, bump up to the same minor version. + // Otherwise increment minor. + // 1.2.0-5 bumps to 1.2.0 + // 1.2.1 bumps to 1.3.0 + if (this.patch !== 0 || this.prerelease.length === 0) { + this.minor++ + } + this.patch = 0 + this.prerelease = [] + break + case 'patch': + // If this is not a pre-release version, it will increment the patch. + // If it is a pre-release it will bump up to the same patch version. + // 1.2.0-5 patches to 1.2.0 + // 1.2.0 patches to 1.2.1 + if (this.prerelease.length === 0) { + this.patch++ + } + this.prerelease = [] + break + // This probably shouldn't be used publicly. + // 1.0.0 "pre" would become 1.0.0-0 which is the wrong direction. + case 'pre': + if (this.prerelease.length === 0) { + this.prerelease = [0] + } else { + var i = this.prerelease.length + while (--i >= 0) { + if (typeof this.prerelease[i] === 'number') { + this.prerelease[i]++ + i = -2 + } + } + if (i === -1) { + // didn't increment anything + this.prerelease.push(0) + } + } + if (identifier) { + // 1.2.0-beta.1 bumps to 1.2.0-beta.2, + // 1.2.0-beta.fooblz or 1.2.0-beta bumps to 1.2.0-beta.0 + if (this.prerelease[0] === identifier) { + if (isNaN(this.prerelease[1])) { + this.prerelease = [identifier, 0] + } + } else { + this.prerelease = [identifier, 0] + } + } + break + + default: + throw new Error('invalid increment argument: ' + release) + } + this.format() + this.raw = this.version + return this +} + +exports.inc = inc +function inc (version, release, loose, identifier) { + if (typeof (loose) === 'string') { + identifier = loose + loose = undefined + } + + try { + return new SemVer(version, loose).inc(release, identifier).version + } catch (er) { + return null + } +} + +exports.diff = diff +function diff (version1, version2) { + if (eq(version1, version2)) { + return null + } else { + var v1 = parse(version1) + var v2 = parse(version2) + var prefix = '' + if (v1.prerelease.length || v2.prerelease.length) { + prefix = 'pre' + var defaultResult = 'prerelease' + } + for (var key in v1) { + if (key === 'major' || key === 'minor' || key === 'patch') { + if (v1[key] !== v2[key]) { + return prefix + key + } + } + } + return defaultResult // may be undefined + } +} + +exports.compareIdentifiers = compareIdentifiers + +var numeric = /^[0-9]+$/ +function compareIdentifiers (a, b) { + var anum = numeric.test(a) + var bnum = numeric.test(b) + + if (anum && bnum) { + a = +a + b = +b + } + + return a === b ? 0 + : (anum && !bnum) ? -1 + : (bnum && !anum) ? 1 + : a < b ? -1 + : 1 +} + +exports.rcompareIdentifiers = rcompareIdentifiers +function rcompareIdentifiers (a, b) { + return compareIdentifiers(b, a) +} + +exports.major = major +function major (a, loose) { + return new SemVer(a, loose).major +} + +exports.minor = minor +function minor (a, loose) { + return new SemVer(a, loose).minor +} + +exports.patch = patch +function patch (a, loose) { + return new SemVer(a, loose).patch +} + +exports.compare = compare +function compare (a, b, loose) { + return new SemVer(a, loose).compare(new SemVer(b, loose)) +} + +exports.compareLoose = compareLoose +function compareLoose (a, b) { + return compare(a, b, true) +} + +exports.compareBuild = compareBuild +function compareBuild (a, b, loose) { + var versionA = new SemVer(a, loose) + var versionB = new SemVer(b, loose) + return versionA.compare(versionB) || versionA.compareBuild(versionB) +} + +exports.rcompare = rcompare +function rcompare (a, b, loose) { + return compare(b, a, loose) +} + +exports.sort = sort +function sort (list, loose) { + return list.sort(function (a, b) { + return exports.compareBuild(a, b, loose) + }) +} + +exports.rsort = rsort +function rsort (list, loose) { + return list.sort(function (a, b) { + return exports.compareBuild(b, a, loose) + }) +} + +exports.gt = gt +function gt (a, b, loose) { + return compare(a, b, loose) > 0 +} + +exports.lt = lt +function lt (a, b, loose) { + return compare(a, b, loose) < 0 +} + +exports.eq = eq +function eq (a, b, loose) { + return compare(a, b, loose) === 0 +} + +exports.neq = neq +function neq (a, b, loose) { + return compare(a, b, loose) !== 0 +} + +exports.gte = gte +function gte (a, b, loose) { + return compare(a, b, loose) >= 0 +} + +exports.lte = lte +function lte (a, b, loose) { + return compare(a, b, loose) <= 0 +} + +exports.cmp = cmp +function cmp (a, op, b, loose) { + switch (op) { + case '===': + if (typeof a === 'object') + a = a.version + if (typeof b === 'object') + b = b.version + return a === b + + case '!==': + if (typeof a === 'object') + a = a.version + if (typeof b === 'object') + b = b.version + return a !== b + + case '': + case '=': + case '==': + return eq(a, b, loose) + + case '!=': + return neq(a, b, loose) + + case '>': + return gt(a, b, loose) + + case '>=': + return gte(a, b, loose) + + case '<': + return lt(a, b, loose) + + case '<=': + return lte(a, b, loose) + + default: + throw new TypeError('Invalid operator: ' + op) + } +} + +exports.Comparator = Comparator +function Comparator (comp, options) { + if (!options || typeof options !== 'object') { + options = { + loose: !!options, + includePrerelease: false + } + } + + if (comp instanceof Comparator) { + if (comp.loose === !!options.loose) { + return comp + } else { + comp = comp.value + } + } + + if (!(this instanceof Comparator)) { + return new Comparator(comp, options) + } + + debug('comparator', comp, options) + this.options = options + this.loose = !!options.loose + this.parse(comp) + + if (this.semver === ANY) { + this.value = '' + } else { + this.value = this.operator + this.semver.version + } + + debug('comp', this) +} + +var ANY = {} +Comparator.prototype.parse = function (comp) { + var r = this.options.loose ? re[t.COMPARATORLOOSE] : re[t.COMPARATOR] + var m = comp.match(r) + + if (!m) { + throw new TypeError('Invalid comparator: ' + comp) + } + + this.operator = m[1] !== undefined ? m[1] : '' + if (this.operator === '=') { + this.operator = '' + } + + // if it literally is just '>' or '' then allow anything. + if (!m[2]) { + this.semver = ANY + } else { + this.semver = new SemVer(m[2], this.options.loose) + } +} + +Comparator.prototype.toString = function () { + return this.value +} + +Comparator.prototype.test = function (version) { + debug('Comparator.test', version, this.options.loose) + + if (this.semver === ANY || version === ANY) { + return true + } + + if (typeof version === 'string') { + try { + version = new SemVer(version, this.options) + } catch (er) { + return false + } + } + + return cmp(version, this.operator, this.semver, this.options) +} + +Comparator.prototype.intersects = function (comp, options) { + if (!(comp instanceof Comparator)) { + throw new TypeError('a Comparator is required') + } + + if (!options || typeof options !== 'object') { + options = { + loose: !!options, + includePrerelease: false + } + } + + var rangeTmp + + if (this.operator === '') { + if (this.value === '') { + return true + } + rangeTmp = new Range(comp.value, options) + return satisfies(this.value, rangeTmp, options) + } else if (comp.operator === '') { + if (comp.value === '') { + return true + } + rangeTmp = new Range(this.value, options) + return satisfies(comp.semver, rangeTmp, options) + } + + var sameDirectionIncreasing = + (this.operator === '>=' || this.operator === '>') && + (comp.operator === '>=' || comp.operator === '>') + var sameDirectionDecreasing = + (this.operator === '<=' || this.operator === '<') && + (comp.operator === '<=' || comp.operator === '<') + var sameSemVer = this.semver.version === comp.semver.version + var differentDirectionsInclusive = + (this.operator === '>=' || this.operator === '<=') && + (comp.operator === '>=' || comp.operator === '<=') + var oppositeDirectionsLessThan = + cmp(this.semver, '<', comp.semver, options) && + ((this.operator === '>=' || this.operator === '>') && + (comp.operator === '<=' || comp.operator === '<')) + var oppositeDirectionsGreaterThan = + cmp(this.semver, '>', comp.semver, options) && + ((this.operator === '<=' || this.operator === '<') && + (comp.operator === '>=' || comp.operator === '>')) + + return sameDirectionIncreasing || sameDirectionDecreasing || + (sameSemVer && differentDirectionsInclusive) || + oppositeDirectionsLessThan || oppositeDirectionsGreaterThan +} + +exports.Range = Range +function Range (range, options) { + if (!options || typeof options !== 'object') { + options = { + loose: !!options, + includePrerelease: false + } + } + + if (range instanceof Range) { + if (range.loose === !!options.loose && + range.includePrerelease === !!options.includePrerelease) { + return range + } else { + return new Range(range.raw, options) + } + } + + if (range instanceof Comparator) { + return new Range(range.value, options) + } + + if (!(this instanceof Range)) { + return new Range(range, options) + } + + this.options = options + this.loose = !!options.loose + this.includePrerelease = !!options.includePrerelease + + // First, split based on boolean or || + this.raw = range + this.set = range.split(/\s*\|\|\s*/).map(function (range) { + return this.parseRange(range.trim()) + }, this).filter(function (c) { + // throw out any that are not relevant for whatever reason + return c.length + }) + + if (!this.set.length) { + throw new TypeError('Invalid SemVer Range: ' + range) + } + + this.format() +} + +Range.prototype.format = function () { + this.range = this.set.map(function (comps) { + return comps.join(' ').trim() + }).join('||').trim() + return this.range +} + +Range.prototype.toString = function () { + return this.range +} + +Range.prototype.parseRange = function (range) { + var loose = this.options.loose + range = range.trim() + // `1.2.3 - 1.2.4` => `>=1.2.3 <=1.2.4` + var hr = loose ? re[t.HYPHENRANGELOOSE] : re[t.HYPHENRANGE] + range = range.replace(hr, hyphenReplace) + debug('hyphen replace', range) + // `> 1.2.3 < 1.2.5` => `>1.2.3 <1.2.5` + range = range.replace(re[t.COMPARATORTRIM], comparatorTrimReplace) + debug('comparator trim', range, re[t.COMPARATORTRIM]) + + // `~ 1.2.3` => `~1.2.3` + range = range.replace(re[t.TILDETRIM], tildeTrimReplace) + + // `^ 1.2.3` => `^1.2.3` + range = range.replace(re[t.CARETTRIM], caretTrimReplace) + + // normalize spaces + range = range.split(/\s+/).join(' ') + + // At this point, the range is completely trimmed and + // ready to be split into comparators. + + var compRe = loose ? re[t.COMPARATORLOOSE] : re[t.COMPARATOR] + var set = range.split(' ').map(function (comp) { + return parseComparator(comp, this.options) + }, this).join(' ').split(/\s+/) + if (this.options.loose) { + // in loose mode, throw out any that are not valid comparators + set = set.filter(function (comp) { + return !!comp.match(compRe) + }) + } + set = set.map(function (comp) { + return new Comparator(comp, this.options) + }, this) + + return set +} + +Range.prototype.intersects = function (range, options) { + if (!(range instanceof Range)) { + throw new TypeError('a Range is required') + } + + return this.set.some(function (thisComparators) { + return ( + isSatisfiable(thisComparators, options) && + range.set.some(function (rangeComparators) { + return ( + isSatisfiable(rangeComparators, options) && + thisComparators.every(function (thisComparator) { + return rangeComparators.every(function (rangeComparator) { + return thisComparator.intersects(rangeComparator, options) + }) + }) + ) + }) + ) + }) +} + +// take a set of comparators and determine whether there +// exists a version which can satisfy it +function isSatisfiable (comparators, options) { + var result = true + var remainingComparators = comparators.slice() + var testComparator = remainingComparators.pop() + + while (result && remainingComparators.length) { + result = remainingComparators.every(function (otherComparator) { + return testComparator.intersects(otherComparator, options) + }) + + testComparator = remainingComparators.pop() + } + + return result +} + +// Mostly just for testing and legacy API reasons +exports.toComparators = toComparators +function toComparators (range, options) { + return new Range(range, options).set.map(function (comp) { + return comp.map(function (c) { + return c.value + }).join(' ').trim().split(' ') + }) +} + +// comprised of xranges, tildes, stars, and gtlt's at this point. +// already replaced the hyphen ranges +// turn into a set of JUST comparators. +function parseComparator (comp, options) { + debug('comp', comp, options) + comp = replaceCarets(comp, options) + debug('caret', comp) + comp = replaceTildes(comp, options) + debug('tildes', comp) + comp = replaceXRanges(comp, options) + debug('xrange', comp) + comp = replaceStars(comp, options) + debug('stars', comp) + return comp +} + +function isX (id) { + return !id || id.toLowerCase() === 'x' || id === '*' +} + +// ~, ~> --> * (any, kinda silly) +// ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0 <3.0.0 +// ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0 <2.1.0 +// ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0 <1.3.0 +// ~1.2.3, ~>1.2.3 --> >=1.2.3 <1.3.0 +// ~1.2.0, ~>1.2.0 --> >=1.2.0 <1.3.0 +function replaceTildes (comp, options) { + return comp.trim().split(/\s+/).map(function (comp) { + return replaceTilde(comp, options) + }).join(' ') +} + +function replaceTilde (comp, options) { + var r = options.loose ? re[t.TILDELOOSE] : re[t.TILDE] + return comp.replace(r, function (_, M, m, p, pr) { + debug('tilde', comp, _, M, m, p, pr) + var ret + + if (isX(M)) { + ret = '' + } else if (isX(m)) { + ret = '>=' + M + '.0.0 <' + (+M + 1) + '.0.0' + } else if (isX(p)) { + // ~1.2 == >=1.2.0 <1.3.0 + ret = '>=' + M + '.' + m + '.0 <' + M + '.' + (+m + 1) + '.0' + } else if (pr) { + debug('replaceTilde pr', pr) + ret = '>=' + M + '.' + m + '.' + p + '-' + pr + + ' <' + M + '.' + (+m + 1) + '.0' + } else { + // ~1.2.3 == >=1.2.3 <1.3.0 + ret = '>=' + M + '.' + m + '.' + p + + ' <' + M + '.' + (+m + 1) + '.0' + } + + debug('tilde return', ret) + return ret + }) +} + +// ^ --> * (any, kinda silly) +// ^2, ^2.x, ^2.x.x --> >=2.0.0 <3.0.0 +// ^2.0, ^2.0.x --> >=2.0.0 <3.0.0 +// ^1.2, ^1.2.x --> >=1.2.0 <2.0.0 +// ^1.2.3 --> >=1.2.3 <2.0.0 +// ^1.2.0 --> >=1.2.0 <2.0.0 +function replaceCarets (comp, options) { + return comp.trim().split(/\s+/).map(function (comp) { + return replaceCaret(comp, options) + }).join(' ') +} + +function replaceCaret (comp, options) { + debug('caret', comp, options) + var r = options.loose ? re[t.CARETLOOSE] : re[t.CARET] + return comp.replace(r, function (_, M, m, p, pr) { + debug('caret', comp, _, M, m, p, pr) + var ret + + if (isX(M)) { + ret = '' + } else if (isX(m)) { + ret = '>=' + M + '.0.0 <' + (+M + 1) + '.0.0' + } else if (isX(p)) { + if (M === '0') { + ret = '>=' + M + '.' + m + '.0 <' + M + '.' + (+m + 1) + '.0' + } else { + ret = '>=' + M + '.' + m + '.0 <' + (+M + 1) + '.0.0' + } + } else if (pr) { + debug('replaceCaret pr', pr) + if (M === '0') { + if (m === '0') { + ret = '>=' + M + '.' + m + '.' + p + '-' + pr + + ' <' + M + '.' + m + '.' + (+p + 1) + } else { + ret = '>=' + M + '.' + m + '.' + p + '-' + pr + + ' <' + M + '.' + (+m + 1) + '.0' + } + } else { + ret = '>=' + M + '.' + m + '.' + p + '-' + pr + + ' <' + (+M + 1) + '.0.0' + } + } else { + debug('no pr') + if (M === '0') { + if (m === '0') { + ret = '>=' + M + '.' + m + '.' + p + + ' <' + M + '.' + m + '.' + (+p + 1) + } else { + ret = '>=' + M + '.' + m + '.' + p + + ' <' + M + '.' + (+m + 1) + '.0' + } + } else { + ret = '>=' + M + '.' + m + '.' + p + + ' <' + (+M + 1) + '.0.0' + } + } + + debug('caret return', ret) + return ret + }) +} + +function replaceXRanges (comp, options) { + debug('replaceXRanges', comp, options) + return comp.split(/\s+/).map(function (comp) { + return replaceXRange(comp, options) + }).join(' ') +} + +function replaceXRange (comp, options) { + comp = comp.trim() + var r = options.loose ? re[t.XRANGELOOSE] : re[t.XRANGE] + return comp.replace(r, function (ret, gtlt, M, m, p, pr) { + debug('xRange', comp, ret, gtlt, M, m, p, pr) + var xM = isX(M) + var xm = xM || isX(m) + var xp = xm || isX(p) + var anyX = xp + + if (gtlt === '=' && anyX) { + gtlt = '' + } + + // if we're including prereleases in the match, then we need + // to fix this to -0, the lowest possible prerelease value + pr = options.includePrerelease ? '-0' : '' + + if (xM) { + if (gtlt === '>' || gtlt === '<') { + // nothing is allowed + ret = '<0.0.0-0' + } else { + // nothing is forbidden + ret = '*' + } + } else if (gtlt && anyX) { + // we know patch is an x, because we have any x at all. + // replace X with 0 + if (xm) { + m = 0 + } + p = 0 + + if (gtlt === '>') { + // >1 => >=2.0.0 + // >1.2 => >=1.3.0 + // >1.2.3 => >= 1.2.4 + gtlt = '>=' + if (xm) { + M = +M + 1 + m = 0 + p = 0 + } else { + m = +m + 1 + p = 0 + } + } else if (gtlt === '<=') { + // <=0.7.x is actually <0.8.0, since any 0.7.x should + // pass. Similarly, <=7.x is actually <8.0.0, etc. + gtlt = '<' + if (xm) { + M = +M + 1 + } else { + m = +m + 1 + } + } + + ret = gtlt + M + '.' + m + '.' + p + pr + } else if (xm) { + ret = '>=' + M + '.0.0' + pr + ' <' + (+M + 1) + '.0.0' + pr + } else if (xp) { + ret = '>=' + M + '.' + m + '.0' + pr + + ' <' + M + '.' + (+m + 1) + '.0' + pr + } + + debug('xRange return', ret) + + return ret + }) +} + +// Because * is AND-ed with everything else in the comparator, +// and '' means "any version", just remove the *s entirely. +function replaceStars (comp, options) { + debug('replaceStars', comp, options) + // Looseness is ignored here. star is always as loose as it gets! + return comp.trim().replace(re[t.STAR], '') +} + +// This function is passed to string.replace(re[t.HYPHENRANGE]) +// M, m, patch, prerelease, build +// 1.2 - 3.4.5 => >=1.2.0 <=3.4.5 +// 1.2.3 - 3.4 => >=1.2.0 <3.5.0 Any 3.4.x will do +// 1.2 - 3.4 => >=1.2.0 <3.5.0 +function hyphenReplace ($0, + from, fM, fm, fp, fpr, fb, + to, tM, tm, tp, tpr, tb) { + if (isX(fM)) { + from = '' + } else if (isX(fm)) { + from = '>=' + fM + '.0.0' + } else if (isX(fp)) { + from = '>=' + fM + '.' + fm + '.0' + } else { + from = '>=' + from + } + + if (isX(tM)) { + to = '' + } else if (isX(tm)) { + to = '<' + (+tM + 1) + '.0.0' + } else if (isX(tp)) { + to = '<' + tM + '.' + (+tm + 1) + '.0' + } else if (tpr) { + to = '<=' + tM + '.' + tm + '.' + tp + '-' + tpr + } else { + to = '<=' + to + } + + return (from + ' ' + to).trim() +} + +// if ANY of the sets match ALL of its comparators, then pass +Range.prototype.test = function (version) { + if (!version) { + return false + } + + if (typeof version === 'string') { + try { + version = new SemVer(version, this.options) + } catch (er) { + return false + } + } + + for (var i = 0; i < this.set.length; i++) { + if (testSet(this.set[i], version, this.options)) { + return true + } + } + return false +} + +function testSet (set, version, options) { + for (var i = 0; i < set.length; i++) { + if (!set[i].test(version)) { + return false + } + } + + if (version.prerelease.length && !options.includePrerelease) { + // Find the set of versions that are allowed to have prereleases + // For example, ^1.2.3-pr.1 desugars to >=1.2.3-pr.1 <2.0.0 + // That should allow `1.2.3-pr.2` to pass. + // However, `1.2.4-alpha.notready` should NOT be allowed, + // even though it's within the range set by the comparators. + for (i = 0; i < set.length; i++) { + debug(set[i].semver) + if (set[i].semver === ANY) { + continue + } + + if (set[i].semver.prerelease.length > 0) { + var allowed = set[i].semver + if (allowed.major === version.major && + allowed.minor === version.minor && + allowed.patch === version.patch) { + return true + } + } + } + + // Version has a -pre, but it's not one of the ones we like. + return false + } + + return true +} + +exports.satisfies = satisfies +function satisfies (version, range, options) { + try { + range = new Range(range, options) + } catch (er) { + return false + } + return range.test(version) +} + +exports.maxSatisfying = maxSatisfying +function maxSatisfying (versions, range, options) { + var max = null + var maxSV = null + try { + var rangeObj = new Range(range, options) + } catch (er) { + return null + } + versions.forEach(function (v) { + if (rangeObj.test(v)) { + // satisfies(v, range, options) + if (!max || maxSV.compare(v) === -1) { + // compare(max, v, true) + max = v + maxSV = new SemVer(max, options) + } + } + }) + return max +} + +exports.minSatisfying = minSatisfying +function minSatisfying (versions, range, options) { + var min = null + var minSV = null + try { + var rangeObj = new Range(range, options) + } catch (er) { + return null + } + versions.forEach(function (v) { + if (rangeObj.test(v)) { + // satisfies(v, range, options) + if (!min || minSV.compare(v) === 1) { + // compare(min, v, true) + min = v + minSV = new SemVer(min, options) + } + } + }) + return min +} + +exports.minVersion = minVersion +function minVersion (range, loose) { + range = new Range(range, loose) + + var minver = new SemVer('0.0.0') + if (range.test(minver)) { + return minver + } + + minver = new SemVer('0.0.0-0') + if (range.test(minver)) { + return minver + } + + minver = null + for (var i = 0; i < range.set.length; ++i) { + var comparators = range.set[i] + + comparators.forEach(function (comparator) { + // Clone to avoid manipulating the comparator's semver object. + var compver = new SemVer(comparator.semver.version) + switch (comparator.operator) { + case '>': + if (compver.prerelease.length === 0) { + compver.patch++ + } else { + compver.prerelease.push(0) + } + compver.raw = compver.format() + /* fallthrough */ + case '': + case '>=': + if (!minver || gt(minver, compver)) { + minver = compver + } + break + case '<': + case '<=': + /* Ignore maximum versions */ + break + /* istanbul ignore next */ + default: + throw new Error('Unexpected operation: ' + comparator.operator) + } + }) + } + + if (minver && range.test(minver)) { + return minver + } + + return null +} + +exports.validRange = validRange +function validRange (range, options) { + try { + // Return '*' instead of '' so that truthiness works. + // This will throw if it's invalid anyway + return new Range(range, options).range || '*' + } catch (er) { + return null + } +} + +// Determine if version is less than all the versions possible in the range +exports.ltr = ltr +function ltr (version, range, options) { + return outside(version, range, '<', options) +} + +// Determine if version is greater than all the versions possible in the range. +exports.gtr = gtr +function gtr (version, range, options) { + return outside(version, range, '>', options) +} + +exports.outside = outside +function outside (version, range, hilo, options) { + version = new SemVer(version, options) + range = new Range(range, options) + + var gtfn, ltefn, ltfn, comp, ecomp + switch (hilo) { + case '>': + gtfn = gt + ltefn = lte + ltfn = lt + comp = '>' + ecomp = '>=' + break + case '<': + gtfn = lt + ltefn = gte + ltfn = gt + comp = '<' + ecomp = '<=' + break + default: + throw new TypeError('Must provide a hilo val of "<" or ">"') + } + + // If it satisifes the range it is not outside + if (satisfies(version, range, options)) { + return false + } + + // From now on, variable terms are as if we're in "gtr" mode. + // but note that everything is flipped for the "ltr" function. + + for (var i = 0; i < range.set.length; ++i) { + var comparators = range.set[i] + + var high = null + var low = null + + comparators.forEach(function (comparator) { + if (comparator.semver === ANY) { + comparator = new Comparator('>=0.0.0') + } + high = high || comparator + low = low || comparator + if (gtfn(comparator.semver, high.semver, options)) { + high = comparator + } else if (ltfn(comparator.semver, low.semver, options)) { + low = comparator + } + }) + + // If the edge version comparator has a operator then our version + // isn't outside it + if (high.operator === comp || high.operator === ecomp) { + return false + } + + // If the lowest version comparator has an operator and our version + // is less than it then it isn't higher than the range + if ((!low.operator || low.operator === comp) && + ltefn(version, low.semver)) { + return false + } else if (low.operator === ecomp && ltfn(version, low.semver)) { + return false + } + } + return true +} + +exports.prerelease = prerelease +function prerelease (version, options) { + var parsed = parse(version, options) + return (parsed && parsed.prerelease.length) ? parsed.prerelease : null +} + +exports.intersects = intersects +function intersects (r1, r2, options) { + r1 = new Range(r1, options) + r2 = new Range(r2, options) + return r1.intersects(r2) +} + +exports.coerce = coerce +function coerce (version, options) { + if (version instanceof SemVer) { + return version + } + + if (typeof version === 'number') { + version = String(version) + } + + if (typeof version !== 'string') { + return null + } + + options = options || {} + + var match = null + if (!options.rtl) { + match = version.match(re[t.COERCE]) + } else { + // Find the right-most coercible string that does not share + // a terminus with a more left-ward coercible string. + // Eg, '1.2.3.4' wants to coerce '2.3.4', not '3.4' or '4' + // + // Walk through the string checking with a /g regexp + // Manually set the index so as to pick up overlapping matches. + // Stop when we get a match that ends at the string end, since no + // coercible string can be more right-ward without the same terminus. + var next + while ((next = re[t.COERCERTL].exec(version)) && + (!match || match.index + match[0].length !== version.length) + ) { + if (!match || + next.index + next[0].length !== match.index + match[0].length) { + match = next + } + re[t.COERCERTL].lastIndex = next.index + next[1].length + next[2].length + } + // leave it in a clean state + re[t.COERCERTL].lastIndex = -1 + } + + if (match === null) { + return null + } + + return parse(match[2] + + '.' + (match[3] || '0') + + '.' + (match[4] || '0'), options) +} + + +/***/ }), +/* 925 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +const EventEmitter = __webpack_require__(379); + +const written = new WeakMap(); + +class ProgressEmitter extends EventEmitter { + constructor(source, destination) { + super(); + this._source = source; + this._destination = destination; + } + + set written(value) { + written.set(this, value); + this.emitProgress(); + } + + get written() { + return written.get(this); + } + + emitProgress() { + const {size, written} = this; + this.emit('progress', { + src: this._source, + dest: this._destination, + size, + written, + percent: written === size ? 1 : written / size + }); + } +} + +module.exports = ProgressEmitter; + + +/***/ }), +/* 926 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +const blacklist = [ + // # All + '^npm-debug\\.log$', // Error log for npm + '^\\..*\\.swp$', // Swap file for vim state + + // # macOS + '^\\.DS_Store$', // Stores custom folder attributes + '^\\.AppleDouble$', // Stores additional file resources + '^\\.LSOverride$', // Contains the absolute path to the app to be used + '^Icon\\r$', // Custom Finder icon: http://superuser.com/questions/298785/icon-file-on-os-x-desktop + '^\\._.*', // Thumbnail + '^\\.Spotlight-V100(?:$|\\/)', // Directory that might appear on external disk + '\\.Trashes', // File that might appear on external disk + '^__MACOSX$', // Resource fork + + // # Linux + '~$', // Backup file + + // # Windows + '^Thumbs\\.db$', // Image file cache + '^ehthumbs\\.db$', // Folder config file + '^Desktop\\.ini$', // Stores custom folder attributes + '@eaDir$' // Synology Diskstation "hidden" folder where the server stores thumbnails +]; + +exports.re = () => { + throw new Error('`junk.re` was renamed to `junk.regex`'); +}; + +exports.regex = new RegExp(blacklist.join('|')); + +exports.is = filename => exports.regex.test(filename); + +exports.not = filename => !exports.is(filename); + +// TODO: Remove this for the next major release +exports.default = module.exports; + + +/***/ }), +/* 927 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +const NestedError = __webpack_require__(928); + +class CpyError extends NestedError { + constructor(message, nested) { + super(message, nested); + Object.assign(this, nested); + this.name = 'CpyError'; + } +} + +module.exports = CpyError; + + +/***/ }), +/* 928 */ +/***/ (function(module, exports, __webpack_require__) { + +var inherits = __webpack_require__(29).inherits; + +var NestedError = function (message, nested) { + this.nested = nested; + + if (message instanceof Error) { + nested = message; + } else if (typeof message !== 'undefined') { + Object.defineProperty(this, 'message', { + value: message, + writable: true, + enumerable: false, + configurable: true + }); + } + + Error.captureStackTrace(this, this.constructor); + var oldStackDescriptor = Object.getOwnPropertyDescriptor(this, 'stack'); + var stackDescriptor = buildStackDescriptor(oldStackDescriptor, nested); + Object.defineProperty(this, 'stack', stackDescriptor); +}; + +function buildStackDescriptor(oldStackDescriptor, nested) { + if (oldStackDescriptor.get) { + return { + get: function () { + var stack = oldStackDescriptor.get.call(this); + return buildCombinedStacks(stack, this.nested); + } + }; + } else { + var stack = oldStackDescriptor.value; + return { + value: buildCombinedStacks(stack, nested) + }; + } +} + +function buildCombinedStacks(stack, nested) { + if (nested) { + stack += '\nCaused By: ' + nested.stack; + } + return stack; +} + +inherits(NestedError, Error); +NestedError.prototype.name = 'NestedError'; + + +module.exports = NestedError; + + +/***/ }), +/* 929 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -108562,7 +110627,9 @@ __webpack_require__.r(__webpack_exports__); * to Kibana itself. */ -const isKibanaDep = depVersion => depVersion.includes('../../packages/'); +const isKibanaDep = depVersion => // For ../kibana-extra/ directory (legacy only) +depVersion.includes('../../kibana/packages/') || // For plugins/ directory +depVersion.includes('../../packages/'); /** * This prepares the dependencies for an _external_ project. */ diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index f57365905292b..444d46307b059 100644 --- a/packages/kbn-pm/package.json +++ b/packages/kbn-pm/package.json @@ -39,7 +39,7 @@ "babel-loader": "^8.0.6", "chalk": "^2.4.2", "cmd-shim": "^2.1.0", - "cpy": "^7.3.0", + "cpy": "^8.0.0", "dedent": "^0.7.0", "del": "^5.1.0", "execa": "^3.2.0", @@ -63,8 +63,8 @@ "tempy": "^0.3.0", "typescript": "3.7.2", "unlazy-loader": "^0.1.3", - "webpack": "^4.41.0", - "webpack-cli": "^3.3.9", + "webpack": "^4.41.5", + "webpack-cli": "^3.3.10", "wrap-ansi": "^3.0.1", "write-pkg": "^4.0.0" }, diff --git a/packages/kbn-pm/src/production/prepare_project_dependencies.ts b/packages/kbn-pm/src/production/prepare_project_dependencies.ts index af0575b95e51a..9817770166480 100644 --- a/packages/kbn-pm/src/production/prepare_project_dependencies.ts +++ b/packages/kbn-pm/src/production/prepare_project_dependencies.ts @@ -25,7 +25,11 @@ import { Project } from '../utils/project'; * to the Kibana root directory or `../kibana-extra/{plugin}` relative * to Kibana itself. */ -const isKibanaDep = (depVersion: string) => depVersion.includes('../../packages/'); +const isKibanaDep = (depVersion: string) => + // For ../kibana-extra/ directory (legacy only) + depVersion.includes('../../kibana/packages/') || + // For plugins/ directory + depVersion.includes('../../packages/'); /** * This prepares the dependencies for an _external_ project. diff --git a/packages/kbn-storybook/package.json b/packages/kbn-storybook/package.json index 6948ae81806eb..73deadba0a619 100644 --- a/packages/kbn-storybook/package.json +++ b/packages/kbn-storybook/package.json @@ -27,6 +27,6 @@ "rxjs": "6.5.2", "serve-static": "1.14.1", "styled-components": "^3", - "webpack": "4.34.0" + "webpack": "^4.41.5" } } \ No newline at end of file diff --git a/packages/kbn-test/src/functional_tests/tasks.js b/packages/kbn-test/src/functional_tests/tasks.js index d50f6a15c2e0b..8645923a13d30 100644 --- a/packages/kbn-test/src/functional_tests/tasks.js +++ b/packages/kbn-test/src/functional_tests/tasks.js @@ -59,6 +59,19 @@ const makeSuccessMessage = options => { * @property {string} options.esFrom Optionally run from source instead of snapshot */ export async function runTests(options) { + if (!process.env.KBN_NP_PLUGINS_BUILT) { + const log = options.createLogger(); + log.warning('❗️❗️❗️'); + log.warning('❗️❗️❗️'); + log.warning('❗️❗️❗️'); + log.warning( + " Don't forget to use `node scripts/build_kibana_platform_plugins` to build plugins you plan on testing" + ); + log.warning('❗️❗️❗️'); + log.warning('❗️❗️❗️'); + log.warning('❗️❗️❗️'); + } + for (const configPath of options.configs) { const log = options.createLogger(); const opts = { diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index 4bb4c660a01ab..fc245ca3fe921 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -33,13 +33,13 @@ "@babel/core": "^7.5.5", "@elastic/eui": "0.0.55", "@kbn/babel-preset": "1.0.0", - "autoprefixer": "9.6.1", + "autoprefixer": "^9.7.4", "babel-loader": "^8.0.6", "brace": "0.11.1", "chalk": "^2.4.2", "chokidar": "3.2.1", "core-js": "^3.2.1", - "css-loader": "^2.1.1", + "css-loader": "^3.4.2", "expose-loader": "^0.7.5", "file-loader": "^4.2.0", "grunt": "1.0.4", @@ -54,7 +54,7 @@ "keymirror": "0.1.1", "moment": "^2.24.0", "node-sass": "^4.13.1", - "postcss": "^7.0.5", + "postcss": "^7.0.26", "postcss-loader": "^3.0.0", "raw-loader": "^3.1.0", "react-dom": "^16.12.0", @@ -64,10 +64,10 @@ "redux": "3.7.2", "redux-thunk": "2.2.0", "regenerator-runtime": "^0.13.3", - "sass-loader": "^7.3.1", + "sass-loader": "^8.0.2", "sinon": "^7.4.2", - "style-loader": "^0.23.1", - "webpack": "^4.41.0", + "style-loader": "^1.1.3", + "webpack": "^4.41.5", "webpack-dev-server": "^3.8.2", "yeoman-generator": "1.1.1", "yo": "2.0.6" diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index fc9d159ea9b95..0b1a31619fdf9 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -17,7 +17,7 @@ "@yarnpkg/lockfile": "^1.1.0", "angular": "^1.7.9", "core-js": "^3.2.1", - "css-loader": "^2.1.1", + "css-loader": "^3.4.2", "custom-event-polyfill": "^0.3.0", "del": "^5.1.0", "jquery": "^3.4.1", @@ -30,7 +30,7 @@ "read-pkg": "^5.2.0", "regenerator-runtime": "^0.13.3", "symbol-observable": "^1.2.0", - "webpack": "4.41.0", + "webpack": "^4.41.5", "whatwg-fetch": "^3.0.0" } } diff --git a/renovate.json5 b/renovate.json5 index 1fbe83476d4a8..642c4a98b5799 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -123,6 +123,14 @@ '@types/bluebird', ], }, + { + groupSlug: 'browserslist-useragent', + groupName: 'browserslist-useragent related packages', + packageNames: [ + 'browserslist-useragent', + '@types/browserslist-useragent', + ], + }, { groupSlug: 'chance', groupName: 'chance related packages', @@ -929,6 +937,14 @@ '@types/vinyl-fs', ], }, + { + groupSlug: 'watchpack', + groupName: 'watchpack related packages', + packageNames: [ + 'watchpack', + '@types/watchpack', + ], + }, { groupSlug: 'webpack', groupName: 'webpack related packages', diff --git a/scripts/build_kibana_platform_plugins.js b/scripts/build_kibana_platform_plugins.js new file mode 100644 index 0000000000000..4d6963144d085 --- /dev/null +++ b/scripts/build_kibana_platform_plugins.js @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +require('@kbn/optimizer/target/cli'); diff --git a/src/cli/cluster/cluster_manager.test.ts b/src/cli/cluster/cluster_manager.test.ts index bd37e854e1691..707778861fb59 100644 --- a/src/cli/cluster/cluster_manager.test.ts +++ b/src/cli/cluster/cluster_manager.test.ts @@ -17,7 +17,24 @@ * under the License. */ +import * as Rx from 'rxjs'; + import { mockCluster } from './cluster_manager.test.mocks'; + +jest.mock('./run_kbn_optimizer', () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires,no-shadow + const Rx = require('rxjs'); + + return { + runKbnOptimizer: () => + new Rx.BehaviorSubject({ + type: 'compiler success', + durSec: 0, + bundles: [], + }), + }; +}); + jest.mock('readline', () => ({ createInterface: jest.fn(() => ({ on: jest.fn(), @@ -26,6 +43,13 @@ jest.mock('readline', () => ({ })), })); +const mockConfig: any = { + get: (key: string) => { + expect(key).toBe('optimize.enabled'); + return false; + }, +}; + import { sample } from 'lodash'; import { ClusterManager } from './cluster_manager'; @@ -51,7 +75,7 @@ describe('CLI cluster manager', () => { }); test('has two workers', () => { - const manager = new ClusterManager({}, {} as any); + const manager = new ClusterManager({}, mockConfig); expect(manager.workers).toHaveLength(2); for (const worker of manager.workers) expect(worker).toBeInstanceOf(Worker); @@ -61,7 +85,7 @@ describe('CLI cluster manager', () => { }); test('delivers broadcast messages to other workers', () => { - const manager = new ClusterManager({}, {} as any); + const manager = new ClusterManager({}, mockConfig); for (const worker of manager.workers) { Worker.prototype.start.call(worker); // bypass the debounced start method @@ -86,92 +110,59 @@ describe('CLI cluster manager', () => { test('correctly configures `BasePathProxy`.', async () => { const basePathProxyMock = { start: jest.fn() }; - new ClusterManager({}, {} as any, basePathProxyMock as any); + new ClusterManager({}, mockConfig, basePathProxyMock as any); expect(basePathProxyMock.start).toHaveBeenCalledWith({ shouldRedirectFromOldBasePath: expect.any(Function), - blockUntil: expect.any(Function), + delayUntil: expect.any(Function), }); }); - describe('proxy is configured with the correct `shouldRedirectFromOldBasePath` and `blockUntil` functions.', () => { + describe('basePathProxy config', () => { let clusterManager: ClusterManager; let shouldRedirectFromOldBasePath: (path: string) => boolean; - let blockUntil: () => Promise; + let delayUntil: () => Rx.Observable; + beforeEach(async () => { const basePathProxyMock = { start: jest.fn() }; - - clusterManager = new ClusterManager({}, {} as any, basePathProxyMock as any); - - jest.spyOn(clusterManager.server, 'on'); - jest.spyOn(clusterManager.server, 'off'); - - [[{ blockUntil, shouldRedirectFromOldBasePath }]] = basePathProxyMock.start.mock.calls; - }); - - test('`shouldRedirectFromOldBasePath()` returns `false` for unknown paths.', () => { - expect(shouldRedirectFromOldBasePath('')).toBe(false); - expect(shouldRedirectFromOldBasePath('some-path/')).toBe(false); - expect(shouldRedirectFromOldBasePath('some-other-path')).toBe(false); + clusterManager = new ClusterManager({}, mockConfig, basePathProxyMock as any); + [[{ delayUntil, shouldRedirectFromOldBasePath }]] = basePathProxyMock.start.mock.calls; }); - test('`shouldRedirectFromOldBasePath()` returns `true` for `app` and other known paths.', () => { - expect(shouldRedirectFromOldBasePath('app/')).toBe(true); - expect(shouldRedirectFromOldBasePath('login')).toBe(true); - expect(shouldRedirectFromOldBasePath('logout')).toBe(true); - expect(shouldRedirectFromOldBasePath('status')).toBe(true); - }); - - test('`blockUntil()` resolves immediately if worker has already crashed.', async () => { - clusterManager.server.crashed = true; - - await expect(blockUntil()).resolves.not.toBeDefined(); - expect(clusterManager.server.on).not.toHaveBeenCalled(); - expect(clusterManager.server.off).not.toHaveBeenCalled(); + describe('shouldRedirectFromOldBasePath()', () => { + test('returns `false` for unknown paths.', () => { + expect(shouldRedirectFromOldBasePath('')).toBe(false); + expect(shouldRedirectFromOldBasePath('some-path/')).toBe(false); + expect(shouldRedirectFromOldBasePath('some-other-path')).toBe(false); + }); + + test('returns `true` for `app` and other known paths.', () => { + expect(shouldRedirectFromOldBasePath('app/')).toBe(true); + expect(shouldRedirectFromOldBasePath('login')).toBe(true); + expect(shouldRedirectFromOldBasePath('logout')).toBe(true); + expect(shouldRedirectFromOldBasePath('status')).toBe(true); + }); }); - test('`blockUntil()` resolves immediately if worker is already listening.', async () => { - clusterManager.server.listening = true; - - await expect(blockUntil()).resolves.not.toBeDefined(); - expect(clusterManager.server.on).not.toHaveBeenCalled(); - expect(clusterManager.server.off).not.toHaveBeenCalled(); - }); - - test('`blockUntil()` resolves when worker crashes.', async () => { - const blockUntilPromise = blockUntil(); - - expect(clusterManager.server.on).toHaveBeenCalledTimes(2); - expect(clusterManager.server.on).toHaveBeenCalledWith('crashed', expect.any(Function)); - - 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'); - expect(clusterManager.server.off).not.toHaveBeenCalled(); - - onCrashed(); - await expect(blockUntilPromise).resolves.not.toBeDefined(); - - expect(clusterManager.server.off).toHaveBeenCalledTimes(2); - }); - - test('`blockUntil()` resolves when worker starts listening.', async () => { - const blockUntilPromise = blockUntil(); - - expect(clusterManager.server.on).toHaveBeenCalledTimes(2); - expect(clusterManager.server.on).toHaveBeenCalledWith('listening', expect.any(Function)); - - 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'); - expect(clusterManager.server.off).not.toHaveBeenCalled(); - - onListening(); - await expect(blockUntilPromise).resolves.not.toBeDefined(); - - expect(clusterManager.server.off).toHaveBeenCalledTimes(2); + describe('delayUntil()', () => { + test('returns an observable which emits when the server and kbnOptimizer are ready and completes', async () => { + clusterManager.serverReady$.next(false); + clusterManager.optimizerReady$.next(false); + clusterManager.kbnOptimizerReady$.next(false); + + const events: Array = []; + delayUntil().subscribe( + () => events.push('next'), + error => events.push(error), + () => events.push('complete') + ); + + clusterManager.serverReady$.next(true); + expect(events).toEqual([]); + + clusterManager.kbnOptimizerReady$.next(true); + expect(events).toEqual(['next', 'complete']); + }); }); }); }); diff --git a/src/cli/cluster/cluster_manager.ts b/src/cli/cluster/cluster_manager.ts index 3fa4bdcbc5fa5..2f308915fb332 100644 --- a/src/cli/cluster/cluster_manager.ts +++ b/src/cli/cluster/cluster_manager.ts @@ -19,22 +19,29 @@ 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 * as Rx from 'rxjs'; +import { startWith, mapTo, filter, map, take, tap } from 'rxjs/operators'; +import { runKbnOptimizer } from './run_kbn_optimizer'; import { LegacyConfig } from '../../core/server/legacy'; import { BasePathProxyServer } from '../../core/server/http'; -// @ts-ignore -import Log from '../log'; +import { Log } from './log'; import { Worker } from './worker'; process.env.kbnWorkerType = 'managr'; +const firstAllTrue = (...sources: Array>) => + Rx.combineLatest(...sources).pipe( + filter(values => values.every(v => v === true)), + take(1), + mapTo(undefined) + ); + export class ClusterManager { public optimizer: Worker; public server: Worker; @@ -42,10 +49,17 @@ export class ClusterManager { private watcher: FSWatcher | null = null; private basePathProxy: BasePathProxyServer | undefined; - private log: any; + private log: Log; private addedCount = 0; private inReplMode: boolean; + // exposed for testing + public readonly serverReady$ = new Rx.ReplaySubject(1); + // exposed for testing + public readonly optimizerReady$ = new Rx.ReplaySubject(1); + // exposed for testing + public readonly kbnOptimizerReady$ = new Rx.ReplaySubject(1); + constructor( opts: Record, config: LegacyConfig, @@ -55,6 +69,23 @@ export class ClusterManager { this.inReplMode = !!opts.repl; this.basePathProxy = basePathProxy; + if (config.get('optimize.enabled') !== false) { + // run @kbn/optimizer and write it's state to kbnOptimizerReady$ + runKbnOptimizer(opts, config) + .pipe( + map(({ state }) => state.phase === 'success' || state.phase === 'issue'), + tap({ + error: error => { + this.log.bad('New platform optimizer error', error.stack); + process.exit(1); + }, + }) + ) + .subscribe(this.kbnOptimizerReady$); + } else { + this.kbnOptimizerReady$.next(true); + } + const serverArgv = []; const optimizerArgv = ['--plugins.initialize=false', '--server.autoListen=false']; @@ -86,6 +117,27 @@ export class ClusterManager { })), ]; + // write server status to the serverReady$ subject + Rx.merge( + Rx.fromEvent(this.server, 'starting').pipe(mapTo(false)), + Rx.fromEvent(this.server, 'listening').pipe(mapTo(true)), + Rx.fromEvent(this.server, 'crashed').pipe(mapTo(true)) + ) + .pipe(startWith(this.server.listening || this.server.crashed)) + .subscribe(this.serverReady$); + + // write optimizer status to the optimizerReady$ subject + Rx.merge( + Rx.fromEvent(this.optimizer, 'optimizeStatus'), + Rx.defer(() => { + if (this.optimizer.fork) { + this.optimizer.fork.send({ optimizeReady: '?' }); + } + }) + ) + .pipe(map((msg: any) => msg && !!msg.success)) + .subscribe(this.optimizerReady$); + // broker messages between workers this.workers.forEach(worker => { worker.on('broadcast', msg => { @@ -109,8 +161,6 @@ export class ClusterManager { }); }); - bindAll(this, 'onWatcherAdd', 'onWatcherError', 'onWatcherChange'); - if (opts.open) { this.setupOpen( formatUrl({ @@ -137,11 +187,11 @@ export class ClusterManager { .reduce( (acc, path) => acc.concat( - resolve(path, 'test'), - resolve(path, 'build'), - resolve(path, 'target'), - resolve(path, 'scripts'), - resolve(path, 'docs') + resolve(path, 'test/**'), + resolve(path, 'build/**'), + resolve(path, 'target/**'), + resolve(path, 'scripts/**'), + resolve(path, 'docs/**') ), [] as string[] ); @@ -152,33 +202,36 @@ export class ClusterManager { startCluster() { this.setupManualRestart(); - invoke(this.workers, 'start'); + for (const worker of this.workers) { + worker.start(); + } if (this.basePathProxy) { this.basePathProxy.start({ - blockUntil: this.blockUntil.bind(this), - shouldRedirectFromOldBasePath: this.shouldRedirectFromOldBasePath.bind(this), + delayUntil: () => firstAllTrue(this.serverReady$, this.kbnOptimizerReady$), + + shouldRedirectFromOldBasePath: (path: string) => { + // strip `s/{id}` prefix when checking for need to redirect + if (path.startsWith('s/')) { + path = path + .split('/') + .slice(2) + .join('/'); + } + + const isApp = path.startsWith('app/'); + const isKnownShortPath = ['login', 'logout', 'status'].includes(path); + return isApp || isKnownShortPath; + }, }); } } setupOpen(openUrl: string) { - const serverListening$ = Rx.merge( - Rx.fromEvent(this.server, 'listening').pipe(mapTo(true)), - Rx.fromEvent(this.server, 'fork:exit').pipe(mapTo(false)), - Rx.fromEvent(this.server, 'crashed').pipe(mapTo(false)) - ); - - const optimizeSuccess$ = Rx.fromEvent(this.optimizer, 'optimizeStatus').pipe( - map((msg: any) => !!msg.success) - ); - - Rx.combineLatest(serverListening$, optimizeSuccess$) - .pipe( - filter(([serverListening, optimizeSuccess]) => serverListening && optimizeSuccess), - take(1) - ) + firstAllTrue(this.serverReady$, this.kbnOptimizerReady$, this.optimizerReady$) .toPromise() - .then(() => opn(openUrl)); + .then(() => { + opn(openUrl); + }); } setupWatching(extraPaths: string[], pluginInternalDirsIgnore: string[]) { @@ -187,53 +240,51 @@ export class ClusterManager { // eslint-disable-next-line @typescript-eslint/no-var-requires const { fromRoot } = require('../../core/server/utils'); - const watchPaths = [ - fromRoot('src/core'), - fromRoot('src/legacy/core_plugins'), - fromRoot('src/legacy/server'), - fromRoot('src/legacy/ui'), - fromRoot('src/legacy/utils'), - fromRoot('x-pack/legacy/common'), - fromRoot('x-pack/legacy/plugins'), - fromRoot('x-pack/legacy/server'), - fromRoot('config'), - ...extraPaths, - ].map(path => resolve(path)); + const watchPaths = Array.from( + new Set( + [ + fromRoot('src/core'), + fromRoot('src/legacy/core_plugins'), + fromRoot('src/legacy/server'), + fromRoot('src/legacy/ui'), + fromRoot('src/legacy/utils'), + fromRoot('x-pack/legacy/common'), + fromRoot('x-pack/legacy/plugins'), + fromRoot('x-pack/legacy/server'), + fromRoot('config'), + ...extraPaths, + ].map(path => resolve(path)) + ) + ); const ignorePaths = [ + /[\\\/](\..*|node_modules|bower_components|public|__[a-z0-9_]+__|coverage)[\\\/]/, + /\.test\.(js|ts)$/, + ...pluginInternalDirsIgnore, fromRoot('src/legacy/server/sass/__tmp__'), fromRoot('x-pack/legacy/plugins/reporting/.chromium'), fromRoot('x-pack/legacy/plugins/siem/cypress'), fromRoot('x-pack/legacy/plugins/apm/cypress'), fromRoot('x-pack/legacy/plugins/apm/scripts'), - fromRoot('x-pack/legacy/plugins/canvas/canvas_plugin_src'), // prevents server from restarting twice for Canvas plugin changes + fromRoot('x-pack/legacy/plugins/canvas/canvas_plugin_src'), // prevents server from restarting twice for Canvas plugin changes, + 'plugins/java_languageserver', ]; - this.watcher = chokidar.watch(uniq(watchPaths), { + this.watcher = chokidar.watch(watchPaths, { cwd: fromRoot('.'), - ignored: [ - /[\\\/](\..*|node_modules|bower_components|public|__[a-z0-9_]+__|coverage)[\\\/]/, - /\.test\.(js|ts)$/, - ...pluginInternalDirsIgnore, - ...ignorePaths, - 'plugins/java_languageserver', - ], + ignored: ignorePaths, }) as FSWatcher; this.watcher.on('add', this.onWatcherAdd); this.watcher.on('error', this.onWatcherError); + this.watcher.once('ready', () => { + // start sending changes to workers + this.watcher!.removeListener('add', this.onWatcherAdd); + this.watcher!.on('all', this.onWatcherChange); - this.watcher.on( - 'ready', - once(() => { - // start sending changes to workers - this.watcher!.removeListener('add', this.onWatcherAdd); - this.watcher!.on('all', this.onWatcherChange); - - this.log.good('watching for changes', `(${this.addedCount} files)`); - this.startCluster(); - }) - ); + this.log.good('watching for changes', `(${this.addedCount} files)`); + this.startCluster(); + }); } setupManualRestart() { @@ -249,7 +300,20 @@ export class ClusterManager { let nls = 0; const clear = () => (nls = 0); - const clearSoon = debounce(clear, 2000); + + let clearTimer: number | undefined; + const clearSoon = () => { + clearSoon.cancel(); + clearTimer = setTimeout(() => { + clearTimer = undefined; + clear(); + }); + }; + + clearSoon.cancel = () => { + clearTimeout(clearTimer); + clearTimer = undefined; + }; rl.setPrompt(''); rl.prompt(); @@ -274,41 +338,18 @@ export class ClusterManager { }); } - onWatcherAdd() { + onWatcherAdd = () => { this.addedCount += 1; - } + }; - onWatcherChange(e: any, path: string) { - invoke(this.workers, 'onChange', path); - } + onWatcherChange = (e: any, path: string) => { + for (const worker of this.workers) { + worker.onChange(path); + } + }; - onWatcherError(err: any) { + onWatcherError = (err: any) => { this.log.bad('failed to watch files!\n', err.stack); process.exit(1); // eslint-disable-line no-process-exit - } - - shouldRedirectFromOldBasePath(path: string) { - // strip `s/{id}` prefix when checking for need to redirect - if (path.startsWith('s/')) { - path = path - .split('/') - .slice(2) - .join('/'); - } - - const isApp = path.startsWith('app/'); - const isKnownShortPath = ['login', 'logout', 'status'].includes(path); - return isApp || isKnownShortPath; - } - - blockUntil() { - // Wait until `server` worker either crashes or starts to listen. - if (this.server.listening || this.server.crashed) { - return Promise.resolve(); - } - - return Rx.race(Rx.fromEvent(this.server, 'listening'), Rx.fromEvent(this.server, 'crashed')) - .pipe(first()) - .toPromise(); - } + }; } diff --git a/src/cli/cluster/log.ts b/src/cli/cluster/log.ts new file mode 100644 index 0000000000000..af73059c0758e --- /dev/null +++ b/src/cli/cluster/log.ts @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Chalk from 'chalk'; + +export class Log { + constructor(private readonly quiet: boolean, private readonly silent: boolean) {} + + good(label: string, ...args: any[]) { + if (this.quiet || this.silent) { + return; + } + + // eslint-disable-next-line no-console + console.log(Chalk.black.bgGreen(` ${label.trim()} `), ...args); + } + + warn(label: string, ...args: any[]) { + if (this.quiet || this.silent) { + return; + } + + // eslint-disable-next-line no-console + console.log(Chalk.black.bgYellow(` ${label.trim()} `), ...args); + } + + bad(label: string, ...args: any[]) { + if (this.silent) { + return; + } + + // eslint-disable-next-line no-console + console.log(Chalk.white.bgRed(` ${label.trim()} `), ...args); + } + + write(label: string, ...args: any[]) { + // eslint-disable-next-line no-console + console.log(` ${label.trim()} `, ...args); + } +} diff --git a/src/cli/cluster/run_kbn_optimizer.ts b/src/cli/cluster/run_kbn_optimizer.ts new file mode 100644 index 0000000000000..7752d4a45ab65 --- /dev/null +++ b/src/cli/cluster/run_kbn_optimizer.ts @@ -0,0 +1,79 @@ +/* + * 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 Chalk from 'chalk'; +import moment from 'moment'; +import { + ToolingLog, + pickLevelFromFlags, + ToolingLogTextWriter, + parseLogLevel, + REPO_ROOT, +} from '@kbn/dev-utils'; +import { runOptimizer, OptimizerConfig, logOptimizerState } from '@kbn/optimizer'; + +import { LegacyConfig } from '../../core/server/legacy'; + +export function runKbnOptimizer(opts: Record, config: LegacyConfig) { + const optimizerConfig = OptimizerConfig.create({ + repoRoot: REPO_ROOT, + watch: true, + oss: !!opts.oss, + examples: !!opts.runExamples, + pluginPaths: config.get('plugins.paths'), + }); + + const dim = Chalk.dim('np bld'); + const name = Chalk.magentaBright('@kbn/optimizer'); + const time = () => moment().format('HH:mm:ss.SSS'); + const level = (msgType: string) => { + switch (msgType) { + case 'info': + return Chalk.green(msgType); + case 'success': + return Chalk.cyan(msgType); + case 'debug': + return Chalk.gray(msgType); + default: + return msgType; + } + }; + const { flags: levelFlags } = parseLogLevel(pickLevelFromFlags(opts)); + const toolingLog = new ToolingLog(); + const has = (obj: T, x: any): x is keyof T => obj.hasOwnProperty(x); + + toolingLog.setWriters([ + { + write(msg) { + if (has(levelFlags, msg.type) && !levelFlags[msg.type]) { + return false; + } + + ToolingLogTextWriter.write( + process.stdout, + `${dim} log [${time()}] [${level(msg.type)}][${name}] `, + msg + ); + return true; + }, + }, + ]); + + return runOptimizer(optimizerConfig).pipe(logOptimizerState(toolingLog, optimizerConfig)); +} diff --git a/src/cli/cluster/worker.test.ts b/src/cli/cluster/worker.test.ts index 4f9337681e083..e775f71442a77 100644 --- a/src/cli/cluster/worker.test.ts +++ b/src/cli/cluster/worker.test.ts @@ -20,8 +20,8 @@ import { mockCluster } from './cluster_manager.test.mocks'; import { Worker, ClusterWorker } from './worker'; -// @ts-ignore -import Log from '../log'; + +import { Log } from './log'; const workersToShutdown: Worker[] = []; diff --git a/src/cli/cluster/worker.ts b/src/cli/cluster/worker.ts index fb87f1a87654c..c73d3edbf7df7 100644 --- a/src/cli/cluster/worker.ts +++ b/src/cli/cluster/worker.ts @@ -199,6 +199,7 @@ export class Worker extends EventEmitter { } this.fork = cluster.fork(this.env) as ClusterWorker; + this.emit('starting'); this.forkBinder = new BinderFor(this.fork); // when the fork sends a message, comes online, or loses its connection, then react diff --git a/src/cli/command.js b/src/cli/command.js index 06ee87e3198fd..6f083bb2a1fa2 100644 --- a/src/cli/command.js +++ b/src/cli/command.js @@ -18,17 +18,17 @@ */ import _ from 'lodash'; +import Chalk from 'chalk'; import help from './help'; import { Command } from 'commander'; -import { red } from './color'; Command.prototype.error = function(err) { if (err && err.message) err = err.message; console.log( ` -${red(' ERROR ')} ${err} +${Chalk.white.bgRed(' ERROR ')} ${err} ${help(this, ' ')} ` diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 9cf5691b88399..be3fc319389d7 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -195,7 +195,7 @@ export default function(program) { [] ) .option('--plugins ', 'an alias for --plugin-dir', pluginDirCollector) - .option('--optimize', 'Optimize and then stop the server'); + .option('--optimize', 'Run the legacy plugin optimizer and then stop the server'); if (CAN_REPL) { command.option('--repl', 'Run the server with a REPL prompt and access to the server object'); diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index f8699364fa9e2..fa0edd8faadd7 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -56,6 +56,7 @@ - [On the server side](#on-the-server-side) - [On the client side](#on-the-client-side) - [Updates an application navlink at runtime](#updates-an-app-navlink-at-runtime) + - [Logging config migration](#logging-config-migration) Make no mistake, it is going to take a lot of work to move certain plugins to the new platform. Our target is to migrate the entire repo over to the new platform throughout 7.x and to remove the legacy plugin system no later than 8.0, and this is only possible if teams start on the effort now. @@ -1163,6 +1164,7 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy'; | Legacy Platform | New Platform | Notes | | ------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | +| `import 'ui/management'` | `management.sections` | | | `import 'ui/apply_filters'` | N/A. Replaced by triggering an APPLY_FILTER_TRIGGER trigger. | Directive is deprecated. | | `import 'ui/filter_bar'` | `import { FilterBar } from '../data/public'` | Directive is deprecated. | | `import 'ui/query_bar'` | `import { QueryStringInput } from '../data/public'` | Directives are deprecated. | @@ -1240,7 +1242,7 @@ This table shows where these uiExports have moved to in the New Platform. In mos | `inspectorViews` | | Should be an API on the data (?) plugin. | | `interpreter` | | Should be an API on the interpreter plugin. | | `links` | n/a | Not necessary, just register your app via `core.application.register` | -| `managementSections` | [`plugins.management.sections.register`](/rfcs/text/0006_management_section_service.md) | API finalized, implementation in progress. | +| `managementSections` | [`plugins.management.sections.register`](/rfcs/text/0006_management_section_service.md) | | | `mappings` | | Part of SavedObjects, see [#33587](https://github.com/elastic/kibana/issues/33587) | | `migrations` | | Part of SavedObjects, see [#33587](https://github.com/elastic/kibana/issues/33587) | | `navbarExtensions` | n/a | Deprecated | @@ -1654,4 +1656,7 @@ export class MyPlugin implements Plugin { tooltip: 'Application disabled', }) } -``` \ No newline at end of file +``` + +### Logging config migration +[Read](./server/logging/README.md#logging-config-migration) \ No newline at end of file diff --git a/src/core/TESTING.md b/src/core/TESTING.md index aac54a4a14680..9abc2bb77d7d1 100644 --- a/src/core/TESTING.md +++ b/src/core/TESTING.md @@ -29,7 +29,6 @@ This document outlines best practices and patterns for testing Kibana Plugins. - [Testing dependencies usages](#testing-dependencies-usages) - [Testing components consuming the dependencies](#testing-components-consuming-the-dependencies) - [Testing optional plugin dependencies](#testing-optional-plugin-dependencies) - - [Plugin Contracts](#plugin-contracts) ## Strategy @@ -1082,7 +1081,3 @@ describe('Plugin', () => { }); }); ``` - -## Plugin Contracts - -_How to test your plugin's exposed API_ diff --git a/src/core/public/application/application_service.mock.ts b/src/core/public/application/application_service.mock.ts index dee47315fc322..d2a827d381be5 100644 --- a/src/core/public/application/application_service.mock.ts +++ b/src/core/public/application/application_service.mock.ts @@ -43,12 +43,17 @@ const createInternalSetupContractMock = (): jest.Mocked => ({ - capabilities: capabilitiesServiceMock.createStartContract().capabilities, - navigateToApp: jest.fn(), - getUrlForApp: jest.fn(), - registerMountContext: jest.fn(), -}); +const createStartContractMock = (): jest.Mocked => { + const currentAppId$ = new Subject(); + + return { + currentAppId$: currentAppId$.asObservable(), + capabilities: capabilitiesServiceMock.createStartContract().capabilities, + navigateToApp: jest.fn(), + getUrlForApp: jest.fn(), + registerMountContext: jest.fn(), + }; +}; const createInternalStartContractMock = (): jest.Mocked => { const currentAppId$ = new Subject(); diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index 18716bd872842..5487ca53170dd 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -580,7 +580,6 @@ describe('#start()', () => { it('creates URLs with path parameter', async () => { service.setup(setupDeps); - const { getUrlForApp } = await service.start(startDeps); expect(getUrlForApp('app1', { path: 'deep/link' })).toBe('/base-path/app/app1/deep/link'); @@ -588,6 +587,16 @@ describe('#start()', () => { expect(getUrlForApp('app1', { path: '//deep/link//' })).toBe('/base-path/app/app1/deep/link'); expect(getUrlForApp('app1', { path: 'deep/link///' })).toBe('/base-path/app/app1/deep/link'); }); + + it('creates absolute URLs when `absolute` parameter is true', async () => { + service.setup(setupDeps); + const { getUrlForApp } = await service.start(startDeps); + + expect(getUrlForApp('app1', { absolute: true })).toBe('http://localhost/base-path/app/app1'); + expect(getUrlForApp('app2', { path: 'deep/link', absolute: true })).toBe( + 'http://localhost/base-path/app/app2/deep/link' + ); + }); }); describe('navigateToApp', () => { diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index d100457f4027f..77f06e316c0aa 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -272,8 +272,13 @@ export class ApplicationService { takeUntil(this.stop$) ), registerMountContext: this.mountContext.registerContext, - getUrlForApp: (appId, { path }: { path?: string } = {}) => - http.basePath.prepend(getAppUrl(availableMounters, appId, path)), + getUrlForApp: ( + appId, + { path, absolute = false }: { path?: string; absolute?: boolean } = {} + ) => { + const relUrl = http.basePath.prepend(getAppUrl(availableMounters, appId, path)); + return absolute ? relativeToAbsolute(relUrl) : relUrl; + }, navigateToApp: async (appId, { path, state }: { path?: string; state?: any } = {}) => { if (await this.shouldNavigate(overlays)) { this.appLeaveHandlers.delete(this.currentAppId$.value!); @@ -364,3 +369,10 @@ const updateStatus = (app: T, statusUpdaters: AppUpdaterWrapp ...changes, }; }; + +function relativeToAbsolute(url: string) { + // convert all link urls to absolute urls + const a = document.createElement('a'); + a.setAttribute('href', url); + return a.href; +} diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 17fdfc627187e..977bb7a52da22 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -593,11 +593,17 @@ export interface ApplicationStart { navigateToApp(appId: string, options?: { path?: string; state?: any }): Promise; /** - * Returns a relative URL to a given app, including the global base path. + * Returns an URL to a given app, including the global base path. + * By default, the URL is relative (/basePath/app/my-app). + * Use the `absolute` option to generate an absolute url (http://host:port/basePath/app/my-app) + * + * Note that when generating absolute urls, the protocol, host and port are determined from the browser location. + * * @param appId * @param options.path - optional path inside application to deep link to + * @param options.absolute - if true, will returns an absolute url instead of a relative one */ - getUrlForApp(appId: string, options?: { path?: string }): string; + getUrlForApp(appId: string, options?: { path?: string; absolute?: boolean }): string; /** * Register a context provider for application mounting. Will only be available to applications that depend on the @@ -612,11 +618,19 @@ export interface ApplicationStart { contextName: T, provider: IContextProvider ): void; + + /** + * An observable that emits the current application id and each subsequent id update. + */ + currentAppId$: Observable; } /** @internal */ export interface InternalApplicationStart - extends Pick { + extends Pick< + ApplicationStart, + 'capabilities' | 'navigateToApp' | 'getUrlForApp' | 'currentAppId$' + > { /** * Apps available based on the current capabilities. * Should be used to show navigation links and make routing decisions. @@ -640,7 +654,6 @@ export interface InternalApplicationStart ): void; // Internal APIs - currentAppId$: Observable; getComponent(): JSX.Element | null; } diff --git a/src/core/public/http/fetch.test.ts b/src/core/public/http/fetch.test.ts index a99b7607d7149..efd9fdd053674 100644 --- a/src/core/public/http/fetch.test.ts +++ b/src/core/public/http/fetch.test.ts @@ -37,6 +37,7 @@ describe('Fetch', () => { }); afterEach(() => { fetchMock.restore(); + fetchInstance.removeAllInterceptors(); }); describe('http requests', () => { @@ -287,6 +288,42 @@ describe('Fetch', () => { }); }); + it('preserves the name of the original error', async () => { + expect.assertions(1); + + const abortError = new DOMException('The operation was aborted.', 'AbortError'); + + fetchMock.get('*', Promise.reject(abortError)); + + await fetchInstance.fetch('/my/path').catch(e => { + expect(e.name).toEqual('AbortError'); + }); + }); + + it('exposes the request to the interceptors in case of aborted request', async () => { + const responseErrorSpy = jest.fn(); + const abortError = new DOMException('The operation was aborted.', 'AbortError'); + + fetchMock.get('*', Promise.reject(abortError)); + + fetchInstance.intercept({ + responseError: responseErrorSpy, + }); + + await expect(fetchInstance.fetch('/my/path')).rejects.toThrow(); + + expect(responseErrorSpy).toHaveBeenCalledTimes(1); + const interceptedResponse = responseErrorSpy.mock.calls[0][0]; + + expect(interceptedResponse.request).toEqual( + expect.objectContaining({ + method: 'GET', + url: 'http://localhost/myBase/my/path', + }) + ); + expect(interceptedResponse.error.name).toEqual('AbortError'); + }); + it('should support get() helper', async () => { fetchMock.get('*', {}); await fetchInstance.get('/my/path', { method: 'POST' }); @@ -368,11 +405,6 @@ describe('Fetch', () => { fetchMock.get('*', { foo: 'bar' }); }); - afterEach(() => { - fetchMock.restore(); - fetchInstance.removeAllInterceptors(); - }); - it('should make request and receive response', async () => { fetchInstance.intercept({}); diff --git a/src/core/public/http/fetch.ts b/src/core/public/http/fetch.ts index 1043b50dff958..b433acdb6dbb9 100644 --- a/src/core/public/http/fetch.ts +++ b/src/core/public/http/fetch.ts @@ -146,11 +146,7 @@ export class Fetch { try { response = await window.fetch(request); } catch (err) { - if (err.name === 'AbortError') { - throw err; - } else { - throw new HttpFetchError(err.message, request); - } + throw new HttpFetchError(err.message, err.name ?? 'Error', request); } const contentType = response.headers.get('Content-Type') || ''; @@ -170,11 +166,11 @@ export class Fetch { } } } catch (err) { - throw new HttpFetchError(err.message, request, response, body); + throw new HttpFetchError(err.message, err.name ?? 'Error', request, response, body); } if (!response.ok) { - throw new HttpFetchError(response.statusText, request, response, body); + throw new HttpFetchError(response.statusText, 'Error', request, response, body); } return { fetchOptions, request, response, body }; diff --git a/src/core/public/http/http_fetch_error.ts b/src/core/public/http/http_fetch_error.ts index 2156df5798974..74aed4049613e 100644 --- a/src/core/public/http/http_fetch_error.ts +++ b/src/core/public/http/http_fetch_error.ts @@ -21,16 +21,19 @@ import { IHttpFetchError } from './types'; /** @internal */ export class HttpFetchError extends Error implements IHttpFetchError { + public readonly name: string; public readonly req: Request; public readonly res?: Response; constructor( message: string, + name: string, public readonly request: Request, public readonly response?: Response, public readonly body?: any ) { super(message); + this.name = name; this.req = request; this.res = response; diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts index c38b9da442943..5909572c7e545 100644 --- a/src/core/public/http/types.ts +++ b/src/core/public/http/types.ts @@ -291,6 +291,7 @@ export interface IHttpResponseInterceptorOverrides { /** @public */ export interface IHttpFetchError extends Error { + readonly name: string; readonly request: Request; readonly response?: Response; /** diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index e4788e686dd45..1b7e25f585566 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -121,6 +121,7 @@ export class LegacyPlatformService { const legacyCore: LegacyCoreStart = { ...core, application: { + currentAppId$: core.application.currentAppId$, capabilities: core.application.capabilities, getUrlForApp: core.application.getUrlForApp, navigateToApp: core.application.navigateToApp, diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index 48100cba4f26e..19cfadf70be1b 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -134,6 +134,7 @@ export function createPluginStartContext< ): CoreStart { return { application: { + currentAppId$: deps.application.currentAppId$, capabilities: deps.application.capabilities, navigateToApp: deps.application.navigateToApp, getUrlForApp: deps.application.getUrlForApp, diff --git a/src/core/public/plugins/plugin_loader.test.ts b/src/core/public/plugins/plugin_loader.test.ts index e24be35331f39..e5cbffc3e2d94 100644 --- a/src/core/public/plugins/plugin_loader.test.ts +++ b/src/core/public/plugins/plugin_loader.test.ts @@ -62,7 +62,7 @@ test('`loadPluginBundles` creates a script tag and loads initializer', async () const fakeScriptTag = createdScriptTags[0]; expect(fakeScriptTag.setAttribute).toHaveBeenCalledWith( 'src', - '/bundles/plugin/plugin-a.bundle.js' + '/bundles/plugin/plugin-a/plugin-a.plugin.js' ); expect(fakeScriptTag.setAttribute).toHaveBeenCalledWith('id', 'kbn-plugin-plugin-a'); expect(fakeScriptTag.onload).toBeInstanceOf(Function); @@ -85,7 +85,7 @@ test('`loadPluginBundles` includes the basePath', async () => { const fakeScriptTag = createdScriptTags[0]; expect(fakeScriptTag.setAttribute).toHaveBeenCalledWith( 'src', - '/mybasepath/bundles/plugin/plugin-a.bundle.js' + '/mybasepath/bundles/plugin/plugin-a/plugin-a.plugin.js' ); }); @@ -96,7 +96,7 @@ test('`loadPluginBundles` rejects if script.onerror is called', async () => { fakeScriptTag1.onerror(new Error('Whoa there!')); await expect(loadPromise).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to load \\"plugin-a\\" bundle (/bundles/plugin/plugin-a.bundle.js)"` + `"Failed to load \\"plugin-a\\" bundle (/bundles/plugin/plugin-a/plugin-a.plugin.js)"` ); }); @@ -105,7 +105,7 @@ test('`loadPluginBundles` rejects if timeout is reached', async () => { // Override the timeout to 1 ms for testi. loadPluginBundle(addBasePath, 'plugin-a', { timeoutMs: 1 }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Timeout reached when loading \\"plugin-a\\" bundle (/bundles/plugin/plugin-a.bundle.js)"` + `"Timeout reached when loading \\"plugin-a\\" bundle (/bundles/plugin/plugin-a/plugin-a.plugin.js)"` ); }); @@ -120,6 +120,6 @@ test('`loadPluginBundles` rejects if bundle does attach an initializer to window fakeScriptTag1.onload(); await expect(loadPromise).rejects.toThrowErrorMatchingInlineSnapshot( - `"Definition of plugin \\"plugin-a\\" should be a function (/bundles/plugin/plugin-a.bundle.js)."` + `"Definition of plugin \\"plugin-a\\" should be a function (/bundles/plugin/plugin-a/plugin-a.plugin.js)."` ); }); diff --git a/src/core/public/plugins/plugin_loader.ts b/src/core/public/plugins/plugin_loader.ts index 776ed7d7c5570..63aba0dde2af8 100644 --- a/src/core/public/plugins/plugin_loader.ts +++ b/src/core/public/plugins/plugin_loader.ts @@ -74,7 +74,7 @@ export const loadPluginBundle: LoadPluginBundle = < const coreWindow = (window as unknown) as CoreWindow; // Assumes that all plugin bundles get put into the bundles/plugins subdirectory - const bundlePath = addBasePath(`/bundles/plugin/${pluginName}.bundle.js`); + const bundlePath = addBasePath(`/bundles/plugin/${pluginName}/${pluginName}.plugin.js`); script.setAttribute('src', bundlePath); script.setAttribute('id', `kbn-plugin-${pluginName}`); script.setAttribute('async', ''); diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index aa7ca4fee675e..f0289cc2b8355 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -98,8 +98,10 @@ export interface ApplicationSetup { // @public (undocumented) export interface ApplicationStart { capabilities: RecursiveReadonly; + currentAppId$: Observable; getUrlForApp(appId: string, options?: { path?: string; + absolute?: boolean; }): string; navigateToApp(appId: string, options?: { path?: string; @@ -731,6 +733,8 @@ export type IContextProvider, TContextName export interface IHttpFetchError extends Error { // (undocumented) readonly body?: any; + // (undocumented) + readonly name: string; // @deprecated (undocumented) readonly req: Request; // (undocumented) diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index 3aa7f9e2aa8ad..4fa51dcd5a082 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -115,6 +115,9 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({ renameFromRoot('optimize.lazyHost', 'optimize.watchHost'), renameFromRoot('optimize.lazyPrebuild', 'optimize.watchPrebuild'), renameFromRoot('optimize.lazyProxyTimeout', 'optimize.watchProxyTimeout'), + renameFromRoot('xpack.xpack_main.telemetry.config', 'telemetry.config'), + renameFromRoot('xpack.xpack_main.telemetry.url', 'telemetry.url'), + renameFromRoot('xpack.xpack_main.telemetry.enabled', 'telemetry.enabled'), renameFromRoot('xpack.telemetry.enabled', 'telemetry.enabled'), renameFromRoot('xpack.telemetry.config', 'telemetry.config'), renameFromRoot('xpack.telemetry.banner', 'telemetry.banner'), diff --git a/src/core/server/config/env.ts b/src/core/server/config/env.ts index db363fcd4d751..05a8f40a09a88 100644 --- a/src/core/server/config/env.ts +++ b/src/core/server/config/env.ts @@ -100,6 +100,11 @@ export class Env { this.binDir = resolve(this.homeDir, 'bin'); this.logDir = resolve(this.homeDir, 'log'); + /** + * BEWARE: this needs to stay roughly synchronized with the @kbn/optimizer + * `packages/kbn-optimizer/src/optimizer_config.ts` determines the paths + * that should be searched for plugins to build + */ this.pluginSearchPaths = [ resolve(this.homeDir, 'src', 'plugins'), ...(options.cliArgs.oss ? [] : [resolve(this.homeDir, 'x-pack', 'plugins')]), diff --git a/src/core/server/http/base_path_proxy_server.ts b/src/core/server/http/base_path_proxy_server.ts index 276e3955a4678..e418726465efa 100644 --- a/src/core/server/http/base_path_proxy_server.ts +++ b/src/core/server/http/base_path_proxy_server.ts @@ -17,13 +17,17 @@ * under the License. */ -import apm from 'elastic-apm-node'; - -import { ByteSizeValue } from '@kbn/config-schema'; -import { Server, Request } from 'hapi'; import Url from 'url'; import { Agent as HttpsAgent, ServerOptions as TlsOptions } from 'https'; + +import apm from 'elastic-apm-node'; +import { ByteSizeValue } from '@kbn/config-schema'; +import { Server, Request, ResponseToolkit } from 'hapi'; import { sample } from 'lodash'; +import BrowserslistUserAgent from 'browserslist-useragent'; +import * as Rx from 'rxjs'; +import { take } from 'rxjs/operators'; + import { DevConfig } from '../dev'; import { Logger } from '../logging'; import { HttpConfig } from './http_config'; @@ -33,9 +37,37 @@ const alphabet = 'abcdefghijklmnopqrztuvwxyz'.split(''); export interface BasePathProxyServerOptions { shouldRedirectFromOldBasePath: (path: string) => boolean; - blockUntil: () => Promise; + delayUntil: () => Rx.Observable; } +// Before we proxy request to a target port we may want to wait until some +// condition is met (e.g. until target listener is ready). +const checkForBrowserCompat = (log: Logger) => async (request: Request, h: ResponseToolkit) => { + if (!request.headers['user-agent'] || process.env.BROWSERSLIST_ENV === 'production') { + return h.continue; + } + + const matches = BrowserslistUserAgent.matchesUA(request.headers['user-agent'], { + env: 'dev', + allowHigherVersions: true, + ignoreMinor: true, + ignorePath: true, + }); + + if (!matches) { + log.warn(` + Request with user-agent [${request.headers['user-agent']}] + seems like it is coming from a browser that is not supported by the dev browserlist. + + Please run Kibana with the environment variable BROWSERSLIST_ENV=production to enable + support for all production browsers (like IE). + + `); + } + + return h.continue; +}; + export class BasePathProxyServer { private server?: Server; private httpsAgent?: HttpsAgent; @@ -108,7 +140,7 @@ export class BasePathProxyServer { } private setupRoutes({ - blockUntil, + delayUntil, shouldRedirectFromOldBasePath, }: Readonly) { if (this.server === undefined) { @@ -122,6 +154,9 @@ export class BasePathProxyServer { }, method: 'GET', path: '/', + options: { + pre: [checkForBrowserCompat(this.log)], + }, }); this.server.route({ @@ -138,11 +173,14 @@ export class BasePathProxyServer { method: '*', options: { pre: [ + checkForBrowserCompat(this.log), // Before we proxy request to a target port we may want to wait until some // condition is met (e.g. until target listener is ready). async (request, responseToolkit) => { apm.setTransactionName(`${request.method.toUpperCase()} /{basePath}/{kbnPath*}`); - await blockUntil(); + await delayUntil() + .pipe(take(1)) + .toPromise(); return responseToolkit.continue; }, ], @@ -172,10 +210,13 @@ export class BasePathProxyServer { method: '*', options: { pre: [ + checkForBrowserCompat(this.log), // Before we proxy request to a target port we may want to wait until some // condition is met (e.g. until target listener is ready). async (request, responseToolkit) => { - await blockUntil(); + await delayUntil() + .pipe(take(1)) + .toPromise(); return responseToolkit.continue; }, ], diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index 230a229b36888..81d756f47d760 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -19,8 +19,7 @@ import { Request } from 'hapi'; import { merge } from 'lodash'; import { Socket } from 'net'; - -import querystring from 'querystring'; +import { stringify } from 'query-string'; import { schema } from '@kbn/config-schema'; @@ -55,7 +54,8 @@ function createKibanaRequestMock({ socket = new Socket(), routeTags, }: RequestFixtureOptions = {}) { - const queryString = querystring.stringify(query); + const queryString = stringify(query, { sort: false }); + return KibanaRequest.from( createRawRequestMock({ headers, diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index e8e20580a36db..46436461505c0 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -88,7 +88,7 @@ beforeEach(() => { contracts: new Map([['plugin-id', 'plugin-value']]), uiPlugins: { public: new Map([['plugin-id', {} as DiscoveredPlugin]]), - internal: new Map([['plugin-id', { entryPointPath: 'path/to/plugin/public' }]]), + internal: new Map([['plugin-id', { publicTargetDir: 'path/to/target/public' }]]), browserConfigs: new Map(), }, }, diff --git a/src/core/server/legacy/plugins/__snapshots__/get_nav_links.test.ts.snap b/src/core/server/legacy/plugins/__snapshots__/get_nav_links.test.ts.snap new file mode 100644 index 0000000000000..c1b7164908ed6 --- /dev/null +++ b/src/core/server/legacy/plugins/__snapshots__/get_nav_links.test.ts.snap @@ -0,0 +1,56 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` 1`] = ` +Array [ + Object { + "category": undefined, + "disableSubUrlTracking": undefined, + "disabled": false, + "euiIconType": undefined, + "hidden": false, + "icon": undefined, + "id": "link-a", + "linkToLastSubUrl": true, + "order": 0, + "subUrlBase": "/some-custom-url", + "title": "AppA", + "tooltip": "", + "url": "/some-custom-url", + }, + Object { + "category": undefined, + "disableSubUrlTracking": true, + "disabled": false, + "euiIconType": undefined, + "hidden": false, + "icon": undefined, + "id": "link-b", + "linkToLastSubUrl": true, + "order": 0, + "subUrlBase": "/url-b", + "title": "AppB", + "tooltip": "", + "url": "/url-b", + }, + Object { + "category": undefined, + "euiIconType": undefined, + "icon": undefined, + "id": "app-a", + "linkToLastSubUrl": true, + "order": 0, + "title": "AppA", + "url": "/app/app-a", + }, + Object { + "category": undefined, + "euiIconType": undefined, + "icon": undefined, + "id": "app-b", + "linkToLastSubUrl": true, + "order": 0, + "title": "AppB", + "url": "/app/app-b", + }, +] +`; diff --git a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts index 1c6ab91a39279..44f02f0c90d4e 100644 --- a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts +++ b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts @@ -30,72 +30,8 @@ import { collectUiExports as collectLegacyUiExports } from '../../../../legacy/u import { LoggerFactory } from '../../logging'; import { PackageInfo } from '../../config'; - -import { - LegacyUiExports, - LegacyNavLink, - LegacyPluginSpec, - LegacyPluginPack, - LegacyConfig, -} from '../types'; - -const REMOVE_FROM_ARRAY: LegacyNavLink[] = []; - -function getUiAppsNavLinks({ uiAppSpecs = [] }: LegacyUiExports, pluginSpecs: LegacyPluginSpec[]) { - return uiAppSpecs.flatMap(spec => { - if (!spec) { - return REMOVE_FROM_ARRAY; - } - - const id = spec.pluginId || spec.id; - - if (!id) { - throw new Error('Every app must specify an id'); - } - - if (spec.pluginId && !pluginSpecs.some(plugin => plugin.getId() === spec.pluginId)) { - throw new Error(`Unknown plugin id "${spec.pluginId}"`); - } - - const listed = typeof spec.listed === 'boolean' ? spec.listed : true; - - if (spec.hidden || !listed) { - return REMOVE_FROM_ARRAY; - } - - return { - id, - category: spec.category, - title: spec.title, - order: typeof spec.order === 'number' ? spec.order : 0, - icon: spec.icon, - euiIconType: spec.euiIconType, - url: spec.url || `/app/${id}`, - linkToLastSubUrl: spec.linkToLastSubUrl, - }; - }); -} - -function getNavLinks(uiExports: LegacyUiExports, pluginSpecs: LegacyPluginSpec[]) { - return (uiExports.navLinkSpecs || []) - .map(spec => ({ - id: spec.id, - category: spec.category, - title: spec.title, - order: typeof spec.order === 'number' ? spec.order : 0, - url: spec.url, - subUrlBase: spec.subUrlBase || spec.url, - disableSubUrlTracking: spec.disableSubUrlTracking, - icon: spec.icon, - euiIconType: spec.euiIconType, - linkToLastSub: 'linkToLastSubUrl' in spec ? spec.linkToLastSubUrl : false, - hidden: 'hidden' in spec ? spec.hidden : false, - disabled: 'disabled' in spec ? spec.disabled : false, - tooltip: spec.tooltip || '', - })) - .concat(getUiAppsNavLinks(uiExports, pluginSpecs)) - .sort((a, b) => a.order - b.order); -} +import { LegacyPluginSpec, LegacyPluginPack, LegacyConfig } from '../types'; +import { getNavLinks } from './get_nav_links'; export async function findLegacyPluginSpecs( settings: unknown, diff --git a/src/core/server/legacy/plugins/get_nav_links.test.ts b/src/core/server/legacy/plugins/get_nav_links.test.ts new file mode 100644 index 0000000000000..dcb19020f769e --- /dev/null +++ b/src/core/server/legacy/plugins/get_nav_links.test.ts @@ -0,0 +1,283 @@ +/* + * 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 { LegacyUiExports, LegacyPluginSpec, LegacyAppSpec, LegacyNavLinkSpec } from '../types'; +import { getNavLinks } from './get_nav_links'; + +const createLegacyExports = ({ + uiAppSpecs = [], + navLinkSpecs = [], +}: { + uiAppSpecs?: LegacyAppSpec[]; + navLinkSpecs?: LegacyNavLinkSpec[]; +}): LegacyUiExports => ({ + uiAppSpecs, + navLinkSpecs, + injectedVarsReplacers: [], + defaultInjectedVarProviders: [], + savedObjectMappings: [], + savedObjectSchemas: {}, + savedObjectMigrations: {}, + savedObjectValidations: {}, +}); + +const createPluginSpecs = (...ids: string[]): LegacyPluginSpec[] => + ids.map( + id => + ({ + getId: () => id, + } as LegacyPluginSpec) + ); + +describe('getNavLinks', () => { + describe('generating from uiAppSpecs', () => { + it('generates navlinks from legacy app specs', () => { + const navlinks = getNavLinks( + createLegacyExports({ + uiAppSpecs: [ + { + id: 'app-a', + title: 'AppA', + pluginId: 'pluginA', + }, + { + id: 'app-b', + title: 'AppB', + pluginId: 'pluginA', + }, + ], + }), + createPluginSpecs('pluginA') + ); + + expect(navlinks.length).toEqual(2); + expect(navlinks[0]).toEqual( + expect.objectContaining({ + id: 'app-a', + title: 'AppA', + url: '/app/app-a', + }) + ); + expect(navlinks[1]).toEqual( + expect.objectContaining({ + id: 'app-b', + title: 'AppB', + url: '/app/app-b', + }) + ); + }); + + it('uses the app id to generates the navlink id even if pluginId is specified', () => { + const navlinks = getNavLinks( + createLegacyExports({ + uiAppSpecs: [ + { + id: 'app-a', + title: 'AppA', + pluginId: 'pluginA', + }, + { + id: 'app-b', + title: 'AppB', + pluginId: 'pluginA', + }, + ], + }), + createPluginSpecs('pluginA') + ); + + expect(navlinks.length).toEqual(2); + expect(navlinks[0].id).toEqual('app-a'); + expect(navlinks[1].id).toEqual('app-b'); + }); + + it('throws if an app reference a missing plugin', () => { + expect(() => { + getNavLinks( + createLegacyExports({ + uiAppSpecs: [ + { + id: 'app-a', + title: 'AppA', + pluginId: 'notExistingPlugin', + }, + ], + }), + createPluginSpecs('pluginA') + ); + }).toThrowErrorMatchingInlineSnapshot(`"Unknown plugin id \\"notExistingPlugin\\""`); + }); + + it('uses all known properties of the navlink', () => { + const navlinks = getNavLinks( + createLegacyExports({ + uiAppSpecs: [ + { + id: 'app-a', + title: 'AppA', + category: { + label: 'My Category', + }, + order: 42, + url: '/some-custom-url', + icon: 'fa-snowflake', + euiIconType: 'euiIcon', + linkToLastSubUrl: true, + hidden: false, + }, + ], + }), + [] + ); + expect(navlinks.length).toBe(1); + expect(navlinks[0]).toEqual({ + id: 'app-a', + title: 'AppA', + category: { + label: 'My Category', + }, + order: 42, + url: '/some-custom-url', + icon: 'fa-snowflake', + euiIconType: 'euiIcon', + linkToLastSubUrl: true, + }); + }); + }); + + describe('generating from navLinkSpecs', () => { + it('generates navlinks from legacy navLink specs', () => { + const navlinks = getNavLinks( + createLegacyExports({ + navLinkSpecs: [ + { + id: 'link-a', + title: 'AppA', + url: '/some-custom-url', + }, + { + id: 'link-b', + title: 'AppB', + url: '/some-other-url', + disableSubUrlTracking: true, + }, + ], + }), + createPluginSpecs('pluginA') + ); + + expect(navlinks.length).toEqual(2); + expect(navlinks[0]).toEqual( + expect.objectContaining({ + id: 'link-a', + title: 'AppA', + url: '/some-custom-url', + hidden: false, + disabled: false, + }) + ); + expect(navlinks[1]).toEqual( + expect.objectContaining({ + id: 'link-b', + title: 'AppB', + url: '/some-other-url', + disableSubUrlTracking: true, + }) + ); + }); + + it('only uses known properties to create the navlink', () => { + const navlinks = getNavLinks( + createLegacyExports({ + navLinkSpecs: [ + { + id: 'link-a', + title: 'AppA', + category: { + label: 'My Second Cat', + }, + order: 72, + url: '/some-other-custom', + subUrlBase: '/some-other-custom/sub', + disableSubUrlTracking: true, + icon: 'fa-corn', + euiIconType: 'euiIconBis', + linkToLastSubUrl: false, + hidden: false, + tooltip: 'My other tooltip', + }, + ], + }), + [] + ); + expect(navlinks.length).toBe(1); + expect(navlinks[0]).toEqual({ + id: 'link-a', + title: 'AppA', + category: { + label: 'My Second Cat', + }, + order: 72, + url: '/some-other-custom', + subUrlBase: '/some-other-custom/sub', + disableSubUrlTracking: true, + icon: 'fa-corn', + euiIconType: 'euiIconBis', + linkToLastSubUrl: false, + hidden: false, + disabled: false, + tooltip: 'My other tooltip', + }); + }); + }); + + describe('generating from both apps and navlinks', () => { + const navlinks = getNavLinks( + createLegacyExports({ + uiAppSpecs: [ + { + id: 'app-a', + title: 'AppA', + }, + { + id: 'app-b', + title: 'AppB', + }, + ], + navLinkSpecs: [ + { + id: 'link-a', + title: 'AppA', + url: '/some-custom-url', + }, + { + id: 'link-b', + title: 'AppB', + url: '/url-b', + disableSubUrlTracking: true, + }, + ], + }), + [] + ); + + expect(navlinks.length).toBe(4); + expect(navlinks).toMatchSnapshot(); + }); +}); diff --git a/src/core/server/legacy/plugins/get_nav_links.ts b/src/core/server/legacy/plugins/get_nav_links.ts new file mode 100644 index 0000000000000..067fb204ca7f3 --- /dev/null +++ b/src/core/server/legacy/plugins/get_nav_links.ts @@ -0,0 +1,82 @@ +/* + * 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 { + LegacyUiExports, + LegacyNavLink, + LegacyPluginSpec, + LegacyNavLinkSpec, + LegacyAppSpec, +} from '../types'; + +function legacyAppToNavLink(spec: LegacyAppSpec): LegacyNavLink { + if (!spec.id) { + throw new Error('Every app must specify an id'); + } + return { + id: spec.id, + category: spec.category, + title: spec.title ?? spec.id, + order: typeof spec.order === 'number' ? spec.order : 0, + icon: spec.icon, + euiIconType: spec.euiIconType, + url: spec.url || `/app/${spec.id}`, + linkToLastSubUrl: spec.linkToLastSubUrl ?? true, + }; +} + +function legacyLinkToNavLink(spec: LegacyNavLinkSpec): LegacyNavLink { + return { + id: spec.id, + category: spec.category, + title: spec.title, + order: typeof spec.order === 'number' ? spec.order : 0, + url: spec.url, + subUrlBase: spec.subUrlBase || spec.url, + disableSubUrlTracking: spec.disableSubUrlTracking, + icon: spec.icon, + euiIconType: spec.euiIconType, + linkToLastSubUrl: spec.linkToLastSubUrl ?? true, + hidden: spec.hidden ?? false, + disabled: spec.disabled ?? false, + tooltip: spec.tooltip ?? '', + }; +} + +function isHidden(app: LegacyAppSpec) { + return app.listed === false || app.hidden === true; +} + +export function getNavLinks(uiExports: LegacyUiExports, pluginSpecs: LegacyPluginSpec[]) { + const navLinkSpecs = uiExports.navLinkSpecs || []; + const appSpecs = (uiExports.uiAppSpecs || []).filter( + app => app !== undefined && !isHidden(app) + ) as LegacyAppSpec[]; + + const pluginIds = (pluginSpecs || []).map(spec => spec.getId()); + appSpecs.forEach(spec => { + if (spec.pluginId && !pluginIds.includes(spec.pluginId)) { + throw new Error(`Unknown plugin id "${spec.pluginId}"`); + } + }); + + return [...navLinkSpecs.map(legacyLinkToNavLink), ...appSpecs.map(legacyAppToNavLink)].sort( + (a, b) => a.order - b.order + ); +} diff --git a/src/core/server/legacy/types.ts b/src/core/server/legacy/types.ts index d51058ca561c6..0c1a7730f92a7 100644 --- a/src/core/server/legacy/types.ts +++ b/src/core/server/legacy/types.ts @@ -131,16 +131,20 @@ export type VarsReplacer = ( * @internal * @deprecated */ -export type LegacyNavLinkSpec = Record & ChromeNavLink; +export type LegacyNavLinkSpec = Partial & { + id: string; + title: string; + url: string; +}; /** * @internal * @deprecated */ -export type LegacyAppSpec = Pick< - ChromeNavLink, - 'title' | 'order' | 'icon' | 'euiIconType' | 'url' | 'linkToLastSubUrl' | 'hidden' | 'category' -> & { pluginId?: string; id?: string; listed?: boolean }; +export type LegacyAppSpec = Partial & { + pluginId?: string; + listed?: boolean; +}; /** * @internal diff --git a/src/core/server/logging/README.md b/src/core/server/logging/README.md index 65fe64b045801..3fbec7a45148d 100644 --- a/src/core/server/logging/README.md +++ b/src/core/server/logging/README.md @@ -1,4 +1,12 @@ # Logging +- [Loggers, Appenders and Layouts](#loggers-appenders-and-layouts) +- [Logger hierarchy](#logger-hierarchy) +- [Log level](#log-level) +- [Layouts](#layouts) + - [Pattern layout](#pattern-layout) + - [JSON layout](#json-layout) +- [Configuration](#configuration) +- [Usage](#usage) The way logging works in Kibana is inspired by `log4j 2` logging framework used by [Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/settings.html#logging). The main idea is to have consistent logging behaviour (configuration, log format etc.) across the entire Elastic Stack @@ -52,12 +60,68 @@ custom appenders, so one should always make the choice explicitly. There are two types of layout supported at the moment: `pattern` and `json`. -With `pattern` layout it's possible to define a string pattern with special placeholders wrapped into curly braces that +### Pattern layout +With `pattern` layout it's possible to define a string pattern with special placeholders `%conversion_pattern` (see the table below) that will be replaced with data from the actual log message. By default the following pattern is used: -`[{timestamp}][{level}][{context}] {message}`. Also `highlight` option can be enabled for `pattern` layout so that +`[%date][%level][%logger]%meta %message`. Also `highlight` option can be enabled for `pattern` layout so that some parts of the log message are highlighted with different colors that may be quite handy if log messages are forwarded to the terminal with color support. +`pattern` layout uses a sub-set of [log4j2 pattern syntax](https://logging.apache.org/log4j/2.x/manual/layouts.html#PatternLayout) +and **doesn't implement** all `log4j2` capabilities. The conversions that are provided out of the box are: +#### level +Outputs the [level](#log-level) of the logging event. +Example of `%level` output: +```bash +TRACE +DEBUG +INFO +``` + +##### logger +Outputs the name of the logger that published the logging event. +Example of `%logger` output: +```bash +server +server.http +server.http.Kibana +``` + +#### message +Outputs the application supplied message associated with the logging event. + +#### meta +Outputs the entries of `meta` object data in **json** format, if one is present in the event. +Example of `%meta` output: +```bash +// Meta{from: 'v7', to: 'v8'} +'{"from":"v7","to":"v8"}' +// Meta empty object +'{}' +// no Meta provided +'' +``` + +##### date +Outputs the date of the logging event. The date conversion specifier may be followed by a set of braces containing a name of predefined date format and canonical timezone name. +Timezone name is expected to be one from [TZ database name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) +Example of `%date` output: + +| Conversion pattern | Example | +| ---------------------------------------- | ---------------------------------------------------------------- | +| `%date` | `2012-02-01T14:30:22.011Z` uses `ISO8601` format by default | +| `%date{ISO8601}` | `2012-02-01T14:30:22.011Z` | +| `%date{ISO8601_TZ}` | `2012-02-01T09:30:22.011-05:00` `ISO8601` with timezone | +| `%date{ISO8601_TZ}{America/Los_Angeles}` | `2012-02-01T06:30:22.011-08:00` | +| `%date{ABSOLUTE}` | `09:30:22.011` | +| `%date{ABSOLUTE}{America/Los_Angeles}` | `06:30:22.011` | +| `%date{UNIX}` | `1328106622` | +| `%date{UNIX_MILLIS}` | `1328106622011` | + +#### pid +Outputs the process ID. + +### JSON layout With `json` layout log messages will be formatted as JSON strings that include timestamp, log level, context, message text and any other metadata that may be associated with the log message itself. @@ -88,7 +152,7 @@ logging: kind: console layout: kind: pattern - pattern: [{timestamp}][{level}] {message} + pattern: "[%date][%level] %message" json-file-appender: kind: file path: /var/log/kibana-json.log @@ -179,3 +243,81 @@ The log will be less verbose with `warn` level for the `server` context: [2017-07-25T18:54:41.639Z][ERROR][server] Message with `error` log level. [2017-07-25T18:54:41.639Z][FATAL][server] Message with `fatal` log level. ``` + +### Logging config migration +Compatibility with the legacy logging system is assured until the end of the `v7` version. +All log messages handled by `root` context are forwarded to the legacy logging service. If you re-write +root appenders, make sure that it contains `default` appender to provide backward compatibility. +**Note**: If you define an appender for a context, the log messages aren't handled by the +`root` context anymore and not forwarded to the legacy logging service. + +#### logging.dest +By default logs in *stdout*. With new Kibana logging you can use pre-existing `console` appender or +define a custom one. +```yaml +logging: + loggers: + - context: your-plugin + appenders: [console] +``` +Logs in a *file* if given file path. You should define a custom appender with `kind: file` +```yaml + +logging: + appenders: + file: + kind: file + path: /var/log/kibana.log + layout: + kind: pattern + loggers: + - context: your-plugin + appenders: [file] +``` +#### logging.json +Defines the format of log output. Logs in JSON if `true`. With new logging config you can adjust +the output format with [layouts](#layouts). + +#### logging.quiet +Suppresses all logging output other than error messages. With new logging, config can be achieved +with adjusting minimum required [logging level](#log-level) +```yaml + loggers: + - context: my-plugin + appenders: [console] + level: error +# or for all output +logging.root.level: error +``` + +#### logging.silent: +Suppresses all logging output. +```yaml +logging.root.level: off +``` + +#### logging.verbose: +Logs all events +```yaml +logging.root.level: all +``` + +#### logging.timezone +Set to the canonical timezone id to log events using that timezone. New logging config allows +to [specify timezone](#date) for `layout: pattern`. +```yaml +logging: + appenders: + custom-console: + kind: console + layout: + kind: pattern + highlight: true + pattern: "[%level] [%date{ISO8601_TZ}{America/Los_Angeles}][%logger] %message" +``` + +#### logging.events +Define a custom logger for a specific context. + +#### logging.filter +TBD diff --git a/src/core/server/logging/integration_tests/logging.test.ts b/src/core/server/logging/integration_tests/logging.test.ts index 7142f91300f12..b88f5ba2c2b60 100644 --- a/src/core/server/logging/integration_tests/logging.test.ts +++ b/src/core/server/logging/integration_tests/logging.test.ts @@ -29,7 +29,7 @@ function createRoot() { layout: { highlight: false, kind: 'pattern', - pattern: '{level}|{context}|{message}', + pattern: '%level|%logger|%message', }, }, }, diff --git a/src/core/server/logging/layouts/__snapshots__/json_layout.test.ts.snap b/src/core/server/logging/layouts/__snapshots__/json_layout.test.ts.snap index da57023c94286..14c071b40ad7a 100644 --- a/src/core/server/logging/layouts/__snapshots__/json_layout.test.ts.snap +++ b/src/core/server/logging/layouts/__snapshots__/json_layout.test.ts.snap @@ -1,9 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`\`format()\` correctly formats error record with meta-data 1`] = `"{\\"@timestamp\\":\\"2012-02-01T09:30:22.011-05:00\\",\\"context\\":\\"context-with-meta\\",\\"level\\":\\"DEBUG\\",\\"message\\":\\"message-with-meta\\",\\"meta\\":{\\"from\\":\\"v7\\",\\"to\\":\\"v8\\"},\\"pid\\":5355}"`; - -exports[`\`format()\` correctly formats record with meta-data 1`] = `"{\\"@timestamp\\":\\"2012-02-01T09:30:22.011-05:00\\",\\"context\\":\\"context-with-meta\\",\\"level\\":\\"DEBUG\\",\\"message\\":\\"message-with-meta\\",\\"meta\\":{\\"from\\":\\"v7\\",\\"to\\":\\"v8\\"},\\"pid\\":5355}"`; - exports[`\`format()\` correctly formats record. 1`] = `"{\\"@timestamp\\":\\"2012-02-01T09:30:22.011-05:00\\",\\"context\\":\\"context-1\\",\\"error\\":{\\"message\\":\\"Some error message\\",\\"name\\":\\"Some error name\\",\\"stack\\":\\"Some error stack\\"},\\"level\\":\\"FATAL\\",\\"message\\":\\"message-1\\",\\"pid\\":5355}"`; exports[`\`format()\` correctly formats record. 2`] = `"{\\"@timestamp\\":\\"2012-02-01T09:30:22.011-05:00\\",\\"context\\":\\"context-2\\",\\"level\\":\\"ERROR\\",\\"message\\":\\"message-2\\",\\"pid\\":5355}"`; diff --git a/src/core/server/logging/layouts/conversions/timestamp.ts b/src/core/server/logging/layouts/conversions/date.ts similarity index 83% rename from src/core/server/logging/layouts/conversions/timestamp.ts rename to src/core/server/logging/layouts/conversions/date.ts index 6db6fc6eeb6bf..d3ed54fb98240 100644 --- a/src/core/server/logging/layouts/conversions/timestamp.ts +++ b/src/core/server/logging/layouts/conversions/date.ts @@ -22,7 +22,7 @@ import { last } from 'lodash'; import { Conversion } from './type'; import { LogRecord } from '../../log_record'; -const timestampRegExp = /{timestamp({(?[^}]+)})?({(?[^}]+)})?}/gi; +const dateRegExp = /%date({(?[^}]+)})?({(?[^}]+)})?/g; const formats = { ISO8601: 'ISO8601', @@ -54,10 +54,11 @@ function formatDate(date: Date, dateFormat: string = formats.ISO8601, timezone?: } function validateDateFormat(input: string) { - if (Reflect.has(formats, input)) return; - throw new Error( - `Date format expected one of ${Reflect.ownKeys(formats).join(', ')}, but given: ${input}` - ); + if (!Reflect.has(formats, input)) { + throw new Error( + `Date format expected one of ${Reflect.ownKeys(formats).join(', ')}, but given: ${input}` + ); + } } function validateTimezone(timezone: string) { @@ -66,7 +67,7 @@ function validateTimezone(timezone: string) { } function validate(rawString: string) { - for (const matched of rawString.matchAll(timestampRegExp)) { + for (const matched of rawString.matchAll(dateRegExp)) { const { format, timezone } = matched.groups!; if (format) { @@ -78,9 +79,9 @@ function validate(rawString: string) { } } -export const TimestampConversion: Conversion = { - pattern: timestampRegExp, - formatter(record: LogRecord, highlight: boolean, ...matched: any[]) { +export const DateConversion: Conversion = { + pattern: dateRegExp, + convert(record: LogRecord, highlight: boolean, ...matched: any[]) { const groups: Record = last(matched); const { format, timezone } = groups; diff --git a/src/core/server/logging/layouts/conversions/index.ts b/src/core/server/logging/layouts/conversions/index.ts new file mode 100644 index 0000000000000..23e6aded6c6f7 --- /dev/null +++ b/src/core/server/logging/layouts/conversions/index.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +export { Conversion } from './type'; + +export { LoggerConversion } from './logger'; +export { LevelConversion } from './level'; +export { MessageConversion } from './message'; +export { MetaConversion } from './meta'; +export { PidConversion } from './pid'; +export { DateConversion } from './date'; diff --git a/src/core/server/logging/layouts/conversions/level.ts b/src/core/server/logging/layouts/conversions/level.ts index 02ed86dd2c24f..58b271140eff5 100644 --- a/src/core/server/logging/layouts/conversions/level.ts +++ b/src/core/server/logging/layouts/conversions/level.ts @@ -32,8 +32,8 @@ const LEVEL_COLORS = new Map([ ]); export const LevelConversion: Conversion = { - pattern: /{level}/gi, - formatter(record: LogRecord, highlight: boolean) { + pattern: /%level/g, + convert(record: LogRecord, highlight: boolean) { let message = record.level.id.toUpperCase().padEnd(5); if (highlight && LEVEL_COLORS.has(record.level)) { const color = LEVEL_COLORS.get(record.level)!; diff --git a/src/core/server/logging/layouts/conversions/context.ts b/src/core/server/logging/layouts/conversions/logger.ts similarity index 89% rename from src/core/server/logging/layouts/conversions/context.ts rename to src/core/server/logging/layouts/conversions/logger.ts index d1fa9ca84f555..debb1737ab95a 100644 --- a/src/core/server/logging/layouts/conversions/context.ts +++ b/src/core/server/logging/layouts/conversions/logger.ts @@ -22,9 +22,9 @@ import chalk from 'chalk'; import { Conversion } from './type'; import { LogRecord } from '../../log_record'; -export const ContextConversion: Conversion = { - pattern: /{context}/gi, - formatter(record: LogRecord, highlight: boolean) { +export const LoggerConversion: Conversion = { + pattern: /%logger/g, + convert(record: LogRecord, highlight: boolean) { let message = record.context; if (highlight) { message = chalk.magenta(message); diff --git a/src/core/server/logging/layouts/conversions/message.ts b/src/core/server/logging/layouts/conversions/message.ts index b95a89b12b780..f8c5e68ada4fb 100644 --- a/src/core/server/logging/layouts/conversions/message.ts +++ b/src/core/server/logging/layouts/conversions/message.ts @@ -21,8 +21,8 @@ import { Conversion } from './type'; import { LogRecord } from '../../log_record'; export const MessageConversion: Conversion = { - pattern: /{message}/gi, - formatter(record: LogRecord) { + pattern: /%message/g, + convert(record: LogRecord) { // Error stack is much more useful than just the message. return (record.error && record.error.stack) || record.message; }, diff --git a/src/core/server/logging/layouts/conversions/meta.ts b/src/core/server/logging/layouts/conversions/meta.ts index f6d4557e0db53..ee8c207389fbe 100644 --- a/src/core/server/logging/layouts/conversions/meta.ts +++ b/src/core/server/logging/layouts/conversions/meta.ts @@ -20,8 +20,8 @@ import { Conversion } from './type'; import { LogRecord } from '../../log_record'; export const MetaConversion: Conversion = { - pattern: /{meta}/gi, - formatter(record: LogRecord) { - return record.meta ? `[${JSON.stringify(record.meta)}]` : ''; + pattern: /%meta/g, + convert(record: LogRecord) { + return record.meta ? `${JSON.stringify(record.meta)}` : ''; }, }; diff --git a/src/core/server/logging/layouts/conversions/pid.ts b/src/core/server/logging/layouts/conversions/pid.ts index 0fcdd93fcda0c..37d34a4f1cf8b 100644 --- a/src/core/server/logging/layouts/conversions/pid.ts +++ b/src/core/server/logging/layouts/conversions/pid.ts @@ -21,8 +21,8 @@ import { Conversion } from './type'; import { LogRecord } from '../../log_record'; export const PidConversion: Conversion = { - pattern: /{pid}/gi, - formatter(record: LogRecord) { + pattern: /%pid/g, + convert(record: LogRecord) { return String(record.pid); }, }; diff --git a/src/core/server/logging/layouts/conversions/type.ts b/src/core/server/logging/layouts/conversions/type.ts index 34a6475138814..a57a1f954e53a 100644 --- a/src/core/server/logging/layouts/conversions/type.ts +++ b/src/core/server/logging/layouts/conversions/type.ts @@ -20,6 +20,6 @@ import { LogRecord } from 'kibana/server'; export interface Conversion { pattern: RegExp; - formatter: (record: LogRecord, highlight: boolean) => string; + convert: (record: LogRecord, highlight: boolean) => string; validate?: (input: string) => void; } diff --git a/src/core/server/logging/layouts/json_layout.test.ts b/src/core/server/logging/layouts/json_layout.test.ts index ec8c44ec62a22..77e2876c143da 100644 --- a/src/core/server/logging/layouts/json_layout.test.ts +++ b/src/core/server/logging/layouts/json_layout.test.ts @@ -90,34 +90,68 @@ test('`format()` correctly formats record with meta-data', () => { const layout = new JsonLayout(); expect( - layout.format({ - context: 'context-with-meta', - level: LogLevel.Debug, - message: 'message-with-meta', - timestamp, - pid: 5355, - meta: { - from: 'v7', - to: 'v8', - }, - }) - ).toMatchSnapshot(); + JSON.parse( + layout.format({ + context: 'context-with-meta', + level: LogLevel.Debug, + message: 'message-with-meta', + timestamp, + pid: 5355, + meta: { + from: 'v7', + to: 'v8', + }, + }) + ) + ).toStrictEqual({ + '@timestamp': '2012-02-01T09:30:22.011-05:00', + context: 'context-with-meta', + level: 'DEBUG', + message: 'message-with-meta', + meta: { + from: 'v7', + to: 'v8', + }, + pid: 5355, + }); }); test('`format()` correctly formats error record with meta-data', () => { const layout = new JsonLayout(); expect( - layout.format({ - context: 'context-with-meta', - level: LogLevel.Debug, - message: 'message-with-meta', - timestamp, - pid: 5355, - meta: { - from: 'v7', - to: 'v8', - }, - }) - ).toMatchSnapshot(); + JSON.parse( + layout.format({ + context: 'error-with-meta', + level: LogLevel.Debug, + error: { + message: 'Some error message', + name: 'Some error name', + stack: 'Some error stack', + }, + message: 'Some error message', + timestamp, + pid: 5355, + meta: { + from: 'v7', + to: 'v8', + }, + }) + ) + ).toStrictEqual({ + '@timestamp': '2012-02-01T09:30:22.011-05:00', + context: 'error-with-meta', + level: 'DEBUG', + error: { + message: 'Some error message', + name: 'Some error name', + stack: 'Some error stack', + }, + message: 'Some error message', + meta: { + from: 'v7', + to: 'v8', + }, + pid: 5355, + }); }); diff --git a/src/core/server/logging/layouts/layouts.test.ts b/src/core/server/logging/layouts/layouts.test.ts index aa1c54c846bc6..b1fb836f40d5d 100644 --- a/src/core/server/logging/layouts/layouts.test.ts +++ b/src/core/server/logging/layouts/layouts.test.ts @@ -33,12 +33,12 @@ test('`configSchema` creates correct schema for `pattern` layout.', () => { const validConfig = { highlight: true, kind: 'pattern', - pattern: '{message}', + pattern: '%message', }; expect(layoutsSchema.validate(validConfig)).toEqual({ highlight: true, kind: 'pattern', - pattern: '{message}', + pattern: '%message', }); const wrongConfig2 = { kind: 'pattern', pattern: 1 }; @@ -56,7 +56,7 @@ test('`create()` creates correct layout.', () => { const patternLayout = Layouts.create({ highlight: false, kind: 'pattern', - pattern: '[{timestamp}][{level}][{context}] {message}', + pattern: '[%date][%level][%logger] %message', }); expect(patternLayout).toBeInstanceOf(PatternLayout); diff --git a/src/core/server/logging/layouts/pattern_layout.test.ts b/src/core/server/logging/layouts/pattern_layout.test.ts index 2d948ea59c6d1..cce55b147e0ed 100644 --- a/src/core/server/logging/layouts/pattern_layout.test.ts +++ b/src/core/server/logging/layouts/pattern_layout.test.ts @@ -88,12 +88,12 @@ test('`createConfigSchema()` creates correct schema.', () => { const validConfig = { highlight: true, kind: 'pattern', - pattern: '{message}', + pattern: '%message', }; expect(layoutSchema.validate(validConfig)).toEqual({ highlight: true, kind: 'pattern', - pattern: '{message}', + pattern: '%message', }); const wrongConfig1 = { kind: 'json' }; @@ -112,7 +112,7 @@ test('`format()` correctly formats record with full pattern.', () => { }); test('`format()` correctly formats record with custom pattern.', () => { - const layout = new PatternLayout('mock-{message}-{context}-{message}'); + const layout = new PatternLayout('mock-%message-%logger-%message'); for (const record of records) { expect(layout.format(record)).toMatchSnapshot(); @@ -134,7 +134,7 @@ test('`format()` correctly formats record with meta data.', () => { to: 'v8', }, }) - ).toBe('[2012-02-01T14:30:22.011Z][DEBUG][context-meta][{"from":"v7","to":"v8"}] message-meta'); + ).toBe('[2012-02-01T14:30:22.011Z][DEBUG][context-meta]{"from":"v7","to":"v8"} message-meta'); expect( layout.format({ @@ -145,7 +145,7 @@ test('`format()` correctly formats record with meta data.', () => { pid: 5355, meta: {}, }) - ).toBe('[2012-02-01T14:30:22.011Z][DEBUG][context-meta][{}] message-meta'); + ).toBe('[2012-02-01T14:30:22.011Z][DEBUG][context-meta]{} message-meta'); expect( layout.format({ @@ -167,7 +167,7 @@ test('`format()` correctly formats record with highlighting.', () => { }); test('allows specifying the PID in custom pattern', () => { - const layout = new PatternLayout('{pid}-{context}-{message}'); + const layout = new PatternLayout('%pid-%logger-%message'); for (const record of records) { expect(layout.format(record)).toMatchSnapshot(); @@ -175,7 +175,7 @@ test('allows specifying the PID in custom pattern', () => { }); test('`format()` allows specifying pattern with meta.', () => { - const layout = new PatternLayout('{context}-{meta}-{message}'); + const layout = new PatternLayout('%logger-%meta-%message'); const record = { context: 'context', level: LogLevel.Debug, @@ -187,7 +187,7 @@ test('`format()` allows specifying pattern with meta.', () => { to: 'v8', }, }; - expect(layout.format(record)).toBe('context-[{"from":"v7","to":"v8"}]-message'); + expect(layout.format(record)).toBe('context-{"from":"v7","to":"v8"}-message'); }); describe('format', () => { @@ -207,31 +207,31 @@ describe('format', () => { describe('supports specifying a predefined format', () => { it('ISO8601', () => { - const layout = new PatternLayout('[{timestamp{ISO8601}}][{context}]'); + const layout = new PatternLayout('[%date{ISO8601}][%logger]'); expect(layout.format(record)).toBe('[2012-02-01T14:30:22.011Z][context]'); }); it('ISO8601_TZ', () => { - const layout = new PatternLayout('[{timestamp{ISO8601_TZ}}][{context}]'); + const layout = new PatternLayout('[%date{ISO8601_TZ}][%logger]'); expect(layout.format(record)).toBe('[2012-02-01T09:30:22.011-05:00][context]'); }); it('ABSOLUTE', () => { - const layout = new PatternLayout('[{timestamp{ABSOLUTE}}][{context}]'); + const layout = new PatternLayout('[%date{ABSOLUTE}][%logger]'); expect(layout.format(record)).toBe('[09:30:22.011][context]'); }); it('UNIX', () => { - const layout = new PatternLayout('[{timestamp{UNIX}}][{context}]'); + const layout = new PatternLayout('[%date{UNIX}][%logger]'); expect(layout.format(record)).toBe('[1328106622][context]'); }); it('UNIX_MILLIS', () => { - const layout = new PatternLayout('[{timestamp{UNIX_MILLIS}}][{context}]'); + const layout = new PatternLayout('[%date{UNIX_MILLIS}][%logger]'); expect(layout.format(record)).toBe('[1328106622011][context]'); }); @@ -239,42 +239,38 @@ describe('format', () => { describe('supports specifying a predefined format and timezone', () => { it('ISO8601', () => { - const layout = new PatternLayout('[{timestamp{ISO8601}{America/Los_Angeles}}][{context}]'); + const layout = new PatternLayout('[%date{ISO8601}{America/Los_Angeles}][%logger]'); expect(layout.format(record)).toBe('[2012-02-01T14:30:22.011Z][context]'); }); it('ISO8601_TZ', () => { - const layout = new PatternLayout( - '[{timestamp{ISO8601_TZ}{America/Los_Angeles}}][{context}]' - ); + const layout = new PatternLayout('[%date{ISO8601_TZ}{America/Los_Angeles}][%logger]'); expect(layout.format(record)).toBe('[2012-02-01T06:30:22.011-08:00][context]'); }); it('ABSOLUTE', () => { - const layout = new PatternLayout('[{timestamp{ABSOLUTE}{America/Los_Angeles}}][{context}]'); + const layout = new PatternLayout('[%date{ABSOLUTE}{America/Los_Angeles}][%logger]'); expect(layout.format(record)).toBe('[06:30:22.011][context]'); }); it('UNIX', () => { - const layout = new PatternLayout('[{timestamp{UNIX}{America/Los_Angeles}}][{context}]'); + const layout = new PatternLayout('[%date{UNIX}{America/Los_Angeles}][%logger]'); expect(layout.format(record)).toBe('[1328106622][context]'); }); it('UNIX_MILLIS', () => { - const layout = new PatternLayout( - '[{timestamp{UNIX_MILLIS}{America/Los_Angeles}}][{context}]' - ); + const layout = new PatternLayout('[%date{UNIX_MILLIS}{America/Los_Angeles}][%logger]'); expect(layout.format(record)).toBe('[1328106622011][context]'); }); }); it('formats several conversions patterns correctly', () => { const layout = new PatternLayout( - '[{timestamp{ABSOLUTE}{America/Los_Angeles}}][{context}][{timestamp{UNIX}}]' + '[%date{ABSOLUTE}{America/Los_Angeles}][%logger][%date{UNIX}]' ); expect(layout.format(record)).toBe('[06:30:22.011][context][1328106622]'); @@ -284,45 +280,44 @@ describe('format', () => { describe('schema', () => { describe('pattern', () => { - describe('{timestamp}', () => { - it('does not fail when {timestamp} not present', () => { + describe('%date', () => { + it('does not fail when %date not present', () => { expect(patternSchema.validate('')).toBe(''); expect(patternSchema.validate('{pid}')).toBe('{pid}'); }); - it('does not fail on {timestamp} without params', () => { - expect(patternSchema.validate('{timestamp}')).toBe('{timestamp}'); - expect(patternSchema.validate('{timestamp}}')).toBe('{timestamp}}'); - expect(patternSchema.validate('{{timestamp}}')).toBe('{{timestamp}}'); + it('does not fail on %date without params', () => { + expect(patternSchema.validate('%date')).toBe('%date'); + expect(patternSchema.validate('%date')).toBe('%date'); + expect(patternSchema.validate('{%date}')).toBe('{%date}'); + expect(patternSchema.validate('%date%date')).toBe('%date%date'); }); - it('does not fail on {timestamp} with predefined date format', () => { - expect(patternSchema.validate('{timestamp{ISO8601}}')).toBe('{timestamp{ISO8601}}'); + it('does not fail on %date with predefined date format', () => { + expect(patternSchema.validate('%date{ISO8601}')).toBe('%date{ISO8601}'); }); - it('does not fail on {timestamp} with predefined date format and valid timezone', () => { - expect(patternSchema.validate('{timestamp{ISO8601_TZ}{Europe/Berlin}}')).toBe( - '{timestamp{ISO8601_TZ}{Europe/Berlin}}' + it('does not fail on %date with predefined date format and valid timezone', () => { + expect(patternSchema.validate('%date{ISO8601_TZ}{Europe/Berlin}')).toBe( + '%date{ISO8601_TZ}{Europe/Berlin}' ); }); - it('fails on {timestamp} with unknown date format', () => { - expect(() => - patternSchema.validate('{timestamp{HH:MM:SS}}') - ).toThrowErrorMatchingInlineSnapshot( + it('fails on %date with unknown date format', () => { + expect(() => patternSchema.validate('%date{HH:MM:SS}')).toThrowErrorMatchingInlineSnapshot( `"Date format expected one of ISO8601, ISO8601_TZ, ABSOLUTE, UNIX, UNIX_MILLIS, but given: HH:MM:SS"` ); }); - it('fails on {timestamp} with predefined date format and invalid timezone', () => { + it('fails on %date with predefined date format and invalid timezone', () => { expect(() => - patternSchema.validate('{timestamp{ISO8601_TZ}{Europe/Kibana}}') + patternSchema.validate('%date{ISO8601_TZ}{Europe/Kibana}') ).toThrowErrorMatchingInlineSnapshot(`"Unknown timezone: Europe/Kibana"`); }); - it('validates several {timestamp} in pattern', () => { + it('validates several %date in pattern', () => { expect(() => - patternSchema.validate('{timestamp{ISO8601_TZ}{Europe/Berlin}}{message}{timestamp{HH}}') + patternSchema.validate('%date{ISO8601_TZ}{Europe/Berlin}%message%date{HH}') ).toThrowErrorMatchingInlineSnapshot( `"Date format expected one of ISO8601, ISO8601_TZ, ABSOLUTE, UNIX, UNIX_MILLIS, but given: HH"` ); diff --git a/src/core/server/logging/layouts/pattern_layout.ts b/src/core/server/logging/layouts/pattern_layout.ts index 0a2a25a135069..9490db149cc0f 100644 --- a/src/core/server/logging/layouts/pattern_layout.ts +++ b/src/core/server/logging/layouts/pattern_layout.ts @@ -21,23 +21,24 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { LogRecord } from '../log_record'; import { Layout } from './layouts'; - -import { Conversion } from './conversions/type'; -import { ContextConversion } from './conversions/context'; -import { LevelConversion } from './conversions/level'; -import { MetaConversion } from './conversions/meta'; -import { MessageConversion } from './conversions/message'; -import { PidConversion } from './conversions/pid'; -import { TimestampConversion } from './conversions/timestamp'; +import { + Conversion, + LoggerConversion, + LevelConversion, + MetaConversion, + MessageConversion, + PidConversion, + DateConversion, +} from './conversions'; /** * Default pattern used by PatternLayout if it's not overridden in the configuration. */ -const DEFAULT_PATTERN = `[{timestamp}][{level}][{context}]{meta} {message}`; +const DEFAULT_PATTERN = `[%date][%level][%logger]%meta %message`; export const patternSchema = schema.string({ validate: string => { - TimestampConversion.validate!(string); + DateConversion.validate!(string); }, }); @@ -48,12 +49,12 @@ const patternLayoutSchema = schema.object({ }); const conversions: Conversion[] = [ - ContextConversion, + LoggerConversion, MessageConversion, LevelConversion, MetaConversion, PidConversion, - TimestampConversion, + DateConversion, ]; /** @internal */ @@ -77,7 +78,7 @@ export class PatternLayout implements Layout { for (const conversion of conversions) { recordString = recordString.replace( conversion.pattern, - conversion.formatter.bind(null, record, this.highlight) + conversion.convert.bind(null, record, this.highlight) ); } diff --git a/src/core/server/logging/logging_config.test.ts b/src/core/server/logging/logging_config.test.ts index b3631abb9ff00..75f571d34c25c 100644 --- a/src/core/server/logging/logging_config.test.ts +++ b/src/core/server/logging/logging_config.test.ts @@ -59,7 +59,7 @@ test('`getLoggerContext()` returns correct joined context name.', () => { test('correctly fills in default config.', () => { const configValue = new LoggingConfig(config.schema.validate({})); - expect(configValue.appenders.size).toBe(3); + expect(configValue.appenders.size).toBe(2); expect(configValue.appenders.get('default')).toEqual({ kind: 'console', @@ -69,10 +69,6 @@ test('correctly fills in default config.', () => { kind: 'console', layout: { kind: 'pattern', highlight: true }, }); - expect(configValue.appenders.get('file')).toEqual({ - kind: 'file', - layout: { kind: 'pattern', highlight: false }, - }); }); test('correctly fills in custom `appenders` config.', () => { @@ -83,16 +79,11 @@ test('correctly fills in custom `appenders` config.', () => { kind: 'console', layout: { kind: 'pattern' }, }, - file: { - kind: 'file', - layout: { kind: 'pattern' }, - path: 'path', - }, }, }) ); - expect(configValue.appenders.size).toBe(3); + expect(configValue.appenders.size).toBe(2); expect(configValue.appenders.get('default')).toEqual({ kind: 'console', @@ -103,12 +94,6 @@ test('correctly fills in custom `appenders` config.', () => { kind: 'console', layout: { kind: 'pattern' }, }); - - expect(configValue.appenders.get('file')).toEqual({ - kind: 'file', - layout: { kind: 'pattern' }, - path: 'path', - }); }); test('correctly fills in default `loggers` config.', () => { diff --git a/src/core/server/logging/logging_config.ts b/src/core/server/logging/logging_config.ts index f1fbf787737b4..8f80be7d79cb1 100644 --- a/src/core/server/logging/logging_config.ts +++ b/src/core/server/logging/logging_config.ts @@ -140,13 +140,6 @@ export class LoggingConfig { layout: { kind: 'pattern', highlight: true }, } as AppenderConfigType, ], - [ - 'file', - { - kind: 'file', - layout: { kind: 'pattern', highlight: false }, - } as AppenderConfigType, - ], ]); /** diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index 6768e85c8db17..df618b2c0a706 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -22,6 +22,7 @@ import { mockDiscover, mockPackage } from './plugins_service.test.mocks'; import { resolve, join } from 'path'; import { BehaviorSubject, from } from 'rxjs'; import { schema } from '@kbn/config-schema'; +import { createAbsolutePathSerializer } from '@kbn/dev-utils'; import { ConfigPath, ConfigService, Env } from '../config'; import { rawConfigServiceMock } from '../config/raw_config_service.mock'; @@ -48,6 +49,8 @@ let mockPluginSystem: jest.Mocked; const setupDeps = coreMock.createInternalSetup(); const logger = loggingServiceMock.create(); +expect.addSnapshotSerializer(createAbsolutePathSerializer()); + ['path-1', 'path-2', 'path-3', 'path-4', 'path-5'].forEach(path => { jest.doMock(join(path, 'server'), () => ({}), { virtual: true, @@ -540,10 +543,10 @@ describe('PluginsService', () => { expect(uiPlugins.internal).toMatchInlineSnapshot(` Map { "plugin-1" => Object { - "entryPointPath": "path-1/public", + "publicTargetDir": /path-1/target/public, }, "plugin-2" => Object { - "entryPointPath": "path-2/public", + "publicTargetDir": /path-2/target/public, }, } `); diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 5a50cf8ea8ba2..427cc19a8614f 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -17,6 +17,7 @@ * under the License. */ +import Path from 'path'; import { Observable } from 'rxjs'; import { filter, first, map, mergeMap, tap, toArray } from 'rxjs/operators'; import { CoreService } from '../../types'; @@ -214,7 +215,9 @@ export class PluginsService implements CoreService ({ elasticsearchUrl: url.format( Object.assign(url.parse(head(_legacyEsConfig.hosts)), { auth: false }) diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/index.ts b/src/legacy/core_plugins/dashboard_embeddable_container/index.ts index 714203de20385..4a609225e6d7f 100644 --- a/src/legacy/core_plugins/dashboard_embeddable_container/index.ts +++ b/src/legacy/core_plugins/dashboard_embeddable_container/index.ts @@ -17,13 +17,7 @@ * under the License. */ -import { resolve } from 'path'; - // eslint-disable-next-line import/no-default-export export default function(kibana: any) { - return new kibana.Plugin({ - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }, - }); + return new kibana.Plugin({}); } diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/index.scss b/src/legacy/core_plugins/dashboard_embeddable_container/public/index.scss deleted file mode 100644 index 548e85746f866..0000000000000 --- a/src/legacy/core_plugins/dashboard_embeddable_container/public/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - -@import '../../../../plugins/dashboard_embeddable_container/public/index'; diff --git a/src/legacy/core_plugins/data/index.ts b/src/legacy/core_plugins/data/index.ts index c91500cd545d4..428f0c305a375 100644 --- a/src/legacy/core_plugins/data/index.ts +++ b/src/legacy/core_plugins/data/index.ts @@ -37,7 +37,6 @@ export default function DataPlugin(kibana: any) { uiExports: { interpreter: ['plugins/data/search/expressions/boot'], injectDefaultVars: () => ({}), - styleSheetPaths: resolve(__dirname, 'public/index.scss'), mappings, savedObjectsManagement: { query: { diff --git a/src/legacy/core_plugins/data/public/index.scss b/src/legacy/core_plugins/data/public/index.scss deleted file mode 100644 index 22877e217279f..0000000000000 --- a/src/legacy/core_plugins/data/public/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - -@import '../../../../plugins/data/public/index' diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_hash.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_hash.test.ts index 5ff68c5426e34..f0ad595476486 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_hash.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_hash.test.ts @@ -17,9 +17,10 @@ * under the License. */ -import { geoHashBucketAgg, IBucketGeoHashGridAggConfig } from './geo_hash'; +import { geoHashBucketAgg } from './geo_hash'; import { AggConfigs, IAggConfigs } from '../agg_configs'; import { BUCKET_TYPES } from './bucket_agg_types'; +import { IBucketAggConfig } from './_bucket_agg_type'; jest.mock('ui/new_platform'); @@ -77,79 +78,26 @@ describe('Geohash Agg', () => { it('should select precision parameter', () => { expect(precisionParam.name).toEqual('precision'); }); - - describe('precision parameter write', () => { - const zoomToGeoHashPrecision: Record = { - 0: 1, - 1: 2, - 2: 2, - 3: 2, - 4: 3, - 5: 3, - 6: 4, - 7: 4, - 8: 4, - 9: 5, - 10: 5, - 11: 6, - 12: 6, - 13: 6, - 14: 7, - 15: 7, - 16: 8, - 17: 8, - 18: 8, - 19: 9, - 20: 9, - 21: 10, - }; - - Object.keys(zoomToGeoHashPrecision).forEach((zoomLevel: string) => { - it(`zoom level ${zoomLevel} should correspond to correct geohash-precision`, () => { - const aggConfigs = getAggConfigs({ - autoPrecision: true, - mapZoom: zoomLevel, - }); - - const { [BUCKET_TYPES.GEOHASH_GRID]: params } = aggConfigs.aggs[0].toDsl(); - - expect(params.precision).toEqual(zoomToGeoHashPrecision[zoomLevel]); - }); - }); - }); }); describe('getRequestAggs', () => { describe('initial aggregation creation', () => { let aggConfigs: IAggConfigs; - let geoHashGridAgg: IBucketGeoHashGridAggConfig; + let geoHashGridAgg: IBucketAggConfig; beforeEach(() => { aggConfigs = getAggConfigs(); - geoHashGridAgg = aggConfigs.aggs[0] as IBucketGeoHashGridAggConfig; + geoHashGridAgg = aggConfigs.aggs[0] as IBucketAggConfig; }); it('should create filter, geohash_grid, and geo_centroid aggregations', () => { - const requestAggs = geoHashBucketAgg.getRequestAggs( - geoHashGridAgg - ) as IBucketGeoHashGridAggConfig[]; + const requestAggs = geoHashBucketAgg.getRequestAggs(geoHashGridAgg) as IBucketAggConfig[]; expect(requestAggs.length).toEqual(3); expect(requestAggs[0].type.name).toEqual('filter'); expect(requestAggs[1].type.name).toEqual('geohash_grid'); expect(requestAggs[2].type.name).toEqual('geo_centroid'); }); - - it('should set mapCollar in vis session state', () => { - const [, geoHashAgg] = geoHashBucketAgg.getRequestAggs( - geoHashGridAgg - ) as IBucketGeoHashGridAggConfig[]; - - expect(geoHashAgg).toHaveProperty('lastMapCollar'); - expect(geoHashAgg.lastMapCollar).toHaveProperty('top_left'); - expect(geoHashAgg.lastMapCollar).toHaveProperty('bottom_right'); - expect(geoHashAgg.lastMapCollar).toHaveProperty('zoom'); - }); }); }); @@ -157,8 +105,8 @@ describe('Geohash Agg', () => { it('should only create geohash_grid and geo_centroid aggregations when isFilteredByCollar is false', () => { const aggConfigs = getAggConfigs({ isFilteredByCollar: false }); const requestAggs = geoHashBucketAgg.getRequestAggs( - aggConfigs.aggs[0] as IBucketGeoHashGridAggConfig - ) as IBucketGeoHashGridAggConfig[]; + aggConfigs.aggs[0] as IBucketAggConfig + ) as IBucketAggConfig[]; expect(requestAggs.length).toEqual(2); expect(requestAggs[0].type.name).toEqual('geohash_grid'); @@ -168,8 +116,8 @@ describe('Geohash Agg', () => { it('should only create filter and geohash_grid aggregations when useGeocentroid is false', () => { const aggConfigs = getAggConfigs({ useGeocentroid: false }); const requestAggs = geoHashBucketAgg.getRequestAggs( - aggConfigs.aggs[0] as IBucketGeoHashGridAggConfig - ) as IBucketGeoHashGridAggConfig[]; + aggConfigs.aggs[0] as IBucketAggConfig + ) as IBucketAggConfig[]; expect(requestAggs.length).toEqual(2); expect(requestAggs[0].type.name).toEqual('filter'); @@ -178,23 +126,28 @@ describe('Geohash Agg', () => { }); describe('aggregation creation after map interaction', () => { - let originalRequestAggs: IBucketGeoHashGridAggConfig[]; + let originalRequestAggs: IBucketAggConfig[]; beforeEach(() => { originalRequestAggs = geoHashBucketAgg.getRequestAggs( - getAggConfigs().aggs[0] as IBucketGeoHashGridAggConfig - ) as IBucketGeoHashGridAggConfig[]; + getAggConfigs({ + boundingBox: { + top_left: { lat: 1, lon: -1 }, + bottom_right: { lat: -1, lon: 1 }, + }, + }).aggs[0] as IBucketAggConfig + ) as IBucketAggConfig[]; }); it('should change geo_bounding_box filter aggregation and vis session state when map movement is outside map collar', () => { const [, geoBoxingBox] = geoHashBucketAgg.getRequestAggs( getAggConfigs({ - mapBounds: { + boundingBox: { top_left: { lat: 10.0, lon: -10.0 }, bottom_right: { lat: 9.0, lon: -9.0 }, }, - }).aggs[0] as IBucketGeoHashGridAggConfig - ) as IBucketGeoHashGridAggConfig[]; + }).aggs[0] as IBucketAggConfig + ) as IBucketAggConfig[]; expect(originalRequestAggs[1].params).not.toEqual(geoBoxingBox.params); }); @@ -202,24 +155,14 @@ describe('Geohash Agg', () => { it('should not change geo_bounding_box filter aggregation and vis session state when map movement is within map collar', () => { const [, geoBoxingBox] = geoHashBucketAgg.getRequestAggs( getAggConfigs({ - mapBounds: { + boundingBox: { top_left: { lat: 1, lon: -1 }, bottom_right: { lat: -1, lon: 1 }, }, - }).aggs[0] as IBucketGeoHashGridAggConfig - ) as IBucketGeoHashGridAggConfig[]; + }).aggs[0] as IBucketAggConfig + ) as IBucketAggConfig[]; expect(originalRequestAggs[1].params).toEqual(geoBoxingBox.params); }); - - it('should change geo_bounding_box filter aggregation and vis session state when map zoom level changes', () => { - const [, geoBoxingBox] = geoHashBucketAgg.getRequestAggs( - getAggConfigs({ - mapZoom: -1, - }).aggs[0] as IBucketGeoHashGridAggConfig - ) as IBucketGeoHashGridAggConfig[]; - - expect(originalRequestAggs[1].lastMapCollar).not.toEqual(geoBoxingBox.lastMapCollar); - }); }); }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_hash.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_hash.ts index afd4e18dd266c..8732f926b0fb2 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_hash.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/geo_hash.ts @@ -18,69 +18,22 @@ */ import { i18n } from '@kbn/i18n'; -import { geohashColumns } from 'ui/vis/map/decode_geo_hash'; -import chrome from 'ui/chrome'; import { BucketAggType, IBucketAggConfig } from './_bucket_agg_type'; import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; - -import { geoContains, scaleBounds, GeoBoundingBox } from './lib/geo_utils'; import { BUCKET_TYPES } from './bucket_agg_types'; -import { AggGroupNames } from '../agg_groups'; -const config = chrome.getUiSettingsClient(); +const defaultBoundingBox = { + top_left: { lat: 1, lon: 1 }, + bottom_right: { lat: 0, lon: 0 }, +}; const defaultPrecision = 2; -const maxPrecision = parseInt(config.get('visualization:tileMap:maxPrecision'), 10) || 12; -/** - * Map Leaflet zoom levels to geohash precision levels. - * The size of a geohash column-width on the map should be at least `minGeohashPixels` pixels wide. - */ -const zoomPrecision: any = {}; -const minGeohashPixels = 16; - -for (let zoom = 0; zoom <= 21; zoom += 1) { - const worldPixels = 256 * Math.pow(2, zoom); - zoomPrecision[zoom] = 1; - for (let precision = 2; precision <= maxPrecision; precision += 1) { - const columns = geohashColumns(precision); - if (worldPixels / columns >= minGeohashPixels) { - zoomPrecision[zoom] = precision; - } else { - break; - } - } -} - -function getPrecision(val: string) { - let precision = parseInt(val, 10); - - if (Number.isNaN(precision)) { - precision = defaultPrecision; - } - - if (precision > maxPrecision) { - return maxPrecision; - } - - return precision; -} - -const isOutsideCollar = (bounds: GeoBoundingBox, collar: MapCollar) => - bounds && collar && !geoContains(collar, bounds); const geohashGridTitle = i18n.translate('data.search.aggs.buckets.geohashGridTitle', { defaultMessage: 'Geohash', }); -interface MapCollar extends GeoBoundingBox { - zoom?: unknown; -} - -export interface IBucketGeoHashGridAggConfig extends IBucketAggConfig { - lastMapCollar: MapCollar; -} - -export const geoHashBucketAgg = new BucketAggType({ +export const geoHashBucketAgg = new BucketAggType({ name: BUCKET_TYPES.GEOHASH_GRID, title: geohashGridTitle, params: [ @@ -97,13 +50,8 @@ export const geoHashBucketAgg = new BucketAggType({ { name: 'precision', default: defaultPrecision, - deserialize: getPrecision, write(aggConfig, output) { - const currZoom = aggConfig.params.mapZoom; - const autoPrecisionVal = zoomPrecision[currZoom]; - output.params.precision = aggConfig.params.autoPrecision - ? autoPrecisionVal - : getPrecision(aggConfig.params.precision); + output.params.precision = aggConfig.params.precision; }, }, { @@ -117,17 +65,7 @@ export const geoHashBucketAgg = new BucketAggType({ write: () => {}, }, { - name: 'mapZoom', - default: 2, - write: () => {}, - }, - { - name: 'mapCenter', - default: [0, 0], - write: () => {}, - }, - { - name: 'mapBounds', + name: 'boundingBox', default: null, write: () => {}, }, @@ -137,46 +75,22 @@ export const geoHashBucketAgg = new BucketAggType({ const params = agg.params; if (params.isFilteredByCollar && agg.getField()) { - const { mapBounds, mapZoom } = params; - if (mapBounds) { - let mapCollar: MapCollar; - - if ( - mapBounds && - (!agg.lastMapCollar || - agg.lastMapCollar.zoom !== mapZoom || - isOutsideCollar(mapBounds, agg.lastMapCollar)) - ) { - mapCollar = scaleBounds(mapBounds); - mapCollar.zoom = mapZoom; - agg.lastMapCollar = mapCollar; - } else { - mapCollar = agg.lastMapCollar; - } - const boundingBox = { - ignore_unmapped: true, - [agg.getField().name]: { - top_left: mapCollar.top_left, - bottom_right: mapCollar.bottom_right, - }, - }; - aggs.push( - agg.aggConfigs.createAggConfig( - { - type: 'filter', - id: 'filter_agg', - enabled: true, - params: { - geo_bounding_box: boundingBox, - }, - schema: { - group: AggGroupNames.Buckets, + aggs.push( + agg.aggConfigs.createAggConfig( + { + type: 'filter', + id: 'filter_agg', + enabled: true, + params: { + geo_bounding_box: { + ignore_unmapped: true, + [agg.getField().name]: params.boundingBox || defaultBoundingBox, }, - } as any, - { addToAggConfigs: false } - ) - ); - } + }, + } as any, + { addToAggConfigs: false } + ) + ); } aggs.push(agg); @@ -196,6 +110,6 @@ export const geoHashBucketAgg = new BucketAggType({ ); } - return aggs as IBucketGeoHashGridAggConfig[]; + return aggs; }, }); diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/lib/geo_utils.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/lib/geo_utils.ts deleted file mode 100644 index 639b6d1fbb03e..0000000000000 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/lib/geo_utils.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; - -interface GeoBoundingBoxCoordinate { - lat: number; - lon: number; -} - -export interface GeoBoundingBox { - top_left: GeoBoundingBoxCoordinate; - bottom_right: GeoBoundingBoxCoordinate; -} - -export function geoContains(collar: GeoBoundingBox, bounds: GeoBoundingBox) { - // test if bounds top_left is outside collar - if (bounds.top_left.lat > collar.top_left.lat || bounds.top_left.lon < collar.top_left.lon) { - return false; - } - - // test if bounds bottom_right is outside collar - if ( - bounds.bottom_right.lat < collar.bottom_right.lat || - bounds.bottom_right.lon > collar.bottom_right.lon - ) { - return false; - } - - // both corners are inside collar so collar contains bounds - return true; -} - -export function scaleBounds(bounds: GeoBoundingBox): GeoBoundingBox { - const scale = 0.5; // scale bounds by 50% - - const topLeft = bounds.top_left; - const bottomRight = bounds.bottom_right; - let latDiff = _.round(Math.abs(topLeft.lat - bottomRight.lat), 5); - const lonDiff = _.round(Math.abs(bottomRight.lon - topLeft.lon), 5); - // map height can be zero when vis is first created - if (latDiff === 0) latDiff = lonDiff; - - const latDelta = latDiff * scale; - let topLeftLat = _.round(topLeft.lat, 5) + latDelta; - if (topLeftLat > 90) topLeftLat = 90; - let bottomRightLat = _.round(bottomRight.lat, 5) - latDelta; - if (bottomRightLat < -90) bottomRightLat = -90; - const lonDelta = lonDiff * scale; - let topLeftLon = _.round(topLeft.lon, 5) - lonDelta; - if (topLeftLon < -180) topLeftLon = -180; - let bottomRightLon = _.round(bottomRight.lon, 5) + lonDelta; - if (bottomRightLon > 180) bottomRightLon = 180; - - return { - top_left: { lat: topLeftLat, lon: topLeftLon }, - bottom_right: { lat: bottomRightLat, lon: bottomRightLon }, - }; -} diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts index 0d1b2472bb8e2..e24aca08271c7 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts @@ -18,13 +18,14 @@ */ import { i18n } from '@kbn/i18n'; -import { noop } from 'lodash'; +import { noop, identity } from 'lodash'; import { forwardModifyAggConfigOnSearchRequestStart } from './nested_agg_helpers'; import { IMetricAggConfig, MetricAggParam } from '../metric_agg_type'; import { parentPipelineAggWriter } from './parent_pipeline_agg_writer'; import { Schemas } from '../../schemas'; +import { fieldFormats } from '../../../../../../../../plugins/data/public'; const metricAggFilter = [ '!top_hits', @@ -100,7 +101,7 @@ const parentPipelineAggHelper = { } else { subAgg = agg.aggConfigs.byId(agg.getParam('metricAgg')); } - return subAgg.type.getFormat(subAgg); + return subAgg ? subAgg.type.getFormat(subAgg) : new (fieldFormats.FieldFormat.from(identity))(); }, }; diff --git a/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts b/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts index 3956bda1812ad..e7c98e575fdb4 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts @@ -17,12 +17,14 @@ * under the License. */ +import { identity } from 'lodash'; import { i18n } from '@kbn/i18n'; import { siblingPipelineAggWriter } from './sibling_pipeline_agg_writer'; import { forwardModifyAggConfigOnSearchRequestStart } from './nested_agg_helpers'; import { IMetricAggConfig, MetricAggParam } from '../metric_agg_type'; import { Schemas } from '../../schemas'; +import { fieldFormats } from '../../../../../../../../plugins/data/public'; const metricAggFilter: string[] = [ '!top_hits', @@ -115,8 +117,9 @@ const siblingPipelineAggHelper = { getFormat(agg: IMetricAggConfig) { const customMetric = agg.getParam('customMetric'); - - return customMetric.type.getFormat(customMetric); + return customMetric + ? customMetric.type.getFormat(customMetric) + : new (fieldFormats.FieldFormat.from(identity))(); }, }; diff --git a/src/legacy/core_plugins/embeddable_api/index.ts b/src/legacy/core_plugins/embeddable_api/index.ts index 465e13df10bbc..52206e3d0f105 100644 --- a/src/legacy/core_plugins/embeddable_api/index.ts +++ b/src/legacy/core_plugins/embeddable_api/index.ts @@ -17,14 +17,9 @@ * under the License. */ -import { resolve } from 'path'; import { LegacyPluginApi, LegacyPluginSpec, ArrayOrItem } from 'src/legacy/plugin_discovery/types'; // eslint-disable-next-line import/no-default-export export default function(kibana: LegacyPluginApi): ArrayOrItem { - return new kibana.Plugin({ - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }, - }); + return new kibana.Plugin({}); } diff --git a/src/legacy/core_plugins/embeddable_api/public/index.scss b/src/legacy/core_plugins/embeddable_api/public/index.scss deleted file mode 100644 index 3f1977b909c31..0000000000000 --- a/src/legacy/core_plugins/embeddable_api/public/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - -@import '../../../../plugins/embeddable/public/index'; 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 index 4ceffbfc1c197..624d000dd8d7a 100644 --- a/src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx @@ -55,14 +55,12 @@ export const createInputControlVisController = (deps: InputControlVisDependencie } 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(); - } + this.visParams = visParams; + this.controls = []; + this.controls = await this.initControls(); + const [{ i18n }] = await deps.core.getStartServices(); + this.I18nContext = i18n.Context; + this.drawVis(); } destroy() { diff --git a/src/legacy/core_plugins/inspector_views/package.json b/src/legacy/core_plugins/inspector_views/package.json deleted file mode 100644 index 74c61c2bcfd2a..0000000000000 --- a/src/legacy/core_plugins/inspector_views/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "inspector_views", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/inspector_views/public/index.scss b/src/legacy/core_plugins/inspector_views/public/index.scss deleted file mode 100644 index d6a076c540f88..0000000000000 --- a/src/legacy/core_plugins/inspector_views/public/index.scss +++ /dev/null @@ -1,4 +0,0 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - -// Temporary reference -@import '../../../../plugins/inspector/public/views/index'; diff --git a/src/legacy/core_plugins/interpreter/index.ts b/src/legacy/core_plugins/interpreter/index.ts index db6f17a2960a9..9427a2f8a2d0f 100644 --- a/src/legacy/core_plugins/interpreter/index.ts +++ b/src/legacy/core_plugins/interpreter/index.ts @@ -31,7 +31,6 @@ export default function InterpreterPlugin(kibana: any) { injectDefaultVars: server => ({ serverBasePath: server.config().get('server.basePath'), }), - styleSheetPaths: resolve(__dirname, 'public/index.scss'), }, config: (Joi: any) => { return Joi.object({ diff --git a/src/legacy/core_plugins/interpreter/public/index.scss b/src/legacy/core_plugins/interpreter/public/index.scss deleted file mode 100644 index 360f35020764d..0000000000000 --- a/src/legacy/core_plugins/interpreter/public/index.scss +++ /dev/null @@ -1,4 +0,0 @@ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; - -@import '../../../../plugins/expressions/public/index'; diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 395e0da218307..ea81193c1dd0a 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -25,18 +25,17 @@ import { migrations } from './migrations'; import { importApi } from './server/routes/api/import'; import { exportApi } from './server/routes/api/export'; import { managementApi } from './server/routes/api/management'; -import * as systemApi from './server/lib/system_api'; import mappings from './mappings.json'; import { getUiSettingDefaults } from './ui_setting_defaults'; import { registerCspCollector } from './server/lib/csp_usage_collector'; import { injectVars } from './inject_vars'; import { i18n } from '@kbn/i18n'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; +import { kbnBaseUrl } from '../../../plugins/kibana_legacy/server'; const mkdirAsync = promisify(Fs.mkdir); export default function(kibana) { - const kbnBaseUrl = '/app/kibana'; return new kibana.Plugin({ id: 'kibana', config: function(Joi) { @@ -323,7 +322,6 @@ export default function(kibana) { exportApi(server); managementApi(server); registerCspCollector(usageCollection, server); - server.expose('systemApi', systemApi); server.injectUiAppVars('kibana', () => injectVars(server)); }, }); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/index.ts b/src/legacy/core_plugins/kibana/public/dashboard/index.ts index d0157882689d3..5b9fb8c0b6360 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/index.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/index.ts @@ -25,5 +25,5 @@ export { createSavedDashboardLoader } from './saved_dashboard/saved_dashboards'; // Core will be looking for this when loading our plugin in the new platform export const plugin = (context: PluginInitializerContext) => { - return new DashboardPlugin(); + return new DashboardPlugin(context); }; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy.ts index 9c13337a71126..cedb6fbc9b5ef 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy.ts @@ -19,18 +19,12 @@ import { PluginInitializerContext } from 'kibana/public'; import { npSetup, npStart } from './legacy_imports'; -import { start as data } from '../../../data/public/legacy'; -import { start as embeddables } from '../../../embeddable_api/public/np_ready/public/legacy'; import { plugin } from './index'; (async () => { - const instance = plugin({} as PluginInitializerContext); + const instance = plugin({ + env: npSetup.plugins.kibanaLegacy.env, + } as PluginInitializerContext); instance.setup(npSetup.core, npSetup.plugins); - instance.start(npStart.core, { - ...npStart.plugins, - data, - npData: npStart.plugins.data, - embeddables, - navigation: npStart.plugins.navigation, - }); + instance.start(npStart.core, npStart.plugins); })(); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts index e608eb7b7f48c..cc104c1a931d0 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts @@ -24,8 +24,9 @@ import { AppMountContext, ChromeStart, IUiSettingsClient, - LegacyCoreStart, + CoreStart, SavedObjectsClientContract, + PluginInitializerContext, } from 'kibana/public'; import { Storage } from '../../../../../../plugins/kibana_utils/public'; import { @@ -43,13 +44,14 @@ import { import { initDashboardApp } from './legacy_app'; import { IEmbeddableStart } from '../../../../../../plugins/embeddable/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../../plugins/navigation/public'; -import { DataPublicPluginStart as NpDataStart } from '../../../../../../plugins/data/public'; +import { DataPublicPluginStart } from '../../../../../../plugins/data/public'; import { SharePluginStart } from '../../../../../../plugins/share/public'; import { KibanaLegacyStart } from '../../../../../../plugins/kibana_legacy/public'; export interface RenderDeps { - core: LegacyCoreStart; - npDataStart: NpDataStart; + pluginInitializerContext: PluginInitializerContext; + core: CoreStart; + data: DataPublicPluginStart; navigation: NavigationStart; savedObjectsClient: SavedObjectsClientContract; savedDashboards: SavedObjectLoader; @@ -58,8 +60,8 @@ export interface RenderDeps { uiSettings: IUiSettingsClient; chrome: ChromeStart; addBasePath: (path: string) => string; - savedQueryService: NpDataStart['query']['savedQueries']; - embeddables: IEmbeddableStart; + savedQueryService: DataPublicPluginStart['query']['savedQueries']; + embeddable: IEmbeddableStart; localStorage: Storage; share: SharePluginStart; config: KibanaLegacyStart['config']; @@ -71,7 +73,11 @@ export const renderApp = (element: HTMLElement, appBasePath: string, deps: Rende if (!angularModuleInstance) { angularModuleInstance = createLocalAngularModule(deps.core, deps.navigation); // global routing stuff - configureAppAngularModule(angularModuleInstance, deps.core as LegacyCoreStart, true); + configureAppAngularModule( + angularModuleInstance, + { core: deps.core, env: deps.pluginInitializerContext.env }, + true + ); initDashboardApp(angularModuleInstance, deps); } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx index f94acf2dc1991..c0a0693431295 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx @@ -103,7 +103,7 @@ export function initDashboardAppDirective(app: any, deps: RenderDeps) { $route, $scope, $routeParams, - indexPatterns: deps.npDataStart.indexPatterns, + indexPatterns: deps.data.indexPatterns, kbnUrlStateStorage, history, ...deps, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx index 3f9343ededd13..465203be0d34c 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx @@ -96,6 +96,7 @@ export class DashboardAppController { }; constructor({ + pluginInitializerContext, $scope, $route, $routeParams, @@ -103,10 +104,10 @@ export class DashboardAppController { localStorage, indexPatterns, savedQueryService, - embeddables, + embeddable, share, dashboardCapabilities, - npDataStart: { query: queryService }, + data: { query: queryService }, core: { notifications, overlays, @@ -141,7 +142,7 @@ export class DashboardAppController { const dashboardStateManager = new DashboardStateManager({ savedDashboard: dash, hideWriteControls: dashboardConfig.getHideWriteControls(), - kibanaVersion: injectedMetadata.getKibanaVersion(), + kibanaVersion: pluginInitializerContext.env.packageInfo.version, kbnUrlStateStorage, history, }); @@ -186,9 +187,9 @@ export class DashboardAppController { let panelIndexPatterns: IndexPattern[] = []; Object.values(container.getChildIds()).forEach(id => { - const embeddable = container.getChild(id); - if (isErrorEmbeddable(embeddable)) return; - const embeddableIndexPatterns = (embeddable.getOutput() as any).indexPatterns; + const embeddableInstance = container.getChild(id); + if (isErrorEmbeddable(embeddableInstance)) return; + const embeddableIndexPatterns = (embeddableInstance.getOutput() as any).indexPatterns; if (!embeddableIndexPatterns) return; panelIndexPatterns.push(...embeddableIndexPatterns); }); @@ -284,7 +285,7 @@ export class DashboardAppController { let outputSubscription: Subscription | undefined; const dashboardDom = document.getElementById('dashboardViewport'); - const dashboardFactory = embeddables.getEmbeddableFactory( + const dashboardFactory = embeddable.getEmbeddableFactory( DASHBOARD_CONTAINER_TYPE ) as DashboardContainerFactory; dashboardFactory @@ -818,8 +819,8 @@ export class DashboardAppController { if (dashboardContainer && !isErrorEmbeddable(dashboardContainer)) { openAddPanelFlyout({ embeddable: dashboardContainer, - getAllFactories: embeddables.getEmbeddableFactories, - getFactory: embeddables.getEmbeddableFactory, + getAllFactories: embeddable.getEmbeddableFactories, + getFactory: embeddable.getEmbeddableFactory, notifications, overlays, SavedObjectFinder: getSavedObjectFinder(savedObjects, uiSettings), @@ -829,7 +830,7 @@ export class DashboardAppController { navActions[TopNavIds.VISUALIZE] = async () => { const type = 'visualization'; - const factory = embeddables.getEmbeddableFactory(type); + const factory = embeddable.getEmbeddableFactory(type); if (!factory) { throw new EmbeddableFactoryNotFoundError(type); } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js index b0f70b7a0c68f..ce9cc85be57b2 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js @@ -99,7 +99,7 @@ export function initDashboardApp(app, deps) { // syncs `_g` portion of url with query services const { stop: stopSyncingGlobalStateWithUrl } = syncQuery( - deps.npDataStart.query, + deps.data.query, kbnUrlStateStorage ); @@ -137,36 +137,31 @@ export function initDashboardApp(app, deps) { }, resolve: { dash: function($rootScope, $route, redirectWhenMissing, kbnUrl, history) { - return ensureDefaultIndexPattern(deps.core, deps.npDataStart, $rootScope, kbnUrl).then( - () => { - const savedObjectsClient = deps.savedObjectsClient; - const title = $route.current.params.title; - if (title) { - return savedObjectsClient - .find({ - search: `"${title}"`, - search_fields: 'title', - type: 'dashboard', - }) - .then(results => { - // The search isn't an exact match, lets see if we can find a single exact match to use - const matchingDashboards = results.savedObjects.filter( - dashboard => - dashboard.attributes.title.toLowerCase() === title.toLowerCase() - ); - if (matchingDashboards.length === 1) { - history.replace(createDashboardEditUrl(matchingDashboards[0].id)); - } else { - history.replace( - `${DashboardConstants.LANDING_PAGE_PATH}?filter="${title}"` - ); - $route.reload(); - } - return new Promise(() => {}); - }); - } + return ensureDefaultIndexPattern(deps.core, deps.data, $rootScope, kbnUrl).then(() => { + const savedObjectsClient = deps.savedObjectsClient; + const title = $route.current.params.title; + if (title) { + return savedObjectsClient + .find({ + search: `"${title}"`, + search_fields: 'title', + type: 'dashboard', + }) + .then(results => { + // The search isn't an exact match, lets see if we can find a single exact match to use + const matchingDashboards = results.savedObjects.filter( + dashboard => dashboard.attributes.title.toLowerCase() === title.toLowerCase() + ); + if (matchingDashboards.length === 1) { + history.replace(createDashboardEditUrl(matchingDashboards[0].id)); + } else { + history.replace(`${DashboardConstants.LANDING_PAGE_PATH}?filter="${title}"`); + $route.reload(); + } + return new Promise(() => {}); + }); } - ); + }); }, }, }) @@ -177,7 +172,7 @@ export function initDashboardApp(app, deps) { requireUICapability: 'dashboard.createNew', resolve: { dash: function(redirectWhenMissing, $rootScope, kbnUrl) { - return ensureDefaultIndexPattern(deps.core, deps.npDataStart, $rootScope, kbnUrl) + return ensureDefaultIndexPattern(deps.core, deps.data, $rootScope, kbnUrl) .then(() => { return deps.savedDashboards.get(); }) @@ -197,7 +192,7 @@ export function initDashboardApp(app, deps) { dash: function($rootScope, $route, redirectWhenMissing, kbnUrl, history) { const id = $route.current.params.id; - return ensureDefaultIndexPattern(deps.core, deps.npDataStart, $rootScope, kbnUrl) + return ensureDefaultIndexPattern(deps.core, deps.data, $rootScope, kbnUrl) .then(() => { return deps.savedDashboards.get(id); }) diff --git a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts index 09ae49f2305fd..7d330676e79ed 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts @@ -20,19 +20,16 @@ import { BehaviorSubject } from 'rxjs'; import { App, + AppMountParameters, CoreSetup, CoreStart, - LegacyCoreStart, Plugin, + PluginInitializerContext, SavedObjectsClientContract, } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { RenderDeps } from './np_ready/application'; -import { DataStart } from '../../../data/public'; -import { - DataPublicPluginStart as NpDataStart, - DataPublicPluginSetup as NpDataSetup, -} from '../../../../../plugins/data/public'; +import { DataPublicPluginStart, DataPublicPluginSetup } from '../../../../../plugins/data/public'; import { IEmbeddableStart } from '../../../../../plugins/embeddable/public'; import { Storage } from '../../../../../plugins/kibana_utils/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; @@ -52,9 +49,8 @@ import { createKbnUrlTracker } from '../../../../../plugins/kibana_utils/public' import { getQueryStateContainer } from '../../../../../plugins/data/public'; export interface DashboardPluginStartDependencies { - data: DataStart; - npData: NpDataStart; - embeddables: IEmbeddableStart; + data: DataPublicPluginStart; + embeddable: IEmbeddableStart; navigation: NavigationStart; share: SharePluginStart; kibanaLegacy: KibanaLegacyStart; @@ -63,14 +59,14 @@ export interface DashboardPluginStartDependencies { export interface DashboardPluginSetupDependencies { home: HomePublicPluginSetup; kibanaLegacy: KibanaLegacySetup; - data: NpDataSetup; + data: DataPublicPluginSetup; } export class DashboardPlugin implements Plugin { private startDependencies: { - npDataStart: NpDataStart; + data: DataPublicPluginStart; savedObjectsClient: SavedObjectsClientContract; - embeddables: IEmbeddableStart; + embeddable: IEmbeddableStart; navigation: NavigationStart; share: SharePluginStart; dashboardConfig: KibanaLegacyStart['dashboardConfig']; @@ -79,12 +75,11 @@ export class DashboardPlugin implements Plugin { private appStateUpdater = new BehaviorSubject(() => ({})); private stopUrlTracking: (() => void) | undefined = undefined; - public setup( - core: CoreSetup, - { home, kibanaLegacy, data: npData }: DashboardPluginSetupDependencies - ) { + constructor(private initializerContext: PluginInitializerContext) {} + + public setup(core: CoreSetup, { home, kibanaLegacy, data }: DashboardPluginSetupDependencies) { const { querySyncStateContainer, stop: stopQuerySyncStateContainer } = getQueryStateContainer( - npData.query + data.query ); const { appMounted, appUnMounted, stop: stopUrlTracker } = createKbnUrlTracker({ baseUrl: core.http.basePath.prepend('/app/kibana'), @@ -106,41 +101,43 @@ export class DashboardPlugin implements Plugin { const app: App = { id: '', title: 'Dashboards', - mount: async ({ core: contextCore }, params) => { + mount: async (params: AppMountParameters) => { + const [coreStart] = await core.getStartServices(); if (this.startDependencies === null) { throw new Error('not started yet'); } appMounted(); const { savedObjectsClient, - embeddables, + embeddable, navigation, share, - npDataStart, + data: dataStart, dashboardConfig, } = this.startDependencies; const savedDashboards = createSavedDashboardLoader({ savedObjectsClient, - indexPatterns: npDataStart.indexPatterns, - chrome: contextCore.chrome, - overlays: contextCore.overlays, + indexPatterns: dataStart.indexPatterns, + chrome: coreStart.chrome, + overlays: coreStart.overlays, }); const deps: RenderDeps = { - core: contextCore as LegacyCoreStart, + pluginInitializerContext: this.initializerContext, + core: coreStart, dashboardConfig, navigation, share, - npDataStart, + data: dataStart, savedObjectsClient, savedDashboards, - chrome: contextCore.chrome, - addBasePath: contextCore.http.basePath.prepend, - uiSettings: contextCore.uiSettings, + chrome: coreStart.chrome, + addBasePath: coreStart.http.basePath.prepend, + uiSettings: coreStart.uiSettings, config: kibanaLegacy.config, - savedQueryService: npDataStart.query.savedQueries, - embeddables, - dashboardCapabilities: contextCore.application.capabilities.dashboard, + savedQueryService: dataStart.query.savedQueries, + embeddable, + dashboardCapabilities: coreStart.application.capabilities.dashboard, localStorage: new Storage(localStorage), }; const { renderApp } = await import('./np_ready/application'); @@ -178,18 +175,17 @@ export class DashboardPlugin implements Plugin { start( { savedObjects: { client: savedObjectsClient } }: CoreStart, { - data: dataStart, - embeddables, + embeddable, navigation, - npData, + data, share, kibanaLegacy: { dashboardConfig }, }: DashboardPluginStartDependencies ) { this.startDependencies = { - npDataStart: npData, + data, savedObjectsClient, - embeddables, + embeddable, navigation, share, dashboardConfig, diff --git a/src/legacy/core_plugins/kibana/public/home/index.ts b/src/legacy/core_plugins/kibana/public/home/index.ts index c4e58e1a5e1ae..74b6da33c6542 100644 --- a/src/legacy/core_plugins/kibana/public/home/index.ts +++ b/src/legacy/core_plugins/kibana/public/home/index.ts @@ -17,42 +17,13 @@ * under the License. */ +import { PluginInitializerContext } from 'kibana/public'; import { npSetup, npStart } from 'ui/new_platform'; -import chrome from 'ui/chrome'; -import { HomePlugin, LegacyAngularInjectedDependencies } from './plugin'; -import { TelemetryOptInProvider } from '../../../telemetry/public/services'; -import { IPrivate } from '../../../../../plugins/kibana_legacy/public'; +import { HomePlugin } from './plugin'; -/** - * Get dependencies relying on the global angular context. - * They also have to get resolved together with the legacy imports above - */ -async function getAngularDependencies(): Promise { - const injector = await chrome.dangerouslyGetActiveInjector(); - - const Private = injector.get('Private'); - - const telemetryEnabled = npStart.core.injectedMetadata.getInjectedVar('telemetryEnabled'); - const telemetryBanner = npStart.core.injectedMetadata.getInjectedVar('telemetryBanner'); - const telemetryOptInProvider = Private(TelemetryOptInProvider); - - return { - telemetryOptInProvider, - shouldShowTelemetryOptIn: - telemetryEnabled && telemetryBanner && !telemetryOptInProvider.getOptIn(), - }; -} +const instance = new HomePlugin({ + env: npSetup.plugins.kibanaLegacy.env, +} as PluginInitializerContext); +instance.setup(npSetup.core, npSetup.plugins); -(async () => { - const instance = new HomePlugin(); - instance.setup(npSetup.core, { - ...npSetup.plugins, - __LEGACY: { - metadata: npStart.core.injectedMetadata.getLegacyMetadata(), - getAngularDependencies, - }, - }); - instance.start(npStart.core, { - ...npStart.plugins, - }); -})(); +instance.start(npStart.core, npStart.plugins); diff --git a/src/legacy/core_plugins/kibana/public/home/kibana_services.ts b/src/legacy/core_plugins/kibana/public/home/kibana_services.ts index 66c4d995e2566..6cb1531be6b5b 100644 --- a/src/legacy/core_plugins/kibana/public/home/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/home/kibana_services.ts @@ -21,14 +21,13 @@ import { ChromeStart, DocLinksStart, HttpStart, - LegacyNavLink, NotificationsSetup, OverlayStart, SavedObjectsClientContract, IUiSettingsClient, - UiSettingsState, } from 'kibana/public'; import { UiStatsMetricType } from '@kbn/analytics'; +import { TelemetryPluginStart } from '../../../../../plugins/telemetry/public'; import { Environment, HomePublicPluginSetup, @@ -38,22 +37,9 @@ import { KibanaLegacySetup } from '../../../../../plugins/kibana_legacy/public'; export interface HomeKibanaServices { indexPatternService: any; - metadata: { - app: unknown; - bundleId: string; - nav: LegacyNavLink[]; - version: string; - branch: string; - buildNum: number; - buildSha: string; - basePath: string; - serverName: string; - devMode: boolean; - uiSettings: { defaults: UiSettingsState; user?: UiSettingsState | undefined }; - }; + kibanaVersion: string; getInjected: (name: string, defaultValue?: any) => unknown; chrome: ChromeStart; - telemetryOptInProvider: any; uiSettings: IUiSettingsClient; config: KibanaLegacySetup['config']; homeConfig: HomePublicPluginSetup['config']; @@ -64,10 +50,10 @@ export interface HomeKibanaServices { banners: OverlayStart['banners']; trackUiMetric: (type: UiStatsMetricType, eventNames: string | string[], count?: number) => void; getBasePath: () => string; - shouldShowTelemetryOptIn: boolean; docLinks: DocLinksStart; addBasePath: (url: string) => string; environment: Environment; + telemetry?: TelemetryPluginStart; } let services: HomeKibanaServices | null = null; diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/home.test.js.snap b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/home.test.js.snap index 4563b633c3dfc..9d27362e62739 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/home.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/home.test.js.snap @@ -1054,7 +1054,6 @@ 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/np_ready/components/__snapshots__/sample_data_view_data_button.test.js.snap b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/sample_data_view_data_button.test.js.snap index e08d802406fff..661d1d33a5283 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/sample_data_view_data_button.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/sample_data_view_data_button.test.js.snap @@ -14,6 +14,7 @@ exports[`should render popover when appLinks is not empty 1`] = ` } closePopover={[Function]} + data-test-subj="launchSampleDataSetecommerce" display="inlineBlock" hasArrow={true} id="sampleDataLinksecommerce" diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/welcome.test.tsx.snap b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/welcome.test.tsx.snap index 6f76ceecbba13..df7cc7bcbaed0 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/welcome.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/welcome.test.tsx.snap @@ -67,44 +67,6 @@ exports[`should render a Welcome screen with no telemetry disclaimer 1`] = ` - - - - - - - - - - - @@ -200,16 +162,16 @@ exports[`should render a Welcome screen with the telemetry disclaimer 1`] = ` /> diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.js index 0c09c6c3c74fc..617a1810028fc 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.js @@ -51,7 +51,6 @@ export class Home extends Component { getServices().homeConfig.disableWelcomeScreen || props.localStorage.getItem(KEY_ENABLE_WELCOME) === 'false' ); - const currentOptInStatus = this.props.getOptInStatus(); this.state = { // If welcome is enabled, we wait for loading to complete // before rendering. This prevents an annoying flickering @@ -60,7 +59,6 @@ export class Home extends Component { isLoading: isWelcomeEnabled, isNewKibanaInstance: false, isWelcomeEnabled, - currentOptInStatus, }; } @@ -224,8 +222,7 @@ export class Home extends Component { ); } @@ -264,6 +261,8 @@ Home.propTypes = { localStorage: PropTypes.object.isRequired, urlBasePath: PropTypes.string.isRequired, mlEnabled: PropTypes.bool.isRequired, - onOptInSeen: PropTypes.func.isRequired, - getOptInStatus: PropTypes.func.isRequired, + telemetry: PropTypes.shape({ + telemetryService: PropTypes.any, + telemetryNotifications: PropTypes.any, + }), }; diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js index f6c91b412381c..d7531864582a3 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js @@ -35,7 +35,7 @@ export function HomeApp({ directories }) { getBasePath, addBasePath, environment, - telemetryOptInProvider: { setOptInNoticeSeen, getOptIn }, + telemetry, } = getServices(); const isCloudEnabled = environment.cloud; const mlEnabled = environment.ml; @@ -84,8 +84,7 @@ export function HomeApp({ directories }) { find={savedObjectsClient.find} localStorage={localStorage} urlBasePath={getBasePath()} - onOptInSeen={setOptInNoticeSeen} - getOptInStatus={getOptIn} + telemetry={telemetry} /> diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/sample_data_view_data_button.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/sample_data_view_data_button.js index e6f5c07c94f9f..cb43c18a8e78b 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/sample_data_view_data_button.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/sample_data_view_data_button.js @@ -112,6 +112,7 @@ export class SampleDataViewDataButton extends React.Component { closePopover={this.closePopover} panelPaddingSize="none" anchorPosition="downCenter" + data-test-subj={`launchSampleDataSet${this.props.id}`} > diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial/replace_template_strings.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial/replace_template_strings.js index daf996444eb3c..c7e623657bf71 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial/replace_template_strings.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial/replace_template_strings.js @@ -33,7 +33,7 @@ mustacheWriter.escapedValue = function escapedValue(token, context) { }; export function replaceTemplateStrings(text, params = {}) { - const { getInjected, metadata, docLinks } = getServices(); + const { getInjected, kibanaVersion, docLinks } = getServices(); const variables = { // '{' and '}' can not be used in template since they are used as template tags. @@ -58,7 +58,7 @@ export function replaceTemplateStrings(text, params = {}) { version: docLinks.DOC_LINK_VERSION, }, kibana: { - version: metadata.version, + version: kibanaVersion, }, }, params: params, diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx index 55c469fa58fc6..d9da47a2b43da 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx @@ -20,6 +20,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { Welcome } from './welcome'; +import { telemetryPluginMock } from '../../../../../../../plugins/telemetry/public/mocks'; jest.mock('../../kibana_services', () => ({ getServices: () => ({ @@ -29,27 +30,32 @@ jest.mock('../../kibana_services', () => ({ })); test('should render a Welcome screen with the telemetry disclaimer', () => { + const telemetry = telemetryPluginMock.createSetupContract(); const component = shallow( // @ts-ignore - {}} onOptInSeen={() => {}} /> + {}} telemetry={telemetry} /> ); expect(component).toMatchSnapshot(); }); test('should render a Welcome screen with the telemetry disclaimer when optIn is true', () => { + const telemetry = telemetryPluginMock.createSetupContract(); + telemetry.telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true); const component = shallow( // @ts-ignore - {}} onOptInSeen={() => {}} currentOptInStatus={true} /> + {}} telemetry={telemetry} /> ); expect(component).toMatchSnapshot(); }); test('should render a Welcome screen with the telemetry disclaimer when optIn is false', () => { + const telemetry = telemetryPluginMock.createSetupContract(); + telemetry.telemetryService.getIsOptedIn = jest.fn().mockReturnValue(false); const component = shallow( // @ts-ignore - {}} onOptInSeen={() => {}} currentOptInStatus={false} /> + {}} telemetry={telemetry} /> ); expect(component).toMatchSnapshot(); @@ -59,19 +65,21 @@ test('should render a Welcome screen with no telemetry disclaimer', () => { // @ts-ignore const component = shallow( // @ts-ignore - {}} onOptInSeen={() => {}} /> + {}} telemetry={null} /> ); expect(component).toMatchSnapshot(); }); test('fires opt-in seen when mounted', () => { - const seen = jest.fn(); - + const telemetry = telemetryPluginMock.createSetupContract(); + const mockSetOptedInNoticeSeen = jest.fn(); + // @ts-ignore + telemetry.telemetryNotifications.setOptedInNoticeSeen = mockSetOptedInNoticeSeen; shallow( // @ts-ignore - {}} onOptInSeen={seen} /> + {}} telemetry={telemetry} /> ); - expect(seen).toHaveBeenCalled(); + expect(mockSetOptedInNoticeSeen).toHaveBeenCalled(); }); diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx index 6983aabc4c7b1..7906caeda1b38 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx @@ -38,13 +38,14 @@ import { import { METRIC_TYPE } from '@kbn/analytics'; import { FormattedMessage } from '@kbn/i18n/react'; import { getServices } from '../../kibana_services'; +import { TelemetryPluginStart } from '../../../../../../../plugins/telemetry/public'; +import { PRIVACY_STATEMENT_URL } from '../../../../../../../plugins/telemetry/common/constants'; import { SampleDataCard } from './sample_data'; interface Props { urlBasePath: string; onSkip: () => void; - onOptInSeen: () => any; - currentOptInStatus: boolean; + telemetry?: TelemetryPluginStart; } /** @@ -75,8 +76,11 @@ export class Welcome extends React.Component { }; componentDidMount() { + const { telemetry } = this.props; this.services.trackUiMetric(METRIC_TYPE.LOADED, 'welcomeScreenMount'); - this.props.onOptInSeen(); + if (telemetry) { + telemetry.telemetryNotifications.setOptedInNoticeSeen(); + } document.addEventListener('keydown', this.hideOnEsc); } @@ -85,7 +89,13 @@ export class Welcome extends React.Component { } private renderTelemetryEnabledOrDisabledText = () => { - if (this.props.currentOptInStatus) { + const { telemetry } = this.props; + if (!telemetry) { + return null; + } + + const isOptedIn = telemetry.telemetryService.getIsOptedIn(); + if (isOptedIn) { return ( { }; render() { - const { urlBasePath } = this.props; + const { urlBasePath, telemetry } = this.props; return (
@@ -154,24 +164,24 @@ export class Welcome extends React.Component { onDecline={this.onSampleDataDecline} /> - - - - - - {this.renderTelemetryEnabledOrDisabledText()} - - + {!!telemetry && ( + + + + + + + {this.renderTelemetryEnabledOrDisabledText()} + + + + )}
diff --git a/src/legacy/core_plugins/kibana/public/home/plugin.ts b/src/legacy/core_plugins/kibana/public/home/plugin.ts index e530906d5698e..75e7cc2e453be 100644 --- a/src/legacy/core_plugins/kibana/public/home/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/home/plugin.ts @@ -17,9 +17,16 @@ * under the License. */ -import { CoreSetup, CoreStart, LegacyNavLink, Plugin, UiSettingsState } from 'kibana/public'; +import { + AppMountParameters, + CoreSetup, + CoreStart, + Plugin, + PluginInitializerContext, +} from 'kibana/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { TelemetryPluginStart } from 'src/plugins/telemetry/public'; import { setServices } from './kibana_services'; import { KibanaLegacySetup } from '../../../../../plugins/kibana_legacy/public'; import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; @@ -30,33 +37,13 @@ import { FeatureCatalogueEntry, } from '../../../../../plugins/home/public'; -export interface LegacyAngularInjectedDependencies { - telemetryOptInProvider: any; - shouldShowTelemetryOptIn: boolean; -} - export interface HomePluginStartDependencies { data: DataPublicPluginStart; home: HomePublicPluginStart; + telemetry?: TelemetryPluginStart; } export interface HomePluginSetupDependencies { - __LEGACY: { - metadata: { - app: unknown; - bundleId: string; - nav: LegacyNavLink[]; - version: string; - branch: string; - buildNum: number; - buildSha: string; - basePath: string; - serverName: string; - devMode: boolean; - uiSettings: { defaults: UiSettingsState; user?: UiSettingsState | undefined }; - }; - getAngularDependencies: () => Promise; - }; usageCollection: UsageCollectionSetup; kibanaLegacy: KibanaLegacySetup; home: HomePublicPluginSetup; @@ -67,32 +54,28 @@ export class HomePlugin implements Plugin { private savedObjectsClient: any = null; private environment: Environment | null = null; private directories: readonly FeatureCatalogueEntry[] | null = null; + private telemetry?: TelemetryPluginStart; + + constructor(private initializerContext: PluginInitializerContext) {} - setup( - core: CoreSetup, - { - home, - kibanaLegacy, - usageCollection, - __LEGACY: { getAngularDependencies, ...legacyServices }, - }: HomePluginSetupDependencies - ) { + setup(core: CoreSetup, { home, kibanaLegacy, usageCollection }: HomePluginSetupDependencies) { kibanaLegacy.registerLegacyApp({ id: 'home', title: 'Home', - mount: async ({ core: contextCore }, params) => { + mount: async (params: AppMountParameters) => { const trackUiMetric = usageCollection.reportUiStats.bind(usageCollection, 'Kibana_home'); - const angularDependencies = await getAngularDependencies(); + const [coreStart] = await core.getStartServices(); setServices({ - ...legacyServices, trackUiMetric, - http: contextCore.http, + kibanaVersion: this.initializerContext.env.packageInfo.version, + http: coreStart.http, toastNotifications: core.notifications.toasts, - banners: contextCore.overlays.banners, + banners: coreStart.overlays.banners, getInjected: core.injectedMetadata.getInjectedVar, - docLinks: contextCore.docLinks, + docLinks: coreStart.docLinks, savedObjectsClient: this.savedObjectsClient!, - chrome: contextCore.chrome, + chrome: coreStart.chrome, + telemetry: this.telemetry, uiSettings: core.uiSettings, addBasePath: core.http.basePath.prepend, getBasePath: core.http.basePath.get, @@ -101,7 +84,6 @@ export class HomePlugin implements Plugin { config: kibanaLegacy.config, homeConfig: home.config, directories: this.directories!, - ...angularDependencies, }); const { renderApp } = await import('./np_ready/application'); return await renderApp(params.element); @@ -109,10 +91,11 @@ export class HomePlugin implements Plugin { }); } - start(core: CoreStart, { data, home }: HomePluginStartDependencies) { + start(core: CoreStart, { data, home, telemetry }: HomePluginStartDependencies) { this.environment = home.environment.get(); this.directories = home.featureCatalogue.get(); this.dataStart = data; + this.telemetry = telemetry; this.savedObjectsClient = core.savedObjects.client; } diff --git a/src/legacy/core_plugins/kibana/public/visualize/index.ts b/src/legacy/core_plugins/kibana/public/visualize/index.ts index 83b820a8e3134..c3ae39d9fde25 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/index.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/index.ts @@ -25,5 +25,5 @@ export { VisualizeConstants, createVisualizeEditUrl } from './np_ready/visualize // Core will be looking for this when loading our plugin in the new platform export const plugin = (context: PluginInitializerContext) => { - return new VisualizePlugin(); + return new VisualizePlugin(context); }; diff --git a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts index 428e6cb225710..6082fb8428ac3 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts @@ -19,11 +19,12 @@ import { ChromeStart, - LegacyCoreStart, + CoreStart, SavedObjectsClientContract, ToastsStart, IUiSettingsClient, I18nStart, + PluginInitializerContext, } from 'kibana/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; @@ -38,11 +39,12 @@ import { Chrome } from './legacy_imports'; import { KibanaLegacyStart } from '../../../../../plugins/kibana_legacy/public'; export interface VisualizeKibanaServices { + pluginInitializerContext: PluginInitializerContext; addBasePath: (url: string) => string; chrome: ChromeStart; - core: LegacyCoreStart; + core: CoreStart; data: DataPublicPluginStart; - embeddables: IEmbeddableStart; + embeddable: IEmbeddableStart; getBasePath: () => string; indexPatterns: IndexPatternsContract; legacyChrome: Chrome; diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy.ts index 2d615e3132b01..bc2d700f6c6a1 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/legacy.ts @@ -19,21 +19,19 @@ import { PluginInitializerContext } from 'kibana/public'; import { legacyChrome, npSetup, npStart } from './legacy_imports'; -import { start as embeddables } from '../../../embeddable_api/public/np_ready/public/legacy'; import { start as visualizations } from '../../../visualizations/public/np_ready/public/legacy'; import { plugin } from './index'; -(() => { - const instance = plugin({} as PluginInitializerContext); - instance.setup(npSetup.core, { - ...npSetup.plugins, - __LEGACY: { - legacyChrome, - }, - }); - instance.start(npStart.core, { - ...npStart.plugins, - embeddables, - visualizations, - }); -})(); +const instance = plugin({ + env: npSetup.plugins.kibanaLegacy.env, +} as PluginInitializerContext); +instance.setup(npSetup.core, { + ...npSetup.plugins, + __LEGACY: { + legacyChrome, + }, +}); +instance.start(npStart.core, { + ...npStart.plugins, + visualizations, +}); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts index 44e7e9c2a7413..3d5fd6605f56b 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/application.ts @@ -20,7 +20,7 @@ import angular, { IModule } from 'angular'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; -import { AppMountContext, LegacyCoreStart } from 'kibana/public'; +import { AppMountContext } from 'kibana/public'; import { AppStateProvider, AppState, @@ -53,7 +53,11 @@ export const renderApp = async ( if (!angularModuleInstance) { angularModuleInstance = createLocalAngularModule(deps.core, deps.navigation); // global routing stuff - configureAppAngularModule(angularModuleInstance, deps.core as LegacyCoreStart, true); + configureAppAngularModule( + angularModuleInstance, + { core: deps.core, env: deps.pluginInitializerContext.env }, + true + ); // custom routing stuff initVisualizeApp(angularModuleInstance, deps); } diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js index 46ae45c3a5fa2..27fb9b63843c4 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js @@ -31,6 +31,7 @@ import { getEditBreadcrumbs } from '../breadcrumbs'; import { addHelpMenuToAppChrome } from '../help_menu/help_menu_util'; import { FilterStateManager } from '../../../../../data/public'; import { unhashUrl } from '../../../../../../../plugins/kibana_utils/public'; +import { kbnBaseUrl } from '../../../../../../../plugins/kibana_legacy/public'; import { SavedObjectSaveModal, showSaveModal, @@ -74,7 +75,6 @@ function VisualizeAppController( kbnUrl, redirectWhenMissing, Promise, - kbnBaseUrl, getAppState, globalState ) { diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js index 18a60f7c3c10b..502bd6e56fb1f 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization.js @@ -31,7 +31,7 @@ export function initVisualizationDirective(app, deps) { link: function($scope, element) { $scope.renderFunction = async () => { if (!$scope._handler) { - $scope._handler = await deps.embeddables + $scope._handler = await deps.embeddable .getEmbeddableFactory('visualization') .createFromObject($scope.savedObj, { timeRange: $scope.timeRange, diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js index b2386f83b252c..8032152f88173 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/visualization_editor.js @@ -36,7 +36,7 @@ export function initVisEditorDirective(app, deps) { editor.render({ core: deps.core, data: deps.data, - embeddables: deps.embeddables, + embeddable: deps.embeddable, uiState: $scope.uiState, timeRange: $scope.timeRange, filters: $scope.filters, diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts index 17be5e4051b12..524bc4b3196b7 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts @@ -26,7 +26,7 @@ export interface EditorRenderProps { appState: AppState; core: LegacyCoreStart; data: DataPublicPluginStart; - embeddables: IEmbeddableStart; + embeddable: IEmbeddableStart; filters: Filter[]; uiState: PersistedState; timeRange: TimeRange; diff --git a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts index ce93fe7c2d578..16715677d1e20 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts @@ -20,10 +20,11 @@ import { i18n } from '@kbn/i18n'; import { + AppMountParameters, CoreSetup, CoreStart, - LegacyCoreStart, Plugin, + PluginInitializerContext, SavedObjectsClientContract, } from 'kibana/public'; @@ -45,7 +46,7 @@ import { Chrome } from './legacy_imports'; export interface VisualizePluginStartDependencies { data: DataPublicPluginStart; - embeddables: IEmbeddableStart; + embeddable: IEmbeddableStart; navigation: NavigationStart; share: SharePluginStart; visualizations: VisualizationsStart; @@ -63,13 +64,15 @@ export interface VisualizePluginSetupDependencies { export class VisualizePlugin implements Plugin { private startDependencies: { data: DataPublicPluginStart; - embeddables: IEmbeddableStart; + embeddable: IEmbeddableStart; navigation: NavigationStart; savedObjectsClient: SavedObjectsClientContract; share: SharePluginStart; visualizations: VisualizationsStart; } | null = null; + constructor(private initializerContext: PluginInitializerContext) {} + public async setup( core: CoreSetup, { home, kibanaLegacy, __LEGACY, usageCollection }: VisualizePluginSetupDependencies @@ -77,14 +80,15 @@ export class VisualizePlugin implements Plugin { kibanaLegacy.registerLegacyApp({ id: 'visualize', title: 'Visualize', - mount: async ({ core: contextCore }, params) => { + mount: async (params: AppMountParameters) => { + const [coreStart] = await core.getStartServices(); if (this.startDependencies === null) { throw new Error('not started yet'); } const { savedObjectsClient, - embeddables, + embeddable, navigation, visualizations, data, @@ -93,11 +97,12 @@ export class VisualizePlugin implements Plugin { const deps: VisualizeKibanaServices = { ...__LEGACY, - addBasePath: contextCore.http.basePath.prepend, - core: contextCore as LegacyCoreStart, - chrome: contextCore.chrome, + pluginInitializerContext: this.initializerContext, + addBasePath: coreStart.http.basePath.prepend, + core: coreStart, + chrome: coreStart.chrome, data, - embeddables, + embeddable, getBasePath: core.http.basePath.get, indexPatterns: data.indexPatterns, localStorage: new Storage(localStorage), @@ -106,13 +111,13 @@ export class VisualizePlugin implements Plugin { savedVisualizations: visualizations.getSavedVisualizationsLoader(), savedQueryService: data.query.savedQueries, share, - toastNotifications: contextCore.notifications.toasts, - uiSettings: contextCore.uiSettings, + toastNotifications: coreStart.notifications.toasts, + uiSettings: coreStart.uiSettings, config: kibanaLegacy.config, - visualizeCapabilities: contextCore.application.capabilities.visualize, + visualizeCapabilities: coreStart.application.capabilities.visualize, visualizations, usageCollection, - I18nContext: contextCore.i18n.Context, + I18nContext: coreStart.i18n.Context, }; setServices(deps); @@ -137,11 +142,11 @@ export class VisualizePlugin implements Plugin { public start( core: CoreStart, - { embeddables, navigation, data, share, visualizations }: VisualizePluginStartDependencies + { embeddable, navigation, data, share, visualizations }: VisualizePluginStartDependencies ) { this.startDependencies = { data, - embeddables, + embeddable, navigation, savedObjectsClient: core.savedObjects.client, share, diff --git a/src/legacy/core_plugins/navigation/index.ts b/src/legacy/core_plugins/navigation/index.ts deleted file mode 100644 index 32d5f040760c6..0000000000000 --- a/src/legacy/core_plugins/navigation/index.ts +++ /dev/null @@ -1,42 +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 { resolve } from 'path'; -import { Legacy } from '../../../../kibana'; - -// eslint-disable-next-line import/no-default-export -export default function NavigationPlugin(kibana: any) { - const config: Legacy.PluginSpecOptions = { - id: 'navigation', - require: [], - publicDir: resolve(__dirname, 'public'), - config: (Joi: any) => { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - init: (server: Legacy.Server) => ({}), - uiExports: { - injectDefaultVars: () => ({}), - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }, - }; - - return new kibana.Plugin(config); -} diff --git a/src/legacy/core_plugins/navigation/package.json b/src/legacy/core_plugins/navigation/package.json deleted file mode 100644 index 8fddb8e6aeced..0000000000000 --- a/src/legacy/core_plugins/navigation/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "navigation", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/navigation/public/index.scss b/src/legacy/core_plugins/navigation/public/index.scss deleted file mode 100644 index 8f2221eb4d4c7..0000000000000 --- a/src/legacy/core_plugins/navigation/public/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - -@import '../../../../plugins/navigation/public/top_nav_menu/index'; diff --git a/src/legacy/core_plugins/telemetry/common/constants.ts b/src/legacy/core_plugins/telemetry/common/constants.ts index cf2c9c883871b..52981c04ad34a 100644 --- a/src/legacy/core_plugins/telemetry/common/constants.ts +++ b/src/legacy/core_plugins/telemetry/common/constants.ts @@ -43,11 +43,6 @@ export const getConfigTelemetryDesc = () => { */ export const REPORT_INTERVAL_MS = 86400000; -/* - * Key for the localStorage service - */ -export const LOCALSTORAGE_KEY = 'telemetry.data'; - /** * Link to the Elastic Telemetry privacy statement. */ diff --git a/src/legacy/core_plugins/telemetry/common/get_xpack_config_with_deprecated.ts b/src/legacy/core_plugins/telemetry/common/get_xpack_config_with_deprecated.ts deleted file mode 100644 index 3f7a8d3410993..0000000000000 --- a/src/legacy/core_plugins/telemetry/common/get_xpack_config_with_deprecated.ts +++ /dev/null @@ -1,41 +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 { KibanaConfig } from 'src/legacy/server/kbn_server'; - -export function getXpackConfigWithDeprecated(config: KibanaConfig, configPath: string) { - try { - const deprecatedXpackmainConfig = config.get(`xpack.xpack_main.${configPath}`); - if (typeof deprecatedXpackmainConfig !== 'undefined') { - return deprecatedXpackmainConfig; - } - } catch (err) { - // swallow error - } - try { - const deprecatedXpackConfig = config.get(`xpack.${configPath}`); - if (typeof deprecatedXpackConfig !== 'undefined') { - return deprecatedXpackConfig; - } - } catch (err) { - // swallow error - } - - return config.get(configPath); -} diff --git a/src/legacy/core_plugins/telemetry/index.ts b/src/legacy/core_plugins/telemetry/index.ts index 2a81e3fa05c6c..ec70380d83a0a 100644 --- a/src/legacy/core_plugins/telemetry/index.ts +++ b/src/legacy/core_plugins/telemetry/index.ts @@ -22,14 +22,17 @@ import { resolve } from 'path'; import JoiNamespace from 'joi'; import { Server } from 'hapi'; import { CoreSetup, PluginInitializerContext } from 'src/core/server'; -import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getConfigPath } from '../../../core/server/path'; // @ts-ignore import mappings from './mappings.json'; -import { CONFIG_TELEMETRY, getConfigTelemetryDesc } from './common/constants'; -import { getXpackConfigWithDeprecated } from './common/get_xpack_config_with_deprecated'; -import { telemetryPlugin, replaceTelemetryInjectedVars, FetcherTask, PluginsSetup } from './server'; +import { + telemetryPlugin, + replaceTelemetryInjectedVars, + FetcherTask, + PluginsSetup, + handleOldSettings, +} from './server'; const ENDPOINT_VERSION = 'v2'; @@ -76,16 +79,6 @@ const telemetry = (kibana: any) => { }, uiExports: { managementSections: ['plugins/telemetry/views/management'], - uiSettingDefaults: { - [CONFIG_TELEMETRY]: { - name: i18n.translate('telemetry.telemetryConfigTitle', { - defaultMessage: 'Telemetry opt-in', - }), - description: getConfigTelemetryDesc(), - value: false, - readonly: true, - }, - }, savedObjectSchemas: { telemetry: { isNamespaceAgnostic: true, @@ -98,11 +91,11 @@ const telemetry = (kibana: any) => { injectDefaultVars(server: Server) { const config = server.config(); return { - telemetryEnabled: getXpackConfigWithDeprecated(config, 'telemetry.enabled'), - telemetryUrl: getXpackConfigWithDeprecated(config, 'telemetry.url'), + telemetryEnabled: config.get('telemetry.enabled'), + telemetryUrl: config.get('telemetry.url'), telemetryBanner: config.get('telemetry.allowChangingOptInStatus') !== false && - getXpackConfigWithDeprecated(config, 'telemetry.banner'), + config.get('telemetry.banner'), telemetryOptedIn: config.get('telemetry.optIn'), telemetryOptInStatusUrl: config.get('telemetry.optInStatusUrl'), allowChangingOptInStatus: config.get('telemetry.allowChangingOptInStatus'), @@ -110,14 +103,13 @@ const telemetry = (kibana: any) => { telemetryNotifyUserAboutOptInDefault: false, }; }, - hacks: ['plugins/telemetry/hacks/telemetry_init', 'plugins/telemetry/hacks/telemetry_opt_in'], mappings, }, postInit(server: Server) { const fetcherTask = new FetcherTask(server); fetcherTask.start(); }, - init(server: Server) { + async init(server: Server) { const { usageCollection } = server.newPlatform.setup.plugins; const initializerContext = { env: { @@ -145,6 +137,12 @@ const telemetry = (kibana: any) => { log: server.log, } as any) as CoreSetup; + try { + await handleOldSettings(server); + } catch (err) { + server.log(['warning', 'telemetry'], 'Unable to update legacy telemetry configs.'); + } + const pluginsSetup: PluginsSetup = { usageCollection, }; diff --git a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap b/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap deleted file mode 100644 index 079a43e77616d..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap +++ /dev/null @@ -1,80 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TelemetryForm doesn't render form when not allowed to change optIn status 1`] = `""`; - -exports[`TelemetryForm renders as expected when allows to change optIn status 1`] = ` - - - - - - -

- -

-
-
-
- - -

- - - , - } - } - /> -

-

- - - -

- , - "type": "boolean", - "value": false, - } - } - /> -
-
-
-`; diff --git a/src/legacy/core_plugins/telemetry/public/components/telemetry_form.test.js b/src/legacy/core_plugins/telemetry/public/components/telemetry_form.test.js deleted file mode 100644 index fe0c2c3449af1..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/components/telemetry_form.test.js +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { mockInjectedMetadata } from '../services/telemetry_opt_in.test.mocks'; -import React from 'react'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { TelemetryForm } from './telemetry_form'; -import { TelemetryOptInProvider } from '../services'; - -const buildTelemetryOptInProvider = () => { - const mockHttp = { - post: jest.fn(), - }; - - const mockInjector = { - get: key => { - switch (key) { - case '$http': - return mockHttp; - case 'allowChangingOptInStatus': - return true; - default: - return null; - } - }, - }; - - const chrome = { - addBasePath: url => url, - }; - - return new TelemetryOptInProvider(mockInjector, chrome); -}; - -describe('TelemetryForm', () => { - it('renders as expected when allows to change optIn status', () => { - mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: true }); - - expect( - shallowWithIntl( - - ) - ).toMatchSnapshot(); - }); - - it(`doesn't render form when not allowed to change optIn status`, () => { - mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: false }); - - expect( - shallowWithIntl( - - ) - ).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/__tests__/fetch_telemetry.js b/src/legacy/core_plugins/telemetry/public/hacks/__tests__/fetch_telemetry.js deleted file mode 100644 index ad9ee0998e3bb..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/__tests__/fetch_telemetry.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -import { fetchTelemetry } from '../fetch_telemetry'; - -describe('fetch_telemetry', () => { - it('fetchTelemetry calls expected URL with 20 minutes - now', () => { - const response = Promise.resolve(); - const $http = { - post: sinon.stub(), - }; - const basePath = 'fake'; - const moment = { - subtract: sinon.stub(), - toISOString: () => 'max123', - }; - - moment.subtract.withArgs(20, 'minutes').returns({ - toISOString: () => 'min456', - }); - - $http.post - .withArgs(`fake/api/telemetry/v2/clusters/_stats`, { - unencrypted: true, - timeRange: { - min: 'min456', - max: 'max123', - }, - }) - .returns(response); - - expect(fetchTelemetry($http, { basePath, _moment: () => moment, unencrypted: true })).to.be( - response - ); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/__tests__/telemetry.js b/src/legacy/core_plugins/telemetry/public/hacks/__tests__/telemetry.js deleted file mode 100644 index 74f1de4934a78..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/__tests__/telemetry.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { uiModules } from 'ui/modules'; - -// This overrides settings for other UI tests -uiModules - .get('kibana') - // disable stat reporting while running tests, - // MockInjector used in these tests is not impacted - .constant('telemetryEnabled', false) - .constant('telemetryOptedIn', null) - .constant('telemetryUrl', 'not.a.valid.url.0'); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/fetch_telemetry.js b/src/legacy/core_plugins/telemetry/public/hacks/fetch_telemetry.js deleted file mode 100644 index ede81f638a3fc..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/fetch_telemetry.js +++ /dev/null @@ -1,44 +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 uiChrome from 'ui/chrome'; -import moment from 'moment'; - -/** - * Fetch Telemetry data by calling the Kibana API. - * - * @param {Object} $http The HTTP handler - * @param {String} basePath The base URI - * @param {Function} _moment moment.js, but injectable for tests - * @return {Promise} An array of cluster Telemetry objects. - */ -export function fetchTelemetry( - $http, - { basePath = uiChrome.getBasePath(), _moment = moment, unencrypted = false } = {} -) { - return $http.post(`${basePath}/api/telemetry/v2/clusters/_stats`, { - unencrypted, - timeRange: { - min: _moment() - .subtract(20, 'minutes') - .toISOString(), - max: _moment().toISOString(), - }, - }); -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/telemetry.js b/src/legacy/core_plugins/telemetry/public/hacks/telemetry.js deleted file mode 100644 index 8fa777ead3e4b..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/telemetry.js +++ /dev/null @@ -1,120 +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 { REPORT_INTERVAL_MS, LOCALSTORAGE_KEY } from '../../common/constants'; - -export class Telemetry { - /** - * @param {Object} $injector - AngularJS injector service - * @param {Function} fetchTelemetry Method used to fetch telemetry data (expects an array response) - */ - constructor($injector, fetchTelemetry) { - this._storage = $injector.get('localStorage'); - this._$http = $injector.get('$http'); - this._telemetryUrl = $injector.get('telemetryUrl'); - this._telemetryOptedIn = $injector.get('telemetryOptedIn'); - this._fetchTelemetry = fetchTelemetry; - this._sending = false; - - // try to load the local storage data - const attributes = this._storage.get(LOCALSTORAGE_KEY) || {}; - this._lastReport = attributes.lastReport; - } - - _saveToBrowser() { - // we are the only code that manipulates this key, so it's safe to blindly overwrite the whole object - this._storage.set(LOCALSTORAGE_KEY, { lastReport: this._lastReport }); - } - - /** - * Determine if we are due to send a new report. - * - * @returns {Boolean} true if a new report should be sent. false otherwise. - */ - _checkReportStatus() { - // check if opt-in for telemetry is enabled - if (this._telemetryOptedIn) { - // returns NaN for any malformed or unset (null/undefined) value - const lastReport = parseInt(this._lastReport, 10); - // If it's been a day since we last sent telemetry - if (isNaN(lastReport) || Date.now() - lastReport > REPORT_INTERVAL_MS) { - return true; - } - } - - return false; - } - - /** - * Check report permission and if passes, send the report - * - * @returns {Promise} Always. - */ - _sendIfDue() { - if (this._sending || !this._checkReportStatus()) { - return Promise.resolve(false); - } - - // mark that we are working so future requests are ignored until we're done - this._sending = true; - - return ( - this._fetchTelemetry() - .then(response => { - const clusters = [].concat(response.data); - return Promise.all( - clusters.map(cluster => { - const req = { - method: 'POST', - url: this._telemetryUrl, - data: cluster, - }; - // if passing data externally, then suppress kbnXsrfToken - if (this._telemetryUrl.match(/^https/)) { - req.kbnXsrfToken = false; - } - return this._$http(req); - }) - ); - }) - // the response object is ignored because we do not check it - .then(() => { - // we sent a report, so we need to record and store the current timestamp - this._lastReport = Date.now(); - this._saveToBrowser(); - }) - // no ajaxErrorHandlers for telemetry - .catch(() => null) - .then(() => { - this._sending = false; - return true; // sent, but not necessarilly successfully - }) - ); - } - - /** - * Public method - * - * @returns {Number} `window.setInterval` response to allow cancelling the interval. - */ - start() { - // continuously check if it's due time for a report - return window.setInterval(() => this._sendIfDue(), 60000); - } -} // end class diff --git a/src/legacy/core_plugins/telemetry/public/hacks/telemetry.test.js b/src/legacy/core_plugins/telemetry/public/hacks/telemetry.test.js deleted file mode 100644 index 45a0653cd7a54..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/telemetry.test.js +++ /dev/null @@ -1,306 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Telemetry } from './telemetry'; -import { REPORT_INTERVAL_MS, LOCALSTORAGE_KEY } from '../../common/constants'; - -describe('telemetry class', () => { - const clusters = [{ cluster_uuid: 'fake-123' }, { cluster_uuid: 'fake-456' }]; - const telemetryUrl = 'https://not.a.valid.url.0'; - const mockFetchTelemetry = () => Promise.resolve({ data: clusters }); - // returns a function that behaves like the injector by fetching the requested key from the object directly - // for example: - // { '$http': jest.fn() } would be how to mock the '$http' injector value - const mockInjectorFromObject = object => { - return { get: key => object[key] }; - }; - - describe('constructor', () => { - test('defaults lastReport if unset', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce(undefined), - }, - $http: jest.fn(), - telemetryOptedIn: true, - telemetryUrl, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._storage).toBe(injector.localStorage); - expect(telemetry._$http).toBe(injector.$http); - expect(telemetry._telemetryOptedIn).toBe(injector.telemetryOptedIn); - expect(telemetry._telemetryUrl).toBe(injector.telemetryUrl); - expect(telemetry._fetchTelemetry).toBe(mockFetchTelemetry); - expect(telemetry._sending).toBe(false); - expect(telemetry._lastReport).toBeUndefined(); - - expect(injector.localStorage.get).toHaveBeenCalledTimes(1); - expect(injector.localStorage.get).toHaveBeenCalledWith(LOCALSTORAGE_KEY); - }); - - test('uses lastReport if set', () => { - const lastReport = Date.now(); - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce({ lastReport }), - }, - $http: jest.fn(), - telemetryOptedIn: true, - telemetryUrl, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._storage).toBe(injector.localStorage); - expect(telemetry._$http).toBe(injector.$http); - expect(telemetry._telemetryOptedIn).toBe(injector.telemetryOptedIn); - expect(telemetry._telemetryUrl).toBe(injector.telemetryUrl); - expect(telemetry._fetchTelemetry).toBe(mockFetchTelemetry); - expect(telemetry._sending).toBe(false); - expect(telemetry._lastReport).toBe(lastReport); - - expect(injector.localStorage.get).toHaveBeenCalledTimes(1); - expect(injector.localStorage.get).toHaveBeenCalledWith(LOCALSTORAGE_KEY); - }); - }); - - test('_saveToBrowser uses _lastReport', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce({ random: 'junk', gets: 'thrown away' }), - set: jest.fn(), - }, - }; - const lastReport = Date.now(); - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - telemetry._lastReport = lastReport; - - telemetry._saveToBrowser(); - - expect(injector.localStorage.set).toHaveBeenCalledTimes(1); - expect(injector.localStorage.set).toHaveBeenCalledWith(LOCALSTORAGE_KEY, { lastReport }); - }); - - describe('_checkReportStatus', () => { - // send the report if we get to check the time - const lastReportShouldSendNow = Date.now() - REPORT_INTERVAL_MS - 1; - - test('returns false whenever telemetryOptedIn is null', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce({ lastReport: lastReportShouldSendNow }), - }, - telemetryOptedIn: null, // not yet opted in - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._checkReportStatus()).toBe(false); - }); - - test('returns false whenever telemetryOptedIn is false', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce({ lastReport: lastReportShouldSendNow }), - }, - telemetryOptedIn: false, // opted out explicitly - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._checkReportStatus()).toBe(false); - }); - - // FLAKY: https://github.com/elastic/kibana/issues/27922 - test.skip('returns false if last report is too recent', () => { - const injector = { - localStorage: { - // we expect '>', not '>=' - get: jest.fn().mockReturnValueOnce({ lastReport: Date.now() - REPORT_INTERVAL_MS }), - }, - telemetryOptedIn: true, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._checkReportStatus()).toBe(false); - }); - - test('returns true if last report is not defined', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce({}), - }, - telemetryOptedIn: true, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._checkReportStatus()).toBe(true); - }); - - test('returns true if last report is defined and old enough', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce({ lastReport: lastReportShouldSendNow }), - }, - telemetryOptedIn: true, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._checkReportStatus()).toBe(true); - }); - - test('returns true if last report is defined and old enough as a string', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce({ lastReport: lastReportShouldSendNow.toString() }), - }, - telemetryOptedIn: true, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._checkReportStatus()).toBe(true); - }); - - test('returns true if last report is defined and malformed', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce({ lastReport: { not: { a: 'number' } } }), - }, - telemetryOptedIn: true, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect(telemetry._checkReportStatus()).toBe(true); - }); - }); - - describe('_sendIfDue', () => { - test('ignores and returns false if already sending', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce(undefined), // never sent - }, - telemetryOptedIn: true, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - telemetry._sending = true; - - return expect(telemetry._sendIfDue()).resolves.toBe(false); - }); - - test('ignores and returns false if _checkReportStatus says so', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce(undefined), // never sent, so it would try if opted in - }, - telemetryOptedIn: false, // opted out - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - return expect(telemetry._sendIfDue()).resolves.toBe(false); - }); - - test('sends telemetry when requested', () => { - const now = Date.now(); - const injector = { - $http: jest.fn().mockResolvedValue({}), // ignored response - localStorage: { - get: jest.fn().mockReturnValueOnce({ lastReport: now - REPORT_INTERVAL_MS - 1 }), - set: jest.fn(), - }, - telemetryOptedIn: true, - telemetryUrl, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect.hasAssertions(); - - return telemetry._sendIfDue().then(result => { - expect(result).toBe(true); - expect(telemetry._sending).toBe(false); - - // should be updated - const lastReport = telemetry._lastReport; - - // if the test runs fast enough it should be exactly equal, but probably a few ms greater - expect(lastReport).toBeGreaterThanOrEqual(now); - - expect(injector.$http).toHaveBeenCalledTimes(2); - // assert that it sent every cluster's telemetry - clusters.forEach(cluster => { - expect(injector.$http).toHaveBeenCalledWith({ - method: 'POST', - url: telemetryUrl, - data: cluster, - kbnXsrfToken: false, - }); - }); - - expect(injector.localStorage.set).toHaveBeenCalledTimes(1); - expect(injector.localStorage.set).toHaveBeenCalledWith(LOCALSTORAGE_KEY, { lastReport }); - }); - }); - - test('sends telemetry when requested and catches exceptions', () => { - const lastReport = Date.now() - REPORT_INTERVAL_MS - 1; - const injector = { - $http: jest.fn().mockRejectedValue(new Error('TEST - expected')), // caught failure - localStorage: { - get: jest.fn().mockReturnValueOnce({ lastReport }), - set: jest.fn(), - }, - telemetryOptedIn: true, - telemetryUrl, - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - expect.hasAssertions(); - - return telemetry._sendIfDue().then(result => { - expect(result).toBe(true); // attempted to send - expect(telemetry._sending).toBe(false); - - // should be unchanged - expect(telemetry._lastReport).toBe(lastReport); - expect(injector.localStorage.set).toHaveBeenCalledTimes(0); - - expect(injector.$http).toHaveBeenCalledTimes(2); - // assert that it sent every cluster's telemetry - clusters.forEach(cluster => { - expect(injector.$http).toHaveBeenCalledWith({ - method: 'POST', - url: telemetryUrl, - data: cluster, - kbnXsrfToken: false, - }); - }); - }); - }); - }); - - test('start', () => { - const injector = { - localStorage: { - get: jest.fn().mockReturnValueOnce(undefined), - }, - telemetryOptedIn: false, // opted out - }; - const telemetry = new Telemetry(mockInjectorFromObject(injector), mockFetchTelemetry); - - clearInterval(telemetry.start()); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/telemetry_init.ts b/src/legacy/core_plugins/telemetry/public/hacks/telemetry_init.ts deleted file mode 100644 index 1930d65d5c09b..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/telemetry_init.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 { npStart } from 'ui/new_platform'; -// @ts-ignore -import { uiModules } from 'ui/modules'; -import { isUnauthenticated } from '../services'; -// @ts-ignore -import { Telemetry } from './telemetry'; -// @ts-ignore -import { fetchTelemetry } from './fetch_telemetry'; -// @ts-ignore -import { isOptInHandleOldSettings } from './welcome_banner/handle_old_settings'; -import { TelemetryOptInProvider } from '../services'; - -function telemetryInit($injector: any) { - const $http = $injector.get('$http'); - const Private = $injector.get('Private'); - const config = $injector.get('config'); - const telemetryOptInProvider = Private(TelemetryOptInProvider); - - const telemetryEnabled = npStart.core.injectedMetadata.getInjectedVar('telemetryEnabled'); - const telemetryOptedIn = isOptInHandleOldSettings(config, telemetryOptInProvider); - const sendUsageFrom = npStart.core.injectedMetadata.getInjectedVar('telemetrySendUsageFrom'); - - if (telemetryEnabled && telemetryOptedIn && sendUsageFrom === 'browser') { - // no telemetry for non-logged in users - if (isUnauthenticated()) { - return; - } - - const sender = new Telemetry($injector, () => fetchTelemetry($http)); - sender.start(); - } -} - -uiModules.get('telemetry/hacks').run(telemetryInit); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.js deleted file mode 100644 index 44971e2466794..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.js +++ /dev/null @@ -1,77 +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 { banners, toastNotifications } from 'ui/notify'; -import { EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -/** - * Handle clicks from the user on the opt-in banner. - * - * @param {Object} telemetryOptInProvider the telemetry opt-in provider - * @param {Boolean} optIn {@code true} to opt into telemetry. - * @param {Object} _banners Singleton banners. Can be overridden for tests. - * @param {Object} _toastNotifications Singleton toast notifications. Can be overridden for tests. - */ -export async function clickBanner( - telemetryOptInProvider, - optIn, - { _banners = banners, _toastNotifications = toastNotifications } = {} -) { - const bannerId = telemetryOptInProvider.getBannerId(); - let set = false; - - try { - set = await telemetryOptInProvider.setOptIn(optIn); - } catch (err) { - // set is already false - console.log('Unexpected error while trying to save setting.', err); - } - - if (set) { - _banners.remove(bannerId); - } else { - _toastNotifications.addDanger({ - title: ( - - ), - text: ( - -

- -

- - - -
- ), - }); - } -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.test.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.test.js deleted file mode 100644 index 0caabe826ae57..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.test.js +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { mockInjectedMetadata } from '../../services/telemetry_opt_in.test.mocks'; - -import sinon from 'sinon'; -import { uiModules } from 'ui/modules'; - -uiModules - .get('kibana') - // disable stat reporting while running tests, - // MockInjector used in these tests is not impacted - .constant('telemetryOptedIn', null); - -import { clickBanner } from './click_banner'; -import { TelemetryOptInProvider } from '../../services/telemetry_opt_in'; - -const getMockInjector = ({ simulateFailure }) => { - const get = sinon.stub(); - - const mockHttp = { - post: sinon.stub(), - }; - - if (simulateFailure) { - mockHttp.post.returns(Promise.reject(new Error('something happened'))); - } else { - mockHttp.post.returns(Promise.resolve({})); - } - - get.withArgs('$http').returns(mockHttp); - - return { get }; -}; - -const getTelemetryOptInProvider = ({ simulateFailure = false, simulateError = false } = {}) => { - const injector = getMockInjector({ simulateFailure }); - const chrome = { - addBasePath: url => url, - }; - - const provider = new TelemetryOptInProvider(injector, chrome, false); - - if (simulateError) { - provider.setOptIn = () => Promise.reject('unhandled error'); - } - - return provider; -}; - -describe('click_banner', () => { - it('sets setting successfully and removes banner', async () => { - const banners = { - remove: sinon.spy(), - }; - - const optIn = true; - const bannerId = 'bruce-banner'; - mockInjectedMetadata({ telemetryOptedIn: optIn, allowChangingOptInStatus: true }); - const telemetryOptInProvider = getTelemetryOptInProvider(); - - telemetryOptInProvider.setBannerId(bannerId); - - await clickBanner(telemetryOptInProvider, optIn, { _banners: banners }); - - expect(telemetryOptInProvider.getOptIn()).toBe(optIn); - expect(banners.remove.calledOnce).toBe(true); - expect(banners.remove.calledWith(bannerId)).toBe(true); - }); - - it('sets setting unsuccessfully, adds toast, and does not touch banner', async () => { - const toastNotifications = { - addDanger: sinon.spy(), - }; - const banners = { - remove: sinon.spy(), - }; - const optIn = true; - mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: true }); - const telemetryOptInProvider = getTelemetryOptInProvider({ simulateFailure: true }); - - await clickBanner(telemetryOptInProvider, optIn, { - _banners: banners, - _toastNotifications: toastNotifications, - }); - - expect(telemetryOptInProvider.getOptIn()).toBe(null); - expect(toastNotifications.addDanger.calledOnce).toBe(true); - expect(banners.remove.notCalled).toBe(true); - }); - - it('sets setting unsuccessfully with error, adds toast, and does not touch banner', async () => { - const toastNotifications = { - addDanger: sinon.spy(), - }; - const banners = { - remove: sinon.spy(), - }; - const optIn = false; - mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: true }); - const telemetryOptInProvider = getTelemetryOptInProvider({ simulateError: true }); - - await clickBanner(telemetryOptInProvider, optIn, { - _banners: banners, - _toastNotifications: toastNotifications, - }); - - expect(telemetryOptInProvider.getOptIn()).toBe(null); - expect(toastNotifications.addDanger.calledOnce).toBe(true); - expect(banners.remove.notCalled).toBe(true); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.js deleted file mode 100644 index c03fdb85c4d1c..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.js +++ /dev/null @@ -1,85 +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 { CONFIG_TELEMETRY } from '../../../common/constants'; - -/** - * Clean up any old, deprecated settings and determine if we should continue. - * - * This will update the latest telemetry setting if necessary. - * - * @param {Object} config The advanced settings config object. - * @return {Boolean} {@code true} if the banner should still be displayed. {@code false} if the banner should not be displayed. - */ -const CONFIG_ALLOW_REPORT = 'xPackMonitoring:allowReport'; - -export async function handleOldSettings(config, telemetryOptInProvider) { - const CONFIG_SHOW_BANNER = 'xPackMonitoring:showBanner'; - const oldAllowReportSetting = config.get(CONFIG_ALLOW_REPORT, null); - const oldTelemetrySetting = config.get(CONFIG_TELEMETRY, null); - - let legacyOptInValue = null; - - if (typeof oldTelemetrySetting === 'boolean') { - legacyOptInValue = oldTelemetrySetting; - } else if (typeof oldAllowReportSetting === 'boolean') { - legacyOptInValue = oldAllowReportSetting; - } - - if (legacyOptInValue !== null) { - try { - await telemetryOptInProvider.setOptIn(legacyOptInValue); - - // delete old keys once we've successfully changed the setting (if it fails, we just wait until next time) - config.remove(CONFIG_ALLOW_REPORT); - config.remove(CONFIG_SHOW_BANNER); - config.remove(CONFIG_TELEMETRY); - } finally { - return false; - } - } - - const oldShowSetting = config.get(CONFIG_SHOW_BANNER, null); - - if (oldShowSetting !== null) { - config.remove(CONFIG_SHOW_BANNER); - } - - return true; -} - -export async function isOptInHandleOldSettings(config, telemetryOptInProvider) { - const currentOptInSettting = telemetryOptInProvider.getOptIn(); - - if (typeof currentOptInSettting === 'boolean') { - return currentOptInSettting; - } - - const oldTelemetrySetting = config.get(CONFIG_TELEMETRY, null); - if (typeof oldTelemetrySetting === 'boolean') { - return oldTelemetrySetting; - } - - const oldAllowReportSetting = config.get(CONFIG_ALLOW_REPORT, null); - if (typeof oldAllowReportSetting === 'boolean') { - return oldAllowReportSetting; - } - - return null; -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js deleted file mode 100644 index 8f05675565a5e..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js +++ /dev/null @@ -1,208 +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 { mockInjectedMetadata } from '../../services/telemetry_opt_in.test.mocks'; - -import sinon from 'sinon'; - -import { CONFIG_TELEMETRY } from '../../../common/constants'; -import { handleOldSettings } from './handle_old_settings'; -import { TelemetryOptInProvider } from '../../services/telemetry_opt_in'; - -const getTelemetryOptInProvider = (enabled, { simulateFailure = false } = {}) => { - const $http = { - post: async () => { - if (simulateFailure) { - return Promise.reject(new Error('something happened')); - } - return {}; - }, - }; - - const chrome = { - addBasePath: url => url, - }; - mockInjectedMetadata({ telemetryOptedIn: enabled, allowChangingOptInStatus: true }); - - const $injector = { - get: key => { - if (key === '$http') { - return $http; - } - throw new Error(`unexpected mock injector usage for ${key}`); - }, - }; - - return new TelemetryOptInProvider($injector, chrome, false); -}; - -describe('handle_old_settings', () => { - it('re-uses old "allowReport" setting and stays opted in', async () => { - const config = { - get: sinon.stub(), - remove: sinon.spy(), - set: sinon.stub(), - }; - - const telemetryOptInProvider = getTelemetryOptInProvider(null); - expect(telemetryOptInProvider.getOptIn()).toBe(null); - - config.get.withArgs('xPackMonitoring:allowReport', null).returns(true); - config.set.withArgs(CONFIG_TELEMETRY, true).returns(Promise.resolve(true)); - - expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(false); - - expect(config.get.calledTwice).toBe(true); - expect(config.set.called).toBe(false); - - expect(config.remove.calledThrice).toBe(true); - expect(config.remove.getCall(0).args[0]).toBe('xPackMonitoring:allowReport'); - expect(config.remove.getCall(1).args[0]).toBe('xPackMonitoring:showBanner'); - expect(config.remove.getCall(2).args[0]).toBe(CONFIG_TELEMETRY); - - expect(telemetryOptInProvider.getOptIn()).toBe(true); - }); - - it('re-uses old "telemetry:optIn" setting and stays opted in', async () => { - const config = { - get: sinon.stub(), - remove: sinon.spy(), - set: sinon.stub(), - }; - - const telemetryOptInProvider = getTelemetryOptInProvider(null); - expect(telemetryOptInProvider.getOptIn()).toBe(null); - - config.get.withArgs('xPackMonitoring:allowReport', null).returns(false); - config.get.withArgs(CONFIG_TELEMETRY, null).returns(true); - - expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(false); - - expect(config.get.calledTwice).toBe(true); - expect(config.set.called).toBe(false); - - expect(config.remove.calledThrice).toBe(true); - expect(config.remove.getCall(0).args[0]).toBe('xPackMonitoring:allowReport'); - expect(config.remove.getCall(1).args[0]).toBe('xPackMonitoring:showBanner'); - expect(config.remove.getCall(2).args[0]).toBe(CONFIG_TELEMETRY); - - expect(telemetryOptInProvider.getOptIn()).toBe(true); - }); - - it('re-uses old "allowReport" setting and stays opted out', async () => { - const config = { - get: sinon.stub(), - remove: sinon.spy(), - set: sinon.stub(), - }; - - const telemetryOptInProvider = getTelemetryOptInProvider(null); - expect(telemetryOptInProvider.getOptIn()).toBe(null); - - config.get.withArgs('xPackMonitoring:allowReport', null).returns(false); - config.set.withArgs(CONFIG_TELEMETRY, false).returns(Promise.resolve(true)); - - expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(false); - - expect(config.get.calledTwice).toBe(true); - expect(config.set.called).toBe(false); - expect(config.remove.calledThrice).toBe(true); - expect(config.remove.getCall(0).args[0]).toBe('xPackMonitoring:allowReport'); - expect(config.remove.getCall(1).args[0]).toBe('xPackMonitoring:showBanner'); - expect(config.remove.getCall(2).args[0]).toBe(CONFIG_TELEMETRY); - - expect(telemetryOptInProvider.getOptIn()).toBe(false); - }); - - it('re-uses old "telemetry:optIn" setting and stays opted out', async () => { - const config = { - get: sinon.stub(), - remove: sinon.spy(), - set: sinon.stub(), - }; - - const telemetryOptInProvider = getTelemetryOptInProvider(null); - - config.get.withArgs(CONFIG_TELEMETRY, null).returns(false); - config.get.withArgs('xPackMonitoring:allowReport', null).returns(true); - - expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(false); - - expect(config.get.calledTwice).toBe(true); - expect(config.set.called).toBe(false); - expect(config.remove.calledThrice).toBe(true); - expect(config.remove.getCall(0).args[0]).toBe('xPackMonitoring:allowReport'); - expect(config.remove.getCall(1).args[0]).toBe('xPackMonitoring:showBanner'); - expect(config.remove.getCall(2).args[0]).toBe(CONFIG_TELEMETRY); - - expect(telemetryOptInProvider.getOptIn()).toBe(false); - }); - - it('acknowledges users old setting even if re-setting fails', async () => { - const config = { - get: sinon.stub(), - set: sinon.stub(), - }; - - const telemetryOptInProvider = getTelemetryOptInProvider(null, { simulateFailure: true }); - - config.get.withArgs('xPackMonitoring:allowReport', null).returns(false); - //todo: make the new version of this fail! - config.set.withArgs(CONFIG_TELEMETRY, false).returns(Promise.resolve(false)); - - // note: because it doesn't remove the old settings _and_ returns false, there's no risk of suddenly being opted in - expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(false); - - expect(config.get.calledTwice).toBe(true); - expect(config.set.called).toBe(false); - }); - - it('removes show banner setting and presents user with choice', async () => { - const config = { - get: sinon.stub(), - remove: sinon.spy(), - }; - - const telemetryOptInProvider = getTelemetryOptInProvider(null); - - config.get.withArgs('xPackMonitoring:allowReport', null).returns(null); - config.get.withArgs('xPackMonitoring:showBanner', null).returns(false); - - expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(true); - - expect(config.get.calledThrice).toBe(true); - expect(config.remove.calledOnce).toBe(true); - expect(config.remove.getCall(0).args[0]).toBe('xPackMonitoring:showBanner'); - }); - - it('is effectively ignored on fresh installs', async () => { - const config = { - get: sinon.stub(), - }; - - const telemetryOptInProvider = getTelemetryOptInProvider(null); - - config.get.withArgs('xPackMonitoring:allowReport', null).returns(null); - config.get.withArgs('xPackMonitoring:showBanner', null).returns(null); - - expect(await handleOldSettings(config, telemetryOptInProvider)).toBe(true); - - expect(config.get.calledThrice).toBe(true); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/inject_banner.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/inject_banner.js deleted file mode 100644 index c4c5c3e9e0aa2..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/inject_banner.js +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import chrome from 'ui/chrome'; - -import { fetchTelemetry } from '../fetch_telemetry'; -import { renderBanner } from './render_banner'; -import { renderOptedInBanner } from './render_notice_banner'; -import { shouldShowBanner } from './should_show_banner'; -import { shouldShowOptInBanner } from './should_show_opt_in_banner'; -import { TelemetryOptInProvider, isUnauthenticated } from '../../services'; -import { npStart } from 'ui/new_platform'; - -/** - * Add the Telemetry opt-in banner if the user has not already made a decision. - * - * Note: this is an async function, but Angular fails to use it as one. Its usage does not need to be awaited, - * and thus it can be wrapped in the run method to just be a normal, non-async function. - * - * @param {Object} $injector The Angular injector - */ -async function asyncInjectBanner($injector) { - const Private = $injector.get('Private'); - const telemetryOptInProvider = Private(TelemetryOptInProvider); - const config = $injector.get('config'); - - // and no banner for non-logged in users - if (isUnauthenticated()) { - return; - } - - // and no banner on status page - if (chrome.getApp().id === 'status_page') { - return; - } - - const $http = $injector.get('$http'); - - // determine if the banner should be displayed - if (await shouldShowBanner(telemetryOptInProvider, config)) { - renderBanner(telemetryOptInProvider, () => fetchTelemetry($http, { unencrypted: true })); - } - - if (await shouldShowOptInBanner(telemetryOptInProvider, config)) { - renderOptedInBanner(telemetryOptInProvider, () => fetchTelemetry($http, { unencrypted: true })); - } -} - -/** - * Add the Telemetry opt-in banner when appropriate. - * - * @param {Object} $injector The Angular injector - */ -export function injectBanner($injector) { - const telemetryEnabled = npStart.core.injectedMetadata.getInjectedVar('telemetryEnabled'); - const telemetryBanner = npStart.core.injectedMetadata.getInjectedVar('telemetryBanner'); - if (telemetryEnabled && telemetryBanner) { - asyncInjectBanner($injector); - } -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.js deleted file mode 100644 index 70b5030866620..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.js +++ /dev/null @@ -1,46 +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 { banners } from 'ui/notify'; - -import { clickBanner } from './click_banner'; -import { OptInBanner } from '../../components/opt_in_banner_component'; - -/** - * Render the Telemetry Opt-in banner. - * - * @param {Object} telemetryOptInProvider The telemetry opt-in provider. - * @param {Function} fetchTelemetry Function to pull telemetry on demand. - * @param {Object} _banners Banners singleton, which can be overridden for tests. - */ -export function renderBanner(telemetryOptInProvider, fetchTelemetry, { _banners = banners } = {}) { - const bannerId = _banners.add({ - component: ( - clickBanner(telemetryOptInProvider, optIn)} - fetchTelemetry={fetchTelemetry} - /> - ), - priority: 10000, - }); - - telemetryOptInProvider.setBannerId(bannerId); -} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js deleted file mode 100644 index 9578d462bc85c..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js +++ /dev/null @@ -1,91 +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 { mockInjectedMetadata } from '../../services/telemetry_opt_in.test.mocks'; - -import sinon from 'sinon'; - -import { CONFIG_TELEMETRY } from '../../../common/constants'; -import { shouldShowBanner } from './should_show_banner'; -import { TelemetryOptInProvider } from '../../services'; - -const getMockInjector = () => { - const get = sinon.stub(); - - const mockHttp = { - post: sinon.stub(), - }; - - get.withArgs('$http').returns(mockHttp); - - return { get }; -}; - -const getTelemetryOptInProvider = ({ telemetryOptedIn = null } = {}) => { - mockInjectedMetadata({ telemetryOptedIn, allowChangingOptInStatus: true }); - const injector = getMockInjector(); - const chrome = { - addBasePath: url => url, - }; - - return new TelemetryOptInProvider(injector, chrome); -}; - -describe('should_show_banner', () => { - it('returns whatever handleOldSettings does when telemetry opt-in setting is unset', async () => { - const config = { get: sinon.stub() }; - const telemetryOptInProvider = getTelemetryOptInProvider(); - const handleOldSettingsTrue = sinon.stub(); - const handleOldSettingsFalse = sinon.stub(); - - config.get.withArgs(CONFIG_TELEMETRY, null).returns(null); - handleOldSettingsTrue.returns(Promise.resolve(true)); - handleOldSettingsFalse.returns(Promise.resolve(false)); - - const showBannerTrue = await shouldShowBanner(telemetryOptInProvider, config, { - _handleOldSettings: handleOldSettingsTrue, - }); - const showBannerFalse = await shouldShowBanner(telemetryOptInProvider, config, { - _handleOldSettings: handleOldSettingsFalse, - }); - - expect(showBannerTrue).toBe(true); - expect(showBannerFalse).toBe(false); - - expect(config.get.callCount).toBe(0); - expect(handleOldSettingsTrue.calledOnce).toBe(true); - expect(handleOldSettingsFalse.calledOnce).toBe(true); - }); - - it('returns false if telemetry opt-in setting is set to true', async () => { - const config = { get: sinon.stub() }; - - const telemetryOptInProvider = getTelemetryOptInProvider({ telemetryOptedIn: true }); - - expect(await shouldShowBanner(telemetryOptInProvider, config)).toBe(false); - }); - - it('returns false if telemetry opt-in setting is set to false', async () => { - const config = { get: sinon.stub() }; - - const telemetryOptInProvider = getTelemetryOptInProvider({ telemetryOptedIn: false }); - - expect(await shouldShowBanner(telemetryOptInProvider, config)).toBe(false); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_opt_in_banner.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_opt_in_banner.js deleted file mode 100644 index 45539c4eea46c..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_opt_in_banner.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * Determine if the notice banner should be displayed. - * - * This method can have side-effects related to deprecated config settings. - * - * @param {Object} telemetryOptInProvider The Telemetry opt-in provider singleton. - * @return {Boolean} {@code true} if the banner should be displayed. {@code false} otherwise. - */ -export async function shouldShowOptInBanner(telemetryOptInProvider) { - return telemetryOptInProvider.notifyUserAboutOptInDefault(); -} diff --git a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.js b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.js deleted file mode 100644 index 494ed24bcc1cb..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.js +++ /dev/null @@ -1,148 +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 { mockInjectedMetadata } from './telemetry_opt_in.test.mocks'; -import { TelemetryOptInProvider } from './telemetry_opt_in'; - -describe('TelemetryOptInProvider', () => { - const setup = ({ optedIn, simulatePostError, simulatePutError }) => { - const mockHttp = { - post: jest.fn(async () => { - if (simulatePostError) { - return Promise.reject('Something happened'); - } - }), - put: jest.fn(async () => { - if (simulatePutError) { - return Promise.reject('Something happened'); - } - }), - }; - - const mockChrome = { - addBasePath: url => url, - }; - - mockInjectedMetadata({ - telemetryOptedIn: optedIn, - allowChangingOptInStatus: true, - telemetryNotifyUserAboutOptInDefault: true, - }); - - const mockInjector = { - get: key => { - switch (key) { - case '$http': { - return mockHttp; - } - default: - throw new Error('unexpected injector request: ' + key); - } - }, - }; - - const provider = new TelemetryOptInProvider(mockInjector, mockChrome, false); - return { - provider, - mockHttp, - }; - }; - - it('should return the current opt-in status', () => { - const { provider: optedInProvider } = setup({ optedIn: true }); - expect(optedInProvider.getOptIn()).toEqual(true); - - const { provider: optedOutProvider } = setup({ optedIn: false }); - expect(optedOutProvider.getOptIn()).toEqual(false); - }); - - it('should allow an opt-out to take place', async () => { - const { provider, mockHttp } = setup({ optedIn: true }); - await provider.setOptIn(false); - - expect(mockHttp.post).toHaveBeenCalledWith(`/api/telemetry/v2/optIn`, { enabled: false }); - - expect(provider.getOptIn()).toEqual(false); - }); - - it('should allow an opt-in to take place', async () => { - const { provider, mockHttp } = setup({ optedIn: false }); - await provider.setOptIn(true); - - expect(mockHttp.post).toHaveBeenCalledWith(`/api/telemetry/v2/optIn`, { enabled: true }); - - expect(provider.getOptIn()).toEqual(true); - }); - - it('should gracefully handle errors', async () => { - const { provider, mockHttp } = setup({ optedIn: false, simulatePostError: true }); - await provider.setOptIn(true); - - expect(mockHttp.post).toHaveBeenCalledWith(`/api/telemetry/v2/optIn`, { enabled: true }); - - // opt-in change should not be reflected - expect(provider.getOptIn()).toEqual(false); - }); - - it('should return the current bannerId', () => { - const { provider } = setup({}); - const bannerId = 'bruce-banner'; - provider.setBannerId(bannerId); - expect(provider.getBannerId()).toEqual(bannerId); - }); - - describe('Notice Banner', () => { - it('should return the current bannerId', () => { - const { provider } = setup({}); - const bannerId = 'bruce-wayne'; - provider.setOptInBannerNoticeId(bannerId); - - expect(provider.getOptInBannerNoticeId()).toEqual(bannerId); - expect(provider.getBannerId()).not.toEqual(bannerId); - }); - - it('should persist that a user has seen the notice', async () => { - const { provider, mockHttp } = setup({}); - await provider.setOptInNoticeSeen(); - - expect(mockHttp.put).toHaveBeenCalledWith(`/api/telemetry/v2/userHasSeenNotice`); - - expect(provider.notifyUserAboutOptInDefault()).toEqual(false); - }); - - it('should only call the API once', async () => { - const { provider, mockHttp } = setup({}); - await provider.setOptInNoticeSeen(); - await provider.setOptInNoticeSeen(); - - expect(mockHttp.put).toHaveBeenCalledTimes(1); - - expect(provider.notifyUserAboutOptInDefault()).toEqual(false); - }); - - it('should gracefully handle errors', async () => { - const { provider } = setup({ simulatePutError: true }); - - await provider.setOptInNoticeSeen(); - - // opt-in change should not be reflected - expect(provider.notifyUserAboutOptInDefault()).toEqual(true); - }); - }); -}); diff --git a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js deleted file mode 100644 index 4543266be46df..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js +++ /dev/null @@ -1,60 +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 { - injectedMetadataServiceMock, - notificationServiceMock, - overlayServiceMock, -} from '../../../../../core/public/mocks'; -const injectedMetadataMock = injectedMetadataServiceMock.createStartContract(); - -export function mockInjectedMetadata({ - telemetryOptedIn, - allowChangingOptInStatus, - telemetryNotifyUserAboutOptInDefault, -}) { - const mockGetInjectedVar = jest.fn().mockImplementation(key => { - switch (key) { - case 'telemetryOptedIn': - return telemetryOptedIn; - case 'allowChangingOptInStatus': - return allowChangingOptInStatus; - case 'telemetryNotifyUserAboutOptInDefault': - return telemetryNotifyUserAboutOptInDefault; - default: - throw new Error(`unexpected injectedVar ${key}`); - } - }); - - injectedMetadataMock.getInjectedVar = mockGetInjectedVar; -} - -jest.doMock('ui/new_platform', () => ({ - npSetup: { - core: { - notifications: notificationServiceMock.createSetupContract(), - }, - }, - npStart: { - core: { - injectedMetadata: injectedMetadataMock, - overlays: overlayServiceMock.createStartContract(), - }, - }, -})); diff --git a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.ts b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.ts deleted file mode 100644 index af908bea7f4b1..0000000000000 --- a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.ts +++ /dev/null @@ -1,154 +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 moment from 'moment'; -// @ts-ignore -import { banners, toastNotifications } from 'ui/notify'; -import { npStart } from 'ui/new_platform'; -import { i18n } from '@kbn/i18n'; - -let bannerId: string | null = null; -let optInBannerNoticeId: string | null = null; -let currentOptInStatus = false; -let telemetryNotifyUserAboutOptInDefault = true; - -async function sendOptInStatus($injector: any, chrome: any, enabled: boolean) { - const telemetryOptInStatusUrl = npStart.core.injectedMetadata.getInjectedVar( - 'telemetryOptInStatusUrl' - ) as string; - const $http = $injector.get('$http'); - - try { - const optInStatus = await $http.post( - chrome.addBasePath('/api/telemetry/v2/clusters/_opt_in_stats'), - { - enabled, - unencrypted: false, - } - ); - - if (optInStatus.data && optInStatus.data.length) { - return await fetch(telemetryOptInStatusUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(optInStatus.data), - }); - } - } catch (err) { - // Sending the ping is best-effort. Telemetry tries to send the ping once and discards it immediately if sending fails. - // swallow any errors - } -} -export function TelemetryOptInProvider($injector: any, chrome: any, sendOptInStatusChange = true) { - currentOptInStatus = npStart.core.injectedMetadata.getInjectedVar('telemetryOptedIn') as boolean; - - const allowChangingOptInStatus = npStart.core.injectedMetadata.getInjectedVar( - 'allowChangingOptInStatus' - ) as boolean; - - telemetryNotifyUserAboutOptInDefault = npStart.core.injectedMetadata.getInjectedVar( - 'telemetryNotifyUserAboutOptInDefault' - ) as boolean; - - const provider = { - getBannerId: () => bannerId, - getOptInBannerNoticeId: () => optInBannerNoticeId, - getOptIn: () => currentOptInStatus, - canChangeOptInStatus: () => allowChangingOptInStatus, - notifyUserAboutOptInDefault: () => telemetryNotifyUserAboutOptInDefault, - setBannerId(id: string) { - bannerId = id; - }, - setOptInBannerNoticeId(id: string) { - optInBannerNoticeId = id; - }, - setOptInNoticeSeen: async () => { - const $http = $injector.get('$http'); - - // If they've seen the notice don't spam the API - if (!telemetryNotifyUserAboutOptInDefault) { - return telemetryNotifyUserAboutOptInDefault; - } - - if (optInBannerNoticeId) { - banners.remove(optInBannerNoticeId); - } - - try { - await $http.put(chrome.addBasePath('/api/telemetry/v2/userHasSeenNotice')); - telemetryNotifyUserAboutOptInDefault = false; - } catch (error) { - toastNotifications.addError(error, { - title: i18n.translate('telemetry.optInNoticeSeenErrorTitle', { - defaultMessage: 'Error', - }), - toastMessage: i18n.translate('telemetry.optInNoticeSeenErrorToastText', { - defaultMessage: 'An error occurred dismissing the notice', - }), - }); - telemetryNotifyUserAboutOptInDefault = true; - } - - return telemetryNotifyUserAboutOptInDefault; - }, - setOptIn: async (enabled: boolean) => { - if (!allowChangingOptInStatus) { - return; - } - const $http = $injector.get('$http'); - - try { - await $http.post(chrome.addBasePath('/api/telemetry/v2/optIn'), { enabled }); - if (sendOptInStatusChange) { - await sendOptInStatus($injector, chrome, enabled); - } - currentOptInStatus = enabled; - } catch (error) { - toastNotifications.addError(error, { - title: i18n.translate('telemetry.optInErrorToastTitle', { - defaultMessage: 'Error', - }), - toastMessage: i18n.translate('telemetry.optInErrorToastText', { - defaultMessage: - 'An error occurred while trying to set the usage statistics preference.', - }), - }); - return false; - } - - return true; - }, - fetchExample: async () => { - const $http = $injector.get('$http'); - return $http.post(chrome.addBasePath(`/api/telemetry/v2/clusters/_stats`), { - unencrypted: true, - timeRange: { - min: moment() - .subtract(20, 'minutes') - .toISOString(), - max: moment().toISOString(), - }, - }); - }, - }; - - return provider; -} diff --git a/src/legacy/core_plugins/telemetry/public/views/management/index.js b/src/legacy/core_plugins/telemetry/public/views/management/index.ts similarity index 100% rename from src/legacy/core_plugins/telemetry/public/views/management/index.js rename to src/legacy/core_plugins/telemetry/public/views/management/index.ts diff --git a/src/legacy/core_plugins/telemetry/public/views/management/management.js b/src/legacy/core_plugins/telemetry/public/views/management/management.tsx similarity index 52% rename from src/legacy/core_plugins/telemetry/public/views/management/management.js rename to src/legacy/core_plugins/telemetry/public/views/management/management.tsx index 7032775e391bb..c8ae410e0aa57 100644 --- a/src/legacy/core_plugins/telemetry/public/views/management/management.js +++ b/src/legacy/core_plugins/telemetry/public/views/management/management.tsx @@ -18,30 +18,32 @@ */ import React from 'react'; import routes from 'ui/routes'; - -import { npSetup } from 'ui/new_platform'; -import { TelemetryOptInProvider } from '../../services'; -import { TelemetryForm } from '../../components'; +import { npStart, npSetup } from 'ui/new_platform'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { TelemetryManagementSection } from '../../../../../../plugins/telemetry/public/components'; routes.defaults(/\/management/, { resolve: { - telemetryManagementSection: function(Private) { - const telemetryOptInProvider = Private(TelemetryOptInProvider); - const componentRegistry = npSetup.plugins.advancedSettings.component; + telemetryManagementSection() { + const { telemetry } = npStart.plugins as any; + const { advancedSettings } = npSetup.plugins as any; - const Component = props => ( - - ); + if (telemetry && advancedSettings) { + const componentRegistry = advancedSettings.component; + const Component = (props: any) => ( + + ); - componentRegistry.register( - componentRegistry.componentType.PAGE_FOOTER_COMPONENT, - Component, - true - ); + componentRegistry.register( + componentRegistry.componentType.PAGE_FOOTER_COMPONENT, + Component, + true + ); + } }, }, }); diff --git a/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts index 99090cb2fb7ef..6919b6959aa8c 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts @@ -24,7 +24,6 @@ import { dirname, join } from 'path'; // look for telemetry.yml in the same places we expect kibana.yml import { ensureDeepObject } from './ensure_deep_object'; -import { getXpackConfigWithDeprecated } from '../../../common/get_xpack_config_with_deprecated'; import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server'; /** @@ -85,7 +84,7 @@ export function createTelemetryUsageCollector( isReady: () => true, fetch: async () => { const config = server.config(); - const configPath = getXpackConfigWithDeprecated(config, 'telemetry.config') as string; + const configPath = config.get('telemetry.config') as string; const telemetryPath = join(dirname(configPath), 'telemetry.yml'); return await readTelemetryFile(telemetryPath); }, diff --git a/src/legacy/core_plugins/telemetry/server/fetcher.ts b/src/legacy/core_plugins/telemetry/server/fetcher.ts index 9edd8457f2b89..6e16328c4abd8 100644 --- a/src/legacy/core_plugins/telemetry/server/fetcher.ts +++ b/src/legacy/core_plugins/telemetry/server/fetcher.ts @@ -24,7 +24,6 @@ import { telemetryCollectionManager } from './collection_manager'; import { getTelemetryOptIn, getTelemetrySendUsageFrom } from './telemetry_config'; import { getTelemetrySavedObject, updateTelemetrySavedObject } from './telemetry_repository'; import { REPORT_INTERVAL_MS } from '../common/constants'; -import { getXpackConfigWithDeprecated } from '../common/get_xpack_config_with_deprecated'; export class FetcherTask { private readonly checkDurationMs = 60 * 1000 * 5; @@ -52,7 +51,7 @@ export class FetcherTask { const configTelemetrySendUsageFrom = config.get('telemetry.sendUsageFrom'); const allowChangingOptInStatus = config.get('telemetry.allowChangingOptInStatus'); const configTelemetryOptIn = config.get('telemetry.optIn'); - const telemetryUrl = getXpackConfigWithDeprecated(config, 'telemetry.url') as string; + const telemetryUrl = config.get('telemetry.url') as string; return { telemetryOptIn: getTelemetryOptIn({ diff --git a/src/legacy/core_plugins/telemetry/server/handle_old_settings/handle_old_settings.ts b/src/legacy/core_plugins/telemetry/server/handle_old_settings/handle_old_settings.ts new file mode 100644 index 0000000000000..b28a01bffa44d --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/handle_old_settings/handle_old_settings.ts @@ -0,0 +1,59 @@ +/* + * 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. + */ + +/** + * Clean up any old, deprecated settings and determine if we should continue. + * + * This will update the latest telemetry setting if necessary. + * + * @param {Object} config The advanced settings config object. + * @return {Boolean} {@code true} if the banner should still be displayed. {@code false} if the banner should not be displayed. + */ + +import { Server } from 'hapi'; +import { CONFIG_TELEMETRY } from '../../common/constants'; +import { updateTelemetrySavedObject } from '../telemetry_repository'; + +const CONFIG_ALLOW_REPORT = 'xPackMonitoring:allowReport'; + +export async function handleOldSettings(server: Server) { + const { getSavedObjectsRepository } = server.savedObjects; + const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); + const savedObjectsClient = getSavedObjectsRepository(callWithInternalUser); + const uiSettings = server.uiSettingsServiceFactory({ savedObjectsClient }); + + const oldTelemetrySetting = await uiSettings.get(CONFIG_TELEMETRY); + const oldAllowReportSetting = await uiSettings.get(CONFIG_ALLOW_REPORT); + let legacyOptInValue = null; + + if (typeof oldTelemetrySetting === 'boolean') { + legacyOptInValue = oldTelemetrySetting; + } else if ( + typeof oldAllowReportSetting === 'boolean' && + uiSettings.isOverridden(CONFIG_ALLOW_REPORT) + ) { + legacyOptInValue = oldAllowReportSetting; + } + + if (legacyOptInValue !== null) { + await updateTelemetrySavedObject(savedObjectsClient, { + enabled: legacyOptInValue, + }); + } +} diff --git a/src/legacy/core_plugins/telemetry/server/handle_old_settings/index.ts b/src/legacy/core_plugins/telemetry/server/handle_old_settings/index.ts new file mode 100644 index 0000000000000..77eae0d80db61 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/handle_old_settings/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { handleOldSettings } from './handle_old_settings'; diff --git a/src/legacy/core_plugins/telemetry/server/index.ts b/src/legacy/core_plugins/telemetry/server/index.ts index 6c62d03adf25c..85d7d80234ffc 100644 --- a/src/legacy/core_plugins/telemetry/server/index.ts +++ b/src/legacy/core_plugins/telemetry/server/index.ts @@ -23,6 +23,7 @@ import * as constants from '../common/constants'; export { FetcherTask } from './fetcher'; export { replaceTelemetryInjectedVars } from './telemetry_config'; +export { handleOldSettings } from './handle_old_settings'; export { telemetryCollectionManager } from './collection_manager'; export { PluginsSetup } from './plugin'; export const telemetryPlugin = (initializerContext: PluginInitializerContext) => diff --git a/src/legacy/core_plugins/tile_map/public/tile_map_fn.js b/src/legacy/core_plugins/tile_map/public/tile_map_fn.js index f5cb6fdf93002..2f54d23590c33 100644 --- a/src/legacy/core_plugins/tile_map/public/tile_map_fn.js +++ b/src/legacy/core_plugins/tile_map/public/tile_map_fn.js @@ -43,6 +43,10 @@ export const createTileMapFn = () => ({ geocentroid, }); + if (geohash && geohash.accessor) { + convertedData.meta.geohash = context.columns[geohash.accessor].meta; + } + return { type: 'render', as: 'visualization', diff --git a/src/legacy/core_plugins/tile_map/public/tile_map_visualization.js b/src/legacy/core_plugins/tile_map/public/tile_map_visualization.js index 772edaa4ff4f5..910def8a0c78e 100644 --- a/src/legacy/core_plugins/tile_map/public/tile_map_visualization.js +++ b/src/legacy/core_plugins/tile_map/public/tile_map_visualization.js @@ -23,6 +23,12 @@ import { BaseMapsVisualizationProvider } from './base_maps_visualization'; import { TileMapTooltipFormatterProvider } from './editors/_tooltip_formatter'; import { npStart } from 'ui/new_platform'; import { getFormat } from '../../../ui/public/visualize/loader/pipeline_helpers/utilities'; +import { + scaleBounds, + zoomPrecision, + getPrecision, + geoContains, +} from '../../../ui/public/vis/map/decode_geo_hash'; export const createTileMapVisualization = ({ serviceSettings, $injector }) => { const BaseMapsVisualization = new BaseMapsVisualizationProvider(serviceSettings); @@ -35,42 +41,47 @@ export const createTileMapVisualization = ({ serviceSettings, $injector }) => { this._geohashLayer = null; } + updateGeohashAgg = () => { + const geohashAgg = this._getGeoHashAgg(); + if (!geohashAgg) return; + const updateVarsObject = { + name: 'bounds', + data: {}, + }; + const bounds = this._kibanaMap.getBounds(); + const mapCollar = scaleBounds(bounds); + if (!geoContains(geohashAgg.aggConfigParams.boundingBox, mapCollar)) { + updateVarsObject.data.boundingBox = { + top_left: mapCollar.top_left, + bottom_right: mapCollar.bottom_right, + }; + } else { + updateVarsObject.data.boundingBox = geohashAgg.aggConfigParams.boundingBox; + } + // todo: autoPrecision should be vis parameter, not aggConfig one + updateVarsObject.data.precision = geohashAgg.aggConfigParams.autoPrecision + ? zoomPrecision[this.vis.getUiState().get('mapZoom')] + : getPrecision(geohashAgg.aggConfigParams.precision); + + this.vis.eventsSubject.next(updateVarsObject); + }; + async _makeKibanaMap() { await super._makeKibanaMap(); - const updateGeohashAgg = () => { - const geohashAgg = this._getGeoHashAgg(); - if (!geohashAgg) return; - geohashAgg.params.mapBounds = this._kibanaMap.getBounds(); - geohashAgg.params.mapZoom = this._kibanaMap.getZoomLevel(); - geohashAgg.params.mapCenter = this._kibanaMap.getCenter(); - }; - - updateGeohashAgg(); + let previousPrecision = this._kibanaMap.getGeohashPrecision(); + let precisionChange = false; const uiState = this.vis.getUiState(); uiState.on('change', prop => { if (prop === 'mapZoom' || prop === 'mapCenter') { - updateGeohashAgg(); + this.updateGeohashAgg(); } }); - let previousPrecision = this._kibanaMap.getGeohashPrecision(); - let precisionChange = false; this._kibanaMap.on('zoomchange', () => { - const geohashAgg = this._getGeoHashAgg(); precisionChange = previousPrecision !== this._kibanaMap.getGeohashPrecision(); previousPrecision = this._kibanaMap.getGeohashPrecision(); - if (!geohashAgg) { - return; - } - const isAutoPrecision = - typeof geohashAgg.params.autoPrecision === 'boolean' - ? geohashAgg.params.autoPrecision - : true; - if (isAutoPrecision) { - geohashAgg.params.precision = previousPrecision; - } }); this._kibanaMap.on('zoomend', () => { const geohashAgg = this._getGeoHashAgg(); @@ -78,15 +89,14 @@ export const createTileMapVisualization = ({ serviceSettings, $injector }) => { return; } const isAutoPrecision = - typeof geohashAgg.params.autoPrecision === 'boolean' - ? geohashAgg.params.autoPrecision + typeof geohashAgg.aggConfigParams.autoPrecision === 'boolean' + ? geohashAgg.aggConfigParams.autoPrecision : true; if (!isAutoPrecision) { return; } if (precisionChange) { - updateGeohashAgg(); - this.vis.updateState(); + this.updateGeohashAgg(); } else { //when we filter queries by collar this._updateData(this._geoJsonFeatureCollectionAndMeta); @@ -126,6 +136,14 @@ export const createTileMapVisualization = ({ serviceSettings, $injector }) => { return; } + if ( + !this._geoJsonFeatureCollectionAndMeta || + !geojsonFeatureCollectionAndMeta.featureCollection.features.length + ) { + this._geoJsonFeatureCollectionAndMeta = geojsonFeatureCollectionAndMeta; + this.updateGeohashAgg(); + } + this._geoJsonFeatureCollectionAndMeta = geojsonFeatureCollectionAndMeta; this._recreateGeohashLayer(); } @@ -181,7 +199,6 @@ export const createTileMapVisualization = ({ serviceSettings, $injector }) => { tooltipFormatter: this._geoJsonFeatureCollectionAndMeta ? boundTooltipFormatter : null, mapType: newParams.mapType, isFilteredByCollar: this._isFilteredByCollar(), - fetchBounds: () => this.vis.API.getGeohashBounds(), // TODO: Remove this (elastic/kibana#30593) colorRamp: newParams.colorSchema, heatmap: { heatClusterSize: newParams.heatClusterSize, @@ -194,8 +211,8 @@ export const createTileMapVisualization = ({ serviceSettings, $injector }) => { return; } - const indexPatternName = agg.getIndexPattern().id; - const field = agg.fieldName(); + const indexPatternName = agg.indexPatternId; + const field = agg.aggConfigParams.field; const filter = { meta: { negate: false, index: indexPatternName } }; filter[filterName] = { ignore_unmapped: true }; filter[filterName][field] = filterData; @@ -207,16 +224,16 @@ export const createTileMapVisualization = ({ serviceSettings, $injector }) => { } _getGeoHashAgg() { - return this.vis.getAggConfig().aggs.find(agg => { - return get(agg, 'type.dslName') === 'geohash_grid'; - }); + return ( + this._geoJsonFeatureCollectionAndMeta && this._geoJsonFeatureCollectionAndMeta.meta.geohash + ); } _isFilteredByCollar() { const DEFAULT = false; const agg = this._getGeoHashAgg(); if (agg) { - return get(agg, 'params.isFilteredByCollar', DEFAULT); + return get(agg, 'aggConfigParams.isFilteredByCollar', DEFAULT); } else { return DEFAULT; } diff --git a/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx b/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx index 48a1a6f9d2121..32ea71c0bc005 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/default_editor.tsx @@ -30,7 +30,7 @@ import { DefaultEditorControllerState } from './default_editor_controller'; import { getInitialWidth } from './editor_size'; function DefaultEditor({ - embeddables, + embeddable, savedObj, uiState, timeRange, @@ -56,7 +56,7 @@ function DefaultEditor({ } if (!visHandler.current) { - const embeddableFactory = embeddables.getEmbeddableFactory( + const embeddableFactory = embeddable.getEmbeddableFactory( 'visualization' ) as VisualizeEmbeddableFactory; setFactory(embeddableFactory); @@ -82,7 +82,7 @@ function DefaultEditor({ } visualize(); - }, [uiState, savedObj, timeRange, filters, appState, query, factory, embeddables]); + }, [uiState, savedObj, timeRange, filters, appState, query, factory, embeddable]); useEffect(() => { return () => { diff --git a/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx b/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx index 72838d2d97421..5729618b6ae07 100644 --- a/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx +++ b/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx @@ -17,8 +17,8 @@ * under the License. */ -import React, { useEffect, useMemo } from 'react'; import { get } from 'lodash'; +import React, { useEffect, useMemo } from 'react'; import { EuiIconTip, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -27,7 +27,7 @@ import { VisOptionsProps } from 'src/legacy/core_plugins/vis_default_editor/publ import { tabifyGetColumns } from '../legacy_imports'; import { NumberInputOption, SwitchOption, SelectOption } from '../../../vis_type_vislib/public'; import { TableVisParams } from '../types'; -import { totalAggregations, isAggConfigNumeric } from './utils'; +import { totalAggregations } from './utils'; function TableOptions({ aggs, @@ -44,7 +44,7 @@ function TableOptions({ }), }, ...tabifyGetColumns(aggs.getResponseAggs(), true) - .filter(col => isAggConfigNumeric(get(col, 'aggConfig.type.name'), stateParams.dimensions)) + .filter(col => get(col.aggConfig.type.getFormat(col.aggConfig), 'type.id') === 'number') .map(({ name }) => ({ value: name, text: name })), ], [aggs, stateParams.percentageCol, stateParams.dimensions] diff --git a/src/legacy/core_plugins/vis_type_table/public/components/utils.ts b/src/legacy/core_plugins/vis_type_table/public/components/utils.ts index 365566503e25b..b97c7ccbac0f7 100644 --- a/src/legacy/core_plugins/vis_type_table/public/components/utils.ts +++ b/src/legacy/core_plugins/vis_type_table/public/components/utils.ts @@ -17,20 +17,8 @@ * under the License. */ -import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { AggTypes, Dimensions } from '../types'; - -function isAggConfigNumeric( - type: AggTypes, - { buckets, metrics }: Dimensions = { buckets: [], metrics: [] } -) { - const dimension = - buckets.find(({ aggType }) => aggType === type) || - metrics.find(({ aggType }) => aggType === type); - const formatType = get(dimension, 'format.id') || get(dimension, 'format.params.id'); - return formatType === 'number'; -} +import { AggTypes } from '../types'; const totalAggregations = [ { @@ -65,4 +53,4 @@ const totalAggregations = [ }, ]; -export { isAggConfigNumeric, totalAggregations }; +export { totalAggregations }; diff --git a/src/legacy/core_plugins/vis_type_table/public/vis_controller.ts b/src/legacy/core_plugins/vis_type_table/public/vis_controller.ts index a792fc98842f1..2d27a99bdd8af 100644 --- a/src/legacy/core_plugins/vis_type_table/public/vis_controller.ts +++ b/src/legacy/core_plugins/vis_type_table/public/vis_controller.ts @@ -74,7 +74,7 @@ export class TableVisualizationController { return; } this.$scope.vis = this.vis; - this.$scope.visState = this.vis.getState(); + this.$scope.visState = { params: visParams }; this.$scope.esResponse = esResponse; if (!isEqual(this.$scope.visParams, visParams)) { diff --git a/src/legacy/core_plugins/vis_type_vega/index.ts b/src/legacy/core_plugins/vis_type_vega/index.ts index 52c253c6ac0b5..ccef24f8f9746 100644 --- a/src/legacy/core_plugins/vis_type_vega/index.ts +++ b/src/legacy/core_plugins/vis_type_vega/index.ts @@ -39,17 +39,10 @@ const vegaPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPlugin return { emsTileLayerId: mapConfig.emsTileLayerId, - enableExternalUrls: serverConfig.get('vega.enableExternalUrls'), }; }, }, init: (server: Legacy.Server) => ({}), - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - enableExternalUrls: Joi.boolean().default(false), - }).default(); - }, } as Legacy.PluginSpecOptions); // eslint-disable-next-line import/no-default-export diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js index b2ad45b5d7b6d..868e5729bd494 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js +++ b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js @@ -47,6 +47,7 @@ import { createVegaTypeDefinition } from '../vega_type'; // this test has to be migrated to the newly created integration test environment. // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { npStart } from 'ui/new_platform'; +import { setInjectedVars } from '../services'; const THRESHOLD = 0.1; const PIXEL_DIFF = 30; @@ -60,6 +61,12 @@ describe('VegaVisualizations', () => { let vegaVisualizationDependencies; let visRegComplete = false; + setInjectedVars({ + emsTileLayerId: {}, + enableExternalUrls: true, + esShardTimeout: 10000, + }); + beforeEach(ngMock.module('kibana')); beforeEach( ngMock.inject((Private, $injector) => { diff --git a/src/legacy/core_plugins/vis_type_vega/public/legacy.ts b/src/legacy/core_plugins/vis_type_vega/public/legacy.ts index a7928c7d65e81..38ce706ed13ef 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/legacy.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/legacy.ts @@ -26,9 +26,8 @@ import { LegacyDependenciesPlugin } from './shim'; import { plugin } from '.'; const setupPlugins: Readonly = { - expressions: npSetup.plugins.expressions, + ...npSetup.plugins, visualizations: visualizationsSetup, - data: npSetup.plugins.data, // Temporary solution // It will be removed when all dependent services are migrated to the new platform. @@ -36,7 +35,7 @@ const setupPlugins: Readonly = { }; const startPlugins: Readonly = { - data: npStart.plugins.data, + ...npStart.plugins, }; const pluginInstance = plugin({} as PluginInitializerContext); diff --git a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts b/src/legacy/core_plugins/vis_type_vega/public/plugin.ts index 9721de9848cfc..b354433330caf 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/plugin.ts @@ -31,6 +31,7 @@ import { import { createVegaFn } from './vega_fn'; import { createVegaTypeDefinition } from './vega_type'; +import { VisTypeVegaSetup } from '../../../../plugins/vis_type_vega/public'; /** @internal */ export interface VegaVisualizationDependencies extends LegacyDependenciesPluginSetup { @@ -45,6 +46,7 @@ export interface VegaPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; data: ReturnType; + visTypeVega: VisTypeVegaSetup; __LEGACY: LegacyDependenciesPlugin; } @@ -63,11 +65,11 @@ export class VegaPlugin implements Plugin, void> { public async setup( core: CoreSetup, - { data, expressions, visualizations, __LEGACY }: VegaPluginSetupDependencies + { data, expressions, visualizations, visTypeVega, __LEGACY }: VegaPluginSetupDependencies ) { setInjectedVars({ + enableExternalUrls: visTypeVega.config.enableExternalUrls, esShardTimeout: core.injectedMetadata.getInjectedVar('esShardTimeout') as number, - enableExternalUrls: core.injectedMetadata.getInjectedVar('enableExternalUrls') as boolean, emsTileLayerId: core.injectedMetadata.getInjectedVar('emsTileLayerId', true), }); setUISettings(core.uiSettings); diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_fn.ts b/src/legacy/core_plugins/vis_type_vega/public/vega_fn.ts index afb476472a273..2a0da81a31a96 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_fn.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/vega_fn.ts @@ -73,7 +73,7 @@ export const createVegaFn = ( as: 'visualization', value: { visData: response, - visType: name, + visType: 'vega', visConfig: { spec: args.spec, }, diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts index 126e9d769f0a2..5e593398333c9 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -17,7 +17,7 @@ * under the License. */ -import _ from 'lodash'; +import _, { get } from 'lodash'; import { PersistedState } from 'ui/persisted_state'; import { Subscription } from 'rxjs'; import * as Rx from 'rxjs'; @@ -46,7 +46,6 @@ import { import { dispatchRenderComplete } from '../../../../../plugins/kibana_utils/public'; import { SavedSearch } from '../../../kibana/public/discover/np_ready/types'; import { Vis } from '../np_ready/public'; -import { queryGeohashBounds } from './query_geohash_bounds'; const getKeys = (o: T): Array => Object.keys(o) as Array; @@ -253,17 +252,6 @@ export class VisualizeEmbeddable extends Embeddable { - return queryGeohashBounds(this.savedVisualization.vis, { - filters: this.filters, - query: this.query, - searchSource: this.savedVisualization.searchSource, - }); - }; - // this is a hack to make editor still work, will be removed once we clean up editor this.vis.hasInspector = () => { const visTypesWithoutInspector = [ @@ -290,6 +278,22 @@ export class VisualizeEmbeddable extends Embeddable { + // maps hack, remove once esaggs function is cleaned up and ready to accept variables + if (event.name === 'bounds') { + const agg = this.vis.getAggConfig().aggs.find((a: any) => { + return get(a, 'type.dslName') === 'geohash_grid'; + }); + if ( + agg.params.precision !== event.data.precision || + !_.isEqual(agg.params.boundingBox, event.data.boundingBox) + ) { + agg.params.boundingBox = event.data.boundingBox; + agg.params.precision = event.data.precision; + this.reload(); + } + return; + } + const eventName = event.name === 'brush' ? SELECT_RANGE_TRIGGER : VALUE_CLICK_TRIGGER; npStart.plugins.uiActions.executeTriggerActions(eventName, { @@ -355,7 +359,6 @@ export class VisualizeEmbeddable extends Embeddable ({ render: async (domNode: HTMLElement, config: any, handlers: any) => { const { visData, visConfig, params } = config; const visType = config.visType || visConfig.type; - const $injector = await legacyChrome.dangerouslyGetActiveInjector(); - const $rootScope = $injector.get('$rootScope') as any; - if (handlers.vis) { - // special case in visualize, we need to render first (without executing the expression), for maps to work - if (visConfig) { - $rootScope.$apply(() => { - handlers.vis.setCurrentState({ - type: visType, - params: visConfig, - title: handlers.vis.title, - }); - }); - } - } else { - handlers.vis = new Vis({ - type: visType, - params: visConfig, - }); - } + const vis = new Vis({ + type: visType, + params: visConfig, + }); - handlers.vis.eventsSubject = { next: handlers.event }; + vis.eventsSubject = { next: handlers.event }; - const uiState = handlers.uiState || handlers.vis.getUiState(); + const uiState = handlers.uiState || vis.getUiState(); handlers.onDestroy(() => { unmountComponentAtNode(domNode); @@ -63,9 +47,9 @@ export const visualization = () => ({ const listenOnChange = params ? params.listenOnChange : false; render( { }; export class Build { - constructor({ - log, - sourcePath, - targetPath, - urlImports, - theme, - sourceMap = true, - outputStyle = 'nested', - }) { + constructor({ log, sourcePath, targetPath, urlImports, theme }) { this.log = log; this.sourcePath = sourcePath; this.sourceDir = dirname(this.sourcePath); @@ -73,8 +65,6 @@ export class Build { this.urlImports = urlImports; this.theme = theme; this.includedFiles = [sourcePath]; - this.sourceMap = sourceMap; - this.outputStyle = outputStyle; } /** @@ -97,11 +87,11 @@ export class Build { const rendered = await renderSass({ file: this.sourcePath, outFile: this.targetPath, - sourceMap: this.sourceMap, - outputStyle: this.outputStyle, - sourceMapEmbed: this.sourceMap, - includePaths: [resolve(__dirname, '../../../../node_modules')], importer: this.theme === 'dark' ? DARK_THEME_IMPORTER : undefined, + sourceMap: true, + outputStyle: 'nested', + sourceMapEmbed: true, + includePaths: [resolve(__dirname, '../../../../node_modules')], }); const processor = postcss([autoprefixer]); diff --git a/src/legacy/server/sass/build_all.js b/src/legacy/server/sass/build_all.js index d066e52792ca8..1d3d76d1cb01a 100644 --- a/src/legacy/server/sass/build_all.js +++ b/src/legacy/server/sass/build_all.js @@ -21,7 +21,7 @@ import { resolve } from 'path'; import { Build } from './build'; -export async function buildAll({ styleSheets, log, buildDir, sourceMap, outputStyle }) { +export async function buildAll({ styleSheets, log, buildDir }) { const bundles = await Promise.all( styleSheets.map(async styleSheet => { if (!styleSheet.localPath.endsWith('.scss')) { @@ -31,8 +31,6 @@ export async function buildAll({ styleSheets, log, buildDir, sourceMap, outputSt const bundle = new Build({ sourcePath: styleSheet.localPath, log, - sourceMap, - outputStyle, theme: styleSheet.theme, targetPath: resolve(buildDir, styleSheet.publicPath), urlImports: styleSheet.urlImports, diff --git a/src/legacy/ui/public/legacy_compat/__tests__/xsrf.js b/src/legacy/ui/public/legacy_compat/__tests__/xsrf.js index fc12a18d72823..3ca836e23881a 100644 --- a/src/legacy/ui/public/legacy_compat/__tests__/xsrf.js +++ b/src/legacy/ui/public/legacy_compat/__tests__/xsrf.js @@ -27,13 +27,6 @@ import { $setupXsrfRequestInterceptor } from '../../../../../plugins/kibana_lega import { version } from '../../../../../core/server/utils/package_json'; const xsrfHeader = 'kbn-version'; -const newPlatform = { - injectedMetadata: { - getLegacyMetadata() { - return { version }; - }, - }, -}; describe('chrome xsrf apis', function() { const sandbox = sinon.createSandbox(); @@ -45,7 +38,7 @@ describe('chrome xsrf apis', function() { describe('jQuery support', function() { it('adds a global jQuery prefilter', function() { sandbox.stub($, 'ajaxPrefilter'); - $setupXsrfRequestInterceptor(newPlatform); + $setupXsrfRequestInterceptor(version); expect($.ajaxPrefilter.callCount).to.be(1); }); @@ -54,7 +47,7 @@ describe('chrome xsrf apis', function() { beforeEach(function() { sandbox.stub($, 'ajaxPrefilter'); - $setupXsrfRequestInterceptor(newPlatform); + $setupXsrfRequestInterceptor(version); prefilter = $.ajaxPrefilter.args[0][0]; }); @@ -79,7 +72,7 @@ describe('chrome xsrf apis', function() { beforeEach(function() { sandbox.stub($, 'ajaxPrefilter'); - ngMock.module($setupXsrfRequestInterceptor(newPlatform)); + ngMock.module($setupXsrfRequestInterceptor(version)); }); beforeEach( diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index e300ce4a0caf8..b7994c7f68afb 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -39,10 +39,12 @@ import { import { ManagementSetup, ManagementStart } from '../../../../plugins/management/public'; import { BfetchPublicSetup, BfetchPublicStart } from '../../../../plugins/bfetch/public'; import { UsageCollectionSetup } from '../../../../plugins/usage_collection/public'; +import { TelemetryPluginSetup, TelemetryPluginStart } from '../../../../plugins/telemetry/public'; import { NavigationPublicPluginSetup, NavigationPublicPluginStart, } from '../../../../plugins/navigation/public'; +import { VisTypeVegaSetup } from '../../../../plugins/vis_type_vega/public'; export interface PluginsSetup { bfetch: BfetchPublicSetup; @@ -60,6 +62,8 @@ export interface PluginsSetup { usageCollection: UsageCollectionSetup; advancedSettings: AdvancedSettingsSetup; management: ManagementSetup; + visTypeVega: VisTypeVegaSetup; + telemetry?: TelemetryPluginSetup; } export interface PluginsStart { @@ -77,6 +81,7 @@ export interface PluginsStart { share: SharePluginStart; management: ManagementStart; advancedSettings: AdvancedSettingsStart; + telemetry?: TelemetryPluginStart; } export const npSetup = { diff --git a/src/legacy/ui/public/state_management/global_state.js b/src/legacy/ui/public/state_management/global_state.js index 955759e305950..d8ff38106b978 100644 --- a/src/legacy/ui/public/state_management/global_state.js +++ b/src/legacy/ui/public/state_management/global_state.js @@ -17,7 +17,6 @@ * under the License. */ -import { QueryString } from '../utils/query_string'; import { StateProvider } from './state'; import { uiModules } from '../modules'; import { createLegacyClass } from '../utils/legacy_class'; @@ -35,10 +34,6 @@ export function GlobalStateProvider(Private) { // if the url param is missing, write it back GlobalState.prototype._persistAcrossApps = true; - GlobalState.prototype.removeFromUrl = function(url) { - return QueryString.replaceParamInUrl(url, this._urlParam, null); - }; - return new GlobalState(); } diff --git a/src/legacy/ui/public/system_api/__tests__/system_api.js b/src/legacy/ui/public/system_api/__tests__/system_api.js index 822edaa08fdd6..816024f13f8b2 100644 --- a/src/legacy/ui/public/system_api/__tests__/system_api.js +++ b/src/legacy/ui/public/system_api/__tests__/system_api.js @@ -31,8 +31,8 @@ describe('system_api', () => { }; const newHeaders = addSystemApiHeader(headers); - expect(newHeaders).to.have.property('kbn-system-api'); - expect(newHeaders['kbn-system-api']).to.be(true); + expect(newHeaders).to.have.property('kbn-system-request'); + expect(newHeaders['kbn-system-request']).to.be(true); expect(newHeaders).to.have.property('kbn-version'); expect(newHeaders['kbn-version']).to.be('4.6.0'); @@ -40,7 +40,16 @@ describe('system_api', () => { }); describe('#isSystemApiRequest', () => { - it('returns true for a system API HTTP request', () => { + it('returns true for a system HTTP request', () => { + const mockRequest = { + headers: { + 'kbn-system-request': true, + }, + }; + expect(isSystemApiRequest(mockRequest)).to.be(true); + }); + + it('returns true for a legacy system API HTTP request', () => { const mockRequest = { headers: { 'kbn-system-api': true, diff --git a/src/legacy/ui/public/utils/query_string.js b/src/legacy/ui/public/utils/query_string.js deleted file mode 100644 index 5fbc6da67bc98..0000000000000 --- a/src/legacy/ui/public/utils/query_string.js +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { encodeQueryComponent } from '../../../utils'; - -export const QueryString = {}; - -/***** -/*** originally copied from angular, modified our purposes -/*****/ - -function tryDecodeURIComponent(value) { - try { - return decodeURIComponent(value); - } catch (e) { - // Ignore any invalid uri component - } // eslint-disable-line no-empty -} - -/** - * Parses an escaped url query string into key-value pairs. - * @returns {Object.} - */ -QueryString.decode = function(keyValue) { - const obj = {}; - let keyValueParts; - let key; - - (keyValue || '').split('&').forEach(function(keyValue) { - if (keyValue) { - keyValueParts = keyValue.split('='); - key = tryDecodeURIComponent(keyValueParts[0]); - if (key !== void 0) { - const val = keyValueParts[1] !== void 0 ? tryDecodeURIComponent(keyValueParts[1]) : true; - if (!obj[key]) { - obj[key] = val; - } else if (Array.isArray(obj[key])) { - obj[key].push(val); - } else { - obj[key] = [obj[key], val]; - } - } - } - }); - return obj; -}; - -/** - * Creates a queryString out of an object - * @param {Object} obj - * @return {String} - */ -QueryString.encode = function(obj) { - const parts = []; - const keys = Object.keys(obj).sort(); - keys.forEach(function(key) { - const value = obj[key]; - if (Array.isArray(value)) { - value.forEach(function(arrayValue) { - parts.push(QueryString.param(key, arrayValue)); - }); - } else { - parts.push(QueryString.param(key, value)); - } - }); - return parts.length ? parts.join('&') : ''; -}; - -QueryString.param = function(key, val) { - return ( - encodeQueryComponent(key, true) + (val === true ? '' : '=' + encodeQueryComponent(val, true)) - ); -}; - -/** - * Extracts the query string from a url - * @param {String} url - * @return {Object} - returns an object describing the start/end index of the url in the string. The indices will be - * the same if the url does not have a query string - */ -QueryString.findInUrl = function(url) { - let qsStart = url.indexOf('?'); - let hashStart = url.lastIndexOf('#'); - - if (hashStart === -1) { - // out of bounds - hashStart = url.length; - } - - if (qsStart === -1) { - qsStart = hashStart; - } - - return { - start: qsStart, - end: hashStart, - }; -}; - -QueryString.replaceParamInUrl = function(url, param, newVal) { - const loc = QueryString.findInUrl(url); - const parsed = QueryString.decode(url.substring(loc.start + 1, loc.end)); - - if (newVal != null) { - parsed[param] = newVal; - } else { - delete parsed[param]; - } - - const chars = url.split(''); - chars.splice(loc.start, loc.end - loc.start, '?' + QueryString.encode(parsed)); - return chars.join(''); -}; diff --git a/src/legacy/ui/public/vis/map/decode_geo_hash.ts b/src/legacy/ui/public/vis/map/decode_geo_hash.ts index 6a004771e4648..3f8430b8628d7 100644 --- a/src/legacy/ui/public/vis/map/decode_geo_hash.ts +++ b/src/legacy/ui/public/vis/map/decode_geo_hash.ts @@ -17,6 +17,11 @@ * under the License. */ +import chrome from 'ui/chrome'; +import _ from 'lodash'; + +const config = chrome.getUiSettingsClient(); + interface DecodedGeoHash { latitude: number[]; longitude: number[]; @@ -93,3 +98,95 @@ function geohashCells(precision: number, axis: number) { export function geohashColumns(precision: number): number { return geohashCells(precision, 0); } + +const defaultPrecision = 2; +const maxPrecision = parseInt(config.get('visualization:tileMap:maxPrecision'), 10) || 12; +/** + * Map Leaflet zoom levels to geohash precision levels. + * The size of a geohash column-width on the map should be at least `minGeohashPixels` pixels wide. + */ +export const zoomPrecision: any = {}; +const minGeohashPixels = 16; + +for (let zoom = 0; zoom <= 21; zoom += 1) { + const worldPixels = 256 * Math.pow(2, zoom); + zoomPrecision[zoom] = 1; + for (let precision = 2; precision <= maxPrecision; precision += 1) { + const columns = geohashColumns(precision); + if (worldPixels / columns >= minGeohashPixels) { + zoomPrecision[zoom] = precision; + } else { + break; + } + } +} + +export function getPrecision(val: string) { + let precision = parseInt(val, 10); + + if (Number.isNaN(precision)) { + precision = defaultPrecision; + } + + if (precision > maxPrecision) { + return maxPrecision; + } + + return precision; +} + +interface GeoBoundingBoxCoordinate { + lat: number; + lon: number; +} + +interface GeoBoundingBox { + top_left: GeoBoundingBoxCoordinate; + bottom_right: GeoBoundingBoxCoordinate; +} + +export function scaleBounds(bounds: GeoBoundingBox): GeoBoundingBox { + const scale = 0.5; // scale bounds by 50% + + const topLeft = bounds.top_left; + const bottomRight = bounds.bottom_right; + let latDiff = _.round(Math.abs(topLeft.lat - bottomRight.lat), 5); + const lonDiff = _.round(Math.abs(bottomRight.lon - topLeft.lon), 5); + // map height can be zero when vis is first created + if (latDiff === 0) latDiff = lonDiff; + + const latDelta = latDiff * scale; + let topLeftLat = _.round(topLeft.lat, 5) + latDelta; + if (topLeftLat > 90) topLeftLat = 90; + let bottomRightLat = _.round(bottomRight.lat, 5) - latDelta; + if (bottomRightLat < -90) bottomRightLat = -90; + const lonDelta = lonDiff * scale; + let topLeftLon = _.round(topLeft.lon, 5) - lonDelta; + if (topLeftLon < -180) topLeftLon = -180; + let bottomRightLon = _.round(bottomRight.lon, 5) + lonDelta; + if (bottomRightLon > 180) bottomRightLon = 180; + + return { + top_left: { lat: topLeftLat, lon: topLeftLon }, + bottom_right: { lat: bottomRightLat, lon: bottomRightLon }, + }; +} + +export function geoContains(collar?: GeoBoundingBox, bounds?: GeoBoundingBox) { + if (!bounds || !collar) return false; + // test if bounds top_left is outside collar + if (bounds.top_left.lat > collar.top_left.lat || bounds.top_left.lon < collar.top_left.lon) { + return false; + } + + // test if bounds bottom_right is outside collar + if ( + bounds.bottom_right.lat < collar.bottom_right.lat || + bounds.bottom_right.lon > collar.bottom_right.lon + ) { + return false; + } + + // both corners are inside collar so collar contains bounds + return true; +} diff --git a/src/legacy/ui/ui_exports/ui_export_defaults.js b/src/legacy/ui/ui_exports/ui_export_defaults.js index 1cb23d2ad2a23..bb246d97bfe4e 100644 --- a/src/legacy/ui/ui_exports/ui_export_defaults.js +++ b/src/legacy/ui/ui_exports/ui_export_defaults.js @@ -30,9 +30,6 @@ export const UI_EXPORT_DEFAULTS = { ui: resolve(ROOT, 'src/legacy/ui/public'), __kibanaCore__$: resolve(ROOT, 'src/core/public'), test_harness: resolve(ROOT, 'src/test_harness/public'), - querystring: 'querystring-browser', - moment$: resolve(ROOT, 'webpackShims/moment'), - 'moment-timezone$': resolve(ROOT, 'webpackShims/moment-timezone'), }, styleSheetPaths: ['light', 'dark'].map(theme => ({ diff --git a/src/legacy/ui/ui_render/bootstrap/template.js.hbs b/src/legacy/ui/ui_render/bootstrap/template.js.hbs index 72dd97ff58642..106dbcd9f8ab2 100644 --- a/src/legacy/ui/ui_render/bootstrap/template.js.hbs +++ b/src/legacy/ui/ui_render/bootstrap/template.js.hbs @@ -1,5 +1,6 @@ var kbnCsp = JSON.parse(document.querySelector('kbn-csp').getAttribute('data')); window.__kbnStrictCsp__ = kbnCsp.strictCsp; +window.__kbnDarkMode__ = {{darkMode}}; if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { var legacyBrowserError = document.getElementById('kbn_legacy_browser_error'); @@ -12,17 +13,7 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { loadingMessage.style.display = 'flex'; window.onload = function () { - var files = [ - '{{dllBundlePath}}/vendors_runtime.bundle.dll.js', - {{#each dllJsChunks}} - '{{this}}', - {{/each}} - '{{regularBundlePath}}/kbn-ui-shared-deps/{{sharedDepsFilename}}', - '{{regularBundlePath}}/commons.bundle.js', - '{{regularBundlePath}}/{{appId}}.bundle.js' - ]; - - var failure = function () { + function failure() { // make subsequent calls to failure() noop failure = function () {}; @@ -37,41 +28,73 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { document.body.innerHTML = err.outerHTML; } - function loadStyleSheet(path) { + function loadStyleSheet(url, cb) { var dom = document.createElement('link'); - dom.addEventListener('error', failure); dom.setAttribute('rel', 'stylesheet'); dom.setAttribute('type', 'text/css'); - dom.setAttribute('href', path); + dom.setAttribute('href', url); + dom.addEventListener('load', cb); document.head.appendChild(dom); } - function createJavascriptElement(path) { + function loadScript(url, cb) { var dom = document.createElement('script'); - - dom.setAttribute('defer', 'defer'); + dom.setAttribute('async', ''); dom.addEventListener('error', failure); - dom.setAttribute('src', file); - dom.addEventListener('load', next); + dom.setAttribute('src', url); + dom.addEventListener('load', cb); document.head.appendChild(dom); } - {{#each styleSheetPaths}} - loadStyleSheet('{{this}}'); - {{/each}} + function load(urlSet, cb) { + if (urlSet.deps) { + load({ urls: urlSet.deps }, function () { + load({ urls: urlSet.urls }, cb); + }); + return; + } - (function next() { - var file = files.shift(); - if (!file) return; + var pending = urlSet.urls.length; + urlSet.urls.forEach(function (url) { + var innerCb = function () { + pending = pending - 1; + if (pending === 0 && typeof cb === 'function') { + cb(); + } + } - var dom = document.createElement('script'); + if (typeof url !== 'string') { + load(url, innerCb); + } else if (url.slice(-4) === '.css') { + loadStyleSheet(url, innerCb); + } else { + loadScript(url, innerCb); + } + }); + } - dom.setAttribute('async', ''); - dom.addEventListener('error', failure); - dom.setAttribute('src', file); - dom.addEventListener('load', next); - document.head.appendChild(dom); - }()); + load({ + deps: [ + { + deps: [ + '{{dllBundlePath}}/vendors_runtime.bundle.dll.js' + ], + urls: [ + {{#each dllJsChunks}} + '{{this}}', + {{/each}} + ] + }, + '{{regularBundlePath}}/kbn-ui-shared-deps/{{sharedDepsFilename}}', + '{{regularBundlePath}}/commons.bundle.js', + ], + urls: [ + '{{regularBundlePath}}/{{appId}}.bundle.js', + {{#each styleSheetPaths}} + '{{this}}', + {{/each}}, + ] + }); }; } diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 4158af19bd858..21c10bb20962f 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -142,6 +142,7 @@ export function uiRenderMixin(kbnServer, server, config) { dllJsChunks, styleSheetPaths, sharedDepsFilename: UiSharedDeps.distFilename, + darkMode, }, }); diff --git a/src/legacy/utils/index.js b/src/legacy/utils/index.js index 2e6381b31ecee..a4c0cdf958fc2 100644 --- a/src/legacy/utils/index.js +++ b/src/legacy/utils/index.js @@ -21,7 +21,6 @@ export { BinderBase } from './binder'; export { BinderFor } from './binder_for'; export { deepCloneWithBuffers } from './deep_clone_with_buffers'; export { unset } from './unset'; -export { encodeQueryComponent } from './encode_query_component'; export { watchStdioForLine } from './watch_stdio_for_line'; export { IS_KIBANA_DISTRIBUTABLE } from './artifact_type'; export { IS_KIBANA_RELEASE } from './artifact_type'; diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js index 539c55c969653..a833204eaa0e2 100644 --- a/src/optimize/base_optimizer.js +++ b/src/optimize/base_optimizer.js @@ -39,6 +39,7 @@ import { PUBLIC_PATH_PLACEHOLDER } from './public_path_placeholder'; const POSTCSS_CONFIG_PATH = require.resolve('./postcss.config'); const BABEL_PRESET_PATH = require.resolve('@kbn/babel-preset/webpack_preset'); const ISTANBUL_PRESET_PATH = require.resolve('@kbn/babel-preset/istanbul_preset'); +const EMPTY_MODULE_PATH = require.resolve('./intentionally_empty_module.js'); const BABEL_EXCLUDE_RE = [/[\/\\](webpackShims|node_modules|bower_components)[\/\\]/]; const STATS_WARNINGS_FILTER = new RegExp( [ @@ -62,7 +63,6 @@ export default class BaseOptimizer { constructor(opts) { this.logWithMetadata = opts.logWithMetadata || (() => null); this.uiBundles = opts.uiBundles; - this.newPlatformPluginInfo = opts.newPlatformPluginInfo; this.profile = opts.profile || false; this.workers = opts.workers; @@ -147,7 +147,7 @@ export default class BaseOptimizer { return 1; } - return Math.max(1, Math.min(cpus.length - 1, 7)); + return Math.max(1, Math.min(cpus.length - 1, 3)); } getThreadLoaderPoolConfig() { @@ -247,7 +247,6 @@ export default class BaseOptimizer { cache: true, entry: { ...this.uiBundles.toWebpackEntries(), - ...this._getDiscoveredPluginEntryPoints(), light_theme: [require.resolve('../legacy/ui/public/styles/bootstrap_light.less')], dark_theme: [require.resolve('../legacy/ui/public/styles/bootstrap_dark.less')], }, @@ -262,12 +261,6 @@ export default class BaseOptimizer { sourceMapFilename: '[file].map', publicPath: PUBLIC_PATH_PLACEHOLDER, devtoolModuleFilenameTemplate: '[absolute-resource-path]', - - // When the entry point is loaded, assign it's exported `plugin` - // value to a key on the global `__kbnBundles__` object. - // NOTE: Only actually used by new platform plugins - library: ['__kbnBundles__', '[name]'], - libraryExport: 'plugin', }, optimization: { @@ -308,6 +301,11 @@ export default class BaseOptimizer { filename: '[name].style.css', }), + // ignore scss imports in new-platform code that finds its way into legacy bundles + new webpack.NormalModuleReplacementPlugin(/\.scss$/, resource => { + resource.request = EMPTY_MODULE_PATH; + }), + // replace imports for `uiExports/*` modules with a synthetic module // created by create_ui_exports_module.js new webpack.NormalModuleReplacementPlugin(/^uiExports\//, resource => { @@ -396,7 +394,10 @@ export default class BaseOptimizer { 'node_modules', fromRoot('node_modules'), ], - alias: this.uiBundles.getAliases(), + alias: { + ...this.uiBundles.getAliases(), + tinymath: require.resolve('tinymath/lib/tinymath.es5.js'), + }, }, performance: { @@ -524,17 +525,6 @@ export default class BaseOptimizer { ); } - _getDiscoveredPluginEntryPoints() { - // New platform plugin entry points - return [...this.newPlatformPluginInfo.entries()].reduce( - (entryPoints, [pluginId, pluginInfo]) => { - entryPoints[`plugin/${pluginId}`] = pluginInfo.entryPointPath; - return entryPoints; - }, - {} - ); - } - getPresets() { return IS_CODE_COVERAGE ? [ISTANBUL_PRESET_PATH, BABEL_PRESET_PATH] : [BABEL_PRESET_PATH]; } diff --git a/src/optimize/bundles_route/__tests__/fixtures/plugin/no_placeholder/no_placeholder.plugin.js b/src/optimize/bundles_route/__tests__/fixtures/plugin/no_placeholder/no_placeholder.plugin.js new file mode 100644 index 0000000000000..519e301113ff5 --- /dev/null +++ b/src/optimize/bundles_route/__tests__/fixtures/plugin/no_placeholder/no_placeholder.plugin.js @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = 'BAR'; diff --git a/src/optimize/bundles_route/__tests__/fixtures/plugin/placeholder/placeholder.plugin.js b/src/optimize/bundles_route/__tests__/fixtures/plugin/placeholder/placeholder.plugin.js new file mode 100644 index 0000000000000..8c959f9b4779f --- /dev/null +++ b/src/optimize/bundles_route/__tests__/fixtures/plugin/placeholder/placeholder.plugin.js @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = '__REPLACE_WITH_PUBLIC_PATH__/FOO'; diff --git a/src/optimize/bundles_route/bundles_route.js b/src/optimize/bundles_route/bundles_route.js index f0261d44e0347..0c2e98b5acd63 100644 --- a/src/optimize/bundles_route/bundles_route.js +++ b/src/optimize/bundles_route/bundles_route.js @@ -21,6 +21,7 @@ import { isAbsolute, extname } from 'path'; import LruCache from 'lru-cache'; import * as UiSharedDeps from '@kbn/ui-shared-deps'; import { createDynamicAssetResponse } from './dynamic_asset_response'; +import { assertIsNpUiPluginPublicDirs } from '../np_ui_plugin_public_dirs'; /** * Creates the routes that serves files from `bundlesPath` or from @@ -29,6 +30,7 @@ import { createDynamicAssetResponse } from './dynamic_asset_response'; * PUBLIC_PATH_PLACEHOLDER and replaces them with `publicPath`. * * @param {Object} options + * @property {Array<{id,path}>} options.npUiPluginPublicDirs array of ids and paths that should be served for new platform plugins * @property {string} options.regularBundlesPath * @property {string} options.dllBundlesPath * @property {string} options.basePublicPath @@ -40,11 +42,13 @@ export function createBundlesRoute({ dllBundlesPath, basePublicPath, builtCssPath, + npUiPluginPublicDirs = [], }) { // rather than calculate the fileHash on every request, we - // provide a cache object to `createDynamicAssetResponse()` that + // provide a cache object to `resolveDynamicAssetResponse()` that // will store the 100 most recently used hashes. const fileHashCache = new LruCache(100); + assertIsNpUiPluginPublicDirs(npUiPluginPublicDirs); if (typeof regularBundlesPath !== 'string' || !isAbsolute(regularBundlesPath)) { throw new TypeError( @@ -73,6 +77,14 @@ export function createBundlesRoute({ UiSharedDeps.distDir, fileHashCache ), + ...npUiPluginPublicDirs.map(({ id, path }) => + buildRouteForBundles( + `${basePublicPath}/bundles/plugin/${id}/`, + `/bundles/plugin/${id}/`, + path, + fileHashCache + ) + ), buildRouteForBundles( `${basePublicPath}/bundles/`, '/bundles/', diff --git a/src/optimize/index.js b/src/optimize/index.js index 83825e5a8f386..b7b9f7712358a 100644 --- a/src/optimize/index.js +++ b/src/optimize/index.js @@ -21,6 +21,8 @@ import FsOptimizer from './fs_optimizer'; import { createBundlesRoute } from './bundles_route'; import { DllCompiler } from './dynamic_dll_plugin'; import { fromRoot } from '../core/server/utils'; +import { getNpUiPluginPublicDirs } from './np_ui_plugin_public_dirs'; + export default async (kbnServer, server, config) => { if (!config.get('optimize.enabled')) return; @@ -37,13 +39,14 @@ export default async (kbnServer, server, config) => { return await kbnServer.mixin(require('./watch/watch')); } - const { newPlatform, uiBundles } = kbnServer; + const { uiBundles } = kbnServer; server.route( createBundlesRoute({ regularBundlesPath: uiBundles.getWorkingDir(), dllBundlesPath: DllCompiler.getRawDllConfig().outputPath, basePublicPath: config.get('server.basePath'), builtCssPath: fromRoot('built_assets/css'), + npUiPluginPublicDirs: getNpUiPluginPublicDirs(kbnServer), }) ); @@ -64,7 +67,6 @@ export default async (kbnServer, server, config) => { const optimizer = new FsOptimizer({ logWithMetadata: (tags, message, metadata) => server.logWithMetadata(tags, message, metadata), uiBundles, - newPlatformPluginInfo: newPlatform.__internals.uiPlugins.internal, profile: config.get('optimize.profile'), sourceMaps: config.get('optimize.sourceMaps'), workers: config.get('optimize.workers'), diff --git a/src/optimize/intentionally_empty_module.js b/src/optimize/intentionally_empty_module.js new file mode 100644 index 0000000000000..9880b336e76e5 --- /dev/null +++ b/src/optimize/intentionally_empty_module.js @@ -0,0 +1,18 @@ +/* + * 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. + */ diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.js b/src/optimize/np_ui_plugin_public_dirs.js similarity index 54% rename from src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.js rename to src/optimize/np_ui_plugin_public_dirs.js index ee55f6cc76266..de05fd2b863b8 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.js +++ b/src/optimize/np_ui_plugin_public_dirs.js @@ -17,24 +17,28 @@ * under the License. */ -import { handleOldSettings } from './handle_old_settings'; +export function getNpUiPluginPublicDirs(kbnServer) { + return Array.from(kbnServer.newPlatform.__internals.uiPlugins.internal.entries()).map( + ([id, { publicTargetDir }]) => ({ + id, + path: publicTargetDir, + }) + ); +} -/** - * Determine if the banner should be displayed. - * - * This method can have side-effects related to deprecated config settings. - * - * @param {Object} config The advanced settings config object. - * @param {Object} _handleOldSettings handleOldSettings function, but overridable for tests. - * @return {Boolean} {@code true} if the banner should be displayed. {@code false} otherwise. - */ -export async function shouldShowBanner( - telemetryOptInProvider, - config, - { _handleOldSettings = handleOldSettings } = {} -) { +export function isNpUiPluginPublicDirs(something) { return ( - telemetryOptInProvider.getOptIn() === null && - (await _handleOldSettings(config, telemetryOptInProvider)) + Array.isArray(something) && + something.every( + s => typeof s === 'object' && s && typeof s.id === 'string' && typeof s.path === 'string' + ) ); } + +export function assertIsNpUiPluginPublicDirs(something) { + if (!isNpUiPluginPublicDirs(something)) { + throw new TypeError( + 'npUiPluginPublicDirs must be an array of objects with string `id` and `path` properties' + ); + } +} diff --git a/src/optimize/watch/optmzr_role.js b/src/optimize/watch/optmzr_role.js index ed1c51f933eaa..a31ef7229e5da 100644 --- a/src/optimize/watch/optmzr_role.js +++ b/src/optimize/watch/optmzr_role.js @@ -23,6 +23,7 @@ import WatchServer from './watch_server'; import WatchOptimizer, { STATUS } from './watch_optimizer'; import { DllCompiler } from '../dynamic_dll_plugin'; import { WatchCache } from './watch_cache'; +import { getNpUiPluginPublicDirs } from '../np_ui_plugin_public_dirs'; export default async (kbnServer, kibanaHapiServer, config) => { const logWithMetadata = (tags, message, metadata) => @@ -31,7 +32,6 @@ export default async (kbnServer, kibanaHapiServer, config) => { const watchOptimizer = new WatchOptimizer({ logWithMetadata, uiBundles: kbnServer.uiBundles, - newPlatformPluginInfo: kbnServer.newPlatform.__internals.uiPlugins.internal, profile: config.get('optimize.profile'), sourceMaps: config.get('optimize.sourceMaps'), workers: config.get('optimize.workers'), @@ -48,7 +48,8 @@ export default async (kbnServer, kibanaHapiServer, config) => { config.get('optimize.watchHost'), config.get('optimize.watchPort'), config.get('server.basePath'), - watchOptimizer + watchOptimizer, + getNpUiPluginPublicDirs(kbnServer) ); watchOptimizer.status$.subscribe({ diff --git a/src/optimize/watch/watch_optimizer.js b/src/optimize/watch/watch_optimizer.js index a0595816f6a65..6c20f21c7768e 100644 --- a/src/optimize/watch/watch_optimizer.js +++ b/src/optimize/watch/watch_optimizer.js @@ -106,7 +106,7 @@ export default class WatchOptimizer extends BaseOptimizer { }); } - bindToServer(server, basePath) { + bindToServer(server, basePath, npUiPluginPublicDirs) { // pause all requests received while the compiler is running // and continue once an outcome is reached (aborting the request // with an error if it was a failure). @@ -117,6 +117,7 @@ export default class WatchOptimizer extends BaseOptimizer { server.route( createBundlesRoute({ + npUiPluginPublicDirs: npUiPluginPublicDirs, regularBundlesPath: this.compiler.outputPath, dllBundlesPath: DllCompiler.getRawDllConfig().outputPath, basePublicPath: basePath, diff --git a/src/optimize/watch/watch_server.js b/src/optimize/watch/watch_server.js index f21db0de61824..74a96dc8aea6e 100644 --- a/src/optimize/watch/watch_server.js +++ b/src/optimize/watch/watch_server.js @@ -21,9 +21,10 @@ import { Server } from 'hapi'; import { registerHapiPlugins } from '../../legacy/server/http/register_hapi_plugins'; export default class WatchServer { - constructor(host, port, basePath, optimizer) { + constructor(host, port, basePath, optimizer, npUiPluginPublicDirs) { this.basePath = basePath; this.optimizer = optimizer; + this.npUiPluginPublicDirs = npUiPluginPublicDirs; this.server = new Server({ host: host, port: port, @@ -34,7 +35,7 @@ export default class WatchServer { async init() { await this.optimizer.init(); - this.optimizer.bindToServer(this.server, this.basePath); + this.optimizer.bindToServer(this.server, this.basePath, this.npUiPluginPublicDirs); await this.server.start(); } } diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx index b3e966ddffa4c..bfa74392c14fb 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx @@ -21,11 +21,7 @@ import React, { CSSProperties, useCallback, useEffect, useRef, useState } from ' import { EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { debounce } from 'lodash'; - -// Node v5 querystring for browser. -// @ts-ignore -import * as qs from 'querystring-browser'; - +import { parse } from 'query-string'; import { EuiIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useServicesContext, useEditorReadContext } from '../../../../contexts'; import { useUIAceKeyboardMode } from '../use_ui_ace_keyboard_mode'; @@ -51,6 +47,10 @@ export interface EditorProps { initialTextValue: string; } +interface QueryParams { + load_from: string; +} + const abs: CSSProperties = { position: 'absolute', top: '0', @@ -98,7 +98,8 @@ function EditorUI({ initialTextValue }: EditorProps) { const readQueryParams = () => { const [, queryString] = (window.location.hash || '').split('?'); - return qs.parse(queryString || ''); + + return parse(queryString || '', { sort: false }) as Required; }; const loadBufferFromRemote = (url: string) => { @@ -138,6 +139,7 @@ function EditorUI({ initialTextValue }: EditorProps) { window.addEventListener('hashchange', onHashChange); const initialQueryParams = readQueryParams(); + if (initialQueryParams.load_from) { loadBufferFromRemote(initialQueryParams.load_from); } else { diff --git a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/input.test.js b/src/plugins/console/public/application/models/legacy_core_editor/__tests__/input.test.js new file mode 100644 index 0000000000000..a68a2b3939864 --- /dev/null +++ b/src/plugins/console/public/application/models/legacy_core_editor/__tests__/input.test.js @@ -0,0 +1,568 @@ +/* + * 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 '../legacy_core_editor.test.mocks'; +import RowParser from '../../../../lib/row_parser'; +import { createTokenIterator } from '../../../factories'; +import $ from 'jquery'; +import { create } from '../create'; + +describe('Input', () => { + let coreEditor; + beforeEach(() => { + // Set up our document body + document.body.innerHTML = `
+
+
+
+
`; + + coreEditor = create(document.querySelector('#ConAppEditor')); + + $(coreEditor.getContainer()).show(); + }); + afterEach(() => { + $(coreEditor.getContainer()).hide(); + }); + + describe('.getLineCount', () => { + it('returns the correct line length', async () => { + await coreEditor.setValue('1\n2\n3\n4', true); + expect(coreEditor.getLineCount()).toBe(4); + }); + }); + + describe('Tokenization', () => { + function tokensAsList() { + const iter = createTokenIterator({ + editor: coreEditor, + position: { lineNumber: 1, column: 1 }, + }); + const ret = []; + let t = iter.getCurrentToken(); + const parser = new RowParser(coreEditor); + if (parser.isEmptyToken(t)) { + t = parser.nextNonEmptyToken(iter); + } + while (t) { + ret.push({ value: t.value, type: t.type }); + t = parser.nextNonEmptyToken(iter); + } + + return ret; + } + + let testCount = 0; + + function tokenTest(tokenList, prefix, data) { + if (data && typeof data !== 'string') { + data = JSON.stringify(data, null, 3); + } + if (data) { + if (prefix) { + data = prefix + '\n' + data; + } + } else { + data = prefix; + } + + test('Token test ' + testCount++ + ' prefix: ' + prefix, async function() { + await coreEditor.setValue(data, true); + const tokens = tokensAsList(); + const normTokenList = []; + for (let i = 0; i < tokenList.length; i++) { + normTokenList.push({ type: tokenList[i++], value: tokenList[i] }); + } + + expect(tokens).toEqual(normTokenList); + }); + } + + tokenTest(['method', 'GET', 'url.part', '_search'], 'GET _search'); + + tokenTest(['method', 'GET', 'url.slash', '/', 'url.part', '_search'], 'GET /_search'); + + tokenTest( + [ + 'method', + 'GET', + 'url.protocol_host', + 'http://somehost', + 'url.slash', + '/', + 'url.part', + '_search', + ], + 'GET http://somehost/_search' + ); + + tokenTest(['method', 'GET', 'url.protocol_host', 'http://somehost'], 'GET http://somehost'); + + tokenTest( + ['method', 'GET', 'url.protocol_host', 'http://somehost', 'url.slash', '/'], + 'GET http://somehost/' + ); + + tokenTest( + ['method', 'GET', 'url.protocol_host', 'http://test:user@somehost', 'url.slash', '/'], + 'GET http://test:user@somehost/' + ); + + tokenTest( + ['method', 'GET', 'url.part', '_cluster', 'url.slash', '/', 'url.part', 'nodes'], + 'GET _cluster/nodes' + ); + + tokenTest( + [ + 'method', + 'GET', + 'url.slash', + '/', + 'url.part', + '_cluster', + 'url.slash', + '/', + 'url.part', + 'nodes', + ], + 'GET /_cluster/nodes' + ); + + tokenTest( + ['method', 'GET', 'url.part', 'index', 'url.slash', '/', 'url.part', '_search'], + 'GET index/_search' + ); + + tokenTest(['method', 'GET', 'url.part', 'index'], 'GET index'); + + tokenTest( + ['method', 'GET', 'url.part', 'index', 'url.slash', '/', 'url.part', 'type'], + 'GET index/type' + ); + + tokenTest( + [ + 'method', + 'GET', + 'url.slash', + '/', + 'url.part', + 'index', + 'url.slash', + '/', + 'url.part', + 'type', + 'url.slash', + '/', + ], + 'GET /index/type/' + ); + + tokenTest( + [ + 'method', + 'GET', + 'url.part', + 'index', + 'url.slash', + '/', + 'url.part', + 'type', + 'url.slash', + '/', + 'url.part', + '_search', + ], + 'GET index/type/_search' + ); + + tokenTest( + [ + 'method', + 'GET', + 'url.part', + 'index', + 'url.slash', + '/', + 'url.part', + 'type', + 'url.slash', + '/', + 'url.part', + '_search', + 'url.questionmark', + '?', + 'url.param', + 'value', + 'url.equal', + '=', + 'url.value', + '1', + ], + 'GET index/type/_search?value=1' + ); + + tokenTest( + [ + 'method', + 'GET', + 'url.part', + 'index', + 'url.slash', + '/', + 'url.part', + 'type', + 'url.slash', + '/', + 'url.part', + '1', + ], + 'GET index/type/1' + ); + + tokenTest( + [ + 'method', + 'GET', + 'url.slash', + '/', + 'url.part', + 'index1', + 'url.comma', + ',', + 'url.part', + 'index2', + 'url.slash', + '/', + ], + 'GET /index1,index2/' + ); + + tokenTest( + [ + 'method', + 'GET', + 'url.slash', + '/', + 'url.part', + 'index1', + 'url.comma', + ',', + 'url.part', + 'index2', + 'url.slash', + '/', + 'url.part', + '_search', + ], + 'GET /index1,index2/_search' + ); + + tokenTest( + [ + 'method', + 'GET', + 'url.part', + 'index1', + 'url.comma', + ',', + 'url.part', + 'index2', + 'url.slash', + '/', + 'url.part', + '_search', + ], + 'GET index1,index2/_search' + ); + + tokenTest( + [ + 'method', + 'GET', + 'url.slash', + '/', + 'url.part', + 'index1', + 'url.comma', + ',', + 'url.part', + 'index2', + ], + 'GET /index1,index2' + ); + + tokenTest( + ['method', 'GET', 'url.part', 'index1', 'url.comma', ',', 'url.part', 'index2'], + 'GET index1,index2' + ); + + tokenTest( + ['method', 'GET', 'url.slash', '/', 'url.part', 'index1', 'url.comma', ','], + 'GET /index1,' + ); + + tokenTest( + ['method', 'PUT', 'url.slash', '/', 'url.part', 'index', 'url.slash', '/'], + 'PUT /index/' + ); + + tokenTest( + ['method', 'GET', 'url.part', 'index', 'url.slash', '/', 'url.part', '_search'], + 'GET index/_search ' + ); + + tokenTest(['method', 'PUT', 'url.slash', '/', 'url.part', 'index'], 'PUT /index'); + + tokenTest( + [ + 'method', + 'PUT', + 'url.slash', + '/', + 'url.part', + 'index1', + 'url.comma', + ',', + 'url.part', + 'index2', + 'url.slash', + '/', + 'url.part', + 'type1', + 'url.comma', + ',', + 'url.part', + 'type2', + ], + 'PUT /index1,index2/type1,type2' + ); + + tokenTest( + [ + 'method', + 'PUT', + 'url.slash', + '/', + 'url.part', + 'index1', + 'url.slash', + '/', + 'url.part', + 'type1', + 'url.comma', + ',', + 'url.part', + 'type2', + 'url.comma', + ',', + ], + 'PUT /index1/type1,type2,' + ); + + tokenTest( + [ + 'method', + 'PUT', + 'url.part', + 'index1', + 'url.comma', + ',', + 'url.part', + 'index2', + 'url.slash', + '/', + 'url.part', + 'type1', + 'url.comma', + ',', + 'url.part', + 'type2', + 'url.slash', + '/', + 'url.part', + '1234', + ], + 'PUT index1,index2/type1,type2/1234' + ); + + tokenTest( + [ + 'method', + 'POST', + 'url.part', + '_search', + 'paren.lparen', + '{', + 'variable', + '"q"', + 'punctuation.colon', + ':', + 'paren.lparen', + '{', + 'paren.rparen', + '}', + 'paren.rparen', + '}', + ], + 'POST _search\n' + '{\n' + ' "q": {}\n' + ' \n' + '}' + ); + + tokenTest( + [ + 'method', + 'POST', + 'url.part', + '_search', + 'paren.lparen', + '{', + 'variable', + '"q"', + 'punctuation.colon', + ':', + 'paren.lparen', + '{', + 'variable', + '"s"', + 'punctuation.colon', + ':', + 'paren.lparen', + '{', + 'paren.rparen', + '}', + 'paren.rparen', + '}', + 'paren.rparen', + '}', + ], + 'POST _search\n' + '{\n' + ' "q": { "s": {}}\n' + ' \n' + '}' + ); + + function statesAsList() { + const ret = []; + const maxLine = coreEditor.getLineCount(); + for (let line = 1; line <= maxLine; line++) ret.push(coreEditor.getLineState(line)); + return ret; + } + + function statesTest(statesList, prefix, data) { + if (data && typeof data !== 'string') { + data = JSON.stringify(data, null, 3); + } + if (data) { + if (prefix) { + data = prefix + '\n' + data; + } + } else { + data = prefix; + } + + test('States test ' + testCount++ + ' prefix: ' + prefix, async function() { + await coreEditor.setValue(data, true); + const modes = statesAsList(); + expect(modes).toEqual(statesList); + }); + } + + statesTest( + ['start', 'json', 'json', 'start'], + 'POST _search\n' + '{\n' + ' "query": { "match_all": {} }\n' + '}' + ); + + statesTest( + ['start', 'json', ['json', 'json'], ['json', 'json'], 'json', 'start'], + 'POST _search\n' + '{\n' + ' "query": { \n' + ' "match_all": {} \n' + ' }\n' + '}' + ); + + statesTest( + ['start', 'json', 'json', 'start'], + 'POST _search\n' + '{\n' + ' "script": { "source": "" }\n' + '}' + ); + + statesTest( + ['start', 'json', 'json', 'start'], + 'POST _search\n' + '{\n' + ' "script": ""\n' + '}' + ); + + statesTest( + ['start', 'json', ['json', 'json'], 'json', 'start'], + 'POST _search\n' + '{\n' + ' "script": {\n' + ' }\n' + '}' + ); + + statesTest( + [ + 'start', + 'json', + ['script-start', 'json', 'json', 'json'], + ['script-start', 'json', 'json', 'json'], + ['json', 'json'], + 'json', + 'start', + ], + 'POST _search\n' + + '{\n' + + ' "test": { "script": """\n' + + ' test script\n' + + ' """\n' + + ' }\n' + + '}' + ); + + statesTest( + ['start', 'json', ['script-start', 'json'], ['script-start', 'json'], 'json', 'start'], + 'POST _search\n' + '{\n' + ' "script": """\n' + ' test script\n' + ' """,\n' + '}' + ); + + statesTest( + ['start', 'json', 'json', 'start'], + 'POST _search\n' + '{\n' + ' "script": """test script""",\n' + '}' + ); + + statesTest( + ['start', 'json', ['string_literal', 'json'], ['string_literal', 'json'], 'json', 'start'], + 'POST _search\n' + '{\n' + ' "something": """\n' + ' test script\n' + ' """,\n' + '}' + ); + + statesTest( + [ + 'start', + 'json', + ['string_literal', 'json', 'json', 'json'], + ['string_literal', 'json', 'json', 'json'], + ['json', 'json'], + ['json', 'json'], + 'json', + 'start', + ], + 'POST _search\n' + + '{\n' + + ' "something": { "f" : """\n' + + ' test script\n' + + ' """,\n' + + ' "g": 1\n' + + ' }\n' + + '}' + ); + + statesTest( + ['start', 'json', 'json', 'start'], + 'POST _search\n' + '{\n' + ' "something": """test script""",\n' + '}' + ); + }); +}); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/input_tokenization.test.js b/src/plugins/console/public/application/models/legacy_core_editor/__tests__/input_tokenization.test.js deleted file mode 100644 index 019b3c1d0538a..0000000000000 --- a/src/plugins/console/public/application/models/legacy_core_editor/__tests__/input_tokenization.test.js +++ /dev/null @@ -1,559 +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 '../legacy_core_editor.test.mocks'; -import RowParser from '../../../../lib/row_parser'; -import { createTokenIterator } from '../../../factories'; -import $ from 'jquery'; -import { create } from '../create'; - -describe('Input Tokenization', () => { - let coreEditor; - beforeEach(() => { - // Set up our document body - document.body.innerHTML = `
-
-
-
-
`; - - coreEditor = create(document.querySelector('#ConAppEditor')); - - $(coreEditor.getContainer()).show(); - }); - afterEach(() => { - $(coreEditor.getContainer()).hide(); - }); - - function tokensAsList() { - const iter = createTokenIterator({ - editor: coreEditor, - position: { lineNumber: 1, column: 1 }, - }); - const ret = []; - let t = iter.getCurrentToken(); - const parser = new RowParser(coreEditor); - if (parser.isEmptyToken(t)) { - t = parser.nextNonEmptyToken(iter); - } - while (t) { - ret.push({ value: t.value, type: t.type }); - t = parser.nextNonEmptyToken(iter); - } - - return ret; - } - - let testCount = 0; - - function tokenTest(tokenList, prefix, data) { - if (data && typeof data !== 'string') { - data = JSON.stringify(data, null, 3); - } - if (data) { - if (prefix) { - data = prefix + '\n' + data; - } - } else { - data = prefix; - } - - test('Token test ' + testCount++ + ' prefix: ' + prefix, async function() { - await coreEditor.setValue(data, true); - const tokens = tokensAsList(); - const normTokenList = []; - for (let i = 0; i < tokenList.length; i++) { - normTokenList.push({ type: tokenList[i++], value: tokenList[i] }); - } - - expect(tokens).toEqual(normTokenList); - }); - } - - tokenTest(['method', 'GET', 'url.part', '_search'], 'GET _search'); - - tokenTest(['method', 'GET', 'url.slash', '/', 'url.part', '_search'], 'GET /_search'); - - tokenTest( - [ - 'method', - 'GET', - 'url.protocol_host', - 'http://somehost', - 'url.slash', - '/', - 'url.part', - '_search', - ], - 'GET http://somehost/_search' - ); - - tokenTest(['method', 'GET', 'url.protocol_host', 'http://somehost'], 'GET http://somehost'); - - tokenTest( - ['method', 'GET', 'url.protocol_host', 'http://somehost', 'url.slash', '/'], - 'GET http://somehost/' - ); - - tokenTest( - ['method', 'GET', 'url.protocol_host', 'http://test:user@somehost', 'url.slash', '/'], - 'GET http://test:user@somehost/' - ); - - tokenTest( - ['method', 'GET', 'url.part', '_cluster', 'url.slash', '/', 'url.part', 'nodes'], - 'GET _cluster/nodes' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.slash', - '/', - 'url.part', - '_cluster', - 'url.slash', - '/', - 'url.part', - 'nodes', - ], - 'GET /_cluster/nodes' - ); - - tokenTest( - ['method', 'GET', 'url.part', 'index', 'url.slash', '/', 'url.part', '_search'], - 'GET index/_search' - ); - - tokenTest(['method', 'GET', 'url.part', 'index'], 'GET index'); - - tokenTest( - ['method', 'GET', 'url.part', 'index', 'url.slash', '/', 'url.part', 'type'], - 'GET index/type' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.slash', - '/', - 'url.part', - 'index', - 'url.slash', - '/', - 'url.part', - 'type', - 'url.slash', - '/', - ], - 'GET /index/type/' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.part', - 'index', - 'url.slash', - '/', - 'url.part', - 'type', - 'url.slash', - '/', - 'url.part', - '_search', - ], - 'GET index/type/_search' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.part', - 'index', - 'url.slash', - '/', - 'url.part', - 'type', - 'url.slash', - '/', - 'url.part', - '_search', - 'url.questionmark', - '?', - 'url.param', - 'value', - 'url.equal', - '=', - 'url.value', - '1', - ], - 'GET index/type/_search?value=1' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.part', - 'index', - 'url.slash', - '/', - 'url.part', - 'type', - 'url.slash', - '/', - 'url.part', - '1', - ], - 'GET index/type/1' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.slash', - '/', - 'url.part', - 'index1', - 'url.comma', - ',', - 'url.part', - 'index2', - 'url.slash', - '/', - ], - 'GET /index1,index2/' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.slash', - '/', - 'url.part', - 'index1', - 'url.comma', - ',', - 'url.part', - 'index2', - 'url.slash', - '/', - 'url.part', - '_search', - ], - 'GET /index1,index2/_search' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.part', - 'index1', - 'url.comma', - ',', - 'url.part', - 'index2', - 'url.slash', - '/', - 'url.part', - '_search', - ], - 'GET index1,index2/_search' - ); - - tokenTest( - [ - 'method', - 'GET', - 'url.slash', - '/', - 'url.part', - 'index1', - 'url.comma', - ',', - 'url.part', - 'index2', - ], - 'GET /index1,index2' - ); - - tokenTest( - ['method', 'GET', 'url.part', 'index1', 'url.comma', ',', 'url.part', 'index2'], - 'GET index1,index2' - ); - - tokenTest( - ['method', 'GET', 'url.slash', '/', 'url.part', 'index1', 'url.comma', ','], - 'GET /index1,' - ); - - tokenTest( - ['method', 'PUT', 'url.slash', '/', 'url.part', 'index', 'url.slash', '/'], - 'PUT /index/' - ); - - tokenTest( - ['method', 'GET', 'url.part', 'index', 'url.slash', '/', 'url.part', '_search'], - 'GET index/_search ' - ); - - tokenTest(['method', 'PUT', 'url.slash', '/', 'url.part', 'index'], 'PUT /index'); - - tokenTest( - [ - 'method', - 'PUT', - 'url.slash', - '/', - 'url.part', - 'index1', - 'url.comma', - ',', - 'url.part', - 'index2', - 'url.slash', - '/', - 'url.part', - 'type1', - 'url.comma', - ',', - 'url.part', - 'type2', - ], - 'PUT /index1,index2/type1,type2' - ); - - tokenTest( - [ - 'method', - 'PUT', - 'url.slash', - '/', - 'url.part', - 'index1', - 'url.slash', - '/', - 'url.part', - 'type1', - 'url.comma', - ',', - 'url.part', - 'type2', - 'url.comma', - ',', - ], - 'PUT /index1/type1,type2,' - ); - - tokenTest( - [ - 'method', - 'PUT', - 'url.part', - 'index1', - 'url.comma', - ',', - 'url.part', - 'index2', - 'url.slash', - '/', - 'url.part', - 'type1', - 'url.comma', - ',', - 'url.part', - 'type2', - 'url.slash', - '/', - 'url.part', - '1234', - ], - 'PUT index1,index2/type1,type2/1234' - ); - - tokenTest( - [ - 'method', - 'POST', - 'url.part', - '_search', - 'paren.lparen', - '{', - 'variable', - '"q"', - 'punctuation.colon', - ':', - 'paren.lparen', - '{', - 'paren.rparen', - '}', - 'paren.rparen', - '}', - ], - 'POST _search\n' + '{\n' + ' "q": {}\n' + ' \n' + '}' - ); - - tokenTest( - [ - 'method', - 'POST', - 'url.part', - '_search', - 'paren.lparen', - '{', - 'variable', - '"q"', - 'punctuation.colon', - ':', - 'paren.lparen', - '{', - 'variable', - '"s"', - 'punctuation.colon', - ':', - 'paren.lparen', - '{', - 'paren.rparen', - '}', - 'paren.rparen', - '}', - 'paren.rparen', - '}', - ], - 'POST _search\n' + '{\n' + ' "q": { "s": {}}\n' + ' \n' + '}' - ); - - function statesAsList() { - const ret = []; - const maxLine = coreEditor.getLineCount(); - for (let line = 1; line <= maxLine; line++) ret.push(coreEditor.getLineState(line)); - return ret; - } - - function statesTest(statesList, prefix, data) { - if (data && typeof data !== 'string') { - data = JSON.stringify(data, null, 3); - } - if (data) { - if (prefix) { - data = prefix + '\n' + data; - } - } else { - data = prefix; - } - - test('States test ' + testCount++ + ' prefix: ' + prefix, async function() { - await coreEditor.setValue(data, true); - const modes = statesAsList(); - expect(modes).toEqual(statesList); - }); - } - - statesTest( - ['start', 'json', 'json', 'start'], - 'POST _search\n' + '{\n' + ' "query": { "match_all": {} }\n' + '}' - ); - - statesTest( - ['start', 'json', ['json', 'json'], ['json', 'json'], 'json', 'start'], - 'POST _search\n' + '{\n' + ' "query": { \n' + ' "match_all": {} \n' + ' }\n' + '}' - ); - - statesTest( - ['start', 'json', 'json', 'start'], - 'POST _search\n' + '{\n' + ' "script": { "source": "" }\n' + '}' - ); - - statesTest( - ['start', 'json', 'json', 'start'], - 'POST _search\n' + '{\n' + ' "script": ""\n' + '}' - ); - - statesTest( - ['start', 'json', ['json', 'json'], 'json', 'start'], - 'POST _search\n' + '{\n' + ' "script": {\n' + ' }\n' + '}' - ); - - statesTest( - [ - 'start', - 'json', - ['script-start', 'json', 'json', 'json'], - ['script-start', 'json', 'json', 'json'], - ['json', 'json'], - 'json', - 'start', - ], - 'POST _search\n' + - '{\n' + - ' "test": { "script": """\n' + - ' test script\n' + - ' """\n' + - ' }\n' + - '}' - ); - - statesTest( - ['start', 'json', ['script-start', 'json'], ['script-start', 'json'], 'json', 'start'], - 'POST _search\n' + '{\n' + ' "script": """\n' + ' test script\n' + ' """,\n' + '}' - ); - - statesTest( - ['start', 'json', 'json', 'start'], - 'POST _search\n' + '{\n' + ' "script": """test script""",\n' + '}' - ); - - statesTest( - ['start', 'json', ['string_literal', 'json'], ['string_literal', 'json'], 'json', 'start'], - 'POST _search\n' + '{\n' + ' "something": """\n' + ' test script\n' + ' """,\n' + '}' - ); - - statesTest( - [ - 'start', - 'json', - ['string_literal', 'json', 'json', 'json'], - ['string_literal', 'json', 'json', 'json'], - ['json', 'json'], - ['json', 'json'], - 'json', - 'start', - ], - 'POST _search\n' + - '{\n' + - ' "something": { "f" : """\n' + - ' test script\n' + - ' """,\n' + - ' "g": 1\n' + - ' }\n' + - '}' - ); - - statesTest( - ['start', 'json', 'json', 'start'], - 'POST _search\n' + '{\n' + ' "something": """test script""",\n' + '}' - ); -}); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts index 19a86648d6dd3..47947e985092b 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts +++ b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts @@ -189,8 +189,9 @@ export class LegacyCoreEditor implements CoreEditor { } getLineCount() { - const text = this.getValue(); - return text.split('\n').length; + // Only use this function to return line count as it uses + // a cache. + return this.editor.getSession().getLength(); } addMarker(range: Range) { diff --git a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts index 9679eaa2884ce..1271f167c6cc1 100644 --- a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts +++ b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts @@ -78,7 +78,7 @@ export class SenseEditor { } else { curRow = rowOrPos as number; } - const maxLines = this.coreEditor.getValue().split('\n').length; + const maxLines = this.coreEditor.getLineCount(); for (; curRow < maxLines - 1; curRow++) { if (this.parser.isStartRequestRow(curRow, this.coreEditor)) { break; diff --git a/src/plugins/console/public/index.scss b/src/plugins/console/public/index.scss new file mode 100644 index 0000000000000..370ec54a85539 --- /dev/null +++ b/src/plugins/console/public/index.scss @@ -0,0 +1 @@ +@import 'styles/index' diff --git a/src/plugins/console/public/index.ts b/src/plugins/console/public/index.ts index 2af9d1d16af02..3fec5ff828065 100644 --- a/src/plugins/console/public/index.ts +++ b/src/plugins/console/public/index.ts @@ -17,6 +17,8 @@ * under the License. */ +import './index.scss'; + import { ConsoleUIPlugin } from './plugin'; export { ConsoleUIPlugin as Plugin }; diff --git a/src/plugins/console/public/lib/ace_token_provider/token_provider.ts b/src/plugins/console/public/lib/ace_token_provider/token_provider.ts index 761eb1d206cfe..134ab6c0e82d5 100644 --- a/src/plugins/console/public/lib/ace_token_provider/token_provider.ts +++ b/src/plugins/console/public/lib/ace_token_provider/token_provider.ts @@ -66,7 +66,10 @@ export class AceTokensProvider implements TokensProvider { getTokens(lineNumber: number): Token[] | null { if (lineNumber < 1) return null; - const lineCount = this.session.doc.getAllLines().length; + // Important: must use a .session.getLength because this is a cached value. + // Calculating line length here will lead to performance issues because this function + // may be called inside of tight loops. + const lineCount = this.session.getLength(); if (lineNumber > lineCount) { return null; } diff --git a/src/plugins/console/public/lib/es/es.ts b/src/plugins/console/public/lib/es/es.ts index 52aba98d9e662..f11692e1befad 100644 --- a/src/plugins/console/public/lib/es/es.ts +++ b/src/plugins/console/public/lib/es/es.ts @@ -17,8 +17,8 @@ * under the License. */ -import { stringify as formatQueryString } from 'querystring'; import $ from 'jquery'; +import { stringify } from 'query-string'; const esVersion: string[] = []; @@ -35,7 +35,7 @@ export function send(method: string, path: string, data: any) { const wrappedDfd = $.Deferred(); // eslint-disable-line new-cap const options: JQuery.AjaxSettings = { - url: '../api/console/proxy?' + formatQueryString({ path, method }), + url: '../api/console/proxy?' + stringify({ path, method }, { sort: false }), data, contentType: getContentType(data), cache: false, diff --git a/src/legacy/core_plugins/console_legacy/public/styles/_app.scss b/src/plugins/console/public/styles/_app.scss similarity index 100% rename from src/legacy/core_plugins/console_legacy/public/styles/_app.scss rename to src/plugins/console/public/styles/_app.scss diff --git a/src/legacy/core_plugins/console_legacy/public/styles/index.scss b/src/plugins/console/public/styles/_index.scss similarity index 77% rename from src/legacy/core_plugins/console_legacy/public/styles/index.scss rename to src/plugins/console/public/styles/_index.scss index dc45f6cfdacf5..22dc0e5833d2c 100644 --- a/src/legacy/core_plugins/console_legacy/public/styles/index.scss +++ b/src/plugins/console/public/styles/_index.scss @@ -1,5 +1,3 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - // Prefix all styles with "con" to avoid conflicts. // Examples // conChart diff --git a/src/legacy/core_plugins/console_legacy/public/styles/components/_help.scss b/src/plugins/console/public/styles/components/_help.scss similarity index 100% rename from src/legacy/core_plugins/console_legacy/public/styles/components/_help.scss rename to src/plugins/console/public/styles/components/_help.scss diff --git a/src/legacy/core_plugins/console_legacy/public/styles/components/_history.scss b/src/plugins/console/public/styles/components/_history.scss similarity index 89% rename from src/legacy/core_plugins/console_legacy/public/styles/components/_history.scss rename to src/plugins/console/public/styles/components/_history.scss index efd72245b3c48..5ce5cb52351b8 100644 --- a/src/legacy/core_plugins/console_legacy/public/styles/components/_history.scss +++ b/src/plugins/console/public/styles/components/_history.scss @@ -1,5 +1,3 @@ -@import 'src/legacy/ui/public/styles/_styling_constants'; - .conHistory { @include euiBottomShadow; padding: $euiSizeM; diff --git a/src/legacy/core_plugins/console_legacy/public/styles/components/_index.scss b/src/plugins/console/public/styles/components/_index.scss similarity index 100% rename from src/legacy/core_plugins/console_legacy/public/styles/components/_index.scss rename to src/plugins/console/public/styles/components/_index.scss diff --git a/src/plugins/console/public/types/core_editor.ts b/src/plugins/console/public/types/core_editor.ts index 8de4c78333fee..79dc3ca74200b 100644 --- a/src/plugins/console/public/types/core_editor.ts +++ b/src/plugins/console/public/types/core_editor.ts @@ -181,6 +181,10 @@ export interface CoreEditor { /** * Return the current line count in the buffer. + * + * @remark + * This function should be usable in a tight loop and must make used of a cached + * line count. */ getLineCount(): number; diff --git a/src/plugins/console/server/lib/spec_definitions/es_6_0/mappings.js b/src/plugins/console/server/lib/spec_definitions/es_6_0/mappings.js index 46f32fe58089b..8c31e5bc6fbb2 100644 --- a/src/plugins/console/server/lib/spec_definitions/es_6_0/mappings.js +++ b/src/plugins/console/server/lib/spec_definitions/es_6_0/mappings.js @@ -96,6 +96,7 @@ export default function(api) { doc_values: BOOLEAN, eager_global_ordinals: BOOLEAN, norms: BOOLEAN, + coerce: BOOLEAN, // Not actually available in V6 of ES. Add when updating the autocompletion system. // index_phrases: BOOLEAN, diff --git a/src/plugins/dashboard_embeddable_container/public/_index.scss b/src/plugins/dashboard_embeddable_container/public/index.scss similarity index 100% rename from src/plugins/dashboard_embeddable_container/public/_index.scss rename to src/plugins/dashboard_embeddable_container/public/index.scss diff --git a/src/plugins/dashboard_embeddable_container/public/index.ts b/src/plugins/dashboard_embeddable_container/public/index.ts index 73597525105db..e5f55c06b290c 100644 --- a/src/plugins/dashboard_embeddable_container/public/index.ts +++ b/src/plugins/dashboard_embeddable_container/public/index.ts @@ -17,6 +17,8 @@ * under the License. */ +import './index.scss'; + import { PluginInitializerContext } from '../../../core/public'; import { DashboardEmbeddableContainerPublicPlugin } from './plugin'; diff --git a/src/plugins/data/public/_index.scss b/src/plugins/data/public/index.scss similarity index 100% rename from src/plugins/data/public/_index.scss rename to src/plugins/data/public/index.scss diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 548417f3769aa..8704ca08ae905 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -17,6 +17,8 @@ * under the License. */ +import './index.scss'; + import { PluginInitializerContext } from '../../../core/public'; /* diff --git a/src/plugins/data/public/query/timefilter/lib/parse_querystring.ts b/src/plugins/data/public/query/timefilter/lib/parse_querystring.ts index 467110b6f32ea..2220ad4eef1b7 100644 --- a/src/plugins/data/public/query/timefilter/lib/parse_querystring.ts +++ b/src/plugins/data/public/query/timefilter/lib/parse_querystring.ts @@ -16,8 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - -import qs from 'querystring'; +import { parse } from 'query-string'; export function parseQueryString() { // window.location.search is an empty string @@ -27,5 +26,5 @@ export function parseQueryString() { return {}; } - return qs.parse(hrefSplit[1]); + return parse(hrefSplit[1], { sort: false }); } diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/_index.scss b/src/plugins/data/public/ui/filter_bar/filter_editor/_index.scss index 21ba32ec6a6fe..3d416aade9a53 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/_index.scss +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/_index.scss @@ -1 +1 @@ -@import 'filter_editor'; \ No newline at end of file +@import 'filter_editor'; diff --git a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap index 2f2332bb06e3c..eebbc63f6f1e4 100644 --- a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap +++ b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap @@ -97,6 +97,17 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "management": Object {}, "navLinks": Object {}, }, + "currentAppId$": Observable { + "_isScalar": false, + "source": Subject { + "_isScalar": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, "getUrlForApp": [MockFunction], "navigateToApp": [MockFunction], "registerMountContext": [MockFunction], @@ -738,6 +749,17 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "management": Object {}, "navLinks": Object {}, }, + "currentAppId$": Observable { + "_isScalar": false, + "source": Subject { + "_isScalar": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, "getUrlForApp": [MockFunction], "navigateToApp": [MockFunction], "registerMountContext": [MockFunction], @@ -1361,6 +1383,17 @@ exports[`QueryStringInput Should pass the query language to the language switche "management": Object {}, "navLinks": Object {}, }, + "currentAppId$": Observable { + "_isScalar": false, + "source": Subject { + "_isScalar": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, "getUrlForApp": [MockFunction], "navigateToApp": [MockFunction], "registerMountContext": [MockFunction], @@ -1999,6 +2032,17 @@ exports[`QueryStringInput Should pass the query language to the language switche "management": Object {}, "navLinks": Object {}, }, + "currentAppId$": Observable { + "_isScalar": false, + "source": Subject { + "_isScalar": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, "getUrlForApp": [MockFunction], "navigateToApp": [MockFunction], "registerMountContext": [MockFunction], @@ -2622,6 +2666,17 @@ exports[`QueryStringInput Should render the given query 1`] = ` "management": Object {}, "navLinks": Object {}, }, + "currentAppId$": Observable { + "_isScalar": false, + "source": Subject { + "_isScalar": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, "getUrlForApp": [MockFunction], "navigateToApp": [MockFunction], "registerMountContext": [MockFunction], @@ -3260,6 +3315,17 @@ exports[`QueryStringInput Should render the given query 1`] = ` "management": Object {}, "navLinks": Object {}, }, + "currentAppId$": Observable { + "_isScalar": false, + "source": Subject { + "_isScalar": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + }, "getUrlForApp": [MockFunction], "navigateToApp": [MockFunction], "registerMountContext": [MockFunction], diff --git a/src/plugins/embeddable/public/_index.scss b/src/plugins/embeddable/public/index.scss similarity index 100% rename from src/plugins/embeddable/public/_index.scss rename to src/plugins/embeddable/public/index.scss diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index af6c2acd3a9b1..b0e14a04a9944 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -17,6 +17,8 @@ * under the License. */ +import './index.scss'; + import { PluginInitializerContext } from 'src/core/public'; import { EmbeddablePublicPlugin } from './plugin'; diff --git a/src/plugins/expressions/public/_index.scss b/src/plugins/expressions/public/index.scss similarity index 100% rename from src/plugins/expressions/public/_index.scss rename to src/plugins/expressions/public/index.scss diff --git a/src/plugins/expressions/public/index.ts b/src/plugins/expressions/public/index.ts index 59d529dc9caff..5f64c11f4efe6 100644 --- a/src/plugins/expressions/public/index.ts +++ b/src/plugins/expressions/public/index.ts @@ -17,6 +17,8 @@ * under the License. */ +import './index.scss'; + import { PluginInitializerContext } from '../../../core/public'; import { ExpressionsPublicPlugin } from './plugin'; diff --git a/src/plugins/inspector/public/index.scss b/src/plugins/inspector/public/index.scss new file mode 100644 index 0000000000000..57820cf70cc3f --- /dev/null +++ b/src/plugins/inspector/public/index.scss @@ -0,0 +1 @@ +@import 'views/index' diff --git a/src/plugins/inspector/public/index.ts b/src/plugins/inspector/public/index.ts index e90e05aa2830a..bf06ab88fa79a 100644 --- a/src/plugins/inspector/public/index.ts +++ b/src/plugins/inspector/public/index.ts @@ -17,6 +17,8 @@ * under the License. */ +import './index.scss'; + import { PluginInitializerContext } from '../../../core/public'; import { InspectorPublicPlugin } from './plugin'; diff --git a/src/plugins/kibana_legacy/common/kbn_base_url.ts b/src/plugins/kibana_legacy/common/kbn_base_url.ts new file mode 100644 index 0000000000000..69711626750ea --- /dev/null +++ b/src/plugins/kibana_legacy/common/kbn_base_url.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const kbnBaseUrl = '/app/kibana'; diff --git a/src/plugins/kibana_legacy/public/angular/angular_config.tsx b/src/plugins/kibana_legacy/public/angular/angular_config.tsx index 9a33cff82ed63..67d62cab7409b 100644 --- a/src/plugins/kibana_legacy/public/angular/angular_config.tsx +++ b/src/plugins/kibana_legacy/public/angular/angular_config.tsx @@ -31,7 +31,7 @@ import $ from 'jquery'; import { cloneDeep, forOwn, get, set } from 'lodash'; import React, { Fragment } from 'react'; import * as Rx from 'rxjs'; -import { ChromeBreadcrumb } from 'kibana/public'; +import { ChromeBreadcrumb, EnvironmentMode, PackageInfo } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -79,34 +79,53 @@ function isDummyRoute($route: any, isLocalAngular: boolean) { export const configureAppAngularModule = ( angularModule: IModule, - newPlatform: LegacyCoreStart, + newPlatform: + | LegacyCoreStart + | { + core: CoreStart; + readonly env: { + mode: Readonly; + packageInfo: Readonly; + }; + }, isLocalAngular: boolean ) => { - const legacyMetadata = newPlatform.injectedMetadata.getLegacyMetadata(); - - forOwn(newPlatform.injectedMetadata.getInjectedVars(), (val, name) => { - if (name !== undefined) { - // The legacy platform modifies some of these values, clone to an unfrozen object. - angularModule.value(name, cloneDeep(val)); - } - }); + const core = 'core' in newPlatform ? newPlatform.core : newPlatform; + const packageInfo = + 'injectedMetadata' in newPlatform + ? newPlatform.injectedMetadata.getLegacyMetadata() + : newPlatform.env.packageInfo; + + if ('injectedMetadata' in newPlatform) { + forOwn(newPlatform.injectedMetadata.getInjectedVars(), (val, name) => { + if (name !== undefined) { + // The legacy platform modifies some of these values, clone to an unfrozen object. + angularModule.value(name, cloneDeep(val)); + } + }); + } angularModule - .value('kbnVersion', newPlatform.injectedMetadata.getKibanaVersion()) - .value('buildNum', legacyMetadata.buildNum) - .value('buildSha', legacyMetadata.buildSha) - .value('serverName', legacyMetadata.serverName) - .value('esUrl', getEsUrl(newPlatform)) - .value('uiCapabilities', newPlatform.application.capabilities) - .config(setupCompileProvider(newPlatform)) + .value('kbnVersion', packageInfo.version) + .value('buildNum', packageInfo.buildNum) + .value('buildSha', packageInfo.buildSha) + .value('esUrl', getEsUrl(core)) + .value('uiCapabilities', core.application.capabilities) + .config( + setupCompileProvider( + 'injectedMetadata' in newPlatform + ? newPlatform.injectedMetadata.getLegacyMetadata().devMode + : newPlatform.env.mode.dev + ) + ) .config(setupLocationProvider()) - .config($setupXsrfRequestInterceptor(newPlatform)) - .run(capture$httpLoadingCount(newPlatform)) - .run($setupBreadcrumbsAutoClear(newPlatform, isLocalAngular)) - .run($setupBadgeAutoClear(newPlatform, isLocalAngular)) - .run($setupHelpExtensionAutoClear(newPlatform, isLocalAngular)) - .run($setupUrlOverflowHandling(newPlatform, isLocalAngular)) - .run($setupUICapabilityRedirect(newPlatform)); + .config($setupXsrfRequestInterceptor(packageInfo.version)) + .run(capture$httpLoadingCount(core)) + .run($setupBreadcrumbsAutoClear(core, isLocalAngular)) + .run($setupBadgeAutoClear(core, isLocalAngular)) + .run($setupHelpExtensionAutoClear(core, isLocalAngular)) + .run($setupUrlOverflowHandling(core, isLocalAngular)) + .run($setupUICapabilityRedirect(core)); }; const getEsUrl = (newPlatform: CoreStart) => { @@ -122,10 +141,8 @@ const getEsUrl = (newPlatform: CoreStart) => { }; }; -const setupCompileProvider = (newPlatform: LegacyCoreStart) => ( - $compileProvider: ICompileProvider -) => { - if (!newPlatform.injectedMetadata.getLegacyMetadata().devMode) { +const setupCompileProvider = (devMode: boolean) => ($compileProvider: ICompileProvider) => { + if (!devMode) { $compileProvider.debugInfoEnabled(false); } }; @@ -140,9 +157,7 @@ const setupLocationProvider = () => ($locationProvider: ILocationProvider) => { $locationProvider.hashPrefix(''); }; -export const $setupXsrfRequestInterceptor = (newPlatform: LegacyCoreStart) => { - const version = newPlatform.injectedMetadata.getLegacyMetadata().version; - +export const $setupXsrfRequestInterceptor = (version: string) => { // Configure jQuery prefilter $.ajaxPrefilter(({ kbnXsrfToken = true }: any, originalOptions, jqXHR) => { if (kbnXsrfToken) { diff --git a/src/plugins/kibana_legacy/public/index.ts b/src/plugins/kibana_legacy/public/index.ts index 19833d638fe4c..18f01854de259 100644 --- a/src/plugins/kibana_legacy/public/index.ts +++ b/src/plugins/kibana_legacy/public/index.ts @@ -24,6 +24,7 @@ export const plugin = (initializerContext: PluginInitializerContext) => new KibanaLegacyPlugin(initializerContext); export * from './plugin'; +export { kbnBaseUrl } from '../common/kbn_base_url'; export { initAngularBootstrap } from './angular_bootstrap'; export * from './angular'; diff --git a/src/plugins/kibana_legacy/public/mocks.ts b/src/plugins/kibana_legacy/public/mocks.ts index aab3ab315f0c6..8e9a05b186191 100644 --- a/src/plugins/kibana_legacy/public/mocks.ts +++ b/src/plugins/kibana_legacy/public/mocks.ts @@ -17,6 +17,7 @@ * under the License. */ +import { EnvironmentMode, PackageInfo } from 'kibana/server'; import { KibanaLegacyPlugin } from './plugin'; export type Setup = jest.Mocked>; @@ -28,6 +29,10 @@ const createSetupContract = (): Setup => ({ config: { defaultAppId: 'home', }, + env: {} as { + mode: Readonly; + packageInfo: Readonly; + }, }); const createStartContract = (): Start => ({ diff --git a/src/plugins/kibana_legacy/public/plugin.ts b/src/plugins/kibana_legacy/public/plugin.ts index 86e56c44646c0..2ad620f355848 100644 --- a/src/plugins/kibana_legacy/public/plugin.ts +++ b/src/plugins/kibana_legacy/public/plugin.ts @@ -107,7 +107,17 @@ export class KibanaLegacyPlugin { this.forwards.push({ legacyAppId, newAppId, ...options }); }, + /** + * @deprecated + * The `defaultAppId` config key is temporarily exposed to be used in the legacy platform. + * As this setting is going away, no new code should depend on it. + */ config: this.initializerContext.config.get(), + /** + * @deprecated + * Temporarily exposing the NP env to simulate initializer contexts in the LP. + */ + env: this.initializerContext.env, }; } diff --git a/src/plugins/kibana_legacy/public/utils/system_api.ts b/src/plugins/kibana_legacy/public/utils/system_api.ts index 397de4dcc2bb3..49d4a78584737 100644 --- a/src/plugins/kibana_legacy/public/utils/system_api.ts +++ b/src/plugins/kibana_legacy/public/utils/system_api.ts @@ -19,7 +19,8 @@ import { IRequestConfig } from 'angular'; -const SYSTEM_API_HEADER_NAME = 'kbn-system-api'; +const SYSTEM_REQUEST_HEADER_NAME = 'kbn-system-request'; +const LEGACY_SYSTEM_API_HEADER_NAME = 'kbn-system-api'; /** * Adds a custom header designating request as system API @@ -28,7 +29,7 @@ const SYSTEM_API_HEADER_NAME = 'kbn-system-api'; */ export function addSystemApiHeader(originalHeaders: Record) { const systemApiHeaders = { - [SYSTEM_API_HEADER_NAME]: true, + [SYSTEM_REQUEST_HEADER_NAME]: true, }; return { ...originalHeaders, @@ -44,5 +45,7 @@ export function addSystemApiHeader(originalHeaders: Record) { */ export function isSystemApiRequest(request: IRequestConfig) { const { headers } = request; - return headers && !!headers[SYSTEM_API_HEADER_NAME]; + return ( + headers && (!!headers[SYSTEM_REQUEST_HEADER_NAME] || !!headers[LEGACY_SYSTEM_API_HEADER_NAME]) + ); } diff --git a/src/plugins/kibana_legacy/server/index.ts b/src/plugins/kibana_legacy/server/index.ts index 4d0fe8364a66c..98c754795e947 100644 --- a/src/plugins/kibana_legacy/server/index.ts +++ b/src/plugins/kibana_legacy/server/index.ts @@ -32,6 +32,8 @@ export const config: PluginConfigDescriptor = { ], }; +export { kbnBaseUrl } from '../common/kbn_base_url'; + class Plugin { public setup(core: CoreSetup) {} diff --git a/src/plugins/kibana_utils/common/index.ts b/src/plugins/kibana_utils/common/index.ts index 4551d0e63c4be..3b07674315dce 100644 --- a/src/plugins/kibana_utils/common/index.ts +++ b/src/plugins/kibana_utils/common/index.ts @@ -24,3 +24,4 @@ export * from './state_containers'; export * from './typed_json'; export { createGetterSetter, Get, Set } from './create_getter_setter'; export { distinctUntilChangedWithInitialValue } from './distinct_until_changed_with_initial_value'; +export { url } from './url'; diff --git a/src/plugins/kibana_utils/public/state_management/url/stringify_query_string.test.ts b/src/plugins/kibana_utils/common/url/encode_uri_query.test.ts similarity index 78% rename from src/plugins/kibana_utils/public/state_management/url/stringify_query_string.test.ts rename to src/plugins/kibana_utils/common/url/encode_uri_query.test.ts index 3ca6cb4214682..b600822946299 100644 --- a/src/plugins/kibana_utils/public/state_management/url/stringify_query_string.test.ts +++ b/src/plugins/kibana_utils/common/url/encode_uri_query.test.ts @@ -17,27 +17,10 @@ * under the License. */ -import { encodeUriQuery, stringifyQueryString } from './stringify_query_string'; +import { encodeUriQuery, encodeQuery } from './encode_uri_query'; -describe('stringifyQueryString', () => { - it('stringifyQueryString', () => { - expect( - stringifyQueryString({ - a: 'asdf1234asdf', - b: "-_.!~*'() -_.!~*'()", - c: ':@$, :@$,', - d: "&;=+# &;=+#'", - f: ' ', - g: 'null', - }) - ).toMatchInlineSnapshot( - `"a=asdf1234asdf&b=-_.!~*'()%20-_.!~*'()&c=:@$,%20:@$,&d=%26;%3D%2B%23%20%26;%3D%2B%23'&f=%20&g=null"` - ); - }); -}); - -describe('encodeUriQuery', function() { - it('should correctly encode uri query and not encode chars defined as pchar set in rfc3986', () => { +describe('encodeUriQuery', () => { + test('should correctly encode uri query and not encode chars defined as pchar set in rfc3986', () => { // don't encode alphanum expect(encodeUriQuery('asdf1234asdf')).toBe('asdf1234asdf'); @@ -63,3 +46,25 @@ describe('encodeUriQuery', function() { expect(encodeUriQuery('null')).toBe('null'); }); }); + +describe('encodeQuery', () => { + test('encodeQuery', () => { + expect( + encodeQuery({ + a: 'asdf1234asdf', + b: "-_.!~*'() -_.!~*'()", + c: ':@$, :@$,', + d: "&;=+# &;=+#'", + f: ' ', + g: 'null', + }) + ).toEqual({ + a: 'asdf1234asdf', + b: "-_.!~*'()%20-_.!~*'()", + c: ':@$,%20:@$,', + d: "%26;%3D%2B%23%20%26;%3D%2B%23'", + f: '%20', + g: 'null', + }); + }); +}); diff --git a/src/legacy/utils/encode_query_component.ts b/src/plugins/kibana_utils/common/url/encode_uri_query.ts similarity index 72% rename from src/legacy/utils/encode_query_component.ts rename to src/plugins/kibana_utils/common/url/encode_uri_query.ts index 698d11803649d..fb60f0ceff10f 100644 --- a/src/legacy/utils/encode_query_component.ts +++ b/src/plugins/kibana_utils/common/url/encode_uri_query.ts @@ -17,6 +17,9 @@ * under the License. */ +import { ParsedQuery } from 'query-string'; +import { transform } from 'lodash'; + /** * This method is intended for encoding *key* or *value* parts of query component. We need a custom * method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be @@ -28,11 +31,27 @@ * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" * / "*" / "+" / "," / ";" / "=" */ -export function encodeQueryComponent(val: string, pctEncodeSpaces = false) { +export function encodeUriQuery(val: string, pctEncodeSpaces = false) { return encodeURIComponent(val) .replace(/%40/gi, '@') .replace(/%3A/gi, ':') .replace(/%24/g, '$') .replace(/%2C/gi, ',') + .replace(/%3B/gi, ';') .replace(/%20/g, pctEncodeSpaces ? '%20' : '+'); } + +export const encodeQuery = ( + query: ParsedQuery, + encodeFunction: (val: string, pctEncodeSpaces?: boolean) => string = encodeUriQuery +) => + transform(query, (result, value, key) => { + if (key) { + const singleValue = Array.isArray(value) ? value.join(',') : value; + + result[key] = encodeFunction( + singleValue === undefined || singleValue === null ? '' : singleValue, + true + ); + } + }); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/telemetry_opt_in.js b/src/plugins/kibana_utils/common/url/index.ts similarity index 84% rename from src/legacy/core_plugins/telemetry/public/hacks/telemetry_opt_in.js rename to src/plugins/kibana_utils/common/url/index.ts index 4e53c7ecd7030..7b74f07e598ee 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/telemetry_opt_in.js +++ b/src/plugins/kibana_utils/common/url/index.ts @@ -17,8 +17,9 @@ * under the License. */ -import { uiModules } from 'ui/modules'; +import { encodeUriQuery, encodeQuery } from './encode_uri_query'; -import { injectBanner } from './welcome_banner'; - -uiModules.get('telemetry/hacks').run(injectBanner); +export const url = { + encodeQuery, + encodeUriQuery, +}; diff --git a/src/plugins/kibana_utils/public/history/remove_query_param.ts b/src/plugins/kibana_utils/public/history/remove_query_param.ts index fbf985998b4cd..bf945e5b064aa 100644 --- a/src/plugins/kibana_utils/public/history/remove_query_param.ts +++ b/src/plugins/kibana_utils/public/history/remove_query_param.ts @@ -17,16 +17,18 @@ * under the License. */ +import { parse, stringify } from 'query-string'; import { History, Location } from 'history'; -import { parse } from 'querystring'; -import { stringifyQueryString } from '../state_management/url/stringify_query_string'; // TODO: extract it to ../url +import { url } from '../../common'; export function removeQueryParam(history: History, param: string, replace: boolean = true) { const oldLocation = history.location; const search = (oldLocation.search || '').replace(/^\?/, ''); - const query = parse(search); + const query = parse(search, { sort: false }); + delete query[param]; - const newSearch = stringifyQueryString(query); + + const newSearch = stringify(url.encodeQuery(query), { sort: false, encode: false }); const newLocation: Location = { ...oldLocation, search: newSearch, diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 6a285de12135b..6971d96e471bd 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -26,6 +26,7 @@ export { Set, UiComponent, UiComponentInstance, + url, JsonValue, JsonObject, JsonArray, diff --git a/src/plugins/kibana_utils/public/state_management/url/format.ts b/src/plugins/kibana_utils/public/state_management/url/format.ts index 988ee08627382..2912b665ff014 100644 --- a/src/plugins/kibana_utils/public/state_management/url/format.ts +++ b/src/plugins/kibana_utils/public/state_management/url/format.ts @@ -18,18 +18,22 @@ */ import { format as formatUrl } from 'url'; -import { ParsedUrlQuery } from 'querystring'; +import { stringify, ParsedQuery } from 'query-string'; import { parseUrl, parseUrlHash } from './parse'; -import { stringifyQueryString } from './stringify_query_string'; +import { url as urlUtils } from '../../../common'; export function replaceUrlHashQuery( rawUrl: string, - queryReplacer: (query: ParsedUrlQuery) => ParsedUrlQuery + queryReplacer: (query: ParsedQuery) => ParsedQuery ) { const url = parseUrl(rawUrl); const hash = parseUrlHash(rawUrl); const newQuery = queryReplacer(hash?.query || {}); - const searchQueryString = stringifyQueryString(newQuery); + const searchQueryString = stringify(urlUtils.encodeQuery(newQuery), { + sort: false, + encode: false, + }); + if ((!hash || !hash.search) && !searchQueryString) return rawUrl; // nothing to change. return original url return formatUrl({ ...url, diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts index 1dd204e717213..40a411d425a54 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts @@ -18,11 +18,12 @@ */ import { format as formatUrl } from 'url'; +import { stringify } from 'query-string'; import { createBrowserHistory, History } from 'history'; import { decodeState, encodeState } from '../state_encoder'; import { getCurrentUrl, parseUrl, parseUrlHash } from './parse'; -import { stringifyQueryString } from './stringify_query_string'; import { replaceUrlHashQuery } from './format'; +import { url as urlUtils } from '../../../common'; /** * Parses a kibana url and retrieves all the states encoded into url, @@ -243,11 +244,11 @@ export function getRelativeToHistoryPath(absoluteUrl: string, history: History): return formatUrl({ pathname: stripBasename(parsedUrl.pathname), - search: stringifyQueryString(parsedUrl.query), + search: stringify(urlUtils.encodeQuery(parsedUrl.query), { sort: false, encode: false }), hash: parsedHash ? formatUrl({ pathname: parsedHash.pathname, - search: stringifyQueryString(parsedHash.query), + search: stringify(urlUtils.encodeQuery(parsedHash.query), { sort: false, encode: false }), }) : parsedUrl.hash, }); diff --git a/src/plugins/kibana_utils/public/state_management/url/stringify_query_string.ts b/src/plugins/kibana_utils/public/state_management/url/stringify_query_string.ts deleted file mode 100644 index e951dfac29c02..0000000000000 --- a/src/plugins/kibana_utils/public/state_management/url/stringify_query_string.ts +++ /dev/null @@ -1,57 +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 { stringify, ParsedUrlQuery } from 'querystring'; - -// encodeUriQuery implements the less-aggressive encoding done naturally by -// the browser. We use it to generate the same urls the browser would -export const stringifyQueryString = (query: ParsedUrlQuery) => - stringify(query, undefined, undefined, { - // encode spaces with %20 is needed to produce the same queries as angular does - // https://github.com/angular/angular.js/blob/51c516e7d4f2d10b0aaa4487bd0b52772022207a/src/Angular.js#L1377 - encodeURIComponent: (val: string) => encodeUriQuery(val, true), - }); - -/** - * Extracted from angular.js - * repo: https://github.com/angular/angular.js - * license: MIT - https://github.com/angular/angular.js/blob/51c516e7d4f2d10b0aaa4487bd0b52772022207a/LICENSE - * source: https://github.com/angular/angular.js/blob/51c516e7d4f2d10b0aaa4487bd0b52772022207a/src/Angular.js#L1413-L1432 - */ - -/** - * This method is intended for encoding *key* or *value* parts of query component. We need a custom - * method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be - * encoded per http://tools.ietf.org/html/rfc3986: - * query = *( pchar / "/" / "?" ) - * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" - * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" - * pct-encoded = "%" HEXDIG HEXDIG - * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" - * / "*" / "+" / "," / ";" / "=" - */ -export function encodeUriQuery(val: string, pctEncodeSpaces: boolean = false) { - return encodeURIComponent(val) - .replace(/%40/gi, '@') - .replace(/%3A/gi, ':') - .replace(/%24/g, '$') - .replace(/%2C/gi, ',') - .replace(/%3B/gi, ';') - .replace(/%20/g, pctEncodeSpaces ? '%20' : '+'); -} diff --git a/src/plugins/kibana_utils/server/index.ts b/src/plugins/kibana_utils/server/index.ts index f8b79a1b8b339..b8b768da0192e 100644 --- a/src/plugins/kibana_utils/server/index.ts +++ b/src/plugins/kibana_utils/server/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { Get, Set, createGetterSetter } from '../common'; +export { Get, Set, createGetterSetter, url } from '../common'; diff --git a/src/plugins/navigation/public/index.scss b/src/plugins/navigation/public/index.scss new file mode 100644 index 0000000000000..4734a2915c620 --- /dev/null +++ b/src/plugins/navigation/public/index.scss @@ -0,0 +1 @@ +@import "top_nav_menu/index"; diff --git a/src/plugins/navigation/public/index.ts b/src/plugins/navigation/public/index.ts index 1c0a36c597ce7..5afc91c4445e8 100644 --- a/src/plugins/navigation/public/index.ts +++ b/src/plugins/navigation/public/index.ts @@ -17,6 +17,8 @@ * under the License. */ +import './index.scss'; + import { PluginInitializerContext } from '../../../core/public'; export function plugin(initializerContext: PluginInitializerContext) { return new NavigationPublicPlugin(initializerContext); diff --git a/src/legacy/core_plugins/kibana/server/lib/system_api.js b/src/plugins/telemetry/common/constants.ts similarity index 61% rename from src/legacy/core_plugins/kibana/server/lib/system_api.js rename to src/plugins/telemetry/common/constants.ts index 3e2ab667dd98b..7b7694ed9aed7 100644 --- a/src/legacy/core_plugins/kibana/server/lib/system_api.js +++ b/src/plugins/telemetry/common/constants.ts @@ -17,15 +17,23 @@ * under the License. */ -const SYSTEM_API_HEADER_NAME = 'kbn-system-api'; +/** + * The amount of time, in milliseconds, to wait between reports when enabled. + * Currently 24 hours. + */ +export const REPORT_INTERVAL_MS = 86400000; + +/* + * Key for the localStorage service + */ +export const LOCALSTORAGE_KEY = 'telemetry.data'; /** - * Checks on the *server-side*, if an HTTP request is a system API request - * - * @param request HAPI request object - * @return true if request is a system API request; false, otherwise - * @deprecated Use KibanaRequest#isSystemApi + * Link to Advanced Settings. + */ +export const PATH_TO_ADVANCED_SETTINGS = 'kibana#/management/kibana/settings'; + +/** + * Link to the Elastic Telemetry privacy statement. */ -export function isSystemApiRequest(request) { - return !!request.headers[SYSTEM_API_HEADER_NAME]; -} +export const PRIVACY_STATEMENT_URL = `https://www.elastic.co/legal/privacy-statement`; diff --git a/src/plugins/telemetry/kibana.json b/src/plugins/telemetry/kibana.json new file mode 100644 index 0000000000000..3a28149276c3e --- /dev/null +++ b/src/plugins/telemetry/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "telemetry", + "version": "kibana", + "server": false, + "ui": true +} diff --git a/src/plugins/telemetry/public/components/__snapshots__/opt_in_banner.test.tsx.snap b/src/plugins/telemetry/public/components/__snapshots__/opt_in_banner.test.tsx.snap new file mode 100644 index 0000000000000..87e60869f6c21 --- /dev/null +++ b/src/plugins/telemetry/public/components/__snapshots__/opt_in_banner.test.tsx.snap @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OptInDetailsComponent renders as expected 1`] = ` + + } +> + + + + + + + + + + + + + + + +`; diff --git a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_details_component.test.tsx.snap b/src/plugins/telemetry/public/components/__snapshots__/opt_in_example_flyout.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_details_component.test.tsx.snap rename to src/plugins/telemetry/public/components/__snapshots__/opt_in_example_flyout.test.tsx.snap diff --git a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap b/src/plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap similarity index 97% rename from src/legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap rename to src/plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap index c80485332fa8a..7fa69a7409c6a 100644 --- a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap +++ b/src/plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap @@ -9,6 +9,7 @@ exports[`OptInMessage renders as expected 1`] = ` Object { "privacyStatementLink": { + it('renders as expected', () => { + expect(shallowWithIntl( {}} />)).toMatchSnapshot(); + }); + + it('fires the "onChangeOptInClick" prop with true when a enable is clicked', () => { + const onClick = jest.fn(); + const component = shallowWithIntl(); + + const enableButton = component.findWhere(n => { + const props = n.props(); + return n.type() === EuiButton && props['data-test-subj'] === 'enable'; + }); + + if (!enableButton) { + throw new Error(`Couldn't find any opt in enable button.`); + } + + enableButton.simulate('click'); + expect(onClick).toHaveBeenCalled(); + expect(onClick).toBeCalledWith(true); + }); + + it('fires the "onChangeOptInClick" with false when a disable is clicked', () => { + const onClick = jest.fn(); + const component = shallowWithIntl(); + + const disableButton = component.findWhere(n => { + const props = n.props(); + return n.type() === EuiButton && props['data-test-subj'] === 'disable'; + }); + + if (!disableButton) { + throw new Error(`Couldn't find any opt in disable button.`); + } + + disableButton.simulate('click'); + expect(onClick).toHaveBeenCalled(); + expect(onClick).toBeCalledWith(false); + }); +}); diff --git a/src/legacy/core_plugins/telemetry/public/components/opt_in_banner_component.tsx b/src/plugins/telemetry/public/components/opt_in_banner.tsx similarity index 84% rename from src/legacy/core_plugins/telemetry/public/components/opt_in_banner_component.tsx rename to src/plugins/telemetry/public/components/opt_in_banner.tsx index 2813af9c499e7..adf7b8bc84719 100644 --- a/src/legacy/core_plugins/telemetry/public/components/opt_in_banner_component.tsx +++ b/src/plugins/telemetry/public/components/opt_in_banner.tsx @@ -23,15 +23,12 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { OptInMessage } from './opt_in_message'; interface Props { - fetchTelemetry: () => Promise; - optInClick: (optIn: boolean) => void; + onChangeOptInClick: (isOptIn: boolean) => void; } -/** - * React component for displaying the Telemetry opt-in banner. - */ export class OptInBanner extends React.PureComponent { render() { + const { onChangeOptInClick } = this.props; const title = ( { ); return ( - + - this.props.optInClick(true)}> + onChangeOptInClick(true)}> { - this.props.optInClick(false)}> + onChangeOptInClick(false)}> { it('renders as expected', () => { expect( shallowWithIntl( - ({ data: [] }))} - onClose={jest.fn()} - /> + [])} onClose={jest.fn()} /> ) ).toMatchSnapshot(); }); diff --git a/src/legacy/core_plugins/telemetry/public/components/opt_in_details_component.tsx b/src/plugins/telemetry/public/components/opt_in_example_flyout.tsx similarity index 91% rename from src/legacy/core_plugins/telemetry/public/components/opt_in_details_component.tsx rename to src/plugins/telemetry/public/components/opt_in_example_flyout.tsx index 12ab780e75990..9ecbd4df20560 100644 --- a/src/legacy/core_plugins/telemetry/public/components/opt_in_details_component.tsx +++ b/src/plugins/telemetry/public/components/opt_in_example_flyout.tsx @@ -37,7 +37,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; interface Props { - fetchTelemetry: () => Promise; + fetchExample: () => Promise; onClose: () => void; } @@ -57,22 +57,21 @@ export class OptInExampleFlyout extends React.PureComponent { hasPrivilegeToRead: false, }; - componentDidMount() { - this.props - .fetchTelemetry() - .then(response => - this.setState({ - data: Array.isArray(response.data) ? response.data : null, - isLoading: false, - hasPrivilegeToRead: true, - }) - ) - .catch(err => { - this.setState({ - isLoading: false, - hasPrivilegeToRead: err.status !== 403, - }); + async componentDidMount() { + try { + const { fetchExample } = this.props; + const clusters = await fetchExample(); + this.setState({ + data: Array.isArray(clusters) ? clusters : null, + isLoading: false, + hasPrivilegeToRead: true, }); + } catch (err) { + this.setState({ + isLoading: false, + hasPrivilegeToRead: err.status !== 403, + }); + } } renderBody({ data, isLoading, hasPrivilegeToRead }: State) { diff --git a/src/legacy/core_plugins/telemetry/public/components/opt_in_message.test.tsx b/src/plugins/telemetry/public/components/opt_in_message.test.tsx similarity index 89% rename from src/legacy/core_plugins/telemetry/public/components/opt_in_message.test.tsx rename to src/plugins/telemetry/public/components/opt_in_message.test.tsx index 1a9fabceda907..dbe0941345a02 100644 --- a/src/legacy/core_plugins/telemetry/public/components/opt_in_message.test.tsx +++ b/src/plugins/telemetry/public/components/opt_in_message.test.tsx @@ -22,8 +22,6 @@ import { OptInMessage } from './opt_in_message'; describe('OptInMessage', () => { it('renders as expected', () => { - expect( - shallowWithIntl( [])} />) - ).toMatchSnapshot(); + expect(shallowWithIntl()).toMatchSnapshot(); }); }); diff --git a/src/legacy/core_plugins/telemetry/public/components/opt_in_message.tsx b/src/plugins/telemetry/public/components/opt_in_message.tsx similarity index 81% rename from src/legacy/core_plugins/telemetry/public/components/opt_in_message.tsx rename to src/plugins/telemetry/public/components/opt_in_message.tsx index 4221d78516e10..590a115b2bb6c 100644 --- a/src/legacy/core_plugins/telemetry/public/components/opt_in_message.tsx +++ b/src/plugins/telemetry/public/components/opt_in_message.tsx @@ -20,30 +20,9 @@ import * as React from 'react'; import { EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; - import { PRIVACY_STATEMENT_URL } from '../../common/constants'; -interface Props { - fetchTelemetry: () => Promise; -} - -interface State { - showDetails: boolean; - showExample: boolean; -} - -export class OptInMessage extends React.PureComponent { - public readonly state: State = { - showDetails: false, - showExample: false, - }; - - toggleShowExample = () => { - this.setState(prevState => ({ - showExample: !prevState.showExample, - })); - }; - +export class OptInMessage extends React.PureComponent { render() { return ( @@ -52,7 +31,7 @@ export class OptInMessage extends React.PureComponent { defaultMessage="Want to help us improve the Elastic Stack? Data usage collection is currently disabled. Enabling data usage collection helps us manage and improve our products and services. See our {privacyStatementLink} for more details." values={{ privacyStatementLink: ( - + { it('renders as expected', () => { - expect(shallowWithIntl( {}} />)).toMatchSnapshot(); + expect(shallowWithIntl( {}} />)).toMatchSnapshot(); }); it('fires the "onSeenBanner" prop when a link is clicked', () => { const onLinkClick = jest.fn(); - const component = shallowWithIntl(); + const component = shallowWithIntl(); const button = component.findWhere(n => n.type() === EuiButton); diff --git a/src/legacy/core_plugins/telemetry/public/components/opted_in_notice_banner.tsx b/src/plugins/telemetry/public/components/opted_in_notice_banner.tsx similarity index 75% rename from src/legacy/core_plugins/telemetry/public/components/opted_in_notice_banner.tsx rename to src/plugins/telemetry/public/components/opted_in_notice_banner.tsx index e37fa73ebe7b8..090893964c881 100644 --- a/src/legacy/core_plugins/telemetry/public/components/opted_in_notice_banner.tsx +++ b/src/plugins/telemetry/public/components/opted_in_notice_banner.tsx @@ -20,35 +20,32 @@ /* eslint @elastic/eui/href-or-on-click:0 */ import * as React from 'react'; -import chrome from 'ui/chrome'; import { EuiButton, EuiLink, EuiCallOut, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { PATH_TO_ADVANCED_SETTINGS } from '../../common/constants'; +import { i18n } from '@kbn/i18n'; +import { PATH_TO_ADVANCED_SETTINGS, PRIVACY_STATEMENT_URL } from '../../common/constants'; interface Props { onSeenBanner: () => any; } -/** - * React component for displaying the Telemetry opt-in notice. - */ -export class OptedInBanner extends React.PureComponent { - onLinkClick = () => { - this.props.onSeenBanner(); - return; - }; - +export class OptedInNoticeBanner extends React.PureComponent { render() { + const { onSeenBanner } = this.props; + const bannerTitle = i18n.translate('telemetry.telemetryOptedInNoticeTitle', { + defaultMessage: 'Help us improve the Elastic Stack', + }); + return ( - + @@ -59,10 +56,7 @@ export class OptedInBanner extends React.PureComponent { ), disableLink: ( - + { }} /> - + void; + showAppliesSettingMessage: boolean; + enableSaving: boolean; + query?: any; +} + +interface State { + processing: boolean; + showExample: boolean; + queryMatches: boolean | null; +} - state = { +export class TelemetryManagementSection extends Component { + state: State = { processing: false, showExample: false, queryMatches: null, }; - UNSAFE_componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps: Props) { const { query } = nextProps; const searchTerm = (query.text || '').toLowerCase(); @@ -71,11 +78,10 @@ export class TelemetryForm extends Component { } render() { - const { telemetryOptInProvider } = this.props; - + const { telemetryService } = this.props; const { showExample, queryMatches } = this.state; - if (!telemetryOptInProvider.canChangeOptInStatus()) { + if (!telemetryService.getCanChangeOptInStatus()) { return null; } @@ -87,7 +93,7 @@ export class TelemetryForm extends Component { {showExample && ( telemetryOptInProvider.fetchExample()} + fetchExample={telemetryService.fetchExample} onClose={this.toggleExample} /> )} @@ -106,15 +112,23 @@ export class TelemetryForm extends Component { {this.maybeGetAppliesSettingMessage()} ); - toggleOptIn = async () => { - const newOptInValue = !this.props.telemetryOptInProvider.getOptIn(); + toggleOptIn = async (): Promise => { + const { telemetryService } = this.props; + const newOptInValue = !telemetryService.getIsOptedIn(); return new Promise((resolve, reject) => { - this.setState( - { - enabled: newOptInValue, - processing: true, - }, - () => { - this.props.telemetryOptInProvider.setOptIn(newOptInValue).then( - () => { - this.setState({ processing: false }); - resolve(); - }, - e => { - // something went wrong - this.setState({ processing: false }); - reject(e); - } - ); + this.setState({ processing: true }, async () => { + try { + await telemetryService.setOptIn(newOptInValue); + this.setState({ processing: false }); + resolve(true); + } catch (err) { + this.setState({ processing: false }); + reject(err); } - ); + }); }); }; diff --git a/src/cli/color.js b/src/plugins/telemetry/public/index.ts similarity index 81% rename from src/cli/color.js rename to src/plugins/telemetry/public/index.ts index a02fb551c4181..2f86d7749bb9b 100644 --- a/src/cli/color.js +++ b/src/plugins/telemetry/public/index.ts @@ -17,8 +17,9 @@ * under the License. */ -import chalk from 'chalk'; +import { TelemetryPlugin } from './plugin'; +export { TelemetryPluginStart, TelemetryPluginSetup } from './plugin'; -export const green = chalk.black.bgGreen; -export const red = chalk.white.bgRed; -export const yellow = chalk.black.bgYellow; +export function plugin() { + return new TelemetryPlugin(); +} diff --git a/src/plugins/telemetry/public/mocks.ts b/src/plugins/telemetry/public/mocks.ts new file mode 100644 index 0000000000000..93dc13c327509 --- /dev/null +++ b/src/plugins/telemetry/public/mocks.ts @@ -0,0 +1,85 @@ +/* + * 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. + */ + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { overlayServiceMock } from '../../../core/public/overlays/overlay_service.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { httpServiceMock } from '../../../core/public/http/http_service.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { notificationServiceMock } from '../../../core/public/notifications/notifications_service.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { injectedMetadataServiceMock } from '../../../core/public/injected_metadata/injected_metadata_service.mock'; +import { TelemetryService } from './services/telemetry_service'; +import { TelemetryNotifications } from './services/telemetry_notifications/telemetry_notifications'; +import { TelemetryPluginStart } from './plugin'; + +export function mockTelemetryService({ + reportOptInStatusChange, +}: { reportOptInStatusChange?: boolean } = {}) { + const injectedMetadata = injectedMetadataServiceMock.createStartContract(); + injectedMetadata.getInjectedVar.mockImplementation((key: string) => { + switch (key) { + case 'telemetryNotifyUserAboutOptInDefault': + return true; + case 'allowChangingOptInStatus': + return true; + case 'telemetryOptedIn': + return true; + default: { + throw Error(`Unhandled getInjectedVar key "${key}".`); + } + } + }); + + return new TelemetryService({ + injectedMetadata, + http: httpServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + reportOptInStatusChange, + }); +} + +export function mockTelemetryNotifications({ + telemetryService, +}: { + telemetryService: TelemetryService; +}) { + return new TelemetryNotifications({ + overlays: overlayServiceMock.createStartContract(), + telemetryService, + }); +} + +export type Setup = jest.Mocked; + +export const telemetryPluginMock = { + createSetupContract, +}; + +function createSetupContract(): Setup { + const telemetryService = mockTelemetryService(); + const telemetryNotifications = mockTelemetryNotifications({ telemetryService }); + + const setupContract: Setup = { + telemetryService, + telemetryNotifications, + }; + + return setupContract; +} diff --git a/src/plugins/telemetry/public/plugin.ts b/src/plugins/telemetry/public/plugin.ts new file mode 100644 index 0000000000000..7ba51cacd1949 --- /dev/null +++ b/src/plugins/telemetry/public/plugin.ts @@ -0,0 +1,118 @@ +/* + * 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 { Plugin, CoreStart, CoreSetup, HttpStart } from '../../../core/public'; + +import { TelemetrySender, TelemetryService, TelemetryNotifications } from './services'; + +export interface TelemetryPluginSetup { + telemetryService: TelemetryService; +} + +export interface TelemetryPluginStart { + telemetryService: TelemetryService; + telemetryNotifications: TelemetryNotifications; +} + +export class TelemetryPlugin implements Plugin { + private telemetrySender?: TelemetrySender; + private telemetryNotifications?: TelemetryNotifications; + private telemetryService?: TelemetryService; + + public setup({ http, injectedMetadata, notifications }: CoreSetup): TelemetryPluginSetup { + this.telemetryService = new TelemetryService({ + http, + injectedMetadata, + notifications, + }); + + this.telemetrySender = new TelemetrySender(this.telemetryService); + + return { + telemetryService: this.telemetryService, + }; + } + + public start({ injectedMetadata, http, overlays, application }: CoreStart): TelemetryPluginStart { + if (!this.telemetryService) { + throw Error('Telemetry plugin failed to initialize properly.'); + } + + const telemetryBanner = injectedMetadata.getInjectedVar('telemetryBanner') as boolean; + const sendUsageFrom = injectedMetadata.getInjectedVar('telemetrySendUsageFrom') as + | 'browser' + | 'server'; + + this.telemetryNotifications = new TelemetryNotifications({ + overlays, + telemetryService: this.telemetryService, + }); + + application.currentAppId$.subscribe(appId => { + const isUnauthenticated = this.getIsUnauthenticated(http); + if (isUnauthenticated) { + return; + } + + this.maybeStartTelemetryPoller({ sendUsageFrom }); + if (telemetryBanner) { + this.maybeShowOptedInNotificationBanner(); + this.maybeShowOptInBanner(); + } + }); + + return { + telemetryService: this.telemetryService, + telemetryNotifications: this.telemetryNotifications, + }; + } + + private getIsUnauthenticated(http: HttpStart) { + const { anonymousPaths } = http; + return anonymousPaths.isAnonymous(window.location.pathname); + } + + private maybeStartTelemetryPoller({ sendUsageFrom }: { sendUsageFrom: string }) { + if (!this.telemetrySender) { + return; + } + if (sendUsageFrom === 'browser') { + this.telemetrySender.startChecking(); + } + } + + private maybeShowOptedInNotificationBanner() { + if (!this.telemetryNotifications) { + return; + } + const shouldShowBanner = this.telemetryNotifications.shouldShowOptedInNoticeBanner(); + if (shouldShowBanner) { + this.telemetryNotifications.renderOptedInNoticeBanner(); + } + } + + private maybeShowOptInBanner() { + if (!this.telemetryNotifications) { + return; + } + const shouldShowBanner = this.telemetryNotifications.shouldShowOptInBanner(); + if (shouldShowBanner) { + this.telemetryNotifications.renderOptInBanner(); + } + } +} diff --git a/src/plugins/telemetry/public/services/index.ts b/src/plugins/telemetry/public/services/index.ts new file mode 100644 index 0000000000000..ff4404c626fe0 --- /dev/null +++ b/src/plugins/telemetry/public/services/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { TelemetrySender } from './telemetry_sender'; +export { TelemetryService } from './telemetry_service'; +export { TelemetryNotifications } from './telemetry_notifications'; diff --git a/src/plugins/telemetry/public/services/telemetry_notifications/index.ts b/src/plugins/telemetry/public/services/telemetry_notifications/index.ts new file mode 100644 index 0000000000000..c6ba2cce1edb0 --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_notifications/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { TelemetryNotifications } from './telemetry_notifications'; diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.test.js b/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.test.ts similarity index 56% rename from src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.test.js rename to src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.test.ts index f40e0b188c198..020d8023b6003 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_notice_banner.test.js +++ b/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.test.ts @@ -17,24 +17,27 @@ * under the License. */ -import '../../services/telemetry_opt_in.test.mocks'; -import { renderOptedInBanner } from './render_notice_banner'; +import { renderOptInBanner } from './render_opt_in_banner'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { overlayServiceMock } from '../../../../../core/public/overlays/overlay_service.mock'; -describe('render_notice_banner', () => { +describe('renderOptInBanner', () => { it('adds a banner to banners with priority of 10000', () => { const bannerID = 'brucer-wayne'; + const overlays = overlayServiceMock.createStartContract(); + overlays.banners.add.mockReturnValue(bannerID); - const telemetryOptInProvider = { setOptInBannerNoticeId: jest.fn() }; - const banners = { add: jest.fn().mockReturnValue(bannerID) }; + const returnedBannerId = renderOptInBanner({ + setOptIn: jest.fn(), + overlays, + }); - renderOptedInBanner(telemetryOptInProvider, { _banners: banners }); + expect(overlays.banners.add).toBeCalledTimes(1); - expect(banners.add).toBeCalledTimes(1); - expect(telemetryOptInProvider.setOptInBannerNoticeId).toBeCalledWith(bannerID); + expect(returnedBannerId).toBe(bannerID); + const bannerConfig = overlays.banners.add.mock.calls[0]; - const bannerConfig = banners.add.mock.calls[0][0]; - - expect(bannerConfig.component).not.toBe(undefined); - expect(bannerConfig.priority).toBe(10000); + expect(bannerConfig[0]).not.toBe(undefined); + expect(bannerConfig[1]).toBe(10000); }); }); diff --git a/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.tsx b/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.tsx new file mode 100644 index 0000000000000..6e0164df6403a --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.tsx @@ -0,0 +1,35 @@ +/* + * 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 { CoreStart } from 'kibana/public'; +import { OptInBanner } from '../../components/opt_in_banner'; +import { toMountPoint } from '../../../../kibana_react/public'; + +interface RenderBannerConfig { + overlays: CoreStart['overlays']; + setOptIn: (isOptIn: boolean) => Promise; +} + +export function renderOptInBanner({ setOptIn, overlays }: RenderBannerConfig) { + const mount = toMountPoint(); + const bannerId = overlays.banners.add(mount, 10000); + + return bannerId; +} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.test.js b/src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.test.ts similarity index 52% rename from src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.test.js rename to src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.test.ts index b4a86b36d922f..2d175024a74fb 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.test.js +++ b/src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.test.ts @@ -17,26 +17,27 @@ * under the License. */ -import '../../services/telemetry_opt_in.test.mocks'; -import { renderBanner } from './render_banner'; +import { renderOptedInNoticeBanner } from './render_opted_in_notice_banner'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { overlayServiceMock } from '../../../../../core/public/overlays/overlay_service.mock'; -describe('render_banner', () => { +describe('renderOptedInNoticeBanner', () => { it('adds a banner to banners with priority of 10000', () => { - const bannerID = 'brucer-banner'; + const bannerID = 'brucer-wayne'; + const overlays = overlayServiceMock.createStartContract(); + overlays.banners.add.mockReturnValue(bannerID); - const telemetryOptInProvider = { setBannerId: jest.fn() }; - const banners = { add: jest.fn().mockReturnValue(bannerID) }; - const fetchTelemetry = jest.fn(); + const returnedBannerId = renderOptedInNoticeBanner({ + onSeen: jest.fn(), + overlays, + }); - renderBanner(telemetryOptInProvider, fetchTelemetry, { _banners: banners }); + expect(overlays.banners.add).toBeCalledTimes(1); - expect(banners.add).toBeCalledTimes(1); - expect(fetchTelemetry).toBeCalledTimes(0); - expect(telemetryOptInProvider.setBannerId).toBeCalledWith(bannerID); + expect(returnedBannerId).toBe(bannerID); + const bannerConfig = overlays.banners.add.mock.calls[0]; - const bannerConfig = banners.add.mock.calls[0][0]; - - expect(bannerConfig.component).not.toBe(undefined); - expect(bannerConfig.priority).toBe(10000); + expect(bannerConfig[0]).not.toBe(undefined); + expect(bannerConfig[1]).toBe(10000); }); }); diff --git a/src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.tsx b/src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.tsx new file mode 100644 index 0000000000000..e63e46af6e8ca --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.tsx @@ -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 React from 'react'; +import { CoreStart } from 'kibana/public'; +import { OptedInNoticeBanner } from '../../components/opted_in_notice_banner'; +import { toMountPoint } from '../../../../kibana_react/public'; + +interface RenderBannerConfig { + overlays: CoreStart['overlays']; + onSeen: () => void; +} +export function renderOptedInNoticeBanner({ onSeen, overlays }: RenderBannerConfig) { + const mount = toMountPoint(); + const bannerId = overlays.banners.add(mount, 10000); + + return bannerId; +} diff --git a/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.test.ts b/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.test.ts new file mode 100644 index 0000000000000..f767615d25253 --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.test.ts @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* eslint-disable dot-notation */ +import { mockTelemetryNotifications, mockTelemetryService } from '../../mocks'; + +describe('onSetOptInClick', () => { + it('sets setting successfully and removes banner', async () => { + const optIn = true; + const bannerId = 'bruce-banner'; + + const telemetryService = mockTelemetryService(); + telemetryService.setOptIn = jest.fn(); + const telemetryNotifications = mockTelemetryNotifications({ telemetryService }); + telemetryNotifications['optInBannerId'] = bannerId; + + await telemetryNotifications['onSetOptInClick'](optIn); + expect(telemetryNotifications['overlays'].banners.remove).toBeCalledTimes(1); + expect(telemetryNotifications['overlays'].banners.remove).toBeCalledWith(bannerId); + expect(telemetryService.setOptIn).toBeCalledTimes(1); + expect(telemetryService.setOptIn).toBeCalledWith(optIn); + }); +}); + +describe('setOptedInNoticeSeen', () => { + it('sets setting successfully and removes banner', async () => { + const bannerId = 'bruce-banner'; + + const telemetryService = mockTelemetryService(); + telemetryService.setUserHasSeenNotice = jest.fn(); + const telemetryNotifications = mockTelemetryNotifications({ telemetryService }); + telemetryNotifications['optedInNoticeBannerId'] = bannerId; + await telemetryNotifications.setOptedInNoticeSeen(); + + expect(telemetryNotifications['overlays'].banners.remove).toBeCalledTimes(1); + expect(telemetryNotifications['overlays'].banners.remove).toBeCalledWith(bannerId); + expect(telemetryService.setUserHasSeenNotice).toBeCalledTimes(1); + }); +}); diff --git a/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts b/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts new file mode 100644 index 0000000000000..bf25bb592db82 --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts @@ -0,0 +1,88 @@ +/* + * 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 { CoreStart } from 'kibana/public'; +import { renderOptedInNoticeBanner } from './render_opted_in_notice_banner'; +import { renderOptInBanner } from './render_opt_in_banner'; +import { TelemetryService } from '../telemetry_service'; + +interface TelemetryNotificationsConstructor { + overlays: CoreStart['overlays']; + telemetryService: TelemetryService; +} + +export class TelemetryNotifications { + private readonly overlays: CoreStart['overlays']; + private readonly telemetryService: TelemetryService; + private optedInNoticeBannerId?: string; + private optInBannerId?: string; + + constructor({ overlays, telemetryService }: TelemetryNotificationsConstructor) { + this.telemetryService = telemetryService; + this.overlays = overlays; + } + + public shouldShowOptedInNoticeBanner = (): boolean => { + const userHasSeenOptedInNotice = this.telemetryService.getUserHasSeenOptedInNotice(); + const bannerOnScreen = typeof this.optedInNoticeBannerId !== 'undefined'; + return !bannerOnScreen && userHasSeenOptedInNotice; + }; + + public renderOptedInNoticeBanner = (): void => { + const bannerId = renderOptedInNoticeBanner({ + onSeen: this.setOptedInNoticeSeen, + overlays: this.overlays, + }); + + this.optedInNoticeBannerId = bannerId; + }; + + public shouldShowOptInBanner = (): boolean => { + const isOptedIn = this.telemetryService.getIsOptedIn(); + const bannerOnScreen = typeof this.optInBannerId !== 'undefined'; + return !bannerOnScreen && isOptedIn === null; + }; + + public renderOptInBanner = (): void => { + const bannerId = renderOptInBanner({ + setOptIn: this.onSetOptInClick, + overlays: this.overlays, + }); + + this.optInBannerId = bannerId; + }; + + private onSetOptInClick = async (isOptIn: boolean) => { + if (this.optInBannerId) { + this.overlays.banners.remove(this.optInBannerId); + this.optInBannerId = undefined; + } + + await this.telemetryService.setOptIn(isOptIn); + }; + + public setOptedInNoticeSeen = async (): Promise => { + if (this.optedInNoticeBannerId) { + this.overlays.banners.remove(this.optedInNoticeBannerId); + this.optedInNoticeBannerId = undefined; + } + + await this.telemetryService.setUserHasSeenNotice(); + }; +} diff --git a/src/plugins/telemetry/public/services/telemetry_sender.test.ts b/src/plugins/telemetry/public/services/telemetry_sender.test.ts new file mode 100644 index 0000000000000..e9f5765c10412 --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_sender.test.ts @@ -0,0 +1,272 @@ +/* + * 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. + */ + +/* eslint-disable dot-notation */ +import { TelemetrySender } from './telemetry_sender'; +import { mockTelemetryService } from '../mocks'; +import { REPORT_INTERVAL_MS, LOCALSTORAGE_KEY } from '../../common/constants'; + +class LocalStorageMock implements Partial { + getItem = jest.fn(); + setItem = jest.fn(); +} + +describe('TelemetrySender', () => { + let originalLocalStorage: Storage; + let mockLocalStorage: LocalStorageMock; + beforeAll(() => { + originalLocalStorage = window.localStorage; + }); + + // @ts-ignore + beforeEach(() => (window.localStorage = mockLocalStorage = new LocalStorageMock())); + // @ts-ignore + afterAll(() => (window.localStorage = originalLocalStorage)); + + describe('constructor', () => { + it('defaults lastReport if unset', () => { + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + expect(telemetrySender['lastReported']).toBeUndefined(); + expect(mockLocalStorage.getItem).toBeCalledTimes(1); + expect(mockLocalStorage.getItem).toHaveBeenCalledWith(LOCALSTORAGE_KEY); + }); + + it('uses lastReport if set', () => { + const lastReport = `${Date.now()}`; + mockLocalStorage.getItem.mockReturnValueOnce(JSON.stringify({ lastReport })); + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + expect(telemetrySender['lastReported']).toBe(lastReport); + }); + }); + + describe('saveToBrowser', () => { + it('uses lastReport', () => { + const lastReport = `${Date.now()}`; + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender['lastReported'] = lastReport; + telemetrySender['saveToBrowser'](); + + expect(mockLocalStorage.setItem).toHaveBeenCalledTimes(1); + expect(mockLocalStorage.setItem).toHaveBeenCalledWith( + LOCALSTORAGE_KEY, + JSON.stringify({ lastReport }) + ); + }); + }); + + describe('shouldSendReport', () => { + it('returns false whenever optIn is false', () => { + const telemetryService = mockTelemetryService(); + telemetryService.getIsOptedIn = jest.fn().mockReturnValue(false); + const telemetrySender = new TelemetrySender(telemetryService); + const shouldSendRerpot = telemetrySender['shouldSendReport'](); + + expect(telemetryService.getIsOptedIn).toBeCalledTimes(1); + expect(shouldSendRerpot).toBe(false); + }); + + it('returns true if lastReported is undefined', () => { + const telemetryService = mockTelemetryService(); + telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true); + const telemetrySender = new TelemetrySender(telemetryService); + const shouldSendRerpot = telemetrySender['shouldSendReport'](); + + expect(telemetrySender['lastReported']).toBeUndefined(); + expect(shouldSendRerpot).toBe(true); + }); + + it('returns true if lastReported passed REPORT_INTERVAL_MS', () => { + const lastReported = Date.now() - (REPORT_INTERVAL_MS + 1000); + + const telemetryService = mockTelemetryService(); + telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender['lastReported'] = `${lastReported}`; + const shouldSendRerpot = telemetrySender['shouldSendReport'](); + expect(shouldSendRerpot).toBe(true); + }); + + it('returns false if lastReported is within REPORT_INTERVAL_MS', () => { + const lastReported = Date.now() + 1000; + + const telemetryService = mockTelemetryService(); + telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender['lastReported'] = `${lastReported}`; + const shouldSendRerpot = telemetrySender['shouldSendReport'](); + expect(shouldSendRerpot).toBe(false); + }); + + it('returns true if lastReported is malformed', () => { + const telemetryService = mockTelemetryService(); + telemetryService.getIsOptedIn = jest.fn().mockReturnValue(true); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender['lastReported'] = `random_malformed_string`; + const shouldSendRerpot = telemetrySender['shouldSendReport'](); + expect(shouldSendRerpot).toBe(true); + }); + + describe('sendIfDue', () => { + let originalFetch: typeof window['fetch']; + let mockFetch: jest.Mock; + + beforeAll(() => { + originalFetch = window.fetch; + }); + + // @ts-ignore + beforeEach(() => (window.fetch = mockFetch = jest.fn())); + // @ts-ignore + afterAll(() => (window.fetch = originalFetch)); + + it('does not send if already sending', async () => { + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender['shouldSendReport'] = jest.fn(); + telemetrySender['isSending'] = true; + await telemetrySender['sendIfDue'](); + + expect(telemetrySender['shouldSendReport']).toBeCalledTimes(0); + expect(mockFetch).toBeCalledTimes(0); + }); + + it('does not send if shouldSendReport returns false', async () => { + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(false); + telemetrySender['isSending'] = false; + await telemetrySender['sendIfDue'](); + + expect(telemetrySender['shouldSendReport']).toBeCalledTimes(1); + expect(mockFetch).toBeCalledTimes(0); + }); + + it('sends report if due', async () => { + const mockTelemetryUrl = 'telemetry_cluster_url'; + const mockTelemetryPayload = ['hashed_cluster_usage_data1']; + + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn().mockReturnValue(mockTelemetryUrl); + telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); + telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true); + telemetrySender['isSending'] = false; + await telemetrySender['sendIfDue'](); + + expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); + expect(mockFetch).toBeCalledTimes(1); + expect(mockFetch).toBeCalledWith(mockTelemetryUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: mockTelemetryPayload[0], + }); + }); + + it('sends report separately for every cluster', async () => { + const mockTelemetryUrl = 'telemetry_cluster_url'; + const mockTelemetryPayload = ['hashed_cluster_usage_data1', 'hashed_cluster_usage_data2']; + + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn().mockReturnValue(mockTelemetryUrl); + telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); + telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true); + telemetrySender['isSending'] = false; + await telemetrySender['sendIfDue'](); + + expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); + expect(mockFetch).toBeCalledTimes(2); + }); + + it('updates last lastReported and calls saveToBrowser', async () => { + const mockTelemetryUrl = 'telemetry_cluster_url'; + const mockTelemetryPayload = ['hashed_cluster_usage_data1']; + + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn().mockReturnValue(mockTelemetryUrl); + telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); + telemetrySender['shouldSendReport'] = jest.fn().mockReturnValue(true); + telemetrySender['saveToBrowser'] = jest.fn(); + + await telemetrySender['sendIfDue'](); + + expect(mockFetch).toBeCalledTimes(1); + expect(telemetrySender['lastReported']).toBeDefined(); + expect(telemetrySender['saveToBrowser']).toBeCalledTimes(1); + expect(telemetrySender['isSending']).toBe(false); + }); + + it('catches fetchTelemetry errors and sets isSending to false', async () => { + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn(); + telemetryService.fetchTelemetry = jest.fn().mockImplementation(() => { + throw Error('Error fetching usage'); + }); + await telemetrySender['sendIfDue'](); + expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); + expect(telemetrySender['lastReported']).toBeUndefined(); + expect(telemetrySender['isSending']).toBe(false); + }); + + it('catches fetch errors and sets isSending to false', async () => { + const mockTelemetryPayload = ['hashed_cluster_usage_data1', 'hashed_cluster_usage_data2']; + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetryService.getTelemetryUrl = jest.fn(); + telemetryService.fetchTelemetry = jest.fn().mockReturnValue(mockTelemetryPayload); + mockFetch.mockImplementation(() => { + throw Error('Error sending usage'); + }); + await telemetrySender['sendIfDue'](); + expect(telemetryService.fetchTelemetry).toBeCalledTimes(1); + expect(mockFetch).toBeCalledTimes(2); + expect(telemetrySender['lastReported']).toBeUndefined(); + expect(telemetrySender['isSending']).toBe(false); + }); + }); + }); + describe('startChecking', () => { + let originalSetInterval: typeof window['setInterval']; + let mockSetInterval: jest.Mock; + + beforeAll(() => { + originalSetInterval = window.setInterval; + }); + + // @ts-ignore + beforeEach(() => (window.setInterval = mockSetInterval = jest.fn())); + // @ts-ignore + afterAll(() => (window.setInterval = originalSetInterval)); + + it('calls sendIfDue every 60000 ms', () => { + const telemetryService = mockTelemetryService(); + const telemetrySender = new TelemetrySender(telemetryService); + telemetrySender.startChecking(); + expect(mockSetInterval).toBeCalledTimes(1); + expect(mockSetInterval).toBeCalledWith(telemetrySender['sendIfDue'], 60000); + }); + }); +}); diff --git a/src/plugins/telemetry/public/services/telemetry_sender.ts b/src/plugins/telemetry/public/services/telemetry_sender.ts new file mode 100644 index 0000000000000..fec2db0506eb7 --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_sender.ts @@ -0,0 +1,100 @@ +/* + * 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 { REPORT_INTERVAL_MS, LOCALSTORAGE_KEY } from '../../common/constants'; +import { TelemetryService } from './telemetry_service'; +import { Storage } from '../../../kibana_utils/public'; + +export class TelemetrySender { + private readonly telemetryService: TelemetryService; + private isSending: boolean = false; + private lastReported?: string; + private readonly storage: Storage; + private intervalId?: number; + + constructor(telemetryService: TelemetryService) { + this.telemetryService = telemetryService; + this.storage = new Storage(window.localStorage); + + const attributes = this.storage.get(LOCALSTORAGE_KEY); + if (attributes) { + this.lastReported = attributes.lastReport; + } + } + + private saveToBrowser = () => { + // we are the only code that manipulates this key, so it's safe to blindly overwrite the whole object + this.storage.set(LOCALSTORAGE_KEY, { lastReport: this.lastReported }); + }; + + private shouldSendReport = (): boolean => { + // check if opt-in for telemetry is enabled + if (this.telemetryService.getIsOptedIn()) { + if (!this.lastReported) { + return true; + } + // returns NaN for any malformed or unset (null/undefined) value + const lastReported = parseInt(this.lastReported, 10); + // If it's been a day since we last sent telemetry + if (isNaN(lastReported) || Date.now() - lastReported > REPORT_INTERVAL_MS) { + return true; + } + } + + return false; + }; + + private sendIfDue = async (): Promise => { + if (this.isSending || !this.shouldSendReport()) { + return; + } + + // mark that we are working so future requests are ignored until we're done + this.isSending = true; + try { + const telemetryUrl = this.telemetryService.getTelemetryUrl(); + const telemetryData: any | any[] = await this.telemetryService.fetchTelemetry(); + const clusters: string[] = [].concat(telemetryData); + await Promise.all( + clusters.map( + async cluster => + await fetch(telemetryUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: cluster, + }) + ) + ); + this.lastReported = `${Date.now()}`; + this.saveToBrowser(); + } catch (err) { + // ignore err + } finally { + this.isSending = false; + } + }; + + public startChecking = () => { + if (typeof this.intervalId === 'undefined') { + this.intervalId = window.setInterval(this.sendIfDue, 60000); + } + }; +} diff --git a/src/plugins/telemetry/public/services/telemetry_service.test.ts b/src/plugins/telemetry/public/services/telemetry_service.test.ts new file mode 100644 index 0000000000000..0ebcd52f1423c --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_service.test.ts @@ -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. + */ + +/* eslint-disable dot-notation */ +import { mockTelemetryService } from '../mocks'; + +const mockSubtract = jest.fn().mockImplementation(() => { + return { + toISOString: jest.fn(), + }; +}); + +jest.mock('moment', () => { + return jest.fn().mockImplementation(() => { + return { + subtract: mockSubtract, + toISOString: jest.fn(), + }; + }); +}); + +describe('TelemetryService', () => { + describe('fetchTelemetry', () => { + it('calls expected URL with 20 minutes - now', async () => { + const telemetryService = mockTelemetryService(); + await telemetryService.fetchTelemetry(); + expect(telemetryService['http'].post).toBeCalledWith('/api/telemetry/v2/clusters/_stats', { + body: JSON.stringify({ unencrypted: false, timeRange: {} }), + }); + expect(mockSubtract).toBeCalledWith(20, 'minutes'); + }); + }); + + describe('fetchExample', () => { + it('calls fetchTelemetry with unencrupted: true', async () => { + const telemetryService = mockTelemetryService(); + telemetryService.fetchTelemetry = jest.fn(); + await telemetryService.fetchExample(); + expect(telemetryService.fetchTelemetry).toBeCalledWith({ unencrypted: true }); + }); + }); + + describe('setOptIn', () => { + it('calls api if canChangeOptInStatus', async () => { + const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); + telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + await telemetryService.setOptIn(true); + + expect(telemetryService['http'].post).toBeCalledTimes(1); + }); + + it('sends enabled true if optedIn: true', async () => { + const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); + telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + const optedIn = true; + await telemetryService.setOptIn(optedIn); + + expect(telemetryService['http'].post).toBeCalledWith('/api/telemetry/v2/optIn', { + body: JSON.stringify({ enabled: optedIn }), + }); + }); + + it('sends enabled false if optedIn: false', async () => { + const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); + telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + const optedIn = false; + await telemetryService.setOptIn(optedIn); + + expect(telemetryService['http'].post).toBeCalledWith('/api/telemetry/v2/optIn', { + body: JSON.stringify({ enabled: optedIn }), + }); + }); + + it('does not call reportOptInStatus if reportOptInStatusChange is false', async () => { + const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); + telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + telemetryService['reportOptInStatus'] = jest.fn(); + await telemetryService.setOptIn(true); + + expect(telemetryService['reportOptInStatus']).toBeCalledTimes(0); + expect(telemetryService['http'].post).toBeCalledTimes(1); + }); + + it('calls reportOptInStatus if reportOptInStatusChange is true', async () => { + const telemetryService = mockTelemetryService({ reportOptInStatusChange: true }); + telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + telemetryService['reportOptInStatus'] = jest.fn(); + await telemetryService.setOptIn(true); + + expect(telemetryService['reportOptInStatus']).toBeCalledTimes(1); + expect(telemetryService['http'].post).toBeCalledTimes(1); + }); + + it('adds an error toast on api error', async () => { + const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); + telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + telemetryService['reportOptInStatus'] = jest.fn(); + telemetryService['http'].post = jest.fn().mockImplementation((url: string) => { + if (url === '/api/telemetry/v2/optIn') { + throw Error('failed to update opt in.'); + } + }); + + await telemetryService.setOptIn(true); + expect(telemetryService['http'].post).toBeCalledTimes(1); + expect(telemetryService['reportOptInStatus']).toBeCalledTimes(0); + expect(telemetryService['notifications'].toasts.addError).toBeCalledTimes(1); + }); + + it('adds an error toast on reportOptInStatus error', async () => { + const telemetryService = mockTelemetryService({ reportOptInStatusChange: true }); + telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + telemetryService['reportOptInStatus'] = jest.fn().mockImplementation(() => { + throw Error('failed to report OptIn Status.'); + }); + + await telemetryService.setOptIn(true); + expect(telemetryService['http'].post).toBeCalledTimes(1); + expect(telemetryService['reportOptInStatus']).toBeCalledTimes(1); + expect(telemetryService['notifications'].toasts.addError).toBeCalledTimes(1); + }); + }); +}); diff --git a/src/plugins/telemetry/public/services/telemetry_service.ts b/src/plugins/telemetry/public/services/telemetry_service.ts new file mode 100644 index 0000000000000..073886e7d1327 --- /dev/null +++ b/src/plugins/telemetry/public/services/telemetry_service.ts @@ -0,0 +1,165 @@ +/* + * 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 moment from 'moment'; +import { i18n } from '@kbn/i18n'; +import { CoreStart } from 'kibana/public'; + +interface TelemetryServiceConstructor { + http: CoreStart['http']; + injectedMetadata: CoreStart['injectedMetadata']; + notifications: CoreStart['notifications']; + reportOptInStatusChange?: boolean; +} + +export class TelemetryService { + private readonly http: CoreStart['http']; + private readonly injectedMetadata: CoreStart['injectedMetadata']; + private readonly reportOptInStatusChange: boolean; + private readonly notifications: CoreStart['notifications']; + private isOptedIn: boolean | null; + private userHasSeenOptedInNotice: boolean; + + constructor({ + http, + injectedMetadata, + notifications, + reportOptInStatusChange = true, + }: TelemetryServiceConstructor) { + const isOptedIn = injectedMetadata.getInjectedVar('telemetryOptedIn') as boolean | null; + const userHasSeenOptedInNotice = injectedMetadata.getInjectedVar( + 'telemetryNotifyUserAboutOptInDefault' + ) as boolean; + this.reportOptInStatusChange = reportOptInStatusChange; + this.injectedMetadata = injectedMetadata; + this.notifications = notifications; + this.http = http; + + this.isOptedIn = isOptedIn; + this.userHasSeenOptedInNotice = userHasSeenOptedInNotice; + } + + public getCanChangeOptInStatus = () => { + const allowChangingOptInStatus = this.injectedMetadata.getInjectedVar( + 'allowChangingOptInStatus' + ) as boolean; + return allowChangingOptInStatus; + }; + + public getOptInStatusUrl = () => { + const telemetryOptInStatusUrl = this.injectedMetadata.getInjectedVar( + 'telemetryOptInStatusUrl' + ) as string; + return telemetryOptInStatusUrl; + }; + + public getTelemetryUrl = () => { + const telemetryUrl = this.injectedMetadata.getInjectedVar('telemetryUrl') as string; + return telemetryUrl; + }; + + public getUserHasSeenOptedInNotice = () => { + return this.userHasSeenOptedInNotice; + }; + + public getIsOptedIn = () => { + return this.isOptedIn; + }; + + public fetchExample = async () => { + return await this.fetchTelemetry({ unencrypted: true }); + }; + + public fetchTelemetry = async ({ unencrypted = false } = {}) => { + const now = moment(); + return this.http.post('/api/telemetry/v2/clusters/_stats', { + body: JSON.stringify({ + unencrypted, + timeRange: { + min: now.subtract(20, 'minutes').toISOString(), + max: now.toISOString(), + }, + }), + }); + }; + + public setOptIn = async (optedIn: boolean): Promise => { + const canChangeOptInStatus = this.getCanChangeOptInStatus(); + if (!canChangeOptInStatus) { + return false; + } + + try { + await this.http.post('/api/telemetry/v2/optIn', { + body: JSON.stringify({ enabled: optedIn }), + }); + if (this.reportOptInStatusChange) { + await this.reportOptInStatus(optedIn); + } + this.isOptedIn = optedIn; + } catch (err) { + this.notifications.toasts.addError(err, { + title: i18n.translate('telemetry.optInErrorToastTitle', { + defaultMessage: 'Error', + }), + toastMessage: i18n.translate('telemetry.optInErrorToastText', { + defaultMessage: 'An error occurred while trying to set the usage statistics preference.', + }), + }); + + return false; + } + + return true; + }; + + public setUserHasSeenNotice = async (): Promise => { + try { + await this.http.put('/api/telemetry/v2/userHasSeenNotice'); + this.userHasSeenOptedInNotice = true; + } catch (error) { + this.notifications.toasts.addError(error, { + title: i18n.translate('telemetry.optInNoticeSeenErrorTitle', { + defaultMessage: 'Error', + }), + toastMessage: i18n.translate('telemetry.optInNoticeSeenErrorToastText', { + defaultMessage: 'An error occurred dismissing the notice', + }), + }); + this.userHasSeenOptedInNotice = false; + } + }; + + private reportOptInStatus = async (OptInStatus: boolean): Promise => { + const telemetryOptInStatusUrl = this.getOptInStatusUrl(); + + try { + await fetch(telemetryOptInStatusUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ enabled: OptInStatus }), + }); + } catch (err) { + // Sending the ping is best-effort. Telemetry tries to send the ping once and discards it immediately if sending fails. + // swallow any errors + } + }; +} diff --git a/src/plugins/timelion/server/series_functions/quandl.test.js b/src/plugins/timelion/server/series_functions/quandl.test.js index fe5aab512370f..67d81e56f145f 100644 --- a/src/plugins/timelion/server/series_functions/quandl.test.js +++ b/src/plugins/timelion/server/series_functions/quandl.test.js @@ -17,16 +17,16 @@ * under the License. */ +import { parse } from 'query-string'; import fn from './quandl'; +import moment from 'moment'; +import fetchMock from 'node-fetch'; const parseURL = require('url').parse; -const parseQueryString = require('querystring').parse; const tlConfig = require('./fixtures/tl_config')(); -import moment from 'moment'; -import fetchMock from 'node-fetch'; function parseUrlParams(url) { - return parseQueryString(parseURL(url).query); + return parse(parseURL(url).query, { sort: false }); } jest.mock('node-fetch', () => diff --git a/src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts b/src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts index 3aca50b5b4710..7893ad456e83b 100644 --- a/src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts +++ b/src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts @@ -32,11 +32,11 @@ const numberIntegerRequired = Joi.number() .integer() .required(); const numberOptional = Joi.number().optional(); -const numberRequired = Joi.number().required(); const queryObject = Joi.object({ language: Joi.string().allow(''), query: Joi.string().allow(''), }); +const numberOptionalOrEmptyString = Joi.alternatives(numberOptional, Joi.string().valid('')); const annotationsItems = Joi.object({ color: stringOptionalNullable, @@ -74,6 +74,16 @@ const metricsItems = Joi.object({ numerator: stringOptionalNullable, denominator: stringOptionalNullable, sigma: stringOptionalNullable, + unit: stringOptionalNullable, + model_type: stringOptionalNullable, + mode: stringOptionalNullable, + lag: numberOptional, + alpha: numberOptional, + beta: numberOptional, + gamma: numberOptional, + period: numberOptional, + multiplicative: Joi.boolean(), + window: numberOptional, function: stringOptionalNullable, script: stringOptionalNullable, variables: Joi.array() @@ -121,7 +131,7 @@ const seriesItems = Joi.object({ }) ) .optional(), - fill: numberOptional, + fill: numberOptionalOrEmptyString, filter: Joi.object({ query: stringRequired, language: stringOptionalNullable, @@ -131,11 +141,11 @@ const seriesItems = Joi.object({ hidden: Joi.boolean().optional(), id: stringRequired, label: stringOptionalNullable, - line_width: numberOptional, + line_width: numberOptionalOrEmptyString, metrics: Joi.array().items(metricsItems), offset_time: stringOptionalNullable, override_index_pattern: numberOptional, - point_size: numberRequired, + point_size: numberOptionalOrEmptyString, separate_axis: numberIntegerOptional, seperate_axis: numberIntegerOptional, series_index_pattern: stringOptionalNullable, diff --git a/src/legacy/ui/public/utils/query_string.d.ts b/src/plugins/vis_type_vega/config.ts similarity index 74% rename from src/legacy/ui/public/utils/query_string.d.ts rename to src/plugins/vis_type_vega/config.ts index 959171443185e..c03e86c0a3569 100644 --- a/src/legacy/ui/public/utils/query_string.d.ts +++ b/src/plugins/vis_type_vega/config.ts @@ -17,12 +17,11 @@ * under the License. */ -declare class QueryStringClass { - public decode(queryString: string): any; - public encode(obj: any): string; - public param(key: string, value: string): string; -} +import { schema, TypeOf } from '@kbn/config-schema'; -declare const QueryString: QueryStringClass; +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), + enableExternalUrls: schema.boolean({ defaultValue: false }), +}); -export { QueryString }; +export type ConfigSchema = TypeOf; diff --git a/src/plugins/vis_type_vega/kibana.json b/src/plugins/vis_type_vega/kibana.json new file mode 100644 index 0000000000000..6bfd6c9536df4 --- /dev/null +++ b/src/plugins/vis_type_vega/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "visTypeVega", + "version": "kibana", + "server": true, + "ui": true +} diff --git a/src/plugins/vis_type_vega/public/index.ts b/src/plugins/vis_type_vega/public/index.ts new file mode 100644 index 0000000000000..71f3474f8217e --- /dev/null +++ b/src/plugins/vis_type_vega/public/index.ts @@ -0,0 +1,38 @@ +/* + * 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 { ConfigSchema } from '../config'; + +export const plugin = (initializerContext: PluginInitializerContext) => ({ + setup() { + return { + /** + * The configuration is temporarily exposed to allow the legacy vega plugin to consume + * the setting. Once the vega plugin is migrated completely, this will become an implementation + * detail. + * @deprecated + */ + config: initializerContext.config.get(), + }; + }, + start() {}, +}); + +export type VisTypeVegaSetup = ReturnType['setup']>; diff --git a/src/cli/log.js b/src/plugins/vis_type_vega/server/index.ts similarity index 61% rename from src/cli/log.js rename to src/plugins/vis_type_vega/server/index.ts index 917d06c42c7ca..4c809ff3c5a93 100644 --- a/src/cli/log.js +++ b/src/plugins/vis_type_vega/server/index.ts @@ -17,18 +17,22 @@ * under the License. */ -import _ from 'lodash'; +import { PluginConfigDescriptor } from 'kibana/server'; -const log = _.restParam(function(color, label, rest1) { - console.log.apply(console, [color(` ${_.trim(label)} `)].concat(rest1)); -}); +import { configSchema, ConfigSchema } from '../config'; -import { green, yellow, red } from './color'; +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + enableExternalUrls: true, + }, + schema: configSchema, + deprecations: ({ renameFromRoot }) => [ + renameFromRoot('vega.enableExternalUrls', 'vis_type_vega.enableExternalUrls'), + renameFromRoot('vega.enabled', 'vis_type_vega.enabled'), + ], +}; -export default class Log { - constructor(quiet, silent) { - this.good = quiet || silent ? _.noop : _.partial(log, green); - this.warn = quiet || silent ? _.noop : _.partial(log, yellow); - this.bad = silent ? _.noop : _.partial(log, red); - } -} +export const plugin = () => ({ + setup() {}, + start() {}, +}); diff --git a/test/functional/apps/context/_discover_navigation.js b/test/functional/apps/context/_discover_navigation.js index aabce6baa8783..b906296037888 100644 --- a/test/functional/apps/context/_discover_navigation.js +++ b/test/functional/apps/context/_discover_navigation.js @@ -39,12 +39,10 @@ export default function({ getService, getPageObjects }) { await Promise.all( TEST_COLUMN_NAMES.map(columnName => PageObjects.discover.clickFieldListItemAdd(columnName)) ); - await Promise.all( - TEST_FILTER_COLUMN_NAMES.map(async ([columnName, value]) => { - await PageObjects.discover.clickFieldListItem(columnName); - await PageObjects.discover.clickFieldListPlusFilter(columnName, value); - }) - ); + for (const [columnName, value] of TEST_FILTER_COLUMN_NAMES) { + await PageObjects.discover.clickFieldListItem(columnName); + await PageObjects.discover.clickFieldListPlusFilter(columnName, value); + } }); it('should open the context view with the selected document as anchor', async function() { diff --git a/test/functional/apps/dashboard/panel_controls.js b/test/functional/apps/dashboard/panel_controls.js index 5ec6cf3389c4e..f30f58913bd97 100644 --- a/test/functional/apps/dashboard/panel_controls.js +++ b/test/functional/apps/dashboard/panel_controls.js @@ -54,8 +54,7 @@ export default function({ getService, getPageObjects }) { await PageObjects.dashboard.gotoDashboardLandingPage(); }); - // unskip when issue is fixed https://github.com/elastic/kibana/issues/55992 - describe.skip('visualization object replace flyout', () => { + describe('visualization object replace flyout', () => { let intialDimensions; before(async () => { await PageObjects.dashboard.clickNewDashboard(); diff --git a/test/functional/apps/home/_sample_data.ts b/test/functional/apps/home/_sample_data.ts index 8088b5a0f9da9..8bc528e045566 100644 --- a/test/functional/apps/home/_sample_data.ts +++ b/test/functional/apps/home/_sample_data.ts @@ -84,7 +84,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { }); it('should launch sample flights data set dashboard', async () => { - await PageObjects.home.launchSampleDataSet('flights'); + await PageObjects.home.launchSampleDashboard('flights'); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); const todayYearMonthDay = moment().format('MMM D, YYYY'); @@ -96,7 +96,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { }); it('should render visualizations', async () => { - await PageObjects.home.launchSampleDataSet('flights'); + await PageObjects.home.launchSampleDashboard('flights'); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); log.debug('Checking pie charts rendered'); @@ -115,7 +115,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { }); it('should launch sample logs data set dashboard', async () => { - await PageObjects.home.launchSampleDataSet('logs'); + await PageObjects.home.launchSampleDashboard('logs'); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); const todayYearMonthDay = moment().format('MMM D, YYYY'); @@ -127,7 +127,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { }); it('should launch sample ecommerce data set dashboard', async () => { - await PageObjects.home.launchSampleDataSet('ecommerce'); + await PageObjects.home.launchSampleDashboard('ecommerce'); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); const todayYearMonthDay = moment().format('MMM D, YYYY'); diff --git a/test/functional/config.ie.js b/test/functional/config.ie.js index 5e8ea56a848dc..2c32ccb69db03 100644 --- a/test/functional/config.ie.js +++ b/test/functional/config.ie.js @@ -35,7 +35,6 @@ export default async function({ readConfigFile }) { defaults: { 'accessibility:disableAnimations': true, 'dateFormat:tz': 'UTC', - 'telemetry:optIn': false, 'state:storeInSessionStorage': true, 'notifications:lifetime:info': 10000, }, @@ -43,7 +42,11 @@ export default async function({ readConfigFile }) { kbnTestServer: { ...defaultConfig.get('kbnTestServer'), - serverArgs: [...defaultConfig.get('kbnTestServer.serverArgs'), '--csp.strict=false'], + serverArgs: [ + ...defaultConfig.get('kbnTestServer.serverArgs'), + '--csp.strict=false', + '--telemetry.optIn=false', + ], }, }; } diff --git a/test/functional/config.js b/test/functional/config.js index 134ddf4e84b2d..155e844578c54 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -44,14 +44,17 @@ export default async function({ readConfigFile }) { kbnTestServer: { ...commonConfig.get('kbnTestServer'), - serverArgs: [...commonConfig.get('kbnTestServer.serverArgs'), '--oss'], + serverArgs: [ + ...commonConfig.get('kbnTestServer.serverArgs'), + '--oss', + '--telemetry.optIn=false', + ], }, uiSettings: { defaults: { 'accessibility:disableAnimations': true, 'dateFormat:tz': 'UTC', - 'telemetry:optIn': false, }, }, diff --git a/test/functional/page_objects/home_page.ts b/test/functional/page_objects/home_page.ts index a641fbda023c3..6225b4e3aca62 100644 --- a/test/functional/page_objects/home_page.ts +++ b/test/functional/page_objects/home_page.ts @@ -19,9 +19,12 @@ import { FtrProviderContext } from '../ftr_provider_context'; -export function HomePageProvider({ getService }: FtrProviderContext) { +export function HomePageProvider({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); + const find = getService('find'); + const PageObjects = getPageObjects(['common']); + let isOss = true; class HomePage { async clickSynopsis(title: string) { @@ -63,6 +66,14 @@ export function HomePageProvider({ getService }: FtrProviderContext) { }); } + async launchSampleDashboard(id: string) { + await this.launchSampleDataSet(id); + isOss = await PageObjects.common.isOss(); + if (!isOss) { + await find.clickByLinkText('Dashboard'); + } + } + async launchSampleDataSet(id: string) { await this.addSampleDataSet(id); await testSubjects.click(`launchSampleDataSet${id}`); diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 91495c4024f3a..9679636a39d23 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -69,7 +69,8 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider return window.__RENDERING_SESSION__; }); - describe('rendering service', () => { + // Talked to @dover, he aggreed we can skip these tests that are unexpectedly flaky + describe.skip('rendering service', () => { it('renders "core" application', async () => { await navigateTo('/render/core'); diff --git a/test/scripts/jenkins_build_kibana.sh b/test/scripts/jenkins_build_kibana.sh index 2605655ed7e7a..a7c05b6e5802d 100755 --- a/test/scripts/jenkins_build_kibana.sh +++ b/test/scripts/jenkins_build_kibana.sh @@ -2,6 +2,15 @@ source src/dev/ci_setup/setup_env.sh +echo " -> building kibana platform plugins" +node scripts/build_kibana_platform_plugins \ + --oss \ + --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \ + --verbose; + +# doesn't persist, also set in kibanaPipeline.groovy +export KBN_NP_PLUGINS_BUILT=true + echo " -> downloading es snapshot" node scripts/es snapshot --license=oss --download-only; diff --git a/test/scripts/jenkins_xpack_build_kibana.sh b/test/scripts/jenkins_xpack_build_kibana.sh index 20b12b302cb39..f87d6e1102c45 100755 --- a/test/scripts/jenkins_xpack_build_kibana.sh +++ b/test/scripts/jenkins_xpack_build_kibana.sh @@ -3,6 +3,14 @@ cd "$KIBANA_DIR" source src/dev/ci_setup/setup_env.sh +echo " -> building kibana platform plugins" +node scripts/build_kibana_platform_plugins \ + --scan-dir "$XPACK_DIR/test/plugin_functional/plugins" \ + --verbose; + +# doesn't persist, also set in kibanaPipeline.groovy +export KBN_NP_PLUGINS_BUILT=true + echo " -> downloading es snapshot" node scripts/es snapshot --download-only; diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index dd66586e912d6..dd2e626d1c860 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -87,6 +87,7 @@ def getPostBuildWorker(name, closure) { "TEST_ES_URL=http://elastic:changeme@localhost:${esPort}", "TEST_ES_TRANSPORT_PORT=${esTransportPort}", "IS_PIPELINE_JOB=1", + "KBN_NP_PLUGINS_BUILT=true", ]) { closure() } diff --git a/x-pack/.gitignore b/x-pack/.gitignore index 40a52f88dbbba..6bac5e181861d 100644 --- a/x-pack/.gitignore +++ b/x-pack/.gitignore @@ -4,6 +4,7 @@ /test/functional/failure_debug /test/functional/screenshots /test/functional/apps/reporting/reports/session +/test/reporting/configs/failure_debug/ /legacy/plugins/reporting/.chromium/ /legacy/plugins/reporting/.phantom/ /.aws-config.json diff --git a/x-pack/index.js b/x-pack/index.js index ecb71f26c1609..858c3e8b68d18 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -9,10 +9,8 @@ import { graph } from './legacy/plugins/graph'; import { monitoring } from './legacy/plugins/monitoring'; import { reporting } from './legacy/plugins/reporting'; import { security } from './legacy/plugins/security'; -import { searchprofiler } from './legacy/plugins/searchprofiler'; import { ml } from './legacy/plugins/ml'; import { tilemap } from './legacy/plugins/tilemap'; -import { watcher } from './legacy/plugins/watcher'; import { grokdebugger } from './legacy/plugins/grokdebugger'; import { dashboardMode } from './legacy/plugins/dashboard_mode'; import { logstash } from './legacy/plugins/logstash'; @@ -49,10 +47,8 @@ module.exports = function(kibana) { reporting(kibana), spaces(kibana), security(kibana), - searchprofiler(kibana), ml(kibana), tilemap(kibana), - watcher(kibana), grokdebugger(kibana), dashboardMode(kibana), logstash(kibana), diff --git a/x-pack/legacy/plugins/alerting/README.md b/x-pack/legacy/plugins/alerting/README.md index eb9df042f9254..2a10c41f12b85 100644 --- a/x-pack/legacy/plugins/alerting/README.md +++ b/x-pack/legacy/plugins/alerting/README.md @@ -85,7 +85,7 @@ The following table describes the properties of the `options` object. |---|---|---| |id|Unique identifier for the alert type. For convention purposes, ids starting with `.` are reserved for built in alert types. We recommend using a convention like `.mySpecialAlert` for your alert types to avoid conflicting with another plugin.|string| |name|A user-friendly name for the alert type. These will be displayed in dropdowns when choosing alert types.|string| -|actionGroups|An explicit list of groups the alert type may schedule actions for. Alert `actions` validation will use this array to ensure groups are valid.|string[]| +|actionGroups|An explicit list of groups the alert type may schedule actions for, each specifying the ActionGroup's unique ID and human readable name. Alert `actions` validation will use this configuartion to ensure groups are valid. We highly encourage using `kbn-i18n` to translate the names of actionGroup when registering the AlertType. |Array<{id:string, name:string}>| |validate.params|When developing an alert type, you can choose to accept a series of parameters. You may also have the parameters validated before they are passed to the `executor` function or created as an alert saved object. In order to do this, provide a `@kbn/config-schema` schema that we will use to validate the `params` attribute.|@kbn/config-schema| |executor|This is where the code of the alert type lives. This is a function to be called when executing an alert on an interval basis. For full details, see executor section below.|Function| diff --git a/x-pack/legacy/plugins/alerting/server/alert_type_registry.test.ts b/x-pack/legacy/plugins/alerting/server/alert_type_registry.test.ts index 1087ee9109885..976bed884cd43 100644 --- a/x-pack/legacy/plugins/alerting/server/alert_type_registry.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alert_type_registry.test.ts @@ -118,7 +118,12 @@ describe('list()', () => { registry.register({ id: 'test', name: 'Test', - actionGroups: ['testActionGroup'], + actionGroups: [ + { + id: 'testActionGroup', + name: 'Test Action Group', + }, + ], executor: jest.fn(), }); const result = registry.list(); @@ -126,7 +131,10 @@ describe('list()', () => { Array [ Object { "actionGroups": Array [ - "testActionGroup", + Object { + "id": "testActionGroup", + "name": "Test Action Group", + }, ], "id": "test", "name": "Test", diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts index 38521eea20481..1555a0537158a 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts @@ -86,7 +86,7 @@ describe('create()', () => { alertTypeRegistry.get.mockReturnValue({ id: '123', name: 'Test', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], async executor() {}, }); }); @@ -1884,7 +1884,7 @@ describe('update()', () => { alertTypeRegistry.get.mockReturnValue({ id: '123', name: 'Test', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], async executor() {}, }); }); @@ -2414,7 +2414,7 @@ describe('update()', () => { alertTypeRegistry.get.mockReturnValueOnce({ id: '123', name: 'Test', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], validate: { params: schema.object({ param1: schema.string(), @@ -2646,7 +2646,7 @@ describe('update()', () => { alertTypeRegistry.get.mockReturnValueOnce({ id: '123', name: 'Test', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], async executor() {}, }); savedObjectsClient.bulkGet.mockResolvedValueOnce({ diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.ts index 334eacc05c771..eef6f662a20a2 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.ts @@ -5,7 +5,7 @@ */ import Boom from 'boom'; -import { omit, isEqual } from 'lodash'; +import { omit, isEqual, pluck } from 'lodash'; import { i18n } from '@kbn/i18n'; import { Logger, @@ -639,8 +639,9 @@ export class AlertsClient { private validateActions(alertType: AlertType, actions: NormalizedAlertAction[]): void { const { actionGroups: alertTypeActionGroups } = alertType; const usedAlertActionGroups = actions.map(action => action.group); + const availableAlertTypeActionGroups = new Set(pluck(alertTypeActionGroups, 'id')); const invalidActionGroups = usedAlertActionGroups.filter( - group => !alertTypeActionGroups.includes(group) + group => !availableAlertTypeActionGroups.has(group) ); if (invalidActionGroups.length) { throw Boom.badRequest( diff --git a/x-pack/legacy/plugins/alerting/server/plugin.test.ts b/x-pack/legacy/plugins/alerting/server/plugin.test.ts index 50142434a86dd..872de720243b2 100644 --- a/x-pack/legacy/plugins/alerting/server/plugin.test.ts +++ b/x-pack/legacy/plugins/alerting/server/plugin.test.ts @@ -10,6 +10,34 @@ import { licensingMock } from '../../../../plugins/licensing/server/mocks'; import { encryptedSavedObjectsMock } from '../../../../plugins/encrypted_saved_objects/server/mocks'; describe('Alerting Plugin', () => { + describe('setup()', () => { + it('should log warning when Encrypted Saved Objects plugin is using an ephemeral encryption key', async () => { + const context = coreMock.createPluginInitializerContext(); + const plugin = new Plugin(context); + + const coreSetup = coreMock.createSetup(); + const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); + await plugin.setup( + { + ...coreSetup, + http: { + ...coreSetup.http, + route: jest.fn(), + }, + } as any, + { + licensing: licensingMock.createSetup(), + encryptedSavedObjects: encryptedSavedObjectsSetup, + } as any + ); + + expect(encryptedSavedObjectsSetup.usingEphemeralEncryptionKey).toEqual(true); + expect(context.logger.get().warn).toHaveBeenCalledWith( + 'APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.' + ); + }); + }); + describe('start()', () => { /** * HACK: This test has put together to ensuire the function "getAlertsClientWithRequest" diff --git a/x-pack/legacy/plugins/alerting/server/plugin.ts b/x-pack/legacy/plugins/alerting/server/plugin.ts index e3f7656002d18..2567e470d2e79 100644 --- a/x-pack/legacy/plugins/alerting/server/plugin.ts +++ b/x-pack/legacy/plugins/alerting/server/plugin.ts @@ -69,6 +69,12 @@ export class Plugin { this.isESOUsingEphemeralEncryptionKey = plugins.encryptedSavedObjects.usingEphemeralEncryptionKey; + if (this.isESOUsingEphemeralEncryptionKey) { + this.logger.warn( + 'APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.' + ); + } + // Encrypted attributes plugins.encryptedSavedObjects.registerType({ type: 'alert', diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/legacy/plugins/alerting/server/task_runner/create_execution_handler.test.ts index d86a06767c9d1..02fa09ba97a65 100644 --- a/x-pack/legacy/plugins/alerting/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/legacy/plugins/alerting/server/task_runner/create_execution_handler.test.ts @@ -11,7 +11,10 @@ import { loggingServiceMock } from '../../../../../../src/core/server/mocks'; const alertType: AlertType = { id: 'test', name: 'Test', - actionGroups: ['default', 'other-group'], + actionGroups: [ + { id: 'default', name: 'Default' }, + { id: 'other-group', name: 'Other Group' }, + ], executor: jest.fn(), }; diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/legacy/plugins/alerting/server/task_runner/create_execution_handler.ts index 6b4b47b87b300..737f86a881c1f 100644 --- a/x-pack/legacy/plugins/alerting/server/task_runner/create_execution_handler.ts +++ b/x-pack/legacy/plugins/alerting/server/task_runner/create_execution_handler.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { pluck } from 'lodash'; import { AlertAction, State, Context, AlertType } from '../types'; import { Logger } from '../../../../../../src/core/server'; import { transformActionParams } from './transform_action_params'; @@ -35,8 +36,9 @@ export function createExecutionHandler({ apiKey, alertType, }: CreateExecutionHandlerOptions) { + const alertTypeActionGroups = new Set(pluck(alertType.actionGroups, 'id')); return async ({ actionGroup, context, state, alertInstanceId }: ExecutionHandlerOptions) => { - if (!alertType.actionGroups.includes(actionGroup)) { + if (!alertTypeActionGroups.has(actionGroup)) { logger.error(`Invalid action group "${actionGroup}" for alert "${alertType.id}".`); return; } diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.test.ts index 394c13e1bd24f..b6dd4b3435fcb 100644 --- a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner.test.ts @@ -19,7 +19,7 @@ import { const alertType = { id: 'test', name: 'My test alert', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], executor: jest.fn(), }; let fakeTimer: sinon.SinonFakeTimers; diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.test.ts b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.test.ts index 543b9e7d32e12..7474fcfb4baaa 100644 --- a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.test.ts @@ -16,7 +16,7 @@ import { const alertType = { id: 'test', name: 'My test alert', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], executor: jest.fn(), }; let fakeTimer: sinon.SinonFakeTimers; diff --git a/x-pack/legacy/plugins/alerting/server/types.ts b/x-pack/legacy/plugins/alerting/server/types.ts index 5e8adadf74ac0..95a96fa384c2c 100644 --- a/x-pack/legacy/plugins/alerting/server/types.ts +++ b/x-pack/legacy/plugins/alerting/server/types.ts @@ -42,13 +42,18 @@ export interface AlertExecutorOptions { updatedBy: string | null; } +export interface ActionGroup { + id: string; + name: string; +} + export interface AlertType { id: string; name: string; validate?: { params?: { validate: (object: any) => any }; }; - actionGroups: string[]; + actionGroups: ActionGroup[]; executor: ({ services, params, state }: AlertExecutorOptions) => Promise; } diff --git a/x-pack/legacy/plugins/apm/cypress/package.json b/x-pack/legacy/plugins/apm/cypress/package.json index ef8955fcbd1b0..59f76ba250ad7 100644 --- a/x-pack/legacy/plugins/apm/cypress/package.json +++ b/x-pack/legacy/plugins/apm/cypress/package.json @@ -16,6 +16,6 @@ "p-limit": "^2.2.1", "ts-loader": "^6.1.0", "typescript": "3.7.2", - "webpack": "^4.40.2" + "webpack": "^4.41.5" } } diff --git a/x-pack/legacy/plugins/apm/index.ts b/x-pack/legacy/plugins/apm/index.ts index c52e6742ddae5..fa22dca58a08b 100644 --- a/x-pack/legacy/plugins/apm/index.ts +++ b/x-pack/legacy/plugins/apm/index.ts @@ -72,8 +72,7 @@ export const apm: LegacyPluginInitializer = kibana => { autocreateApmIndexPattern: Joi.boolean().default(true), // service map - serviceMapEnabled: Joi.boolean().default(false), - serviceMapInitialTimeRange: Joi.number().default(60 * 1000 * 60) // last 1 hour + serviceMapEnabled: Joi.boolean().default(false) }).default(); }, diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap index a45357121354f..ed09b71f0c31c 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap @@ -305,7 +305,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` exports[`ErrorGroupOverview -> List should render with data 1`] = ` .c0 { - font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace; + font-family: "Roboto Mono",Consolas,Menlo,Courier,monospace; } .c1 { @@ -316,7 +316,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` } .c2 { - font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace; + font-family: "Roboto Mono",Consolas,Menlo,Courier,monospace; font-size: 16px; max-width: 100%; white-space: nowrap; @@ -325,7 +325,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` } .c3 { - font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace; + font-family: "Roboto Mono",Consolas,Menlo,Courier,monospace; } ( + + + + ), + breadcrumb: i18n.translate('xpack.apm.breadcrumb.settings.customizeUI', { + defaultMessage: 'Customize UI' + }), + name: RouteName.CUSTOMIZE_UI } ]; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/route_names.tsx b/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/route_names.tsx index 0ae7a948be4e1..db57e8356f39b 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/route_names.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/route_names.tsx @@ -22,5 +22,6 @@ export enum RouteName { AGENT_CONFIGURATION = 'agent_configuration', INDICES = 'indices', SERVICE_NODES = 'nodes', - LINK_TO_TRACE = 'link_to_trace' + LINK_TO_TRACE = 'link_to_trace', + CUSTOMIZE_UI = 'customize_ui' } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx index 24fb0b9e5d8a3..4e24460f80ec1 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -27,10 +27,10 @@ import { useUrlParams } from '../../../hooks/useUrlParams'; import { Controls } from './Controls'; import { Cytoscape } from './Cytoscape'; import { getCytoscapeElements } from './get_cytoscape_elements'; -import { LoadingOverlay } from './LoadingOverlay'; import { PlatinumLicensePrompt } from './PlatinumLicensePrompt'; import { Popover } from './Popover'; import { useRefHeight } from './useRefHeight'; +import { useLoadingIndicator } from '../../../hooks/useLoadingIndicator'; interface ServiceMapProps { serviceName?: string; @@ -79,8 +79,9 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { const openToast = useRef(null); const [responses, setResponses] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [percentageLoaded, setPercentageLoaded] = useState(0); + + const { setIsLoading } = useLoadingIndicator(); + const [, _setUnusedState] = useState(false); const elements = useMemo(() => getCytoscapeElements(responses, search), [ @@ -115,14 +116,14 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { } }); setResponses(resp => resp.concat(data)); - setIsLoading(false); const shouldGetNext = responses.length + 1 < MAX_REQUESTS && data.after; if (shouldGetNext) { - setPercentageLoaded(value => value + 30); // increase loading bar 30% await getNext({ after: data.after }); + } else { + setIsLoading(false); } } catch (error) { setIsLoading(false); @@ -134,14 +135,12 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { } } }, - [callApmApi, params, responses.length, notifications.toasts] + [params, setIsLoading, callApmApi, responses.length, notifications.toasts] ); useEffect(() => { const loadServiceMaps = async () => { - setPercentageLoaded(5); await getNext({ reset: true }); - setPercentageLoaded(100); }; loadServiceMaps(); @@ -167,7 +166,7 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { forceUpdate(); }; - if (newElements.length > 0 && percentageLoaded === 100) { + if (newElements.length > 0 && renderedElements.current.length > 0) { openToast.current = notifications.toasts.add({ title: i18n.translate('xpack.apm.newServiceMapData', { defaultMessage: `Newly discovered connections are available.` @@ -193,7 +192,7 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [elements, percentageLoaded]); + }, [elements]); const isValidPlatinumLicense = license?.isActive && @@ -212,10 +211,6 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { height={height} style={cytoscapeDivStyle} > - diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/index.tsx index e1cb07be3d378..7243a86404f04 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/index.tsx @@ -26,7 +26,7 @@ import { useCallApmApi } from '../../../../../hooks/useCallApmApi'; import { transactionSampleRateRt } from '../../../../../../common/runtime_types/transaction_sample_rate_rt'; import { Config } from '../index'; import { SettingsSection } from './SettingsSection'; -import { ServiceSection } from './ServiceSection'; +import { ServiceForm } from '../../../../shared/ServiceForm'; import { DeleteButton } from './DeleteButton'; import { transactionMaxSpansRt } from '../../../../../../common/runtime_types/transaction_max_spans_rt'; import { useFetcher } from '../../../../../hooks/useFetcher'; @@ -176,16 +176,16 @@ export function AddEditFlyout({ } }} > - diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/CustomActionsFlyout/SettingsSection.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/CustomActionsFlyout/SettingsSection.tsx new file mode 100644 index 0000000000000..8cb604d367549 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/CustomActionsFlyout/SettingsSection.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiFieldText, EuiFormRow, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +interface Props { + label: string; + onLabelChange: (label: string) => void; + url: string; + onURLChange: (url: string) => void; +} + +export const SettingsSection = ({ + label, + onLabelChange, + url, + onURLChange +}: Props) => { + return ( + <> + +

+ {i18n.translate( + 'xpack.apm.settings.customizeUI.customActions.flyout.settingsSection.title', + { defaultMessage: 'Action' } + )} +

+
+ + + { + onLabelChange(e.target.value); + }} + /> + + + { + onURLChange(e.target.value); + }} + /> + + + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/CustomActionsFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/CustomActionsFlyout/index.tsx new file mode 100644 index 0000000000000..d04cdd62c303b --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/CustomActionsFlyout/index.tsx @@ -0,0 +1,109 @@ +/* + * 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 { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiPortal, + EuiSpacer, + EuiText, + EuiTitle +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useState } from 'react'; +import { SettingsSection } from './SettingsSection'; +import { ServiceForm } from '../../../../../shared/ServiceForm'; + +interface Props { + onClose: () => void; +} + +export const CustomActionsFlyout = ({ onClose }: Props) => { + const [serviceName, setServiceName] = useState(''); + const [environment, setEnvironment] = useState(''); + const [label, setLabel] = useState(''); + const [url, setURL] = useState(''); + return ( + + + + +

+ {i18n.translate( + 'xpack.apm.settings.customizeUI.customActions.flyout.title', + { + defaultMessage: 'Create custom action' + } + )} +

+
+
+ + +

+ {i18n.translate( + 'xpack.apm.settings.customizeUI.customActions.flyout.label', + { + defaultMessage: + "This action will be shown in the 'Actions' context menu for the trace and error detail components. You can specify any number of links, but only the first three will be shown, in alphabetical order." + } + )} +

+
+ + + + + + +
+ + + + + {i18n.translate( + 'xpack.apm.settings.customizeUI.customActions.flyout.close', + { + defaultMessage: 'Close' + } + )} + + + + + {i18n.translate( + 'xpack.apm.settings.customizeUI.customActions.flyout.save', + { + defaultMessage: 'Save' + } + )} + + + + +
+
+ ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/EmptyPrompt.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/EmptyPrompt.tsx new file mode 100644 index 0000000000000..f39e4b307b24c --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/EmptyPrompt.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +export const EmptyPrompt = ({ + onCreateCustomActionClick +}: { + onCreateCustomActionClick: () => void; +}) => { + return ( + + {i18n.translate( + 'xpack.apm.settings.customizeUI.customActions.emptyPromptTitle', + { + defaultMessage: 'No actions found.' + } + )} + + } + body={ + <> +

+ {i18n.translate( + 'xpack.apm.settings.customizeUI.customActions.emptyPromptText', + { + defaultMessage: + "Let's change that! You can add custom actions to the Actions context menu by the trace and error details for each service. This could be linking to a Kibana dashboard or going to your organization's support portal" + } + )} +

+ + } + actions={ + + {i18n.translate( + 'xpack.apm.settings.customizeUI.customActions.createCustomAction', + { defaultMessage: 'Create custom action' } + )} + + } + /> + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/Title.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/Title.tsx new file mode 100644 index 0000000000000..d7f90e0919733 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/Title.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +export const Title = () => ( + + + + + +

+ {i18n.translate('xpack.apm.settings.customizeUI.customActions', { + defaultMessage: 'Custom actions' + })} +

+
+ + + + +
+
+
+
+); diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/__test__/CustomActions.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/__test__/CustomActions.test.tsx new file mode 100644 index 0000000000000..970de66c64a9a --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/__test__/CustomActions.test.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { fireEvent, render } from '@testing-library/react'; +import { CustomActionsOverview } from '../'; +import { expectTextsInDocument } from '../../../../../../utils/testHelpers'; +import * as hooks from '../../../../../../hooks/useFetcher'; + +describe('CustomActions', () => { + afterEach(() => jest.restoreAllMocks()); + + describe('empty prompt', () => { + it('shows when any actions are available', () => { + // TODO: mock return items + const component = render(); + expectTextsInDocument(component, ['No actions found.']); + }); + it('opens flyout when click to create new action', () => { + spyOn(hooks, 'useFetcher').and.returnValue({ + data: [], + status: 'success' + }); + const { queryByText, getByText } = render(); + expect(queryByText('Service')).not.toBeInTheDocument(); + fireEvent.click(getByText('Create custom action')); + expect(queryByText('Service')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/index.tsx new file mode 100644 index 0000000000000..ae2972f251fc2 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomActionsOverview/index.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiPanel, EuiSpacer } from '@elastic/eui'; +import { isEmpty } from 'lodash'; +import React, { useState } from 'react'; +import { ManagedTable } from '../../../../shared/ManagedTable'; +import { Title } from './Title'; +import { EmptyPrompt } from './EmptyPrompt'; +import { CustomActionsFlyout } from './CustomActionsFlyout'; + +export const CustomActionsOverview = () => { + const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); + + // TODO: change it to correct fields fetched from ES + const columns = [ + { + field: 'actionName', + name: 'Action Name', + truncateText: true + }, + { + field: 'serviceName', + name: 'Service Name' + }, + { + field: 'environment', + name: 'Environment' + }, + { + field: 'lastUpdate', + name: 'Last update' + }, + { + field: 'actions', + name: 'Actions' + } + ]; + + // TODO: change to items fetched from ES. + const items: object[] = []; + + const onCloseFlyout = () => { + setIsFlyoutOpen(false); + }; + + const onCreateCustomActionClick = () => { + setIsFlyoutOpen(true); + }; + + return ( + <> + + + <EuiSpacer size="m" /> + {isFlyoutOpen && <CustomActionsFlyout onClose={onCloseFlyout} />} + {isEmpty(items) ? ( + <EmptyPrompt onCreateCustomActionClick={onCreateCustomActionClick} /> + ) : ( + <ManagedTable + items={items} + columns={columns} + initialPageSize={25} + initialSortField="occurrenceCount" + initialSortDirection="desc" + sortItems={false} + /> + )} + </EuiPanel> + </> + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx new file mode 100644 index 0000000000000..17a4b2f847679 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx @@ -0,0 +1,26 @@ +/* + * 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 { EuiTitle, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { CustomActionsOverview } from './CustomActionsOverview'; + +export const CustomizeUI = () => { + return ( + <> + <EuiTitle size="l"> + <h1> + {i18n.translate('xpack.apm.settings.customizeUI', { + defaultMessage: 'Customize UI' + })} + </h1> + </EuiTitle> + <EuiSpacer size="l" /> + <CustomActionsOverview /> + </> + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx index f3be5abe4d48b..eef386731c5c3 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx @@ -39,20 +39,34 @@ export const Settings: React.FC = props => { id: 0, items: [ { - name: 'Agent Configuration', + name: i18n.translate( + 'xpack.apm.settings.agentConfiguration', + { + defaultMessage: 'Agent Configuration' + } + ), id: '1', // @ts-ignore href: getAPMHref('/settings/agent-configuration', search), - // @ts-ignore isSelected: pathname === '/settings/agent-configuration' }, { - name: 'Indices', + name: i18n.translate('xpack.apm.settings.indices', { + defaultMessage: 'Indices' + }), id: '2', // @ts-ignore href: getAPMHref('/settings/apm-indices', search), - // @ts-ignore isSelected: pathname === '/settings/apm-indices' + }, + { + name: i18n.translate('xpack.apm.settings.customizeUI', { + defaultMessage: 'Customize UI' + }), + id: '3', + // @ts-ignore + href: getAPMHref('/settings/customize-ui', search), + isSelected: pathname === '/settings/customize-ui' } ] } diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.test.tsx index ac728e72fa877..286af610707e1 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.test.tsx @@ -16,6 +16,44 @@ describe('toQuery', () => { }); describe('fromQuery', () => { + it('should not encode the following characters', () => { + expect( + fromQuery({ + a: true, + b: 5000, + c: ':' + }) + ).toEqual('a=true&b=5000&c=:'); + }); + + it('should encode the following characters', () => { + expect( + fromQuery({ + a: '@', + b: '.', + c: ';', + d: ' ' + }) + ).toEqual('a=%40&b=.&c=%3B&d=%20'); + }); + + it('should handle null and undefined', () => { + expect( + fromQuery({ + a: undefined, + b: null + }) + ).toEqual('a=&b='); + }); + + it('should handle arrays', () => { + expect( + fromQuery({ + arr: ['a', 'b'] + }) + ).toEqual('arr=a%2Cb'); + }); + it('should parse object to string', () => { expect( fromQuery({ diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts b/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts index 357ea23d522a0..36465309b736e 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts @@ -4,19 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import qs from 'querystring'; +import { parse, stringify } from 'query-string'; import { LocalUIFilterName } from '../../../../server/lib/ui_filters/local_ui_filters/config'; +import { url } from '../../../../../../../../src/plugins/kibana_utils/public'; export function toQuery(search?: string): APMQueryParamsRaw { - return search ? qs.parse(search.slice(1)) : {}; + return search ? parse(search.slice(1), { sort: false }) : {}; } export function fromQuery(query: Record<string, any>) { - return qs.stringify(query, undefined, undefined, { - encodeURIComponent: (value: string) => { - return encodeURIComponent(value).replace(/%3A/g, ':'); - } - }); + const encodedQuery = url.encodeQuery(query, value => + encodeURIComponent(value).replace(/%3A/g, ':') + ); + + return stringify(encodedQuery, { sort: false, encode: false }); } export type APMQueryParams = { diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/ServiceSection.tsx b/x-pack/legacy/plugins/apm/public/components/shared/ServiceForm/index.tsx similarity index 76% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/ServiceSection.tsx rename to x-pack/legacy/plugins/apm/public/components/shared/ServiceForm/index.tsx index 513dfceaa3ae2..58a203bded715 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/ServiceSection.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/ServiceForm/index.tsx @@ -7,32 +7,32 @@ import { EuiTitle, EuiSpacer, EuiFormRow, EuiText } from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; -import { SelectWithPlaceholder } from '../../../../shared/SelectWithPlaceholder'; -import { useFetcher } from '../../../../../hooks/useFetcher'; import { - getOptionLabel, - omitAllOption -} from '../../../../../../common/agent_configuration_constants'; + omitAllOption, + getOptionLabel +} from '../../../../common/agent_configuration_constants'; +import { useFetcher } from '../../../hooks/useFetcher'; +import { SelectWithPlaceholder } from '../SelectWithPlaceholder'; const SELECT_PLACEHOLDER_LABEL = `- ${i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.serviceSection.selectPlaceholder', + 'xpack.apm.settings.agentConf.flyOut.serviceForm.selectPlaceholder', { defaultMessage: 'Select' } )} -`; interface Props { isReadOnly: boolean; serviceName: string; - setServiceName: (env: string) => void; + onServiceNameChange: (env: string) => void; environment: string; - setEnvironment: (env: string) => void; + onEnvironmentChange: (env: string) => void; } -export function ServiceSection({ +export function ServiceForm({ isReadOnly, serviceName, - setServiceName, + onServiceNameChange, environment, - setEnvironment + onEnvironmentChange }: Props) { const { data: serviceNames = [], status: serviceNamesStatus } = useFetcher( callApmApi => { @@ -60,7 +60,7 @@ export function ServiceSection({ ); const ALREADY_CONFIGURED_TRANSLATED = i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.serviceSection.alreadyConfiguredOption', + 'xpack.apm.settings.agentConf.flyOut.serviceForm.alreadyConfiguredOption', { defaultMessage: 'already configured' } ); @@ -83,7 +83,7 @@ export function ServiceSection({ <EuiTitle size="xs"> <h3> {i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.serviceSection.title', + 'xpack.apm.settings.agentConf.flyOut.serviceForm.title', { defaultMessage: 'Service' } )} </h3> @@ -93,13 +93,13 @@ export function ServiceSection({ <EuiFormRow label={i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.serviceSection.serviceNameSelectLabel', + 'xpack.apm.settings.agentConf.flyOut.serviceForm.serviceNameSelectLabel', { defaultMessage: 'Name' } )} helpText={ !isReadOnly && i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.serviceSection.serviceNameSelectHelpText', + 'xpack.apm.settings.agentConf.flyOut.serviceForm.serviceNameSelectHelpText', { defaultMessage: 'Choose the service you want to configure.' } ) } @@ -115,8 +115,8 @@ export function ServiceSection({ disabled={serviceNamesStatus === 'loading'} onChange={e => { e.preventDefault(); - setServiceName(e.target.value); - setEnvironment(''); + onServiceNameChange(e.target.value); + onEnvironmentChange(''); }} /> )} @@ -124,13 +124,13 @@ export function ServiceSection({ <EuiFormRow label={i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.serviceSection.serviceEnvironmentSelectLabel', + 'xpack.apm.settings.agentConf.flyOut.serviceForm.serviceEnvironmentSelectLabel', { defaultMessage: 'Environment' } )} helpText={ !isReadOnly && i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.serviceSection.serviceEnvironmentSelectHelpText', + 'xpack.apm.settings.agentConf.flyOut.serviceForm.serviceEnvironmentSelectHelpText', { defaultMessage: 'Only a single environment per configuration is supported.' @@ -149,7 +149,7 @@ export function ServiceSection({ disabled={!serviceName || environmentStatus === 'loading'} onChange={e => { e.preventDefault(); - setEnvironment(e.target.value); + onEnvironmentChange(e.target.value); }} /> )} 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 ea1b825c856ad..2f1c3a245e906 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 @@ -4,7 +4,7 @@ exports[`Stackframe when stackframe has source lines should render correctly 1`] .c0 { color: #69707d; padding: 8px 0; - font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace; + font-family: "Roboto Mono",Consolas,Menlo,Courier,monospace; font-size: 14px; } @@ -97,7 +97,7 @@ exports[`Stackframe when stackframe has source lines should render correctly 1`] .c2 { position: relative; - font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace; + font-family: "Roboto Mono",Consolas,Menlo,Courier,monospace; font-size: 14px; border: 1px solid #d3dae6; border-radius: 4px; diff --git a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx index ac8f40a29d93a..d2202fff996b1 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx +++ b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx @@ -9,10 +9,10 @@ import { i18n } from '@kbn/i18n'; import { IHttpFetchError } from 'src/core/public'; import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; import { LoadingIndicatorContext } from '../context/LoadingIndicatorContext'; -import { useComponentId } from './useComponentId'; import { APMClient } from '../services/rest/createCallApmApi'; import { useCallApmApi } from './useCallApmApi'; import { useApmPluginContext } from './useApmPluginContext'; +import { useLoadingIndicator } from './useLoadingIndicator'; export enum FETCH_STATUS { LOADING = 'loading', @@ -44,7 +44,7 @@ export function useFetcher<TReturn>( ): Result<InferResponseType<TReturn>> & { refetch: () => void } { const { notifications } = useApmPluginContext().core; const { preservePreviousData = true } = options; - const id = useComponentId(); + const { setIsLoading } = useLoadingIndicator(); const callApmApi = useCallApmApi(); @@ -67,7 +67,7 @@ export function useFetcher<TReturn>( return; } - dispatchStatus({ id, isLoading: true }); + setIsLoading(true); setResult(prevResult => ({ data: preservePreviousData ? prevResult.data : undefined, // preserve data from previous state while loading next state @@ -78,7 +78,7 @@ export function useFetcher<TReturn>( try { const data = await promise; if (!didCancel) { - dispatchStatus({ id, isLoading: false }); + setIsLoading(false); setResult({ data, status: FETCH_STATUS.SUCCESS, @@ -109,7 +109,7 @@ export function useFetcher<TReturn>( </div> ) }); - dispatchStatus({ id, isLoading: false }); + setIsLoading(false); setResult({ data: undefined, status: FETCH_STATUS.FAILURE, @@ -122,15 +122,15 @@ export function useFetcher<TReturn>( doFetch(); return () => { - dispatchStatus({ id, isLoading: false }); + setIsLoading(false); didCancel = true; }; /* eslint-disable react-hooks/exhaustive-deps */ }, [ counter, - id, preservePreviousData, dispatchStatus, + setIsLoading, ...fnDeps /* eslint-enable react-hooks/exhaustive-deps */ ]); diff --git a/x-pack/legacy/plugins/apm/public/hooks/useLoadingIndicator.ts b/x-pack/legacy/plugins/apm/public/hooks/useLoadingIndicator.ts new file mode 100644 index 0000000000000..5da6bf70e1700 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/hooks/useLoadingIndicator.ts @@ -0,0 +1,28 @@ +/* + * 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 { useContext, useMemo, useEffect } from 'react'; +import { LoadingIndicatorContext } from '../context/LoadingIndicatorContext'; +import { useComponentId } from './useComponentId'; + +export function useLoadingIndicator() { + const { dispatchStatus } = useContext(LoadingIndicatorContext); + const id = useComponentId(); + + useEffect(() => { + return () => { + dispatchStatus({ id, isLoading: false }); + }; + }, [dispatchStatus, id]); + + return useMemo(() => { + return { + setIsLoading: (loading: boolean) => { + dispatchStatus({ id, isLoading: loading }); + } + }; + }, [dispatchStatus, id]); +} diff --git a/x-pack/legacy/plugins/apm/public/style/variables.ts b/x-pack/legacy/plugins/apm/public/style/variables.ts index 8746e05ef3d5e..5a1dcc60114d3 100644 --- a/x-pack/legacy/plugins/apm/public/style/variables.ts +++ b/x-pack/legacy/plugins/apm/public/style/variables.ts @@ -31,9 +31,8 @@ export function pct(value: number): string { export const borderRadius = '4px'; // Fonts -export const fontFamily = '"Open Sans", Helvetica, Arial, sans-serif'; export const fontFamilyCode = - '"SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace'; + '"Roboto Mono", Consolas, Menlo, Courier, monospace'; // Font sizes export const fontSize = '14px'; diff --git a/x-pack/legacy/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts b/x-pack/legacy/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts index bb5fcabb6c9d7..92452c6dafff0 100644 --- a/x-pack/legacy/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts +++ b/x-pack/legacy/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts @@ -21,6 +21,7 @@ import { TRACE_ID } from '../../../common/elasticsearch_fieldnames'; +const MAX_CONNECTIONS_PER_REQUEST = 1000; const MAX_TRACES_TO_INSPECT = 1000; export async function getTraceSampleIds({ @@ -34,16 +35,9 @@ export async function getTraceSampleIds({ environment?: string; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const isTop = !after; + const { start, end, client, indices } = setup; - const { start, end, client, indices, config } = setup; - - const rangeEnd = end; - const rangeStart = isTop - ? rangeEnd - config['xpack.apm.serviceMapInitialTimeRange'] - : start; - - const rangeQuery = { range: rangeFilter(rangeStart, rangeEnd) }; + const rangeQuery = { range: rangeFilter(start, end) }; const query = { bool: { @@ -71,10 +65,9 @@ export async function getTraceSampleIds({ query.bool.filter.push({ term: { [SERVICE_ENVIRONMENT]: environment } }); } - const afterObj = - after && after !== 'top' - ? { after: JSON.parse(Buffer.from(after, 'base64').toString()) } - : {}; + const afterObj = after + ? { after: JSON.parse(Buffer.from(after, 'base64').toString()) } + : {}; const params = { index: [indices['apm_oss.spanIndices']], @@ -84,7 +77,7 @@ export async function getTraceSampleIds({ aggs: { connections: { composite: { - size: 1000, + size: MAX_CONNECTIONS_PER_REQUEST, ...afterObj, sources: [ { [SERVICE_NAME]: { terms: { field: SERVICE_NAME } } }, @@ -119,6 +112,7 @@ export async function getTraceSampleIds({ trace_ids: { terms: { field: TRACE_ID, + size: 10, execution_hint: 'map' as const, // remove bias towards large traces by sorting on trace.id // which will be random-esque @@ -145,9 +139,11 @@ export async function getTraceSampleIds({ const receivedAfterKey = tracesSampleResponse.aggregations?.connections.after_key; - if (!after) { - nextAfter = 'top'; - } else if (receivedAfterKey) { + if ( + receivedAfterKey && + (tracesSampleResponse.aggregations?.connections.buckets.length ?? 0) >= + MAX_CONNECTIONS_PER_REQUEST + ) { nextAfter = Buffer.from(JSON.stringify(receivedAfterKey)).toString( 'base64' ); diff --git a/x-pack/legacy/plugins/beats_management/public/containers/with_url_state.tsx b/x-pack/legacy/plugins/beats_management/public/containers/with_url_state.tsx index 71e9163fe22e7..c8f756da985a7 100644 --- a/x-pack/legacy/plugins/beats_management/public/containers/with_url_state.tsx +++ b/x-pack/legacy/plugins/beats_management/public/containers/with_url_state.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { parse, stringify } from 'querystring'; +import { parse, stringify } from 'query-string'; import React from 'react'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import { FlatObject } from '../frontend_types'; @@ -31,7 +31,9 @@ export class WithURLStateComponent<URLState extends object> extends React.Compon > { private get URLState(): URLState { // slice because parse does not account for the initial ? in the search string - return parse(decodeURIComponent(this.props.history.location.search).substring(1)) as URLState; + return parse(decodeURIComponent(this.props.history.location.search).substring(1), { + sort: false, + }) as URLState; } private historyListener: (() => void) | null = null; @@ -63,10 +65,13 @@ export class WithURLStateComponent<URLState extends object> extends React.Compon newState = state; } - const search: string = stringify({ - ...pastState, - ...newState, - }); + const search: string = stringify( + { + ...pastState, + ...newState, + }, + { sort: false } + ); const newLocation = { ...this.props.history.location, diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.ts index f7f191a48de82..5adbf4ce66c13 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.ts +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.ts @@ -7,8 +7,8 @@ import rison from 'rison-node'; // @ts-ignore Untyped local. import { fetch } from '../../../../common/lib/fetch'; -import { getStartPlugins } from '../../../legacy'; import { CanvasWorkpad } from '../../../../types'; +import { url } from '../../../../../../../../src/plugins/kibana_utils/public'; // type of the desired pdf output (print or preserve_layout) const PDF_LAYOUT_TYPE = 'preserve_layout'; @@ -71,11 +71,10 @@ function getPdfUrlParts( export function getPdfUrl(...args: Arguments): string { const urlParts = getPdfUrlParts(...args); + const param = (key: string, val: any) => + url.encodeUriQuery(key, true) + (val === true ? '' : '=' + url.encodeUriQuery(val, true)); - return `${urlParts.createPdfUri}?${getStartPlugins().__LEGACY.QueryString.param( - 'jobParams', - urlParts.createPdfPayload.jobParams - )}`; + return `${urlParts.createPdfUri}?${param('jobParams', urlParts.createPdfPayload.jobParams)}`; } export function createPdf(...args: Arguments) { diff --git a/x-pack/legacy/plugins/canvas/public/legacy.ts b/x-pack/legacy/plugins/canvas/public/legacy.ts index c16bc124747c6..ea873e6f2296d 100644 --- a/x-pack/legacy/plugins/canvas/public/legacy.ts +++ b/x-pack/legacy/plugins/canvas/public/legacy.ts @@ -13,8 +13,6 @@ import { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; // eslint-d import { Storage } from '../../../../../src/plugins/kibana_utils/public'; // eslint-disable-line import/order // @ts-ignore Untyped Kibana Lib import { formatMsg } from '../../../../../src/plugins/kibana_legacy/public'; // eslint-disable-line import/order -// @ts-ignore Untyped Kibana Lib -import { QueryString } from 'ui/utils/query_string'; // eslint-disable-line import/order const shimCoreSetup = { ...npSetup.core, @@ -33,7 +31,6 @@ const shimStartPlugins: CanvasStartDeps = { absoluteToParsedUrl, // ToDo: Copy directly into canvas formatMsg, - QueryString, storage: Storage, // ToDo: Won't be a part of New Platform. Will need to handle internally trackSubUrlForApp: chrome.trackSubUrlForApp, diff --git a/x-pack/legacy/plugins/canvas/public/lib/app_state.ts b/x-pack/legacy/plugins/canvas/public/lib/app_state.ts index c93e505c595fd..d431202ba75a4 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/app_state.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/app_state.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import querystring from 'querystring'; +import { parse } from 'query-string'; import { get } from 'lodash'; // @ts-ignore untyped local import { getInitialState } from '../state/initial_state'; @@ -38,7 +38,7 @@ export function getDefaultAppState(): AppState { export function getCurrentAppState(): AppState { const history = historyProvider(getWindow()); const { search } = history.getLocation(); - const qs = !!search ? querystring.parse(search.replace(/^\?/, '')) : {}; + const qs = !!search ? parse(search.replace(/^\?/, ''), { sort: false }) : {}; const appState = assignAppState({}, qs); return appState; diff --git a/x-pack/legacy/plugins/canvas/public/lib/modify_url.ts b/x-pack/legacy/plugins/canvas/public/lib/modify_url.ts index 890138c41d7bf..d128dc432e9cf 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/modify_url.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/modify_url.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ParsedUrlQuery } from 'querystring'; +import { ParsedQuery } from 'query-string'; import { format as formatUrl, parse as parseUrl, UrlObject } from 'url'; /** @@ -20,7 +20,7 @@ export interface URLMeaningfulParts { protocol?: string | null; slashes?: boolean | null; port?: string | null; - query: ParsedUrlQuery; + query: ParsedQuery; } /** diff --git a/x-pack/legacy/plugins/canvas/public/plugin.tsx b/x-pack/legacy/plugins/canvas/public/plugin.tsx index a24fd758808ba..44731628cf653 100644 --- a/x-pack/legacy/plugins/canvas/public/plugin.tsx +++ b/x-pack/legacy/plugins/canvas/public/plugin.tsx @@ -43,7 +43,6 @@ export interface CanvasStartDeps { __LEGACY: { absoluteToParsedUrl: (url: string, basePath: string) => any; formatMsg: any; - QueryString: any; storage: typeof Storage; trackSubUrlForApp: Chrome['trackSubUrlForApp']; }; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/webpack.config.js b/x-pack/legacy/plugins/canvas/shareable_runtime/webpack.config.js index c711f9510a10b..0ce722eb90d43 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/webpack.config.js +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/webpack.config.js @@ -99,16 +99,19 @@ module.exports = { { loader: 'css-loader', options: { - modules: true, - localIdentName: '[name]__[local]___[hash:base64:5]', - camelCase: true, + modules: { + localIdentName: '[name]__[local]___[hash:base64:5]', + }, + localsConvention: 'camelCase', sourceMap: !isProd, }, }, { loader: 'postcss-loader', options: { - path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + config: { + path: path.resolve(KIBANA_ROOT, 'src/optimize/postcss.config.js'), + }, }, }, { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/query_params.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/query_params.js index f0871d62976ed..af462bfeffcf5 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/query_params.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/query_params.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { parse } from 'querystring'; +import { parse } from 'query-string'; export function extractQueryParams(queryString) { const hrefSplit = queryString.split('?'); @@ -12,5 +12,5 @@ export function extractQueryParams(queryString) { return {}; } - return parse(hrefSplit[1]); + return parse(hrefSplit[1], { sort: false }); } diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/routing.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/routing.js index bb4b4540b1922..487b1068794f9 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/routing.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/routing.js @@ -9,7 +9,7 @@ */ import { createLocation } from 'history'; -import { stringify } from 'querystring'; +import { stringify } from 'query-string'; import { APPS, BASE_PATH, BASE_PATH_REMOTE_CLUSTERS } from '../../../common/constants'; const isModifiedEvent = event => @@ -22,16 +22,7 @@ const queryParamsFromObject = (params, encodeParams = false) => { return; } - const paramsStr = stringify( - params, - '&', - '=', - encodeParams - ? {} - : { - encodeURIComponent: val => val, // Don't encode special chars - } - ); + const paramsStr = stringify(params, { sort: false, encode: encodeParams }); return `?${paramsStr}`; }; diff --git a/x-pack/legacy/plugins/graph/index.ts b/x-pack/legacy/plugins/graph/index.ts index 143d07cfdbd57..b2d6fd3957d64 100644 --- a/x-pack/legacy/plugins/graph/index.ts +++ b/x-pack/legacy/plugins/graph/index.ts @@ -44,14 +44,6 @@ export const graph: LegacyPluginInitializer = kibana => { }, init(server) { - server.injectUiAppVars('graph', () => { - const config = server.config(); - return { - graphSavePolicy: config.get('xpack.graph.savePolicy'), - canEditDrillDownUrls: config.get('xpack.graph.canEditDrillDownUrls'), - }; - }); - server.plugins.xpack_main.registerFeature({ id: 'graph', name: i18n.translate('xpack.graph.featureRegistry.graphFeatureName', { diff --git a/x-pack/legacy/plugins/graph/public/index.ts b/x-pack/legacy/plugins/graph/public/index.ts index d9854acb9332c..fb60a66fb28cc 100644 --- a/x-pack/legacy/plugins/graph/public/index.ts +++ b/x-pack/legacy/plugins/graph/public/index.ts @@ -7,9 +7,11 @@ import { npSetup, npStart } from 'ui/new_platform'; import { LicensingPluginSetup } from '../../../../plugins/licensing/public'; import { GraphPlugin } from './plugin'; +import { GraphSetup } from '../../../../plugins/graph/public'; type XpackNpSetupDeps = typeof npSetup.plugins & { licensing: LicensingPluginSetup; + graph: GraphSetup; }; (async () => { diff --git a/x-pack/legacy/plugins/graph/public/plugin.ts b/x-pack/legacy/plugins/graph/public/plugin.ts index b4ca4bf423181..4ccaf6b5dfa27 100644 --- a/x-pack/legacy/plugins/graph/public/plugin.ts +++ b/x-pack/legacy/plugins/graph/public/plugin.ts @@ -5,12 +5,19 @@ */ // NP type imports -import { CoreSetup, CoreStart, Plugin, SavedObjectsClientContract } from 'src/core/public'; +import { + AppMountParameters, + CoreSetup, + CoreStart, + Plugin, + SavedObjectsClientContract, +} from 'src/core/public'; import { Plugin as DataPlugin } from 'src/plugins/data/public'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { LicensingPluginSetup } from '../../../../plugins/licensing/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../src/plugins/navigation/public'; import { initAngularBootstrap } from '../../../../../src/plugins/kibana_legacy/public'; +import { GraphSetup } from '../../../../plugins/graph/public'; export interface GraphPluginStartDependencies { npData: ReturnType<DataPlugin['start']>; @@ -19,6 +26,7 @@ export interface GraphPluginStartDependencies { export interface GraphPluginSetupDependencies { licensing: LicensingPluginSetup; + graph: GraphSetup; } export class GraphPlugin implements Plugin { @@ -26,12 +34,13 @@ export class GraphPlugin implements Plugin { private npDataStart: ReturnType<DataPlugin['start']> | null = null; private savedObjectsClient: SavedObjectsClientContract | null = null; - setup(core: CoreSetup, { licensing }: GraphPluginSetupDependencies) { + setup(core: CoreSetup, { licensing, graph }: GraphPluginSetupDependencies) { initAngularBootstrap(); core.application.register({ id: 'graph', title: 'Graph', - mount: async ({ core: contextCore }, params) => { + mount: async (params: AppMountParameters) => { + const [coreStart] = await core.getStartServices(); const { renderApp } = await import('./application'); return renderApp({ ...params, @@ -41,18 +50,16 @@ export class GraphPlugin implements Plugin { savedObjectsClient: this.savedObjectsClient!, addBasePath: core.http.basePath.prepend, getBasePath: core.http.basePath.get, - canEditDrillDownUrls: core.injectedMetadata.getInjectedVar( - 'canEditDrillDownUrls' - ) as boolean, - graphSavePolicy: core.injectedMetadata.getInjectedVar('graphSavePolicy') as string, + canEditDrillDownUrls: graph.config.canEditDrillDownUrls, + graphSavePolicy: graph.config.savePolicy, storage: new Storage(window.localStorage), - capabilities: contextCore.application.capabilities.graph, - coreStart: contextCore, - chrome: contextCore.chrome, - config: contextCore.uiSettings, - toastNotifications: contextCore.notifications.toasts, + capabilities: coreStart.application.capabilities.graph, + coreStart, + chrome: coreStart.chrome, + config: coreStart.uiSettings, + toastNotifications: coreStart.notifications.toasts, indexPatterns: this.npDataStart!.indexPatterns, - overlays: contextCore.overlays, + overlays: coreStart.overlays, }); }, }); diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_results/analyze_in_ml_button.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_results/analyze_in_ml_button.tsx index 536dd24faa7c1..9fbba94407dc0 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_results/analyze_in_ml_button.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_results/analyze_in_ml_button.tsx @@ -8,10 +8,11 @@ import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { encode } from 'rison-node'; -import { QueryString } from 'ui/utils/query_string'; import url from 'url'; +import { stringify } from 'query-string'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { TimeRange } from '../../../../common/http_api/shared/time_range'; +import { url as urlUtils } from '../../../../../../../../src/plugins/kibana_utils/public'; export const AnalyzeInMlButton: React.FunctionComponent<{ jobId: string; @@ -61,7 +62,7 @@ const getOverallAnomalyExplorerLink = (pathname: string, jobId: string, timeRang }, }); - const hash = `/explorer?${QueryString.encode({ _g })}`; + const hash = `/explorer?${stringify(urlUtils.encodeQuery({ _g }), { encode: false })}`; return url.format({ pathname, @@ -94,7 +95,10 @@ const getPartitionSpecificSingleMetricViewerLink = ( }, }); - const hash = `/timeseriesexplorer?${QueryString.encode({ _g, _a })}`; + const hash = `/timeseriesexplorer?${stringify(urlUtils.encodeQuery({ _g, _a }), { + sort: false, + encode: false, + })}`; return url.format({ pathname, diff --git a/x-pack/legacy/plugins/infra/public/containers/with_state_from_location.tsx b/x-pack/legacy/plugins/infra/public/containers/with_state_from_location.tsx index ec6345c49c303..6f7baf6b98b62 100644 --- a/x-pack/legacy/plugins/infra/public/containers/with_state_from_location.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/with_state_from_location.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse, stringify } from 'query-string'; import { Location } from 'history'; import omit from 'lodash/fp/omit'; -import { parse as parseQueryString, stringify as stringifyQueryString } from 'querystring'; import React from 'react'; import { RouteComponentProps, withRouter } from 'react-router-dom'; // eslint-disable-next-line @typescript-eslint/camelcase @@ -102,7 +102,7 @@ const encodeRisonAppState = (state: AnyObject) => ({ export const mapRisonAppLocationToState = <State extends {}>( mapState: (risonAppState: AnyObject) => State = (state: AnyObject) => state as State ) => (location: Location): State => { - const queryValues = parseQueryString(location.search.substring(1)); + const queryValues = parse(location.search.substring(1), { sort: false }); const decodedState = decodeRisonAppState(queryValues); return mapState(decodedState); }; @@ -110,17 +110,20 @@ export const mapRisonAppLocationToState = <State extends {}>( export const mapStateToRisonAppLocation = <State extends {}>( mapState: (state: State) => AnyObject = (state: State) => state ) => (state: State, location: Location): Location => { - const previousQueryValues = parseQueryString(location.search.substring(1)); + const previousQueryValues = parse(location.search.substring(1), { sort: false }); const previousState = decodeRisonAppState(previousQueryValues); const encodedState = encodeRisonAppState({ ...previousState, ...mapState(state), }); - const newQueryValues = stringifyQueryString({ - ...previousQueryValues, - ...encodedState, - }); + const newQueryValues = stringify( + { + ...previousQueryValues, + ...encodedState, + }, + { sort: false } + ); return { ...location, search: `?${newQueryValues}`, diff --git a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_logs.test.tsx b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_logs.test.tsx index a418be01d1ed2..e9ec053f8c609 100644 --- a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_logs.test.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_logs.test.tsx @@ -19,7 +19,7 @@ describe('RedirectToLogs component', () => { expect(component).toMatchInlineSnapshot(` <Redirect - to="/logs/stream?logFilter=(expression:'',kind:kuery)&logPosition=(position:(tiebreaker:0,time:1550671089404),streamLive:!f)&sourceId=default" + to="/logs/stream?sourceId=default&logPosition=(position:(tiebreaker:0,time:1550671089404),streamLive:!f)&logFilter=(expression:'',kind:kuery)" /> `); }); @@ -33,7 +33,7 @@ describe('RedirectToLogs component', () => { expect(component).toMatchInlineSnapshot(` <Redirect - to="/logs/stream?logFilter=(expression:'FILTER_FIELD:FILTER_VALUE',kind:kuery)&logPosition=(position:(tiebreaker:0,time:1550671089404),streamLive:!f)&sourceId=default" + to="/logs/stream?sourceId=default&logPosition=(position:(tiebreaker:0,time:1550671089404),streamLive:!f)&logFilter=(expression:'FILTER_FIELD:FILTER_VALUE',kind:kuery)" /> `); }); @@ -45,7 +45,7 @@ describe('RedirectToLogs component', () => { expect(component).toMatchInlineSnapshot(` <Redirect - to="/logs/stream?logFilter=(expression:'',kind:kuery)&sourceId=SOME-OTHER-SOURCE" + to="/logs/stream?sourceId=SOME-OTHER-SOURCE&logFilter=(expression:'',kind:kuery)" /> `); }); diff --git a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx index 2d1f3a32988aa..1e97072cac109 100644 --- a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx @@ -35,7 +35,7 @@ describe('RedirectToNodeLogs component', () => { expect(component).toMatchInlineSnapshot(` <Redirect - to="/logs?logFilter=(expression:'HOST_FIELD:%20HOST_NAME',kind:kuery)&sourceId=default" + to="/logs?sourceId=default&logFilter=(expression:'HOST_FIELD:%20HOST_NAME',kind:kuery)" /> `); }); @@ -47,7 +47,7 @@ describe('RedirectToNodeLogs component', () => { expect(component).toMatchInlineSnapshot(` <Redirect - to="/logs?logFilter=(expression:'CONTAINER_FIELD:%20CONTAINER_ID',kind:kuery)&sourceId=default" + to="/logs?sourceId=default&logFilter=(expression:'CONTAINER_FIELD:%20CONTAINER_ID',kind:kuery)" /> `); }); @@ -59,7 +59,7 @@ describe('RedirectToNodeLogs component', () => { expect(component).toMatchInlineSnapshot(` <Redirect - to="/logs?logFilter=(expression:'POD_FIELD:%20POD_ID',kind:kuery)&sourceId=default" + to="/logs?sourceId=default&logFilter=(expression:'POD_FIELD:%20POD_ID',kind:kuery)" /> `); }); @@ -73,7 +73,7 @@ describe('RedirectToNodeLogs component', () => { expect(component).toMatchInlineSnapshot(` <Redirect - to="/logs?logFilter=(expression:'HOST_FIELD:%20HOST_NAME',kind:kuery)&logPosition=(position:(tiebreaker:0,time:1550671089404),streamLive:!f)&sourceId=default" + to="/logs?sourceId=default&logPosition=(position:(tiebreaker:0,time:1550671089404),streamLive:!f)&logFilter=(expression:'HOST_FIELD:%20HOST_NAME',kind:kuery)" /> `); }); @@ -89,7 +89,7 @@ describe('RedirectToNodeLogs component', () => { expect(component).toMatchInlineSnapshot(` <Redirect - to="/logs?logFilter=(expression:'(HOST_FIELD:%20HOST_NAME)%20and%20(FILTER_FIELD:FILTER_VALUE)',kind:kuery)&logPosition=(position:(tiebreaker:0,time:1550671089404),streamLive:!f)&sourceId=default" + to="/logs?sourceId=default&logPosition=(position:(tiebreaker:0,time:1550671089404),streamLive:!f)&logFilter=(expression:'(HOST_FIELD:%20HOST_NAME)%20and%20(FILTER_FIELD:FILTER_VALUE)',kind:kuery)" /> `); }); @@ -103,7 +103,7 @@ describe('RedirectToNodeLogs component', () => { expect(component).toMatchInlineSnapshot(` <Redirect - to="/logs?logFilter=(expression:'HOST_FIELD:%20HOST_NAME',kind:kuery)&sourceId=SOME-OTHER-SOURCE" + to="/logs?sourceId=SOME-OTHER-SOURCE&logFilter=(expression:'HOST_FIELD:%20HOST_NAME',kind:kuery)" /> `); }); diff --git a/x-pack/legacy/plugins/infra/public/utils/url_state.tsx b/x-pack/legacy/plugins/infra/public/utils/url_state.tsx index 66bb4308d1d16..58835715fe55c 100644 --- a/x-pack/legacy/plugins/infra/public/utils/url_state.tsx +++ b/x-pack/legacy/plugins/infra/public/utils/url_state.tsx @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse, stringify } from 'query-string'; import { History, Location } from 'history'; import throttle from 'lodash/fp/throttle'; import React from 'react'; import { Route, RouteProps } from 'react-router-dom'; import { decode, encode, RisonValue } from 'rison-node'; - -import { QueryString } from 'ui/utils/query_string'; +import { url } from '../../../../../../src/plugins/kibana_utils/public'; interface UrlStateContainerProps<UrlState> { urlState: UrlState | undefined; @@ -145,7 +145,9 @@ const encodeRisonUrlState = (state: any) => encode(state); export const getQueryStringFromLocation = (location: Location) => location.search.substring(1); export const getParamFromQueryString = (queryString: string, key: string): string | undefined => { - const queryParam = QueryString.decode(queryString)[key]; + const parsedQueryString: Record<string, any> = parse(queryString, { sort: false }); + const queryParam = parsedQueryString[key]; + return Array.isArray(queryParam) ? queryParam[0] : queryParam; }; @@ -153,13 +155,17 @@ export const replaceStateKeyInQueryString = <UrlState extends any>( stateKey: string, urlState: UrlState | undefined ) => (queryString: string) => { - const previousQueryValues = QueryString.decode(queryString); + const previousQueryValues = parse(queryString, { sort: false }); const encodedUrlState = typeof urlState !== 'undefined' ? encodeRisonUrlState(urlState) : undefined; - return QueryString.encode({ - ...previousQueryValues, - [stateKey]: encodedUrlState, - }); + + return stringify( + url.encodeQuery({ + ...previousQueryValues, + [stateKey]: encodedUrlState, + }), + { sort: false, encode: false } + ); }; const replaceQueryStringInLocation = (location: Location, queryString: string): Location => { diff --git a/x-pack/legacy/plugins/infra/public/utils/use_url_state.ts b/x-pack/legacy/plugins/infra/public/utils/use_url_state.ts index 79a5d552bcd78..284af62e52fbb 100644 --- a/x-pack/legacy/plugins/infra/public/utils/use_url_state.ts +++ b/x-pack/legacy/plugins/infra/public/utils/use_url_state.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse, stringify } from 'query-string'; import { Location } from 'history'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { decode, encode, RisonValue } from 'rison-node'; -import { QueryString } from 'ui/utils/query_string'; +import { url } from '../../../../../../src/plugins/kibana_utils/public'; import { useHistory } from './history_context'; @@ -84,7 +85,7 @@ export const useUrlState = <State>({ return [state, setState] as [typeof state, typeof setState]; }; -const decodeRisonUrlState = (value: string | undefined): RisonValue | undefined => { +const decodeRisonUrlState = (value: string | undefined | null): RisonValue | undefined => { try { return value ? decode(value) : undefined; } catch (error) { @@ -99,8 +100,10 @@ const encodeRisonUrlState = (state: any) => encode(state); const getQueryStringFromLocation = (location: Location) => location.search.substring(1); -const getParamFromQueryString = (queryString: string, key: string): string | undefined => { - const queryParam = QueryString.decode(queryString)[key]; +const getParamFromQueryString = (queryString: string, key: string) => { + const parsedQueryString = parse(queryString, { sort: false }); + const queryParam = parsedQueryString[key]; + return Array.isArray(queryParam) ? queryParam[0] : queryParam; }; @@ -108,13 +111,17 @@ export const replaceStateKeyInQueryString = <UrlState extends any>( stateKey: string, urlState: UrlState | undefined ) => (queryString: string) => { - const previousQueryValues = QueryString.decode(queryString); + const previousQueryValues = parse(queryString, { sort: false }); const encodedUrlState = typeof urlState !== 'undefined' ? encodeRisonUrlState(urlState) : undefined; - return QueryString.encode({ - ...previousQueryValues, - [stateKey]: encodedUrlState, - }); + + return stringify( + url.encodeQuery({ + ...previousQueryValues, + [stateKey]: encodedUrlState, + }), + { sort: false, encode: false } + ); }; const replaceQueryStringInLocation = (location: Location, queryString: string): Location => { diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx index 99926c646da22..374e3270b3d45 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx @@ -38,7 +38,10 @@ jest const { TopNavMenu } = npStart.plugins.navigation.ui; -const waitForPromises = () => new Promise(resolve => setTimeout(resolve)); +const waitForPromises = async () => + act(async () => { + await new Promise(resolve => setTimeout(resolve)); + }); function createMockFrame(): jest.Mocked<EditorFrameInstance> { return { @@ -220,6 +223,7 @@ describe('Lens App', () => { }); instance.setProps({ docId: '1234' }); + await waitForPromises(); expect(defaultArgs.core.chrome.setBreadcrumbs).toHaveBeenCalledWith([ @@ -373,8 +377,10 @@ describe('Lens App', () => { async function save({ initialDocId, addToDashboardMode, + lastKnownDoc = { expression: 'kibana 3' }, ...saveProps }: SaveProps & { + lastKnownDoc?: object; initialDocId?: string; addToDashboardMode?: boolean; }) { @@ -392,6 +398,7 @@ describe('Lens App', () => { state: { query: 'fake query', datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, + filters: [], }, }); (args.docStorage.save as jest.Mock).mockImplementation(async ({ id }) => ({ @@ -410,10 +417,12 @@ describe('Lens App', () => { } const onChange = frame.mount.mock.calls[0][1].onChange; - onChange({ - filterableIndexPatterns: [], - doc: ({ id: initialDocId, expression: 'kibana 3' } as unknown) as Document, - }); + act(() => + onChange({ + filterableIndexPatterns: [], + doc: { id: initialDocId, ...lastKnownDoc } as Document, + }) + ); instance.update(); @@ -441,10 +450,12 @@ describe('Lens App', () => { expect(getButton(instance).disableButton).toEqual(true); const onChange = frame.mount.mock.calls[0][1].onChange; - onChange({ - filterableIndexPatterns: [], - doc: ({ id: 'will save this', expression: 'valid expression' } as unknown) as Document, - }); + act(() => + onChange({ + filterableIndexPatterns: [], + doc: ({ id: 'will save this', expression: 'valid expression' } as unknown) as Document, + }) + ); instance.update(); expect(getButton(instance).disableButton).toEqual(true); }); @@ -482,10 +493,12 @@ describe('Lens App', () => { expect(getButton(instance).disableButton).toEqual(true); const onChange = frame.mount.mock.calls[0][1].onChange; - onChange({ - filterableIndexPatterns: [], - doc: ({ id: 'will save this', expression: 'valid expression' } as unknown) as Document, - }); + act(() => + onChange({ + filterableIndexPatterns: [], + doc: ({ id: 'will save this', expression: 'valid expression' } as unknown) as Document, + }) + ); instance.update(); expect(getButton(instance).disableButton).toEqual(false); @@ -559,10 +572,12 @@ describe('Lens App', () => { const instance = mount(<App {...args} />); const onChange = frame.mount.mock.calls[0][1].onChange; - onChange({ - filterableIndexPatterns: [], - doc: ({ id: undefined, expression: 'new expression' } as unknown) as Document, - }); + act(() => + onChange({ + filterableIndexPatterns: [], + doc: ({ id: undefined, expression: 'new expression' } as unknown) as Document, + }) + ); instance.update(); @@ -593,6 +608,38 @@ describe('Lens App', () => { expect(args.redirectTo).toHaveBeenCalledWith('aaa'); }); + + it('saves app filters and does not save pinned filters', async () => { + const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; + const field = ({ name: 'myfield' } as unknown) as IFieldType; + const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType; + + const unpinned = esFilters.buildExistsFilter(field, indexPattern); + const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern); + FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE); + await waitForPromises(); + + const { args } = await save({ + initialDocId: '1234', + newCopyOnSave: false, + newTitle: 'hello there2', + lastKnownDoc: { + expression: 'kibana 3', + state: { + filters: [pinned, unpinned], + }, + }, + }); + + expect(args.docStorage.save).toHaveBeenCalledWith({ + id: '1234', + title: 'hello there2', + expression: 'kibana 3', + state: { + filters: [unpinned], + }, + }); + }); }); }); @@ -658,10 +705,12 @@ describe('Lens App', () => { ); const onChange = frame.mount.mock.calls[0][1].onChange; - onChange({ - filterableIndexPatterns: [{ id: '1', title: 'newIndex' }], - doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, - }); + act(() => + onChange({ + filterableIndexPatterns: [{ id: '1', title: 'newIndex' }], + doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, + }) + ); await waitForPromises(); instance.update(); @@ -674,12 +723,15 @@ describe('Lens App', () => { ); // Do it again to verify that the dirty checking is done right - onChange({ - filterableIndexPatterns: [{ id: '2', title: 'second index' }], - doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, - }); + act(() => + onChange({ + filterableIndexPatterns: [{ id: '2', title: 'second index' }], + doc: ({ id: undefined, expression: 'valid expression' } as unknown) as Document, + }) + ); await waitForPromises(); + instance.update(); expect(TopNavMenu).toHaveBeenLastCalledWith( @@ -689,17 +741,18 @@ describe('Lens App', () => { {} ); }); - it('updates the editor frame when the user changes query or time in the search bar', () => { const args = defaultArgs; args.editorFrame = frame; const instance = mount(<App {...args} />); - instance.find(TopNavMenu).prop('onQuerySubmit')!({ - dateRange: { from: 'now-14d', to: 'now-7d' }, - query: { query: 'new', language: 'lucene' }, - }); + act(() => + instance.find(TopNavMenu).prop('onQuerySubmit')!({ + dateRange: { from: 'now-14d', to: 'now-7d' }, + query: { query: 'new', language: 'lucene' }, + }) + ); instance.update(); @@ -728,7 +781,9 @@ describe('Lens App', () => { const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; const field = ({ name: 'myfield' } as unknown) as IFieldType; - args.data.query.filterManager.setFilters([esFilters.buildExistsFilter(field, indexPattern)]); + act(() => + args.data.query.filterManager.setFilters([esFilters.buildExistsFilter(field, indexPattern)]) + ); instance.update(); @@ -852,10 +907,12 @@ describe('Lens App', () => { const instance = mount(<App {...args} />); - instance.find(TopNavMenu).prop('onQuerySubmit')!({ - dateRange: { from: 'now-14d', to: 'now-7d' }, - query: { query: 'new', language: 'lucene' }, - }); + act(() => + instance.find(TopNavMenu).prop('onQuerySubmit')!({ + dateRange: { from: 'now-14d', to: 'now-7d' }, + query: { query: 'new', language: 'lucene' }, + }) + ); const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; const field = ({ name: 'myfield' } as unknown) as IFieldType; @@ -865,10 +922,10 @@ describe('Lens App', () => { const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern); FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE); - args.data.query.filterManager.setFilters([pinned, unpinned]); + act(() => args.data.query.filterManager.setFilters([pinned, unpinned])); instance.update(); - instance.find(TopNavMenu).prop('onClearSavedQuery')!(); + act(() => instance.find(TopNavMenu).prop('onClearSavedQuery')!()); instance.update(); expect(frame.mount).toHaveBeenLastCalledWith( diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx index c901d4c0c1497..a212cb0a1a879 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -20,6 +20,7 @@ import { EditorFrameInstance } from '../types'; import { NativeRenderer } from '../native_renderer'; import { trackUiEvent } from '../lens_ui_telemetry'; import { + esFilters, Filter, IndexPattern as IndexPatternInstance, IndexPatternsContract, @@ -320,8 +321,22 @@ export function App({ {lastKnownDoc && state.isSaveModalVisible && ( <SavedObjectSaveModal onSave={props => { + const [pinnedFilters, appFilters] = _.partition( + lastKnownDoc.state?.filters, + esFilters.isFilterPinned + ); + const lastDocWithoutPinned = pinnedFilters?.length + ? { + ...lastKnownDoc, + state: { + ...lastKnownDoc.state, + filters: appFilters, + }, + } + : lastKnownDoc; + const doc = { - ...lastKnownDoc, + ...lastDocWithoutPinned, id: props.newCopyOnSave ? undefined : lastKnownDoc.id, title: props.newTitle, }; diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/index.ts b/x-pack/legacy/plugins/lens/public/app_plugin/index.ts index f75dce9b7507f..1460fdfef37e6 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/index.ts +++ b/x-pack/legacy/plugins/lens/public/app_plugin/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './plugin'; +export * from './app'; diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/_index.scss b/x-pack/legacy/plugins/lens/public/datatable_visualization/_index.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/_index.scss rename to x-pack/legacy/plugins/lens/public/datatable_visualization/_index.scss diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/_visualization.scss b/x-pack/legacy/plugins/lens/public/datatable_visualization/_visualization.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/_visualization.scss rename to x-pack/legacy/plugins/lens/public/datatable_visualization/_visualization.scss diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization/expression.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx rename to x-pack/legacy/plugins/lens/public/datatable_visualization/expression.tsx diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization/index.ts b/x-pack/legacy/plugins/lens/public/datatable_visualization/index.ts new file mode 100644 index 0000000000000..6dee47cc632c2 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup } from 'src/core/public'; +import { datatableVisualization } from './visualization'; +import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; +import { datatable, datatableColumns, getDatatableRenderer } from './expression'; +import { FormatFactory } from '../legacy_imports'; +import { EditorFrameSetup } from '../types'; + +export interface DatatableVisualizationPluginSetupPlugins { + expressions: ExpressionsSetup; + formatFactory: FormatFactory; + editorFrame: EditorFrameSetup; +} + +export class DatatableVisualization { + constructor() {} + + setup( + _core: CoreSetup | null, + { expressions, formatFactory, editorFrame }: DatatableVisualizationPluginSetupPlugins + ) { + expressions.registerFunction(() => datatableColumns); + expressions.registerFunction(() => datatable); + expressions.registerRenderer(() => getDatatableRenderer(formatFactory)); + editorFrame.registerVisualization(datatableVisualization); + } +} diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx similarity index 99% rename from x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx rename to x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx index cb9350226575c..0cba22170df1f 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { createMockDatasource } from '../editor_frame_plugin/mocks'; +import { createMockDatasource } from '../editor_frame_service/mocks'; import { DatatableVisualizationState, datatableVisualization, diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx rename to x-pack/legacy/plugins/lens/public/datatable_visualization/visualization.tsx diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/plugin.tsx deleted file mode 100644 index ed047f52ecc0f..0000000000000 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/plugin.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { npSetup } from 'ui/new_platform'; -import { CoreSetup } from 'src/core/public'; -import { getFormat, FormatFactory } from 'ui/visualize/loader/pipeline_helpers/utilities'; -import { datatableVisualization } from './visualization'; -import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; -import { datatable, datatableColumns, getDatatableRenderer } from './expression'; - -export interface DatatableVisualizationPluginSetupPlugins { - expressions: ExpressionsSetup; - // TODO this is a simulated NP plugin. - // Once field formatters are actually migrated, the actual shim can be used - fieldFormat: { - formatFactory: FormatFactory; - }; -} - -class DatatableVisualizationPlugin { - constructor() {} - - setup( - _core: CoreSetup | null, - { expressions, fieldFormat }: DatatableVisualizationPluginSetupPlugins - ) { - expressions.registerFunction(() => datatableColumns); - expressions.registerFunction(() => datatable); - expressions.registerRenderer(() => getDatatableRenderer(fieldFormat.formatFactory)); - - return datatableVisualization; - } - - stop() {} -} - -const plugin = new DatatableVisualizationPlugin(); - -export const datatableVisualizationSetup = () => - plugin.setup(npSetup.core, { - expressions: npSetup.plugins.expressions, - fieldFormat: { - formatFactory: getFormat, - }, - }); -export const datatableVisualizationStop = () => plugin.stop(); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/_index.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/_index.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/_index.scss rename to x-pack/legacy/plugins/lens/public/editor_frame_service/_index.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/__mocks__/suggestion_helpers.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/suggestion_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/__mocks__/suggestion_helpers.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/__mocks__/suggestion_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_chart_switch.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_chart_switch.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_chart_switch.scss rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_chart_switch.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_data_panel_wrapper.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_data_panel_wrapper.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_data_panel_wrapper.scss rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_data_panel_wrapper.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_expression_renderer.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_expression_renderer.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_expression_renderer.scss rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_expression_renderer.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_frame_layout.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_frame_layout.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_frame_layout.scss rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_frame_layout.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_suggestion_panel.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_suggestion_panel.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_suggestion_panel.scss rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_suggestion_panel.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_workspace_panel_wrapper.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_workspace_panel_wrapper.scss rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.test.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/data_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/data_panel_wrapper.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/frame_layout.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/frame_layout.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.scss b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.scss rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.scss diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/index.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/layer_actions.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/layer_actions.test.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.test.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/layer_actions.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/layer_actions.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.test.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/save.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.test.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel_wrapper.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.test.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable_factory.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts similarity index 78% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable_factory.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts index 00cde2ee3e04c..e8bb8914fa292 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable_factory.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; -import { Chrome } from 'ui/chrome'; - -import { capabilities } from 'ui/capabilities'; +import { + Capabilities, + HttpSetup, + RecursiveReadonly, + SavedObjectsClientContract, +} from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { IndexPatternsContract, IndexPattern } from '../../../../../../../src/plugins/data/public'; import { ReactExpressionRendererType } from '../../../../../../../src/plugins/expressions/public'; @@ -24,14 +26,12 @@ import { getEditPath } from '../../../../../../plugins/lens/common'; export class EmbeddableFactory extends AbstractEmbeddableFactory { type = DOC_TYPE; - private chrome: Chrome; - private indexPatternService: IndexPatternsContract; - private expressionRenderer: ReactExpressionRendererType; - constructor( - chrome: Chrome, - expressionRenderer: ReactExpressionRendererType, - indexPatternService: IndexPatternsContract + private coreHttp: HttpSetup, + private capabilities: RecursiveReadonly<Capabilities>, + private savedObjectsClient: SavedObjectsClientContract, + private expressionRenderer: ReactExpressionRendererType, + private indexPatternService: IndexPatternsContract ) { super({ savedObjectMetaData: { @@ -42,13 +42,10 @@ export class EmbeddableFactory extends AbstractEmbeddableFactory { getIconForSavedObject: () => 'lensApp', }, }); - this.chrome = chrome; - this.expressionRenderer = expressionRenderer; - this.indexPatternService = indexPatternService; } public isEditable() { - return capabilities.get().visualize.save as boolean; + return this.capabilities.visualize.save as boolean; } canCreateNew() { @@ -66,7 +63,7 @@ export class EmbeddableFactory extends AbstractEmbeddableFactory { input: Partial<EmbeddableInput> & { id: string }, parent?: IContainer ) { - const store = new SavedObjectIndexStore(this.chrome.getSavedObjectsClient()); + const store = new SavedObjectIndexStore(this.savedObjectsClient); const savedVis = await store.load(savedObjectId); const promises = savedVis.state.datasourceMetaData.filterableIndexPatterns.map( @@ -91,7 +88,7 @@ export class EmbeddableFactory extends AbstractEmbeddableFactory { this.expressionRenderer, { savedVis, - editUrl: this.chrome.addBasePath(getEditPath(savedObjectId)), + editUrl: this.coreHttp.basePath.prepend(getEditPath(savedObjectId)), editable: this.isEditable(), indexPatterns, }, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/expression_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/expression_wrapper.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/index.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/index.ts similarity index 90% rename from x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/index.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/index.ts index f75dce9b7507f..d6e96d74b766c 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/index.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './plugin'; +export * from './service'; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.test.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.test.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.ts similarity index 96% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.ts index 3c466522e1ebe..c5be5f524755d 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/merge_tables.ts @@ -11,7 +11,7 @@ import { KibanaDatatable, } from 'src/plugins/expressions/public'; import { LensMultiTable } from '../types'; -import { toAbsoluteDates } from '../indexpattern_plugin/auto_date'; +import { toAbsoluteDates } from '../indexpattern_datasource/auto_date'; interface MergeTables { layerIds: string[]; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx similarity index 95% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx index b4fc88cb074c7..cd121a1f96a2b 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/mocks.tsx @@ -10,12 +10,10 @@ import { ExpressionsSetup, ExpressionsStart, } from '../../../../../../src/plugins/expressions/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { embeddablePluginMock } from '../../../../../../src/plugins/embeddable/public/mocks'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { expressionsPluginMock } from '../../../../../../src/plugins/expressions/public/mocks'; import { DatasourcePublicAPI, FramePublicAPI, Datasource, Visualization } from '../types'; -import { EditorFrameSetupPlugins, EditorFrameStartPlugins } from './plugin'; +import { EditorFrameSetupPlugins, EditorFrameStartPlugins } from './service'; export function createMockVisualization(): jest.Mocked<Visualization> { return { @@ -108,9 +106,6 @@ export function createMockSetupDependencies() { data: {}, embeddable: embeddablePluginMock.createSetupContract(), expressions: expressionsPluginMock.createSetupContract(), - chrome: { - getSavedObjectsClient: () => {}, - }, } as unknown) as MockedSetupDependencies; } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx similarity index 66% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx index 7a6067dd5f23c..ef4b5f6d7b834 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EditorFramePlugin } from './plugin'; +import { EditorFrameService } from './service'; import { coreMock } from 'src/core/public/mocks'; import { MockedSetupDependencies, @@ -25,14 +25,14 @@ jest.mock('./embeddable/embeddable_factory', () => ({ EmbeddableFactory: class Mock {}, })); -describe('editor_frame plugin', () => { - let pluginInstance: EditorFramePlugin; +describe('editor_frame service', () => { + let pluginInstance: EditorFrameService; let mountpoint: Element; let pluginSetupDependencies: MockedSetupDependencies; let pluginStartDependencies: MockedStartDependencies; beforeEach(() => { - pluginInstance = new EditorFramePlugin(); + pluginInstance = new EditorFrameService(); mountpoint = document.createElement('div'); pluginSetupDependencies = createMockSetupDependencies(); pluginStartDependencies = createMockStartDependencies(); @@ -42,26 +42,28 @@ describe('editor_frame plugin', () => { mountpoint.remove(); }); - it('should create an editor frame instance which mounts and unmounts', () => { - expect(() => { - pluginInstance.setup(coreMock.createSetup(), pluginSetupDependencies); - const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); - const instance = publicAPI.createInstance({}); - instance.mount(mountpoint, { - onError: jest.fn(), - onChange: jest.fn(), - dateRange: { fromDate: '', toDate: '' }, - query: { query: '', language: 'lucene' }, - filters: [], - }); - instance.unmount(); - }).not.toThrowError(); + it('should create an editor frame instance which mounts and unmounts', async () => { + await expect( + (async () => { + pluginInstance.setup(coreMock.createSetup(), pluginSetupDependencies); + const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); + const instance = await publicAPI.createInstance({}); + instance.mount(mountpoint, { + onError: jest.fn(), + onChange: jest.fn(), + dateRange: { fromDate: '', toDate: '' }, + query: { query: '', language: 'lucene' }, + filters: [], + }); + instance.unmount(); + })() + ).resolves.toBeUndefined(); }); - it('should not have child nodes after unmount', () => { + it('should not have child nodes after unmount', async () => { pluginInstance.setup(coreMock.createSetup(), pluginSetupDependencies); const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); - const instance = publicAPI.createInstance({}); + const instance = await publicAPI.createInstance({}); instance.mount(mountpoint, { onError: jest.fn(), onChange: jest.fn(), diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx similarity index 70% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx index e914eb7d7784b..9a3d724705a1a 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx @@ -8,8 +8,6 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; import { CoreSetup, CoreStart } from 'src/core/public'; -import chrome, { Chrome } from 'ui/chrome'; -import { npSetup, npStart } from 'ui/new_platform'; import { ExpressionsSetup, ExpressionsStart, @@ -44,24 +42,35 @@ export interface EditorFrameStartPlugins { data: DataPublicPluginStart; embeddable: IEmbeddableStart; expressions: ExpressionsStart; - chrome: Chrome; } -export class EditorFramePlugin { +async function collectAsyncDefinitions<T extends { id: string }>( + definitions: Array<T | Promise<T>> +) { + const resolvedDefinitions = await Promise.all(definitions); + const definitionMap: Record<string, T> = {}; + resolvedDefinitions.forEach(definition => { + definitionMap[definition.id] = definition; + }); + + return definitionMap; +} + +export class EditorFrameService { constructor() {} - private readonly datasources: Record<string, Datasource> = {}; - private readonly visualizations: Record<string, Visualization> = {}; + private readonly datasources: Array<Datasource | Promise<Datasource>> = []; + private readonly visualizations: Array<Visualization | Promise<Visualization>> = []; public setup(core: CoreSetup, plugins: EditorFrameSetupPlugins): EditorFrameSetup { plugins.expressions.registerFunction(() => mergeTables); return { registerDatasource: datasource => { - this.datasources[datasource.id] = datasource as Datasource<unknown, unknown>; + this.datasources.push(datasource as Datasource<unknown, unknown>); }, registerVisualization: visualization => { - this.visualizations[visualization.id] = visualization as Visualization<unknown, unknown>; + this.visualizations.push(visualization as Visualization<unknown, unknown>); }, }; } @@ -70,27 +79,34 @@ export class EditorFramePlugin { plugins.embeddable.registerEmbeddableFactory( 'lens', new EmbeddableFactory( - plugins.chrome, + core.http, + core.application.capabilities, + core.savedObjects.client, plugins.expressions.ReactExpressionRenderer, plugins.data.indexPatterns ) ); - const createInstance = (): EditorFrameInstance => { + const createInstance = async (): Promise<EditorFrameInstance> => { let domElement: Element; + const [resolvedDatasources, resolvedVisualizations] = await Promise.all([ + collectAsyncDefinitions(this.datasources), + collectAsyncDefinitions(this.visualizations), + ]); + return { mount: (element, { doc, onError, dateRange, query, filters, savedQuery, onChange }) => { domElement = element; - const firstDatasourceId = Object.keys(this.datasources)[0]; - const firstVisualizationId = Object.keys(this.visualizations)[0]; + const firstDatasourceId = Object.keys(resolvedDatasources)[0]; + const firstVisualizationId = Object.keys(resolvedVisualizations)[0]; render( <I18nProvider> <EditorFrame data-test-subj="lnsEditorFrame" onError={onError} - datasourceMap={this.datasources} - visualizationMap={this.visualizations} + datasourceMap={resolvedDatasources} + visualizationMap={resolvedVisualizations} initialDatasourceId={getActiveDatasourceIdFromDoc(doc) || firstDatasourceId || null} initialVisualizationId={ (doc && doc.visualizationType) || firstVisualizationId || null @@ -120,27 +136,4 @@ export class EditorFramePlugin { createInstance, }; } - - public stop() { - return {}; - } } - -const editorFrame = new EditorFramePlugin(); - -export const editorFrameSetup = () => - editorFrame.setup(npSetup.core, { - data: npSetup.plugins.data, - embeddable: npSetup.plugins.embeddable, - expressions: npSetup.plugins.expressions, - }); - -export const editorFrameStart = () => - editorFrame.start(npStart.core, { - data: npStart.plugins.data, - embeddable: npStart.plugins.embeddable, - expressions: npStart.plugins.expressions, - chrome, - }); - -export const editorFrameStop = () => editorFrame.stop(); diff --git a/x-pack/legacy/plugins/lens/public/index.scss b/x-pack/legacy/plugins/lens/public/index.scss index f646b1ed0a9ae..496573f6a1c9a 100644 --- a/x-pack/legacy/plugins/lens/public/index.scss +++ b/x-pack/legacy/plugins/lens/public/index.scss @@ -7,9 +7,9 @@ @import './config_panel'; @import './app_plugin/index'; -@import './datatable_visualization_plugin/index'; +@import 'datatable_visualization/index'; @import './drag_drop/index'; -@import './editor_frame_plugin/index'; -@import './indexpattern_plugin/index'; -@import './xy_visualization_plugin/index'; -@import './metric_visualization_plugin/index'; +@import 'editor_frame_service/index'; +@import 'indexpattern_datasource/index'; +@import 'xy_visualization/index'; +@import 'metric_visualization/index'; diff --git a/x-pack/legacy/plugins/lens/public/index.ts b/x-pack/legacy/plugins/lens/public/index.ts index 9f4141dbcae7d..e49f648906af0 100644 --- a/x-pack/legacy/plugins/lens/public/index.ts +++ b/x-pack/legacy/plugins/lens/public/index.ts @@ -4,4 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { LensPlugin } from './plugin'; + export * from './types'; + +export const plugin = () => new LensPlugin(); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/__mocks__/loader.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/__mocks__/loader.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/__mocks__/state_helpers.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/__mocks__/state_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/__mocks__/state_helpers.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/__mocks__/state_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/__snapshots__/lens_field_icon.test.tsx.snap b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/__snapshots__/lens_field_icon.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/__snapshots__/lens_field_icon.test.tsx.snap rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/__snapshots__/lens_field_icon.test.tsx.snap diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_datapanel.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_datapanel.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/_datapanel.scss rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/_datapanel.scss diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_field_item.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_field_item.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/_field_item.scss rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/_field_item.scss diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/_index.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/_index.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/_index.scss rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/_index.scss diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/auto_date.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.test.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/auto_date.test.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/auto_date.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/auto_date.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/change_indexpattern.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_dimension_panel.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_dimension_panel.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_dimension_panel.scss rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_dimension_panel.scss diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_field_select.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_field_select.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_field_select.scss rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_field_select.scss diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_index.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_index.scss rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_index.scss diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_popover.scss b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_popover.scss rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/_popover.scss diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/bucket_nesting_editor.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/bucket_nesting_editor.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/bucket_nesting_editor.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/bucket_nesting_editor.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/index.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/index.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/index.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/document_field.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/document_field.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/document_field.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/document_field.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_icon.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_icon.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_icon.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_icon.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_icon.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/field_item.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/index.ts new file mode 100644 index 0000000000000..3ca6e3e1ef56e --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/index.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup } from 'src/core/public'; +import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; +import { getIndexPatternDatasource } from './indexpattern'; +import { renameColumns } from './rename_columns'; +import { autoDate } from './auto_date'; +import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; +import { + DataPublicPluginSetup, + DataPublicPluginStart, +} from '../../../../../../src/plugins/data/public'; +import { Datasource, EditorFrameSetup } from '../types'; + +export interface IndexPatternDatasourceSetupPlugins { + expressions: ExpressionsSetup; + data: DataPublicPluginSetup; + editorFrame: EditorFrameSetup; +} + +export interface IndexPatternDatasourceStartPlugins { + data: DataPublicPluginStart; +} + +export class IndexPatternDatasource { + constructor() {} + + setup( + core: CoreSetup<IndexPatternDatasourceStartPlugins>, + { expressions, editorFrame }: IndexPatternDatasourceSetupPlugins + ) { + expressions.registerFunction(renameColumns); + expressions.registerFunction(autoDate); + + editorFrame.registerDatasource( + core.getStartServices().then(([coreStart, { data }]) => + getIndexPatternDatasource({ + core: coreStart, + storage: new Storage(localStorage), + data, + }) + ) as Promise<Datasource> + ); + } +} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts similarity index 97% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index e7def3b9dbf2c..41be22f2c72ed 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import chromeMock from 'ui/chrome'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { SavedObjectsClientContract } from 'kibana/public'; import { getIndexPatternDatasource, IndexPatternColumn, uniqueLabels } from './indexpattern'; import { DatasourcePublicAPI, Operation, Datasource } from '../types'; import { coreMock } from 'src/core/public/mocks'; @@ -15,8 +13,6 @@ import { IndexPatternPersistedState, IndexPatternPrivateState } from './types'; jest.mock('./loader'); jest.mock('../id_generator'); -// chrome, notify, storage are used by ./plugin -jest.mock('ui/chrome'); // Contains old and new platform data plugins, used for interpreter and filter ratio jest.mock('ui/new_platform'); @@ -142,10 +138,8 @@ describe('IndexPattern Data Source', () => { beforeEach(() => { indexPatternDatasource = getIndexPatternDatasource({ - chrome: chromeMock, storage: {} as IStorageWrapper, core: coreMock.createStart(), - savedObjectsClient: {} as SavedObjectsClientContract, data: pluginsMock.createStart().data, }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx similarity index 94% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 2426d7fc14b5d..afb88d1af7951 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -8,7 +8,7 @@ import _ from 'lodash'; import React from 'react'; import { render } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; -import { CoreStart, SavedObjectsClientContract } from 'src/core/public'; +import { CoreStart } from 'src/core/public'; import { i18n } from '@kbn/i18n'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { @@ -21,7 +21,6 @@ import { import { loadInitialState, changeIndexPattern, changeLayerIndexPattern } from './loader'; import { toExpression } from './to_expression'; import { IndexPatternDimensionPanel } from './dimension_panel'; -import { IndexPatternDatasourceSetupPlugins } from './plugin'; import { IndexPatternDataPanel } from './datapanel'; import { getDatasourceSuggestionsForField, @@ -90,20 +89,16 @@ export function uniqueLabels(layers: Record<string, IndexPatternLayer>) { } export function getIndexPatternDatasource({ - chrome, core, storage, - savedObjectsClient, data, -}: Pick<IndexPatternDatasourceSetupPlugins, 'chrome'> & { - // Core start is being required here because it contains the savedObject client - // In the new platform, this plugin wouldn't be initialized until after setup +}: { core: CoreStart; storage: IStorageWrapper; - savedObjectsClient: SavedObjectsClientContract; data: ReturnType<DataPlugin['start']>; }) { - const uiSettings = chrome.getUiSettingsClient(); + const savedObjectsClient = core.savedObjects.client; + const uiSettings = core.uiSettings; const onIndexPatternLoadError = (err: Error) => core.notifications.toasts.addError(err, { title: i18n.translate('xpack.lens.indexPattern.indexPatternLoadError', { @@ -118,7 +113,7 @@ export function getIndexPatternDatasource({ async initialize(state?: IndexPatternPersistedState) { return loadInitialState({ state, - savedObjectsClient, + savedObjectsClient: await savedObjectsClient, defaultIndexPatternId: core.uiSettings.get('defaultIndex'), }); }, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/layerpanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/layerpanel.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/layerpanel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/layerpanel.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/layerpanel.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/lens_field_icon.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/lens_field_icon.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/lens_field_icon.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/lens_field_icon.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/lens_field_icon.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.test.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts similarity index 99% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts index 3ec4b4f4df2ce..ed3d8a91b366d 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts @@ -5,7 +5,6 @@ */ import _ from 'lodash'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { SavedObjectsClientContract, SavedObjectAttributes, HttpSetup } from 'src/core/public'; import { SimpleSavedObject } from 'src/core/public'; import { StateSetter } from '../types'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/mocks.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/mocks.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/mocks.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/mocks.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/__mocks__/index.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/__mocks__/index.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/__mocks__/index.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/cardinality.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/cardinality.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/column_types.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/column_types.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/column_types.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/count.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/count.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/index.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/index.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/metrics.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/metrics.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/metrics.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/index.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/index.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/index.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/index.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/operations.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/operations/operations.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/pure_helpers.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/pure_helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/pure_helpers.test.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/pure_helpers.test.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/pure_helpers.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/pure_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/pure_helpers.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/pure_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.test.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/rename_columns.test.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/rename_columns.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/rename_columns.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/state_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/state_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/to_expression.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/to_expression.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/types.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/types.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/types.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/types.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/utils.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/utils.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/utils.ts rename to x-pack/legacy/plugins/lens/public/indexpattern_datasource/utils.ts diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx deleted file mode 100644 index 11bc52fc48378..0000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { CoreSetup } from 'src/core/public'; -// The following dependencies on ui/* and src/legacy/core_plugins must be mocked when testing -import chrome, { Chrome } from 'ui/chrome'; -import { npSetup, npStart } from 'ui/new_platform'; -import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; -import { getIndexPatternDatasource } from './indexpattern'; -import { renameColumns } from './rename_columns'; -import { autoDate } from './auto_date'; -import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; - -// TODO these are intermediary types because interpreter is not typed yet -// They can get replaced by references to the real interfaces as soon as they -// are available - -export interface IndexPatternDatasourceSetupPlugins { - chrome: Chrome; - expressions: ExpressionsSetup; -} - -class IndexPatternDatasourcePlugin { - constructor() {} - - setup(core: CoreSetup, { expressions }: IndexPatternDatasourceSetupPlugins) { - expressions.registerFunction(renameColumns); - expressions.registerFunction(autoDate); - } - - stop() {} -} - -const plugin = new IndexPatternDatasourcePlugin(); - -export const indexPatternDatasourceSetup = () => { - plugin.setup(npSetup.core, { - chrome, - expressions: npSetup.plugins.expressions, - }); - - return getIndexPatternDatasource({ - core: npStart.core, - chrome, - storage: new Storage(localStorage), - savedObjectsClient: chrome.getSavedObjectsClient(), - data: npStart.plugins.data, - }); -}; -export const indexPatternDatasourceStop = () => plugin.stop(); diff --git a/x-pack/legacy/plugins/lens/public/legacy.ts b/x-pack/legacy/plugins/lens/public/legacy.ts index a39d73f187ece..8023bad34de66 100644 --- a/x-pack/legacy/plugins/lens/public/legacy.ts +++ b/x-pack/legacy/plugins/lens/public/legacy.ts @@ -5,15 +5,12 @@ */ import { npSetup, npStart } from 'ui/new_platform'; -import { start as dataShimStart } from '../../../../../src/legacy/core_plugins/data/public/legacy'; +import { getFormat } from './legacy_imports'; export * from './types'; -import { AppPlugin } from './app_plugin'; +import { plugin } from './index'; -const app = new AppPlugin(); -app.setup(npSetup.core, npSetup.plugins); -app.start(npStart.core, { - ...npStart.plugins, - dataShim: dataShimStart, -}); +const pluginInstance = plugin(); +pluginInstance.setup(npSetup.core, { ...npSetup.plugins, __LEGACY: { formatFactory: getFormat } }); +pluginInstance.start(npStart.core, npStart.plugins); diff --git a/x-pack/legacy/plugins/lens/public/legacy_imports.ts b/x-pack/legacy/plugins/lens/public/legacy_imports.ts new file mode 100644 index 0000000000000..9dcc22ddb1bb7 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/legacy_imports.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { getFormat, FormatFactory } from 'ui/visualize/loader/pipeline_helpers/utilities'; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/auto_scale.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization/auto_scale.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/auto_scale.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization/auto_scale.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/index.scss b/x-pack/legacy/plugins/lens/public/metric_visualization/index.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/index.scss rename to x-pack/legacy/plugins/lens/public/metric_visualization/index.scss diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization/index.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/index.ts new file mode 100644 index 0000000000000..217cc6902fc99 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/index.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup } from 'src/core/public'; +import { FormatFactory } from '../legacy_imports'; +import { metricVisualization } from './metric_visualization'; +import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; +import { metricChart, getMetricChartRenderer } from './metric_expression'; +import { EditorFrameSetup } from '../types'; + +export interface MetricVisualizationPluginSetupPlugins { + expressions: ExpressionsSetup; + formatFactory: FormatFactory; + editorFrame: EditorFrameSetup; +} + +export class MetricVisualization { + constructor() {} + + setup( + _core: CoreSetup | null, + { expressions, formatFactory, editorFrame }: MetricVisualizationPluginSetupPlugins + ) { + expressions.registerFunction(() => metricChart); + + expressions.registerRenderer(() => getMetricChartRenderer(formatFactory)); + + editorFrame.registerVisualization(metricVisualization); + } +} diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.test.tsx similarity index 98% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.test.tsx index a66239e5d30f6..eac35f82a50fa 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.test.tsx @@ -11,7 +11,7 @@ import { MetricConfigPanel } from './metric_config_panel'; import { DatasourceDimensionPanelProps, Operation, DatasourcePublicAPI } from '../types'; import { State } from './types'; import { NativeRendererProps } from '../native_renderer'; -import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_plugin/mocks'; +import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_service/mocks'; describe('MetricConfigPanel', () => { const dragDropContext = { dragging: undefined, setDragging: jest.fn() }; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization/metric_config_panel.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.test.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization/metric_expression.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_suggestions.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts rename to x-pack/legacy/plugins/lens/public/metric_visualization/metric_suggestions.test.ts diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_suggestions.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts rename to x-pack/legacy/plugins/lens/public/metric_visualization/metric_suggestions.ts diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts similarity index 99% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts rename to x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts index c131612399cca..88964b95c2ac7 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.test.ts @@ -6,7 +6,7 @@ import { metricVisualization } from './metric_visualization'; import { State } from './types'; -import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_plugin/mocks'; +import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; import { generateId } from '../id_generator'; import { DatasourcePublicAPI, FramePublicAPI } from '../types'; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization/metric_visualization.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/types.ts b/x-pack/legacy/plugins/lens/public/metric_visualization/types.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/types.ts rename to x-pack/legacy/plugins/lens/public/metric_visualization/types.ts diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/plugin.tsx deleted file mode 100644 index 219ef533a4ba3..0000000000000 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/plugin.tsx +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { npSetup } from 'ui/new_platform'; -import { CoreSetup } from 'src/core/public'; -import { FormatFactory, getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; -import { metricVisualization } from './metric_visualization'; -import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; -import { metricChart, getMetricChartRenderer } from './metric_expression'; - -export interface MetricVisualizationPluginSetupPlugins { - expressions: ExpressionsSetup; - // TODO this is a simulated NP plugin. - // Once field formatters are actually migrated, the actual shim can be used - fieldFormat: { - formatFactory: FormatFactory; - }; -} - -class MetricVisualizationPlugin { - constructor() {} - - setup( - _core: CoreSetup | null, - { expressions, fieldFormat }: MetricVisualizationPluginSetupPlugins - ) { - expressions.registerFunction(() => metricChart); - - expressions.registerRenderer(() => getMetricChartRenderer(fieldFormat.formatFactory)); - - return metricVisualization; - } - - stop() {} -} - -const plugin = new MetricVisualizationPlugin(); - -export const metricVisualizationSetup = () => - plugin.setup(null, { - expressions: npSetup.plugins.expressions, - fieldFormat: { - formatFactory: getFormat, - }, - }); - -export const metricVisualizationStop = () => plugin.stop(); diff --git a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx b/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx index 012c27d3ce3ff..38f48c9cdaf72 100644 --- a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx +++ b/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { createMockDatasource } from '../editor_frame_plugin/mocks'; +import { createMockDatasource } from '../editor_frame_service/mocks'; import { MultiColumnEditor } from './multi_column_editor'; import { mount } from 'enzyme'; diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/plugin.tsx similarity index 55% rename from x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx rename to x-pack/legacy/plugins/lens/public/plugin.tsx index 283f4d2a0689d..634d227559835 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/plugin.tsx @@ -4,97 +4,112 @@ * you may not use this file except in compliance with the Elastic License. */ -import 'ui/autoload/all'; -// Used to run esaggs queries -import 'uiExports/fieldFormats'; -import 'uiExports/search'; -import 'uiExports/visRequestHandlers'; -import 'uiExports/visResponseHandlers'; -// Used for kibana_context function -import 'uiExports/savedObjectTypes'; - import React from 'react'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom'; import { render, unmountComponentAtNode } from 'react-dom'; -import { CoreSetup, CoreStart, SavedObjectsClientContract } from 'src/core/public'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { AppMountParameters, CoreSetup, CoreStart } from 'src/core/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; import rison, { RisonObject, RisonValue } from 'rison-node'; import { isObject } from 'lodash'; -import { DataStart } from '../../../../../../src/legacy/core_plugins/data/public'; -import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; -import { editorFrameSetup, editorFrameStart, editorFrameStop } from '../editor_frame_plugin'; -import { indexPatternDatasourceSetup, indexPatternDatasourceStop } from '../indexpattern_plugin'; -import { addHelpMenuToAppChrome } from '../help_menu_util'; -import { SavedObjectIndexStore } from '../persistence'; -import { xyVisualizationSetup, xyVisualizationStop } from '../xy_visualization_plugin'; -import { metricVisualizationSetup, metricVisualizationStop } from '../metric_visualization_plugin'; -import { - datatableVisualizationSetup, - datatableVisualizationStop, -} from '../datatable_visualization_plugin'; -import { App } from './app'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; +import { EditorFrameService } from './editor_frame_service'; +import { IndexPatternDatasource } from './indexpattern_datasource'; +import { addHelpMenuToAppChrome } from './help_menu_util'; +import { SavedObjectIndexStore } from './persistence'; +import { XyVisualization } from './xy_visualization'; +import { MetricVisualization } from './metric_visualization'; +import { ExpressionsSetup, ExpressionsStart } from '../../../../../src/plugins/expressions/public'; +import { DatatableVisualization } from './datatable_visualization'; +import { App } from './app_plugin'; import { LensReportManager, setReportManager, stopReportManager, trackUiEvent, -} from '../lens_ui_telemetry'; -import { NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../../../../../plugins/lens/common'; -import { KibanaLegacySetup } from '../../../../../../src/plugins/kibana_legacy/public'; -import { EditorFrameStart } from '../types'; +} from './lens_ui_telemetry'; +import { KibanaLegacySetup } from '../../../../../src/plugins/kibana_legacy/public'; +import { NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../../../../plugins/lens/common'; import { addEmbeddableToDashboardUrl, getUrlVars, getLensUrlFromDashboardAbsoluteUrl, -} from '../../../../../../src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper'; +} from '../../../../../src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper'; +import { FormatFactory } from './legacy_imports'; +import { IEmbeddableSetup, IEmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { EditorFrameStart } from './types'; export interface LensPluginSetupDependencies { kibanaLegacy: KibanaLegacySetup; + expressions: ExpressionsSetup; + data: DataPublicPluginSetup; + embeddable: IEmbeddableSetup; + __LEGACY: { + formatFactory: FormatFactory; + }; } export interface LensPluginStartDependencies { data: DataPublicPluginStart; - dataShim: DataStart; + embeddable: IEmbeddableStart; + expressions: ExpressionsStart; } export const isRisonObject = (value: RisonValue): value is RisonObject => { return isObject(value); }; -export class AppPlugin { - private startDependencies: { - data: DataPublicPluginStart; - dataShim: DataStart; - savedObjectsClient: SavedObjectsClientContract; - editorFrame: EditorFrameStart; - } | null = null; - - constructor() {} - - setup(core: CoreSetup, { kibanaLegacy }: LensPluginSetupDependencies) { - // TODO: These plugins should not be called from the top level, but since this is the - // entry point to the app we have no choice until the new platform is ready - const indexPattern = indexPatternDatasourceSetup(); - const datatableVisualization = datatableVisualizationSetup(); - const xyVisualization = xyVisualizationSetup(); - const metricVisualization = metricVisualizationSetup(); - const editorFrameSetupInterface = editorFrameSetup(); +export class LensPlugin { + private datatableVisualization: DatatableVisualization; + private editorFrameService: EditorFrameService; + private createEditorFrame: EditorFrameStart['createInstance'] | null = null; + private indexpatternDatasource: IndexPatternDatasource; + private xyVisualization: XyVisualization; + private metricVisualization: MetricVisualization; + + constructor() { + this.datatableVisualization = new DatatableVisualization(); + this.editorFrameService = new EditorFrameService(); + this.indexpatternDatasource = new IndexPatternDatasource(); + this.xyVisualization = new XyVisualization(); + this.metricVisualization = new MetricVisualization(); + } - editorFrameSetupInterface.registerVisualization(xyVisualization); - editorFrameSetupInterface.registerVisualization(datatableVisualization); - editorFrameSetupInterface.registerVisualization(metricVisualization); - editorFrameSetupInterface.registerDatasource(indexPattern); + setup( + core: CoreSetup<LensPluginStartDependencies>, + { + kibanaLegacy, + expressions, + data, + embeddable, + __LEGACY: { formatFactory }, + }: LensPluginSetupDependencies + ) { + const editorFrameSetupInterface = this.editorFrameService.setup(core, { + data, + embeddable, + expressions, + }); + const dependencies = { + expressions, + data, + editorFrame: editorFrameSetupInterface, + formatFactory, + }; + this.indexpatternDatasource.setup(core, dependencies); + this.xyVisualization.setup(core, dependencies); + this.datatableVisualization.setup(core, dependencies); + this.metricVisualization.setup(core, dependencies); kibanaLegacy.registerLegacyApp({ id: 'lens', title: NOT_INTERNATIONALIZED_PRODUCT_NAME, - mount: async (context, params) => { - if (this.startDependencies === null) { - throw new Error('mounted before start phase'); - } - const { data, savedObjectsClient, editorFrame } = this.startDependencies; - addHelpMenuToAppChrome(context.core.chrome); - const instance = editorFrame.createInstance({}); + mount: async (params: AppMountParameters) => { + const [coreStart, startDependencies] = await core.getStartServices(); + const dataStart = startDependencies.data; + const savedObjectsClient = coreStart.savedObjects.client; + addHelpMenuToAppChrome(coreStart.chrome); + + const instance = await this.createEditorFrame!({}); setReportManager( new LensReportManager({ @@ -108,7 +123,7 @@ export class AppPlugin { return; } // @ts-ignore - decoded.time = data.query.timefilter.timefilter.getTime(); + decoded.time = dataStart.query.timefilter.timefilter.getTime(); urlVars._g = rison.encode(decoded); }; const redirectTo = ( @@ -122,12 +137,12 @@ export class AppPlugin { routeProps.history.push(`/lens/edit/${id}`); } else if (addToDashboardMode && id) { routeProps.history.push(`/lens/edit/${id}`); - const url = context.core.chrome.navLinks.get('kibana:dashboard'); + const url = coreStart.chrome.navLinks.get('kibana:dashboard'); if (!url) { throw new Error('Cannot get last dashboard url'); } const lastDashboardAbsoluteUrl = url.url; - const basePath = context.core.http.basePath.get(); + const basePath = coreStart.http.basePath.get(); const lensUrl = getLensUrlFromDashboardAbsoluteUrl( lastDashboardAbsoluteUrl, basePath, @@ -158,8 +173,8 @@ export class AppPlugin { !!routeProps.location.search && routeProps.location.search.includes('addToDashboard'); return ( <App - core={context.core} - data={data} + core={coreStart} + data={dataStart} editorFrame={instance} storage={new Storage(localStorage)} docId={routeProps.match.params.id} @@ -195,23 +210,11 @@ export class AppPlugin { }); } - start({ savedObjects }: CoreStart, { data, dataShim }: LensPluginStartDependencies) { - this.startDependencies = { - data, - dataShim, - savedObjectsClient: savedObjects.client, - editorFrame: editorFrameStart(), - }; + start(core: CoreStart, startDependencies: LensPluginStartDependencies) { + this.createEditorFrame = this.editorFrameService.start(core, startDependencies).createInstance; } stop() { stopReportManager(); - - // TODO this will be handled by the plugin platform itself - indexPatternDatasourceStop(); - xyVisualizationStop(); - metricVisualizationStop(); - datatableVisualizationStop(); - editorFrameStop(); } } diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index d3d7039552c50..b62b920429e7c 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -47,12 +47,14 @@ export interface EditorFrameInstance { export interface EditorFrameSetup { // generic type on the API functions to pull the "unknown vs. specific type" error into the implementation - registerDatasource: <T, P>(datasource: Datasource<T, P>) => void; - registerVisualization: <T, P>(visualization: Visualization<T, P>) => void; + registerDatasource: <T, P>(datasource: Datasource<T, P> | Promise<Datasource<T, P>>) => void; + registerVisualization: <T, P>( + visualization: Visualization<T, P> | Promise<Visualization<T, P>> + ) => void; } export interface EditorFrameStart { - createInstance: (options: EditorFrameOptions) => EditorFrameInstance; + createInstance: (options: EditorFrameOptions) => Promise<EditorFrameInstance>; } // Hints the default nesting to the data source. 0 is the highest priority diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap b/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap new file mode 100644 index 0000000000000..fd0c4b8212fc6 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap @@ -0,0 +1,482 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`xy_expression XYChart component it renders area 1`] = ` +<Chart + renderer="canvas" +> + <Connect(spec) + legendPosition="top" + rotation={0} + showLegend={false} + showLegendDisplayValue={false} + theme={Object {}} + /> + <Connect(spec) + id="x" + position="bottom" + showGridLines={false} + tickFormat={[Function]} + title="c" + /> + <Connect(spec) + id="y" + position="left" + showGridLines={false} + tickFormat={[Function]} + title="" + /> + <Connect(spec) + data={ + Array [ + Object { + "Label A": 1, + "Label B": 2, + "Label D": "Foo", + "c": "I", + "d": "Foo", + }, + Object { + "Label A": 1, + "Label B": 5, + "Label D": "Bar", + "c": "J", + "d": "Bar", + }, + ] + } + enableHistogramMode={false} + id="Label D" + key="0" + splitSeriesAccessors={ + Array [ + "d", + ] + } + stackAccessors={Array []} + timeZone="UTC" + xAccessor="c" + xScaleType="ordinal" + yAccessors={ + Array [ + "Label A", + "Label B", + ] + } + yScaleType="linear" + /> +</Chart> +`; + +exports[`xy_expression XYChart component it renders bar 1`] = ` +<Chart + renderer="canvas" +> + <Connect(spec) + legendPosition="top" + rotation={0} + showLegend={false} + showLegendDisplayValue={false} + theme={Object {}} + /> + <Connect(spec) + id="x" + position="bottom" + showGridLines={false} + tickFormat={[Function]} + title="c" + /> + <Connect(spec) + id="y" + position="left" + showGridLines={false} + tickFormat={[Function]} + title="" + /> + <Connect(spec) + data={ + Array [ + Object { + "Label A": 1, + "Label B": 2, + "Label D": "Foo", + "c": "I", + "d": "Foo", + }, + Object { + "Label A": 1, + "Label B": 5, + "Label D": "Bar", + "c": "J", + "d": "Bar", + }, + ] + } + enableHistogramMode={false} + id="Label D" + key="0" + splitSeriesAccessors={ + Array [ + "d", + ] + } + stackAccessors={Array []} + timeZone="UTC" + xAccessor="c" + xScaleType="ordinal" + yAccessors={ + Array [ + "Label A", + "Label B", + ] + } + yScaleType="linear" + /> +</Chart> +`; + +exports[`xy_expression XYChart component it renders horizontal bar 1`] = ` +<Chart + renderer="canvas" +> + <Connect(spec) + legendPosition="top" + rotation={90} + showLegend={false} + showLegendDisplayValue={false} + theme={Object {}} + /> + <Connect(spec) + id="x" + position="left" + showGridLines={false} + tickFormat={[Function]} + title="c" + /> + <Connect(spec) + id="y" + position="bottom" + showGridLines={false} + tickFormat={[Function]} + title="" + /> + <Connect(spec) + data={ + Array [ + Object { + "Label A": 1, + "Label B": 2, + "Label D": "Foo", + "c": "I", + "d": "Foo", + }, + Object { + "Label A": 1, + "Label B": 5, + "Label D": "Bar", + "c": "J", + "d": "Bar", + }, + ] + } + enableHistogramMode={false} + id="Label D" + key="0" + splitSeriesAccessors={ + Array [ + "d", + ] + } + stackAccessors={Array []} + timeZone="UTC" + xAccessor="c" + xScaleType="ordinal" + yAccessors={ + Array [ + "Label A", + "Label B", + ] + } + yScaleType="linear" + /> +</Chart> +`; + +exports[`xy_expression XYChart component it renders line 1`] = ` +<Chart + renderer="canvas" +> + <Connect(spec) + legendPosition="top" + rotation={0} + showLegend={false} + showLegendDisplayValue={false} + theme={Object {}} + /> + <Connect(spec) + id="x" + position="bottom" + showGridLines={false} + tickFormat={[Function]} + title="c" + /> + <Connect(spec) + id="y" + position="left" + showGridLines={false} + tickFormat={[Function]} + title="" + /> + <Connect(spec) + data={ + Array [ + Object { + "Label A": 1, + "Label B": 2, + "Label D": "Foo", + "c": "I", + "d": "Foo", + }, + Object { + "Label A": 1, + "Label B": 5, + "Label D": "Bar", + "c": "J", + "d": "Bar", + }, + ] + } + enableHistogramMode={false} + id="Label D" + key="0" + splitSeriesAccessors={ + Array [ + "d", + ] + } + stackAccessors={Array []} + timeZone="UTC" + xAccessor="c" + xScaleType="ordinal" + yAccessors={ + Array [ + "Label A", + "Label B", + ] + } + yScaleType="linear" + /> +</Chart> +`; + +exports[`xy_expression XYChart component it renders stacked area 1`] = ` +<Chart + renderer="canvas" +> + <Connect(spec) + legendPosition="top" + rotation={0} + showLegend={false} + showLegendDisplayValue={false} + theme={Object {}} + /> + <Connect(spec) + id="x" + position="bottom" + showGridLines={false} + tickFormat={[Function]} + title="c" + /> + <Connect(spec) + id="y" + position="left" + showGridLines={false} + tickFormat={[Function]} + title="" + /> + <Connect(spec) + data={ + Array [ + Object { + "Label A": 1, + "Label B": 2, + "Label D": "Foo", + "c": "I", + "d": "Foo", + }, + Object { + "Label A": 1, + "Label B": 5, + "Label D": "Bar", + "c": "J", + "d": "Bar", + }, + ] + } + enableHistogramMode={false} + id="Label D" + key="0" + splitSeriesAccessors={ + Array [ + "d", + ] + } + stackAccessors={ + Array [ + "c", + ] + } + timeZone="UTC" + xAccessor="c" + xScaleType="ordinal" + yAccessors={ + Array [ + "Label A", + "Label B", + ] + } + yScaleType="linear" + /> +</Chart> +`; + +exports[`xy_expression XYChart component it renders stacked bar 1`] = ` +<Chart + renderer="canvas" +> + <Connect(spec) + legendPosition="top" + rotation={0} + showLegend={false} + showLegendDisplayValue={false} + theme={Object {}} + /> + <Connect(spec) + id="x" + position="bottom" + showGridLines={false} + tickFormat={[Function]} + title="c" + /> + <Connect(spec) + id="y" + position="left" + showGridLines={false} + tickFormat={[Function]} + title="" + /> + <Connect(spec) + data={ + Array [ + Object { + "Label A": 1, + "Label B": 2, + "Label D": "Foo", + "c": "I", + "d": "Foo", + }, + Object { + "Label A": 1, + "Label B": 5, + "Label D": "Bar", + "c": "J", + "d": "Bar", + }, + ] + } + enableHistogramMode={false} + id="Label D" + key="0" + splitSeriesAccessors={ + Array [ + "d", + ] + } + stackAccessors={ + Array [ + "c", + ] + } + timeZone="UTC" + xAccessor="c" + xScaleType="ordinal" + yAccessors={ + Array [ + "Label A", + "Label B", + ] + } + yScaleType="linear" + /> +</Chart> +`; + +exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] = ` +<Chart + renderer="canvas" +> + <Connect(spec) + legendPosition="top" + rotation={90} + showLegend={false} + showLegendDisplayValue={false} + theme={Object {}} + /> + <Connect(spec) + id="x" + position="left" + showGridLines={false} + tickFormat={[Function]} + title="c" + /> + <Connect(spec) + id="y" + position="bottom" + showGridLines={false} + tickFormat={[Function]} + title="" + /> + <Connect(spec) + data={ + Array [ + Object { + "Label A": 1, + "Label B": 2, + "Label D": "Foo", + "c": "I", + "d": "Foo", + }, + Object { + "Label A": 1, + "Label B": 5, + "Label D": "Bar", + "c": "J", + "d": "Bar", + }, + ] + } + enableHistogramMode={false} + id="Label D" + key="0" + splitSeriesAccessors={ + Array [ + "d", + ] + } + stackAccessors={ + Array [ + "c", + ] + } + timeZone="UTC" + xAccessor="c" + xScaleType="ordinal" + yAccessors={ + Array [ + "Label A", + "Label B", + ] + } + yScaleType="linear" + /> +</Chart> +`; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap b/x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_visualization.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap rename to x-pack/legacy/plugins/lens/public/xy_visualization/__snapshots__/xy_visualization.test.ts.snap diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/_index.scss b/x-pack/legacy/plugins/lens/public/xy_visualization/_index.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/_index.scss rename to x-pack/legacy/plugins/lens/public/xy_visualization/_index.scss diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/_xy_expression.scss b/x-pack/legacy/plugins/lens/public/xy_visualization/_xy_expression.scss similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/_xy_expression.scss rename to x-pack/legacy/plugins/lens/public/xy_visualization/_xy_expression.scss diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/index.ts similarity index 57% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx rename to x-pack/legacy/plugins/lens/public/xy_visualization/index.ts index 6feece99370ef..86c52e0577616 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/index.ts @@ -4,24 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { npSetup } from 'ui/new_platform'; +import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; import { CoreSetup, IUiSettingsClient } from 'src/core/public'; -import chrome, { Chrome } from 'ui/chrome'; import moment from 'moment-timezone'; -import { getFormat, FormatFactory } from 'ui/visualize/loader/pipeline_helpers/utilities'; +import { FormatFactory } from '../legacy_imports'; import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public'; import { xyVisualization } from './xy_visualization'; import { xyChart, getXyChartRenderer } from './xy_expression'; import { legendConfig, xConfig, layerConfig } from './types'; +import { EditorFrameSetup } from '../types'; export interface XyVisualizationPluginSetupPlugins { expressions: ExpressionsSetup; - chrome: Chrome; - // TODO this is a simulated NP plugin. - // Once field formatters are actually migrated, the actual shim can be used - fieldFormat: { - formatFactory: FormatFactory; - }; + formatFactory: FormatFactory; + editorFrame: EditorFrameSetup; } function getTimeZone(uiSettings: IUiSettingsClient) { @@ -33,16 +29,12 @@ function getTimeZone(uiSettings: IUiSettingsClient) { return configuredTimeZone; } -class XyVisualizationPlugin { +export class XyVisualization { constructor() {} setup( - _core: CoreSetup | null, - { - expressions, - fieldFormat: { formatFactory }, - chrome: { getUiSettingsClient }, - }: XyVisualizationPluginSetupPlugins + core: CoreSetup, + { expressions, formatFactory, editorFrame }: XyVisualizationPluginSetupPlugins ) { expressions.registerFunction(() => legendConfig); expressions.registerFunction(() => xConfig); @@ -52,24 +44,13 @@ class XyVisualizationPlugin { expressions.registerRenderer( getXyChartRenderer({ formatFactory, - timeZone: getTimeZone(getUiSettingsClient()), + chartTheme: core.uiSettings.get<boolean>('theme:darkMode') + ? EUI_CHARTS_THEME_DARK.theme + : EUI_CHARTS_THEME_LIGHT.theme, + timeZone: getTimeZone(core.uiSettings), }) ); - return xyVisualization; + editorFrame.registerVisualization(xyVisualization); } - - stop() {} } - -const plugin = new XyVisualizationPlugin(); - -export const xyVisualizationSetup = () => - plugin.setup(null, { - expressions: npSetup.plugins.expressions, - fieldFormat: { - formatFactory: getFormat, - }, - chrome, - }); -export const xyVisualizationStop = () => plugin.stop(); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/state_helpers.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/state_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/state_helpers.ts rename to x-pack/legacy/plugins/lens/public/xy_visualization/state_helpers.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts rename to x-pack/legacy/plugins/lens/public/xy_visualization/to_expression.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/types.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts rename to x-pack/legacy/plugins/lens/public/xy_visualization/types.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx similarity index 99% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx rename to x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx index 6ed827bc71c68..301c4a58a0ffd 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx @@ -14,7 +14,7 @@ import { State } from './types'; import { Position } from '@elastic/charts'; import { NativeRendererProps } from '../native_renderer'; import { generateId } from '../id_generator'; -import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_plugin/mocks'; +import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_service/mocks'; jest.mock('../id_generator'); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx rename to x-pack/legacy/plugins/lens/public/xy_visualization/xy_config_panel.tsx diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.test.tsx similarity index 93% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx rename to x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.test.tsx index daedb30db3f3e..04e0b80faa200 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.test.tsx @@ -132,6 +132,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'line' }] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component).toMatchSnapshot(); @@ -156,6 +157,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` @@ -184,6 +186,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component.find(Settings).prop('xDomain')).toBeUndefined(); @@ -197,6 +200,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar' }] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component).toMatchSnapshot(); @@ -211,6 +215,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'area' }] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component).toMatchSnapshot(); @@ -225,6 +230,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar_horizontal' }] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component).toMatchSnapshot(); @@ -240,6 +246,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar_stacked' }] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component).toMatchSnapshot(); @@ -255,6 +262,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'area_stacked' }] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component).toMatchSnapshot(); @@ -273,6 +281,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component).toMatchSnapshot(); @@ -284,7 +293,13 @@ describe('xy_expression', () => { test('it passes time zone to the series', () => { const { data, args } = sampleArgs(); const component = shallow( - <XYChart data={data} args={args} formatFactory={getFormatSpy} timeZone="CEST" /> + <XYChart + data={data} + args={args} + formatFactory={getFormatSpy} + timeZone="CEST" + chartTheme={{}} + /> ); expect(component.find(LineSeries).prop('timeZone')).toEqual('CEST'); }); @@ -299,6 +314,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [firstLayer] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(true); @@ -321,6 +337,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(true); @@ -337,6 +354,7 @@ describe('xy_expression', () => { }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(false); @@ -346,7 +364,13 @@ describe('xy_expression', () => { const { data, args } = sampleArgs(); const component = shallow( - <XYChart data={data} args={args} formatFactory={getFormatSpy} timeZone="UTC" /> + <XYChart + data={data} + args={args} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + /> ); expect(component.find(LineSeries).prop('data')).toEqual([ { 'Label A': 1, 'Label B': 2, c: 'I', 'Label D': 'Foo', d: 'Foo' }, @@ -358,7 +382,13 @@ describe('xy_expression', () => { const { data, args } = sampleArgs(); const component = shallow( - <XYChart data={data} args={args} formatFactory={getFormatSpy} timeZone="UTC" /> + <XYChart + data={data} + args={args} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + /> ); expect(component.find(LineSeries).prop('yAccessors')).toEqual(['Label A', 'Label B']); }); @@ -372,6 +402,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], xScaleType: 'ordinal' }] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component.find(LineSeries).prop('xScaleType')).toEqual(ScaleType.Ordinal); @@ -386,6 +417,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], yScaleType: 'sqrt' }] }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); expect(component.find(LineSeries).prop('yScaleType')).toEqual(ScaleType.Sqrt); @@ -400,6 +432,7 @@ describe('xy_expression', () => { args={{ ...args }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); @@ -415,6 +448,7 @@ describe('xy_expression', () => { args={{ ...args }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); @@ -429,6 +463,7 @@ describe('xy_expression', () => { data={{ ...data }} args={{ ...args, layers: [{ ...args.layers[0], accessors: ['a'] }] }} formatFactory={getFormatSpy} + chartTheme={{}} timeZone="UTC" /> ); @@ -447,6 +482,7 @@ describe('xy_expression', () => { args={{ ...args }} formatFactory={getFormatSpy} timeZone="UTC" + chartTheme={{}} /> ); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx similarity index 94% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx rename to x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx index c62a8288d6655..27fd6e7064042 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -6,7 +6,6 @@ import React, { useState, useEffect } from 'react'; import ReactDOM from 'react-dom'; -import chrome from 'ui/chrome'; import { Chart, Settings, @@ -15,6 +14,7 @@ import { AreaSeries, BarSeries, Position, + PartialTheme, } from '@elastic/charts'; import { I18nProvider } from '@kbn/i18n/react'; import { @@ -27,16 +27,12 @@ import { import { EuiIcon, EuiText, IconType, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; -import { FormatFactory } from '../../../../../../src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities'; +import { FormatFactory } from '../legacy_imports'; import { LensMultiTable } from '../types'; import { XYArgs, SeriesType, visualizationTypes } from './types'; import { VisualizationContainer } from '../visualization_container'; import { isHorizontalChart } from './state_helpers'; -const IS_DARK_THEME = chrome.getUiSettingsClient().get('theme:darkMode'); -const chartTheme = IS_DARK_THEME ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; - export interface XYChartProps { data: LensMultiTable; args: XYArgs; @@ -49,6 +45,7 @@ export interface XYRender { } type XYChartRenderProps = XYChartProps & { + chartTheme: PartialTheme; formatFactory: FormatFactory; timeZone: string; }; @@ -101,6 +98,7 @@ export const xyChart: ExpressionFunctionDefinition< export const getXyChartRenderer = (dependencies: { formatFactory: FormatFactory; + chartTheme: PartialTheme; timeZone: string; }): ExpressionRenderDefinition<XYChartProps> => ({ name: 'lens_xy_chart_renderer', @@ -146,7 +144,7 @@ export function XYChartReportable(props: XYChartRenderProps) { ); } -export function XYChart({ data, args, formatFactory, timeZone }: XYChartRenderProps) { +export function XYChart({ data, args, formatFactory, timeZone, chartTheme }: XYChartRenderProps) { const { legend, layers } = args; if (Object.values(data.tables).every(table => table.rows.length === 0)) { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts rename to x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.test.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts rename to x-pack/legacy/plugins/lens/public/xy_visualization/xy_suggestions.ts diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts similarity index 99% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts rename to x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts index 89794ec74eaec..a27a8e7754b86 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.test.ts @@ -8,7 +8,7 @@ import { xyVisualization } from './xy_visualization'; import { Position } from '@elastic/charts'; import { Operation } from '../types'; import { State, SeriesType } from './types'; -import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_plugin/mocks'; +import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; import { generateId } from '../id_generator'; import { Ast } from '@kbn/interpreter/target/common'; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx rename to x-pack/legacy/plugins/lens/public/xy_visualization/xy_visualization.tsx diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap deleted file mode 100644 index 495d7a7bcd77e..0000000000000 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap +++ /dev/null @@ -1,1315 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`xy_expression XYChart component it renders area 1`] = ` -<Chart - renderer="canvas" -> - <Connect(spec) - legendPosition="top" - rotation={0} - showLegend={false} - showLegendDisplayValue={false} - theme={ - Object { - "areaSeriesStyle": Object { - "area": Object { - "opacity": 0.3, - }, - "line": Object { - "strokeWidth": 2, - }, - "point": Object { - "fill": "rgba(255, 255, 255, 1)", - "radius": 3, - "strokeWidth": 2, - "visible": false, - }, - }, - "axes": Object { - "axisLineStyle": Object { - "stroke": "rgba(239, 241, 244, 1)", - }, - "axisTitleStyle": Object { - "fill": "rgba(52, 55, 65, 1)", - "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, - 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", - "fontSize": 12, - "padding": 10, - }, - "gridLineStyle": Object { - "horizontal": Object { - "dash": Array [ - 0, - 0, - ], - "opacity": 1, - "stroke": "rgba(239, 241, 244, 1)", - "strokeWidth": 1, - "visible": true, - }, - "vertical": Object { - "dash": Array [ - 4, - 4, - ], - "opacity": 1, - "stroke": "rgba(239, 241, 244, 1)", - "strokeWidth": 1, - "visible": true, - }, - }, - "tickLabelStyle": Object { - "fill": "rgba(105, 112, 125, 1)", - "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, - 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", - "fontSize": 10, - "padding": 8, - }, - "tickLineStyle": Object { - "stroke": "rgba(239, 241, 244, 1)", - "strokeWidth": 1, - "visible": false, - }, - }, - "barSeriesStyle": Object { - "displayValue": Object { - "fill": "rgba(105, 112, 125, 1)", - "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, - 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", - "fontSize": 8, - }, - }, - "chartMargins": Object { - "bottom": 0, - "left": 0, - "right": 0, - "top": 0, - }, - "colors": Object { - "defaultVizColor": "#6092C0", - "vizColors": Array [ - "#54B399", - "#6092C0", - "#D36086", - "#9170B8", - "#CA8EAE", - "#D6BF57", - "#B9A888", - "#DA8B45", - "#AA6556", - "#E7664C", - ], - }, - "crosshair": Object { - "band": Object { - "fill": "rgba(245, 247, 250, 1)", - }, - "line": Object { - "dash": Array [ - 4, - 4, - ], - "stroke": "rgba(105, 112, 125, 1)", - "strokeWidth": 1, - }, - }, - "lineSeriesStyle": Object { - "line": Object { - "strokeWidth": 2, - }, - "point": Object { - "fill": "rgba(255, 255, 255, 1)", - "radius": 3, - "strokeWidth": 2, - }, - }, - "scales": Object { - "barsPadding": 0.25, - "histogramPadding": 0.05, - }, - } - } - /> - <Connect(spec) - id="x" - position="bottom" - showGridLines={false} - tickFormat={[Function]} - title="c" - /> - <Connect(spec) - id="y" - position="left" - showGridLines={false} - tickFormat={[Function]} - title="" - /> - <Connect(spec) - data={ - Array [ - Object { - "Label A": 1, - "Label B": 2, - "Label D": "Foo", - "c": "I", - "d": "Foo", - }, - Object { - "Label A": 1, - "Label B": 5, - "Label D": "Bar", - "c": "J", - "d": "Bar", - }, - ] - } - enableHistogramMode={false} - id="Label D" - key="0" - splitSeriesAccessors={ - Array [ - "d", - ] - } - stackAccessors={Array []} - timeZone="UTC" - xAccessor="c" - xScaleType="ordinal" - yAccessors={ - Array [ - "Label A", - "Label B", - ] - } - yScaleType="linear" - /> -</Chart> -`; - -exports[`xy_expression XYChart component it renders bar 1`] = ` -<Chart - renderer="canvas" -> - <Connect(spec) - legendPosition="top" - rotation={0} - showLegend={false} - showLegendDisplayValue={false} - theme={ - Object { - "areaSeriesStyle": Object { - "area": Object { - "opacity": 0.3, - }, - "line": Object { - "strokeWidth": 2, - }, - "point": Object { - "fill": "rgba(255, 255, 255, 1)", - "radius": 3, - "strokeWidth": 2, - "visible": false, - }, - }, - "axes": Object { - "axisLineStyle": Object { - "stroke": "rgba(239, 241, 244, 1)", - }, - "axisTitleStyle": Object { - "fill": "rgba(52, 55, 65, 1)", - "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, - 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", - "fontSize": 12, - "padding": 10, - }, - "gridLineStyle": Object { - "horizontal": Object { - "dash": Array [ - 0, - 0, - ], - "opacity": 1, - "stroke": "rgba(239, 241, 244, 1)", - "strokeWidth": 1, - "visible": true, - }, - "vertical": Object { - "dash": Array [ - 4, - 4, - ], - "opacity": 1, - "stroke": "rgba(239, 241, 244, 1)", - "strokeWidth": 1, - "visible": true, - }, - }, - "tickLabelStyle": Object { - "fill": "rgba(105, 112, 125, 1)", - "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, - 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", - "fontSize": 10, - "padding": 8, - }, - "tickLineStyle": Object { - "stroke": "rgba(239, 241, 244, 1)", - "strokeWidth": 1, - "visible": false, - }, - }, - "barSeriesStyle": Object { - "displayValue": Object { - "fill": "rgba(105, 112, 125, 1)", - "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, - 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", - "fontSize": 8, - }, - }, - "chartMargins": Object { - "bottom": 0, - "left": 0, - "right": 0, - "top": 0, - }, - "colors": Object { - "defaultVizColor": "#6092C0", - "vizColors": Array [ - "#54B399", - "#6092C0", - "#D36086", - "#9170B8", - "#CA8EAE", - "#D6BF57", - "#B9A888", - "#DA8B45", - "#AA6556", - "#E7664C", - ], - }, - "crosshair": Object { - "band": Object { - "fill": "rgba(245, 247, 250, 1)", - }, - "line": Object { - "dash": Array [ - 4, - 4, - ], - "stroke": "rgba(105, 112, 125, 1)", - "strokeWidth": 1, - }, - }, - "lineSeriesStyle": Object { - "line": Object { - "strokeWidth": 2, - }, - "point": Object { - "fill": "rgba(255, 255, 255, 1)", - "radius": 3, - "strokeWidth": 2, - }, - }, - "scales": Object { - "barsPadding": 0.25, - "histogramPadding": 0.05, - }, - } - } - /> - <Connect(spec) - id="x" - position="bottom" - showGridLines={false} - tickFormat={[Function]} - title="c" - /> - <Connect(spec) - id="y" - position="left" - showGridLines={false} - tickFormat={[Function]} - title="" - /> - <Connect(spec) - data={ - Array [ - Object { - "Label A": 1, - "Label B": 2, - "Label D": "Foo", - "c": "I", - "d": "Foo", - }, - Object { - "Label A": 1, - "Label B": 5, - "Label D": "Bar", - "c": "J", - "d": "Bar", - }, - ] - } - enableHistogramMode={false} - id="Label D" - key="0" - splitSeriesAccessors={ - Array [ - "d", - ] - } - stackAccessors={Array []} - timeZone="UTC" - xAccessor="c" - xScaleType="ordinal" - yAccessors={ - Array [ - "Label A", - "Label B", - ] - } - yScaleType="linear" - /> -</Chart> -`; - -exports[`xy_expression XYChart component it renders horizontal bar 1`] = ` -<Chart - renderer="canvas" -> - <Connect(spec) - legendPosition="top" - rotation={90} - showLegend={false} - showLegendDisplayValue={false} - theme={ - Object { - "areaSeriesStyle": Object { - "area": Object { - "opacity": 0.3, - }, - "line": Object { - "strokeWidth": 2, - }, - "point": Object { - "fill": "rgba(255, 255, 255, 1)", - "radius": 3, - "strokeWidth": 2, - "visible": false, - }, - }, - "axes": Object { - "axisLineStyle": Object { - "stroke": "rgba(239, 241, 244, 1)", - }, - "axisTitleStyle": Object { - "fill": "rgba(52, 55, 65, 1)", - "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, - 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", - "fontSize": 12, - "padding": 10, - }, - "gridLineStyle": Object { - "horizontal": Object { - "dash": Array [ - 0, - 0, - ], - "opacity": 1, - "stroke": "rgba(239, 241, 244, 1)", - "strokeWidth": 1, - "visible": true, - }, - "vertical": Object { - "dash": Array [ - 4, - 4, - ], - "opacity": 1, - "stroke": "rgba(239, 241, 244, 1)", - "strokeWidth": 1, - "visible": true, - }, - }, - "tickLabelStyle": Object { - "fill": "rgba(105, 112, 125, 1)", - "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, - 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", - "fontSize": 10, - "padding": 8, - }, - "tickLineStyle": Object { - "stroke": "rgba(239, 241, 244, 1)", - "strokeWidth": 1, - "visible": false, - }, - }, - "barSeriesStyle": Object { - "displayValue": Object { - "fill": "rgba(105, 112, 125, 1)", - "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, - 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", - "fontSize": 8, - }, - }, - "chartMargins": Object { - "bottom": 0, - "left": 0, - "right": 0, - "top": 0, - }, - "colors": Object { - "defaultVizColor": "#6092C0", - "vizColors": Array [ - "#54B399", - "#6092C0", - "#D36086", - "#9170B8", - "#CA8EAE", - "#D6BF57", - "#B9A888", - "#DA8B45", - "#AA6556", - "#E7664C", - ], - }, - "crosshair": Object { - "band": Object { - "fill": "rgba(245, 247, 250, 1)", - }, - "line": Object { - "dash": Array [ - 4, - 4, - ], - "stroke": "rgba(105, 112, 125, 1)", - "strokeWidth": 1, - }, - }, - "lineSeriesStyle": Object { - "line": Object { - "strokeWidth": 2, - }, - "point": Object { - "fill": "rgba(255, 255, 255, 1)", - "radius": 3, - "strokeWidth": 2, - }, - }, - "scales": Object { - "barsPadding": 0.25, - "histogramPadding": 0.05, - }, - } - } - /> - <Connect(spec) - id="x" - position="left" - showGridLines={false} - tickFormat={[Function]} - title="c" - /> - <Connect(spec) - id="y" - position="bottom" - showGridLines={false} - tickFormat={[Function]} - title="" - /> - <Connect(spec) - data={ - Array [ - Object { - "Label A": 1, - "Label B": 2, - "Label D": "Foo", - "c": "I", - "d": "Foo", - }, - Object { - "Label A": 1, - "Label B": 5, - "Label D": "Bar", - "c": "J", - "d": "Bar", - }, - ] - } - enableHistogramMode={false} - id="Label D" - key="0" - splitSeriesAccessors={ - Array [ - "d", - ] - } - stackAccessors={Array []} - timeZone="UTC" - xAccessor="c" - xScaleType="ordinal" - yAccessors={ - Array [ - "Label A", - "Label B", - ] - } - yScaleType="linear" - /> -</Chart> -`; - -exports[`xy_expression XYChart component it renders line 1`] = ` -<Chart - renderer="canvas" -> - <Connect(spec) - legendPosition="top" - rotation={0} - showLegend={false} - showLegendDisplayValue={false} - theme={ - Object { - "areaSeriesStyle": Object { - "area": Object { - "opacity": 0.3, - }, - "line": Object { - "strokeWidth": 2, - }, - "point": Object { - "fill": "rgba(255, 255, 255, 1)", - "radius": 3, - "strokeWidth": 2, - "visible": false, - }, - }, - "axes": Object { - "axisLineStyle": Object { - "stroke": "rgba(239, 241, 244, 1)", - }, - "axisTitleStyle": Object { - "fill": "rgba(52, 55, 65, 1)", - "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, - 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", - "fontSize": 12, - "padding": 10, - }, - "gridLineStyle": Object { - "horizontal": Object { - "dash": Array [ - 0, - 0, - ], - "opacity": 1, - "stroke": "rgba(239, 241, 244, 1)", - "strokeWidth": 1, - "visible": true, - }, - "vertical": Object { - "dash": Array [ - 4, - 4, - ], - "opacity": 1, - "stroke": "rgba(239, 241, 244, 1)", - "strokeWidth": 1, - "visible": true, - }, - }, - "tickLabelStyle": Object { - "fill": "rgba(105, 112, 125, 1)", - "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, - 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", - "fontSize": 10, - "padding": 8, - }, - "tickLineStyle": Object { - "stroke": "rgba(239, 241, 244, 1)", - "strokeWidth": 1, - "visible": false, - }, - }, - "barSeriesStyle": Object { - "displayValue": Object { - "fill": "rgba(105, 112, 125, 1)", - "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, - 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", - "fontSize": 8, - }, - }, - "chartMargins": Object { - "bottom": 0, - "left": 0, - "right": 0, - "top": 0, - }, - "colors": Object { - "defaultVizColor": "#6092C0", - "vizColors": Array [ - "#54B399", - "#6092C0", - "#D36086", - "#9170B8", - "#CA8EAE", - "#D6BF57", - "#B9A888", - "#DA8B45", - "#AA6556", - "#E7664C", - ], - }, - "crosshair": Object { - "band": Object { - "fill": "rgba(245, 247, 250, 1)", - }, - "line": Object { - "dash": Array [ - 4, - 4, - ], - "stroke": "rgba(105, 112, 125, 1)", - "strokeWidth": 1, - }, - }, - "lineSeriesStyle": Object { - "line": Object { - "strokeWidth": 2, - }, - "point": Object { - "fill": "rgba(255, 255, 255, 1)", - "radius": 3, - "strokeWidth": 2, - }, - }, - "scales": Object { - "barsPadding": 0.25, - "histogramPadding": 0.05, - }, - } - } - /> - <Connect(spec) - id="x" - position="bottom" - showGridLines={false} - tickFormat={[Function]} - title="c" - /> - <Connect(spec) - id="y" - position="left" - showGridLines={false} - tickFormat={[Function]} - title="" - /> - <Connect(spec) - data={ - Array [ - Object { - "Label A": 1, - "Label B": 2, - "Label D": "Foo", - "c": "I", - "d": "Foo", - }, - Object { - "Label A": 1, - "Label B": 5, - "Label D": "Bar", - "c": "J", - "d": "Bar", - }, - ] - } - enableHistogramMode={false} - id="Label D" - key="0" - splitSeriesAccessors={ - Array [ - "d", - ] - } - stackAccessors={Array []} - timeZone="UTC" - xAccessor="c" - xScaleType="ordinal" - yAccessors={ - Array [ - "Label A", - "Label B", - ] - } - yScaleType="linear" - /> -</Chart> -`; - -exports[`xy_expression XYChart component it renders stacked area 1`] = ` -<Chart - renderer="canvas" -> - <Connect(spec) - legendPosition="top" - rotation={0} - showLegend={false} - showLegendDisplayValue={false} - theme={ - Object { - "areaSeriesStyle": Object { - "area": Object { - "opacity": 0.3, - }, - "line": Object { - "strokeWidth": 2, - }, - "point": Object { - "fill": "rgba(255, 255, 255, 1)", - "radius": 3, - "strokeWidth": 2, - "visible": false, - }, - }, - "axes": Object { - "axisLineStyle": Object { - "stroke": "rgba(239, 241, 244, 1)", - }, - "axisTitleStyle": Object { - "fill": "rgba(52, 55, 65, 1)", - "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, - 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", - "fontSize": 12, - "padding": 10, - }, - "gridLineStyle": Object { - "horizontal": Object { - "dash": Array [ - 0, - 0, - ], - "opacity": 1, - "stroke": "rgba(239, 241, 244, 1)", - "strokeWidth": 1, - "visible": true, - }, - "vertical": Object { - "dash": Array [ - 4, - 4, - ], - "opacity": 1, - "stroke": "rgba(239, 241, 244, 1)", - "strokeWidth": 1, - "visible": true, - }, - }, - "tickLabelStyle": Object { - "fill": "rgba(105, 112, 125, 1)", - "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, - 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", - "fontSize": 10, - "padding": 8, - }, - "tickLineStyle": Object { - "stroke": "rgba(239, 241, 244, 1)", - "strokeWidth": 1, - "visible": false, - }, - }, - "barSeriesStyle": Object { - "displayValue": Object { - "fill": "rgba(105, 112, 125, 1)", - "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, - 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", - "fontSize": 8, - }, - }, - "chartMargins": Object { - "bottom": 0, - "left": 0, - "right": 0, - "top": 0, - }, - "colors": Object { - "defaultVizColor": "#6092C0", - "vizColors": Array [ - "#54B399", - "#6092C0", - "#D36086", - "#9170B8", - "#CA8EAE", - "#D6BF57", - "#B9A888", - "#DA8B45", - "#AA6556", - "#E7664C", - ], - }, - "crosshair": Object { - "band": Object { - "fill": "rgba(245, 247, 250, 1)", - }, - "line": Object { - "dash": Array [ - 4, - 4, - ], - "stroke": "rgba(105, 112, 125, 1)", - "strokeWidth": 1, - }, - }, - "lineSeriesStyle": Object { - "line": Object { - "strokeWidth": 2, - }, - "point": Object { - "fill": "rgba(255, 255, 255, 1)", - "radius": 3, - "strokeWidth": 2, - }, - }, - "scales": Object { - "barsPadding": 0.25, - "histogramPadding": 0.05, - }, - } - } - /> - <Connect(spec) - id="x" - position="bottom" - showGridLines={false} - tickFormat={[Function]} - title="c" - /> - <Connect(spec) - id="y" - position="left" - showGridLines={false} - tickFormat={[Function]} - title="" - /> - <Connect(spec) - data={ - Array [ - Object { - "Label A": 1, - "Label B": 2, - "Label D": "Foo", - "c": "I", - "d": "Foo", - }, - Object { - "Label A": 1, - "Label B": 5, - "Label D": "Bar", - "c": "J", - "d": "Bar", - }, - ] - } - enableHistogramMode={false} - id="Label D" - key="0" - splitSeriesAccessors={ - Array [ - "d", - ] - } - stackAccessors={ - Array [ - "c", - ] - } - timeZone="UTC" - xAccessor="c" - xScaleType="ordinal" - yAccessors={ - Array [ - "Label A", - "Label B", - ] - } - yScaleType="linear" - /> -</Chart> -`; - -exports[`xy_expression XYChart component it renders stacked bar 1`] = ` -<Chart - renderer="canvas" -> - <Connect(spec) - legendPosition="top" - rotation={0} - showLegend={false} - showLegendDisplayValue={false} - theme={ - Object { - "areaSeriesStyle": Object { - "area": Object { - "opacity": 0.3, - }, - "line": Object { - "strokeWidth": 2, - }, - "point": Object { - "fill": "rgba(255, 255, 255, 1)", - "radius": 3, - "strokeWidth": 2, - "visible": false, - }, - }, - "axes": Object { - "axisLineStyle": Object { - "stroke": "rgba(239, 241, 244, 1)", - }, - "axisTitleStyle": Object { - "fill": "rgba(52, 55, 65, 1)", - "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, - 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", - "fontSize": 12, - "padding": 10, - }, - "gridLineStyle": Object { - "horizontal": Object { - "dash": Array [ - 0, - 0, - ], - "opacity": 1, - "stroke": "rgba(239, 241, 244, 1)", - "strokeWidth": 1, - "visible": true, - }, - "vertical": Object { - "dash": Array [ - 4, - 4, - ], - "opacity": 1, - "stroke": "rgba(239, 241, 244, 1)", - "strokeWidth": 1, - "visible": true, - }, - }, - "tickLabelStyle": Object { - "fill": "rgba(105, 112, 125, 1)", - "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, - 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", - "fontSize": 10, - "padding": 8, - }, - "tickLineStyle": Object { - "stroke": "rgba(239, 241, 244, 1)", - "strokeWidth": 1, - "visible": false, - }, - }, - "barSeriesStyle": Object { - "displayValue": Object { - "fill": "rgba(105, 112, 125, 1)", - "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, - 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", - "fontSize": 8, - }, - }, - "chartMargins": Object { - "bottom": 0, - "left": 0, - "right": 0, - "top": 0, - }, - "colors": Object { - "defaultVizColor": "#6092C0", - "vizColors": Array [ - "#54B399", - "#6092C0", - "#D36086", - "#9170B8", - "#CA8EAE", - "#D6BF57", - "#B9A888", - "#DA8B45", - "#AA6556", - "#E7664C", - ], - }, - "crosshair": Object { - "band": Object { - "fill": "rgba(245, 247, 250, 1)", - }, - "line": Object { - "dash": Array [ - 4, - 4, - ], - "stroke": "rgba(105, 112, 125, 1)", - "strokeWidth": 1, - }, - }, - "lineSeriesStyle": Object { - "line": Object { - "strokeWidth": 2, - }, - "point": Object { - "fill": "rgba(255, 255, 255, 1)", - "radius": 3, - "strokeWidth": 2, - }, - }, - "scales": Object { - "barsPadding": 0.25, - "histogramPadding": 0.05, - }, - } - } - /> - <Connect(spec) - id="x" - position="bottom" - showGridLines={false} - tickFormat={[Function]} - title="c" - /> - <Connect(spec) - id="y" - position="left" - showGridLines={false} - tickFormat={[Function]} - title="" - /> - <Connect(spec) - data={ - Array [ - Object { - "Label A": 1, - "Label B": 2, - "Label D": "Foo", - "c": "I", - "d": "Foo", - }, - Object { - "Label A": 1, - "Label B": 5, - "Label D": "Bar", - "c": "J", - "d": "Bar", - }, - ] - } - enableHistogramMode={false} - id="Label D" - key="0" - splitSeriesAccessors={ - Array [ - "d", - ] - } - stackAccessors={ - Array [ - "c", - ] - } - timeZone="UTC" - xAccessor="c" - xScaleType="ordinal" - yAccessors={ - Array [ - "Label A", - "Label B", - ] - } - yScaleType="linear" - /> -</Chart> -`; - -exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] = ` -<Chart - renderer="canvas" -> - <Connect(spec) - legendPosition="top" - rotation={90} - showLegend={false} - showLegendDisplayValue={false} - theme={ - Object { - "areaSeriesStyle": Object { - "area": Object { - "opacity": 0.3, - }, - "line": Object { - "strokeWidth": 2, - }, - "point": Object { - "fill": "rgba(255, 255, 255, 1)", - "radius": 3, - "strokeWidth": 2, - "visible": false, - }, - }, - "axes": Object { - "axisLineStyle": Object { - "stroke": "rgba(239, 241, 244, 1)", - }, - "axisTitleStyle": Object { - "fill": "rgba(52, 55, 65, 1)", - "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, - 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", - "fontSize": 12, - "padding": 10, - }, - "gridLineStyle": Object { - "horizontal": Object { - "dash": Array [ - 0, - 0, - ], - "opacity": 1, - "stroke": "rgba(239, 241, 244, 1)", - "strokeWidth": 1, - "visible": true, - }, - "vertical": Object { - "dash": Array [ - 4, - 4, - ], - "opacity": 1, - "stroke": "rgba(239, 241, 244, 1)", - "strokeWidth": 1, - "visible": true, - }, - }, - "tickLabelStyle": Object { - "fill": "rgba(105, 112, 125, 1)", - "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, - 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", - "fontSize": 10, - "padding": 8, - }, - "tickLineStyle": Object { - "stroke": "rgba(239, 241, 244, 1)", - "strokeWidth": 1, - "visible": false, - }, - }, - "barSeriesStyle": Object { - "displayValue": Object { - "fill": "rgba(105, 112, 125, 1)", - "fontFamily": "'Inter UI', -apple-system, BlinkMacSystemFont, - 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", - "fontSize": 8, - }, - }, - "chartMargins": Object { - "bottom": 0, - "left": 0, - "right": 0, - "top": 0, - }, - "colors": Object { - "defaultVizColor": "#6092C0", - "vizColors": Array [ - "#54B399", - "#6092C0", - "#D36086", - "#9170B8", - "#CA8EAE", - "#D6BF57", - "#B9A888", - "#DA8B45", - "#AA6556", - "#E7664C", - ], - }, - "crosshair": Object { - "band": Object { - "fill": "rgba(245, 247, 250, 1)", - }, - "line": Object { - "dash": Array [ - 4, - 4, - ], - "stroke": "rgba(105, 112, 125, 1)", - "strokeWidth": 1, - }, - }, - "lineSeriesStyle": Object { - "line": Object { - "strokeWidth": 2, - }, - "point": Object { - "fill": "rgba(255, 255, 255, 1)", - "radius": 3, - "strokeWidth": 2, - }, - }, - "scales": Object { - "barsPadding": 0.25, - "histogramPadding": 0.05, - }, - } - } - /> - <Connect(spec) - id="x" - position="left" - showGridLines={false} - tickFormat={[Function]} - title="c" - /> - <Connect(spec) - id="y" - position="bottom" - showGridLines={false} - tickFormat={[Function]} - title="" - /> - <Connect(spec) - data={ - Array [ - Object { - "Label A": 1, - "Label B": 2, - "Label D": "Foo", - "c": "I", - "d": "Foo", - }, - Object { - "Label A": 1, - "Label B": 5, - "Label D": "Bar", - "c": "J", - "d": "Bar", - }, - ] - } - enableHistogramMode={false} - id="Label D" - key="0" - splitSeriesAccessors={ - Array [ - "d", - ] - } - stackAccessors={ - Array [ - "c", - ] - } - timeZone="UTC" - xAccessor="c" - xScaleType="ordinal" - yAccessors={ - Array [ - "Label A", - "Label B", - ] - } - yScaleType="linear" - /> -</Chart> -`; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/index.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/index.ts deleted file mode 100644 index f75dce9b7507f..0000000000000 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export * from './plugin'; diff --git a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/telemetry_opt_in.test.js.snap b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/telemetry_opt_in.test.js.snap deleted file mode 100644 index 575c47205f9c0..0000000000000 --- a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/telemetry_opt_in.test.js.snap +++ /dev/null @@ -1,576 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TelemetryOptIn should display when telemetry not opted in 1`] = ` -<TelemetryOptIn - intl={ - Object { - "defaultFormats": Object {}, - "defaultLocale": "en", - "formatDate": [Function], - "formatHTMLMessage": [Function], - "formatMessage": [Function], - "formatNumber": [Function], - "formatPlural": [Function], - "formatRelative": [Function], - "formatTime": [Function], - "formats": Object { - "date": Object { - "full": Object { - "day": "numeric", - "month": "long", - "weekday": "long", - "year": "numeric", - }, - "long": Object { - "day": "numeric", - "month": "long", - "year": "numeric", - }, - "medium": Object { - "day": "numeric", - "month": "short", - "year": "numeric", - }, - "short": Object { - "day": "numeric", - "month": "numeric", - "year": "2-digit", - }, - }, - "number": Object { - "currency": Object { - "style": "currency", - }, - "percent": Object { - "style": "percent", - }, - }, - "relative": Object { - "days": Object { - "units": "day", - }, - "hours": Object { - "units": "hour", - }, - "minutes": Object { - "units": "minute", - }, - "months": Object { - "units": "month", - }, - "seconds": Object { - "units": "second", - }, - "years": Object { - "units": "year", - }, - }, - "time": Object { - "full": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "long": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "medium": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - }, - "short": Object { - "hour": "numeric", - "minute": "numeric", - }, - }, - }, - "formatters": Object { - "getDateTimeFormat": [Function], - "getMessageFormat": [Function], - "getNumberFormat": [Function], - "getPluralFormat": [Function], - "getRelativeFormat": [Function], - }, - "locale": "en", - "messages": Object {}, - "now": [Function], - "onError": [Function], - "textComponent": Symbol(react.fragment), - "timeZone": null, - } - } -> - <EuiSpacer - size="s" - > - <div - className="euiSpacer euiSpacer--s" - /> - </EuiSpacer> - <EuiTitle - size="s" - > - <h4 - className="euiTitle euiTitle--small" - > - <FormattedMessage - defaultMessage="Help Elastic support provide better service" - id="xpack.licenseMgmt.telemetryOptIn.customersHelpSupportDescription" - values={Object {}} - > - Help Elastic support provide better service - </FormattedMessage> - </h4> - </EuiTitle> - <EuiSpacer - size="s" - > - <div - className="euiSpacer euiSpacer--s" - /> - </EuiSpacer> - <EuiCheckbox - checked={false} - compressed={false} - disabled={false} - id="isOptingInToTelemetry" - indeterminate={false} - label={ - <span> - <FormattedMessage - defaultMessage="Send basic feature usage statistics to Elastic periodically. {popover}" - id="xpack.licenseMgmt.telemetryOptIn.sendBasicFeatureStatisticsLabel" - values={ - Object { - "popover": <EuiPopover - anchorPosition="downCenter" - button={ - <ForwardRef - onClick={[Function]} - > - <FormattedMessage - defaultMessage="Read more" - id="xpack.licenseMgmt.telemetryOptIn.readMoreLinkText" - values={Object {}} - /> - </ForwardRef> - } - className="eui-AlignBaseline" - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="readMorePopover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - > - <EuiText - className="licManagement__narrowText" - > - <p> - <FormattedMessage - defaultMessage="This feature periodically sends basic feature usage statistics. This information will not be shared outside of Elastic. See an {exampleLink} or read our {telemetryPrivacyStatementLink}. You can disable this feature any time." - id="xpack.licenseMgmt.telemetryOptIn.featureUsageWarningMessage" - values={ - Object { - "exampleLink": <ForwardRef - onClick={[Function]} - > - <FormattedMessage - defaultMessage="example" - id="xpack.licenseMgmt.telemetryOptIn.exampleLinkText" - values={Object {}} - /> - </ForwardRef>, - "telemetryPrivacyStatementLink": <ForwardRef - href="https://www.elastic.co/legal/privacy-statement" - target="_blank" - > - <FormattedMessage - defaultMessage="telemetry privacy statement" - id="xpack.licenseMgmt.telemetryOptIn.telemetryPrivacyStatementLinkText" - values={Object {}} - /> - </ForwardRef>, - } - } - /> - </p> - </EuiText> - </EuiPopover>, - } - } - /> - </span> - } - onChange={[Function]} - > - <div - className="euiCheckbox" - > - <input - checked={false} - className="euiCheckbox__input" - disabled={false} - id="isOptingInToTelemetry" - onChange={[Function]} - type="checkbox" - /> - <div - className="euiCheckbox__square" - /> - <label - className="euiCheckbox__label" - htmlFor="isOptingInToTelemetry" - > - <span> - <FormattedMessage - defaultMessage="Send basic feature usage statistics to Elastic periodically. {popover}" - id="xpack.licenseMgmt.telemetryOptIn.sendBasicFeatureStatisticsLabel" - values={ - Object { - "popover": <EuiPopover - anchorPosition="downCenter" - button={ - <ForwardRef - onClick={[Function]} - > - <FormattedMessage - defaultMessage="Read more" - id="xpack.licenseMgmt.telemetryOptIn.readMoreLinkText" - values={Object {}} - /> - </ForwardRef> - } - className="eui-AlignBaseline" - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="readMorePopover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - > - <EuiText - className="licManagement__narrowText" - > - <p> - <FormattedMessage - defaultMessage="This feature periodically sends basic feature usage statistics. This information will not be shared outside of Elastic. See an {exampleLink} or read our {telemetryPrivacyStatementLink}. You can disable this feature any time." - id="xpack.licenseMgmt.telemetryOptIn.featureUsageWarningMessage" - values={ - Object { - "exampleLink": <ForwardRef - onClick={[Function]} - > - <FormattedMessage - defaultMessage="example" - id="xpack.licenseMgmt.telemetryOptIn.exampleLinkText" - values={Object {}} - /> - </ForwardRef>, - "telemetryPrivacyStatementLink": <ForwardRef - href="https://www.elastic.co/legal/privacy-statement" - target="_blank" - > - <FormattedMessage - defaultMessage="telemetry privacy statement" - id="xpack.licenseMgmt.telemetryOptIn.telemetryPrivacyStatementLinkText" - values={Object {}} - /> - </ForwardRef>, - } - } - /> - </p> - </EuiText> - </EuiPopover>, - } - } - > - Send basic feature usage statistics to Elastic periodically. - <EuiPopover - anchorPosition="downCenter" - button={ - <ForwardRef - onClick={[Function]} - > - <FormattedMessage - defaultMessage="Read more" - id="xpack.licenseMgmt.telemetryOptIn.readMoreLinkText" - values={Object {}} - /> - </ForwardRef> - } - className="eui-AlignBaseline" - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="readMorePopover" - isOpen={false} - ownFocus={true} - panelPaddingSize="m" - > - <EuiOutsideClickDetector - isDisabled={true} - onOutsideClick={[Function]} - > - <div - className="euiPopover euiPopover--anchorDownCenter eui-AlignBaseline" - id="readMorePopover" - onKeyDown={[Function]} - onMouseDown={[Function]} - onMouseUp={[Function]} - onTouchEnd={[Function]} - onTouchStart={[Function]} - > - <div - className="euiPopover__anchor" - > - <EuiLink - onClick={[Function]} - > - <button - className="euiLink euiLink--primary" - onClick={[Function]} - type="button" - > - <FormattedMessage - defaultMessage="Read more" - id="xpack.licenseMgmt.telemetryOptIn.readMoreLinkText" - values={Object {}} - > - Read more - </FormattedMessage> - </button> - </EuiLink> - </div> - </div> - </EuiOutsideClickDetector> - </EuiPopover> - </FormattedMessage> - </span> - </label> - </div> - </EuiCheckbox> -</TelemetryOptIn> -`; - -exports[`TelemetryOptIn should not display when telemetry is opted in 1`] = ` -<TelemetryOptIn - intl={ - Object { - "defaultFormats": Object {}, - "defaultLocale": "en", - "formatDate": [Function], - "formatHTMLMessage": [Function], - "formatMessage": [Function], - "formatNumber": [Function], - "formatPlural": [Function], - "formatRelative": [Function], - "formatTime": [Function], - "formats": Object { - "date": Object { - "full": Object { - "day": "numeric", - "month": "long", - "weekday": "long", - "year": "numeric", - }, - "long": Object { - "day": "numeric", - "month": "long", - "year": "numeric", - }, - "medium": Object { - "day": "numeric", - "month": "short", - "year": "numeric", - }, - "short": Object { - "day": "numeric", - "month": "numeric", - "year": "2-digit", - }, - }, - "number": Object { - "currency": Object { - "style": "currency", - }, - "percent": Object { - "style": "percent", - }, - }, - "relative": Object { - "days": Object { - "units": "day", - }, - "hours": Object { - "units": "hour", - }, - "minutes": Object { - "units": "minute", - }, - "months": Object { - "units": "month", - }, - "seconds": Object { - "units": "second", - }, - "years": Object { - "units": "year", - }, - }, - "time": Object { - "full": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "long": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "medium": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - }, - "short": Object { - "hour": "numeric", - "minute": "numeric", - }, - }, - }, - "formatters": Object { - "getDateTimeFormat": [Function], - "getMessageFormat": [Function], - "getNumberFormat": [Function], - "getPluralFormat": [Function], - "getRelativeFormat": [Function], - }, - "locale": "en", - "messages": Object {}, - "now": [Function], - "onError": [Function], - "textComponent": Symbol(react.fragment), - "timeZone": null, - } - } -/> -`; - -exports[`TelemetryOptIn shouldn't display when telemetry optIn status can't change 1`] = ` -<TelemetryOptIn - intl={ - Object { - "defaultFormats": Object {}, - "defaultLocale": "en", - "formatDate": [Function], - "formatHTMLMessage": [Function], - "formatMessage": [Function], - "formatNumber": [Function], - "formatPlural": [Function], - "formatRelative": [Function], - "formatTime": [Function], - "formats": Object { - "date": Object { - "full": Object { - "day": "numeric", - "month": "long", - "weekday": "long", - "year": "numeric", - }, - "long": Object { - "day": "numeric", - "month": "long", - "year": "numeric", - }, - "medium": Object { - "day": "numeric", - "month": "short", - "year": "numeric", - }, - "short": Object { - "day": "numeric", - "month": "numeric", - "year": "2-digit", - }, - }, - "number": Object { - "currency": Object { - "style": "currency", - }, - "percent": Object { - "style": "percent", - }, - }, - "relative": Object { - "days": Object { - "units": "day", - }, - "hours": Object { - "units": "hour", - }, - "minutes": Object { - "units": "minute", - }, - "months": Object { - "units": "month", - }, - "seconds": Object { - "units": "second", - }, - "years": Object { - "units": "year", - }, - }, - "time": Object { - "full": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "long": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "medium": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - }, - "short": Object { - "hour": "numeric", - "minute": "numeric", - }, - }, - }, - "formatters": Object { - "getDateTimeFormat": [Function], - "getMessageFormat": [Function], - "getNumberFormat": [Function], - "getPluralFormat": [Function], - "getRelativeFormat": [Function], - }, - "locale": "en", - "messages": Object {}, - "now": [Function], - "onError": [Function], - "textComponent": Symbol(react.fragment), - "timeZone": null, - } - } -/> -`; diff --git a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap index 353dc58e6d401..3bb8e4f8608a7 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap +++ b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap @@ -965,7 +965,6 @@ exports[`UploadLicense should display a modal when license requires acknowledgem className="euiSpacer euiSpacer--m" /> </EuiSpacer> - <TelemetryOptIn /> <EuiSpacer size="m" > @@ -1434,7 +1433,6 @@ exports[`UploadLicense should display an error when ES says license is expired 1 className="euiSpacer euiSpacer--m" /> </EuiSpacer> - <TelemetryOptIn /> <EuiSpacer size="m" > @@ -1903,7 +1901,6 @@ exports[`UploadLicense should display an error when ES says license is invalid 1 className="euiSpacer euiSpacer--m" /> </EuiSpacer> - <TelemetryOptIn /> <EuiSpacer size="m" > @@ -2368,7 +2365,6 @@ exports[`UploadLicense should display an error when submitting invalid JSON 1`] className="euiSpacer euiSpacer--m" /> </EuiSpacer> - <TelemetryOptIn /> <EuiSpacer size="m" > @@ -2837,7 +2833,6 @@ exports[`UploadLicense should display error when ES returns error 1`] = ` className="euiSpacer euiSpacer--m" /> </EuiSpacer> - <TelemetryOptIn /> <EuiSpacer size="m" > diff --git a/x-pack/legacy/plugins/license_management/__jest__/telemetry_opt_in.test.js b/x-pack/legacy/plugins/license_management/__jest__/telemetry_opt_in.test.js deleted file mode 100644 index 1b03ce869e52b..0000000000000 --- a/x-pack/legacy/plugins/license_management/__jest__/telemetry_opt_in.test.js +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { - setTelemetryEnabled, - setTelemetryOptInService, -} from '../public/np_ready/application/lib/telemetry'; -import { TelemetryOptIn } from '../public/np_ready/application/components/telemetry_opt_in'; -import { mountWithIntl } from '../../../../test_utils/enzyme_helpers'; - -jest.mock('ui/new_platform'); - -setTelemetryEnabled(true); - -describe('TelemetryOptIn', () => { - test('should display when telemetry not opted in', () => { - setTelemetryOptInService({ - getOptIn: () => false, - canChangeOptInStatus: () => true, - }); - const rendered = mountWithIntl(<TelemetryOptIn />); - expect(rendered).toMatchSnapshot(); - }); - test('should not display when telemetry is opted in', () => { - setTelemetryOptInService({ - getOptIn: () => true, - canChangeOptInStatus: () => true, - }); - const rendered = mountWithIntl(<TelemetryOptIn />); - expect(rendered).toMatchSnapshot(); - }); - test(`shouldn't display when telemetry optIn status can't change`, () => { - setTelemetryOptInService({ - getOptIn: () => false, - canChangeOptInStatus: () => false, - }); - const rendered = mountWithIntl(<TelemetryOptIn />); - expect(rendered).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/app.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/app.js index 7c497518b9df5..6a6c38fa6abb6 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/app.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/app.js @@ -18,7 +18,7 @@ export class App extends Component { } render() { - const { hasPermission, permissionsLoading, permissionsError } = this.props; + const { hasPermission, permissionsLoading, permissionsError, telemetry } = this.props; if (permissionsLoading) { return ( @@ -85,11 +85,12 @@ export class App extends Component { ); } + const withTelemetry = Component => props => <Component {...props} telemetry={telemetry} />; return ( <EuiPageBody> <Switch> - <Route path={`${BASE_PATH}upload_license`} component={UploadLicense} /> - <Route path={BASE_PATH} component={LicenseDashboard} /> + <Route path={`${BASE_PATH}upload_license`} component={withTelemetry(UploadLicense)} /> + <Route path={BASE_PATH} component={withTelemetry(LicenseDashboard)} /> </Switch> </EuiPageBody> ); diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx b/x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx index 2780b54230eba..49bb4ce984e48 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/boot.tsx @@ -11,6 +11,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import * as history from 'history'; import { DocLinksStart, HttpSetup, ToastsSetup, ChromeStart } from 'src/core/public'; +import { TelemetryPluginSetup } from 'src/plugins/telemetry/public'; // @ts-ignore import { App } from './app.container'; // @ts-ignore @@ -34,10 +35,11 @@ interface AppDependencies { toasts: ToastsSetup; docLinks: DocLinksStart; http: HttpSetup; + telemetry?: TelemetryPluginSetup; } export const boot = (deps: AppDependencies) => { - const { I18nContext, element, legacy, toasts, docLinks, http, chrome } = deps; + const { I18nContext, element, legacy, toasts, docLinks, http, chrome, telemetry } = deps; const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; const esBase = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}`; const securityDocumentationLink = `${esBase}/security-settings.html`; @@ -56,15 +58,17 @@ export const boot = (deps: AppDependencies) => { toasts, http, chrome, + telemetry, MANAGEMENT_BREADCRUMB: legacy.MANAGEMENT_BREADCRUMB, }; const store = licenseManagementStore(initialState, services); + render( <I18nContext> <Provider store={store}> <HashRouter> - <App /> + <App telemetry={telemetry} /> </HashRouter> </Provider> </I18nContext>, diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/index.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/index.ts similarity index 100% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/index.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/index.ts diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.tsx similarity index 84% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.tsx index 5e570ae955dbf..eff5c6cc21c43 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/components/telemetry_opt_in/telemetry_opt_in.tsx @@ -6,26 +6,31 @@ import React, { Fragment } from 'react'; import { EuiLink, EuiCheckbox, EuiSpacer, EuiText, EuiTitle, EuiPopover } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { - shouldShowTelemetryOptIn, - getTelemetryFetcher, - PRIVACY_STATEMENT_URL, OptInExampleFlyout, + PRIVACY_STATEMENT_URL, + TelemetryPluginSetup, } from '../../lib/telemetry'; -import { FormattedMessage } from '@kbn/i18n/react'; -export class TelemetryOptIn extends React.Component { - constructor() { - super(); - this.state = { - showMoreTelemetryInfo: false, - isOptingInToTelemetry: false, - showExample: false, - }; - } - isOptingInToTelemetry = () => { - return this.state.isOptingInToTelemetry; +interface State { + showMoreTelemetryInfo: boolean; + showExample: boolean; +} + +interface Props { + onOptInChange: (isOptingInToTelemetry: boolean) => void; + isOptingInToTelemetry: boolean; + isStartTrial: boolean; + telemetry: TelemetryPluginSetup; +} + +export class TelemetryOptIn extends React.Component<Props, State> { + state: State = { + showMoreTelemetryInfo: false, + showExample: false, }; + closeReadMorePopover = () => { this.setState({ showMoreTelemetryInfo: false }); }; @@ -37,20 +42,22 @@ export class TelemetryOptIn extends React.Component { this.setState({ showExample: true }); this.closeReadMorePopover(); }; - onChangeOptIn = event => { + onChangeOptIn = (event: any) => { const isOptingInToTelemetry = event.target.checked; - this.setState({ isOptingInToTelemetry }); + const { onOptInChange } = this.props; + onOptInChange(isOptingInToTelemetry); }; + render() { - const { showMoreTelemetryInfo, isOptingInToTelemetry, showExample } = this.state; - const { isStartTrial } = this.props; + const { showMoreTelemetryInfo, showExample } = this.state; + const { isStartTrial, isOptingInToTelemetry, telemetry } = this.props; let example = null; if (showExample) { example = ( <OptInExampleFlyout onClose={() => this.setState({ showExample: false })} - fetchTelemetry={getTelemetryFetcher} + fetchExample={telemetry.telemetryService.fetchExample} /> ); } @@ -123,7 +130,7 @@ export class TelemetryOptIn extends React.Component { </EuiPopover> ); - return shouldShowTelemetryOptIn() ? ( + return ( <Fragment> {example} {toCurrentCustomers} @@ -144,6 +151,6 @@ export class TelemetryOptIn extends React.Component { onChange={this.onChangeOptIn} /> </Fragment> - ) : null; + ); } } diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.js deleted file mode 100644 index 10da5d7705a8c..0000000000000 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { fetchTelemetry } from '../../../../../../../../src/legacy/core_plugins/telemetry/public/hacks/fetch_telemetry'; -export { PRIVACY_STATEMENT_URL } from '../../../../../../../../src/legacy/core_plugins/telemetry/common/constants'; -export { TelemetryOptInProvider } from '../../../../../../../../src/legacy/core_plugins/telemetry/public/services'; -export { OptInExampleFlyout } from '../../../../../../../../src/legacy/core_plugins/telemetry/public/components'; - -let telemetryEnabled; -let httpClient; -let telemetryOptInService; -export const setTelemetryEnabled = isTelemetryEnabled => { - telemetryEnabled = isTelemetryEnabled; -}; -export const setHttpClient = anHttpClient => { - httpClient = anHttpClient; -}; -export const setTelemetryOptInService = aTelemetryOptInService => { - telemetryOptInService = aTelemetryOptInService; -}; -export const optInToTelemetry = async enableTelemetry => { - await telemetryOptInService.setOptIn(enableTelemetry); -}; -export const shouldShowTelemetryOptIn = () => { - return ( - telemetryEnabled && - !telemetryOptInService.getOptIn() && - telemetryOptInService.canChangeOptInStatus() - ); -}; -export const getTelemetryFetcher = () => { - return fetchTelemetry(httpClient, { unencrypted: true }); -}; diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.ts b/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.ts new file mode 100644 index 0000000000000..9cc4ec5978fdc --- /dev/null +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/lib/telemetry.ts @@ -0,0 +1,24 @@ +/* + * 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 { TelemetryPluginSetup } from '../../../../../../../../src/plugins/telemetry/public'; + +export { OptInExampleFlyout } from '../../../../../../../../src/plugins/telemetry/public/components'; +export { PRIVACY_STATEMENT_URL } from '../../../../../../../../src/plugins/telemetry/common/constants'; +export { TelemetryPluginSetup, shouldShowTelemetryOptIn }; + +function shouldShowTelemetryOptIn( + telemetry?: TelemetryPluginSetup +): telemetry is TelemetryPluginSetup { + if (telemetry) { + const { telemetryService } = telemetry; + const isOptedIn = telemetryService.getIsOptedIn(); + const canChangeOptInStatus = telemetryService.getCanChangeOptInStatus(); + return canChangeOptInStatus && !isOptedIn; + } + + return false; +} diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.js index e14d392fe6706..56c307a0d76e5 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/license_dashboard.js @@ -12,7 +12,7 @@ import { AddLicense } from './add_license'; import { RequestTrialExtension } from './request_trial_extension'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -export const LicenseDashboard = ({ setBreadcrumb } = { setBreadcrumb: () => {} }) => { +export const LicenseDashboard = ({ setBreadcrumb, telemetry } = { setBreadcrumb: () => {} }) => { useEffect(() => { setBreadcrumb('dashboard'); }); @@ -25,7 +25,7 @@ export const LicenseDashboard = ({ setBreadcrumb } = { setBreadcrumb: () => {} } <EuiFlexItem> <AddLicense /> </EuiFlexItem> - <StartTrial /> + <StartTrial telemetry={telemetry} /> <RequestTrialExtension /> <RevertToBasic /> </EuiFlexGroup> diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.ts similarity index 95% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.ts index b9b33e7e3f2cb..1b3c956edc3ab 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/index.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ +// @ts-ignore export { StartTrial } from './start_trial.container'; diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.tsx similarity index 87% rename from x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.js rename to x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.tsx index 532c1d5e1a32f..e0f8ade8e45da 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/license_dashboard/start_trial/start_trial.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { Component } from 'react'; import { EuiButtonEmpty, @@ -22,32 +22,56 @@ import { EuiModalHeaderTitle, } from '@elastic/eui'; -import { TelemetryOptIn } from '../../../components/telemetry_opt_in'; -import { optInToTelemetry } from '../../../lib/telemetry'; import { FormattedMessage } from '@kbn/i18n/react'; +import { TelemetryOptIn } from '../../../components/telemetry_opt_in'; import { EXTERNAL_LINKS } from '../../../../../../common/constants'; import { getDocLinks } from '../../../lib/docs_links'; +import { TelemetryPluginSetup, shouldShowTelemetryOptIn } from '../../../lib/telemetry'; + +interface Props { + loadTrialStatus: () => void; + startLicenseTrial: () => void; + telemetry?: TelemetryPluginSetup; + shouldShowStartTrial: boolean; +} + +interface State { + showConfirmation: boolean; + isOptingInToTelemetry: boolean; +} + +export class StartTrial extends Component<Props, State> { + cancelRef: any; + confirmRef: any; + + state: State = { + showConfirmation: false, + isOptingInToTelemetry: false, + }; -export class StartTrial extends React.PureComponent { - constructor(props) { - super(props); - this.state = { showConfirmation: false }; - } UNSAFE_componentWillMount() { this.props.loadTrialStatus(); } - startLicenseTrial = () => { - const { startLicenseTrial } = this.props; - if (this.telemetryOptIn.isOptingInToTelemetry()) { - optInToTelemetry(true); + + onOptInChange = (isOptingInToTelemetry: boolean) => { + this.setState({ isOptingInToTelemetry }); + }; + + onStartLicenseTrial = () => { + const { telemetry, startLicenseTrial } = this.props; + if (this.state.isOptingInToTelemetry && telemetry) { + telemetry.telemetryService.setOptIn(true); } startLicenseTrial(); }; + cancel = () => { this.setState({ showConfirmation: false }); }; acknowledgeModal() { - const { showConfirmation } = this.state; + const { showConfirmation, isOptingInToTelemetry } = this.state; + const { telemetry } = this.props; + if (!showConfirmation) { return null; } @@ -158,12 +182,14 @@ export class StartTrial extends React.PureComponent { <EuiModalFooter> <EuiFlexGroup justifyContent="spaceBetween" alignItems="center"> <EuiFlexItem grow={false}> - <TelemetryOptIn - isStartTrial={true} - ref={ref => { - this.telemetryOptIn = ref; - }} - /> + {shouldShowTelemetryOptIn(telemetry) && ( + <TelemetryOptIn + telemetry={telemetry} + isStartTrial={true} + onOptInChange={this.onOptInChange} + isOptingInToTelemetry={isOptingInToTelemetry} + /> + )} </EuiFlexItem> <EuiFlexItem grow={false} className="licManagement__ieFlex"> <EuiFlexGroup responsive={false}> @@ -182,7 +208,7 @@ export class StartTrial extends React.PureComponent { <EuiFlexItem grow={false} className="licManagement__ieFlex"> <EuiButton data-test-subj="confirmModalConfirmButton" - onClick={this.startLicenseTrial} + onClick={this.onStartLicenseTrial} fill buttonRef={this.confirmRef} color="primary" diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/upload_license/upload_license.js b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/upload_license/upload_license.js index d814d3e42d49f..e8dd9495a8c2d 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/upload_license/upload_license.js +++ b/x-pack/legacy/plugins/license_management/public/np_ready/application/sections/upload_license/upload_license.js @@ -22,20 +22,28 @@ import { EuiPageContentBody, } from '@elastic/eui'; import { TelemetryOptIn } from '../../components/telemetry_opt_in'; -import { optInToTelemetry } from '../../lib/telemetry'; +import { shouldShowTelemetryOptIn } from '../../lib/telemetry'; import { FormattedMessage } from '@kbn/i18n/react'; export class UploadLicense extends React.PureComponent { + state = { + isOptingInToTelemetry: false, + }; + componentDidMount() { this.props.setBreadcrumb('upload'); this.props.addUploadErrorMessage(''); } + onOptInChange = isOptingInToTelemetry => { + this.setState({ isOptingInToTelemetry }); + }; send = acknowledge => { const file = this.file; const fr = new FileReader(); + fr.onload = ({ target: { result } }) => { - if (this.telemetryOptIn.isOptingInToTelemetry()) { - optInToTelemetry(true); + if (this.state.isOptingInToTelemetry) { + this.props.telemetry?.telemetryService.setOptIn(true); } this.props.uploadLicense(result, this.props.currentLicenseType, acknowledge); }; @@ -116,7 +124,8 @@ export class UploadLicense extends React.PureComponent { } }; render() { - const { currentLicenseType, applying } = this.props; + const { currentLicenseType, applying, telemetry } = this.props; + return ( <Fragment> <EuiPageContent horizontalPosition="center" verticalPosition="center"> @@ -170,11 +179,13 @@ export class UploadLicense extends React.PureComponent { </EuiFlexItem> </EuiFlexGroup> <EuiSpacer size="m" /> - <TelemetryOptIn - ref={ref => { - this.telemetryOptIn = ref; - }} - /> + {shouldShowTelemetryOptIn(telemetry) && ( + <TelemetryOptIn + isOptingInToTelemetry={this.state.isOptingInToTelemetry} + onOptInChange={this.onOptInChange} + telemetry={telemetry} + /> + )} <EuiSpacer size="m" /> <EuiFlexGroup justifyContent="spaceBetween"> <EuiFlexItem grow={false}> diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts b/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts index 1da3c942830ca..60876c9b638d1 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts +++ b/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts @@ -5,11 +5,12 @@ */ import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { TelemetryPluginSetup } from 'src/plugins/telemetry/public'; import { XPackMainPlugin } from '../../../xpack_main/server/xpack_main'; import { PLUGIN } from '../../common/constants'; import { Breadcrumb } from './application/breadcrumbs'; - export interface Plugins { + telemetry: TelemetryPluginSetup; __LEGACY: { xpackInfo: XPackMainPlugin; refreshXpack: () => void; @@ -18,7 +19,7 @@ export interface Plugins { } export class LicenseManagementUIPlugin implements Plugin<void, void, any, any> { - setup({ application, notifications, http }: CoreSetup, { __LEGACY }: Plugins) { + setup({ application, notifications, http }: CoreSetup, { __LEGACY, telemetry }: Plugins) { application.register({ id: PLUGIN.ID, title: PLUGIN.TITLE, @@ -41,6 +42,7 @@ export class LicenseManagementUIPlugin implements Plugin<void, void, any, any> { http, element, chrome, + telemetry, }); }, }); diff --git a/x-pack/legacy/plugins/license_management/public/register_route.ts b/x-pack/legacy/plugins/license_management/public/register_route.ts index fc1678a866ad3..a8f27a7236a47 100644 --- a/x-pack/legacy/plugins/license_management/public/register_route.ts +++ b/x-pack/legacy/plugins/license_management/public/register_route.ts @@ -15,15 +15,6 @@ import routes from 'ui/routes'; import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; import { plugin } from './np_ready'; - -import { - setTelemetryOptInService, - setTelemetryEnabled, - setHttpClient, - TelemetryOptInProvider, - // @ts-ignore -} from './np_ready/application/lib/telemetry'; - import { BASE_PATH } from '../common/constants'; const licenseManagementUiEnabled = chrome.getInjected('licenseManagementUiEnabled'); @@ -51,15 +42,6 @@ if (licenseManagementUiEnabled) { }); }; - const initializeTelemetry = ($injector: any) => { - const telemetryEnabled = npStart.core.injectedMetadata.getInjectedVar('telemetryEnabled'); - const Private = $injector.get('Private'); - const telemetryOptInProvider = Private(TelemetryOptInProvider); - setTelemetryOptInService(telemetryOptInProvider); - setTelemetryEnabled(telemetryEnabled); - setHttpClient($injector.get('$http')); - }; - const template = `<kbn-management-app section="elasticsearch/license_management"> <div id="licenseReactRoot"></div> </kbn-management-app>`; @@ -69,8 +51,6 @@ if (licenseManagementUiEnabled) { controllerAs: 'licenseManagement', controller: class LicenseManagementController { constructor($injector: any, $rootScope: any, $scope: any, $route: any) { - initializeTelemetry($injector); - $scope.$$postDigest(() => { const element = document.getElementById('licenseReactRoot')!; @@ -94,6 +74,7 @@ if (licenseManagementUiEnabled) { }, }, { + telemetry: (npSetup.plugins as any).telemetry, __LEGACY: { xpackInfo, refreshXpack, MANAGEMENT_BREADCRUMB }, } ); diff --git a/x-pack/legacy/plugins/maps/public/actions/map_actions.js b/x-pack/legacy/plugins/maps/public/actions/map_actions.js index 59b54c2434d17..2c6c60db9a012 100644 --- a/x-pack/legacy/plugins/maps/public/actions/map_actions.js +++ b/x-pack/legacy/plugins/maps/public/actions/map_actions.js @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import _ from 'lodash'; import turf from 'turf'; import turfBooleanContains from '@turf/boolean-contains'; +import uuid from 'uuid/v4'; import { getLayerList, getLayerListRaw, @@ -14,7 +16,7 @@ import { getMapReady, getWaitingForMapReadyLayerListRaw, getTransientLayerId, - getTooltipState, + getOpenTooltips, getQuery, } from '../selectors/map_selectors'; import { FLYOUT_STATE } from '../reducers/ui'; @@ -63,7 +65,7 @@ export const CLEAR_GOTO = 'CLEAR_GOTO'; export const TRACK_CURRENT_LAYER_STATE = 'TRACK_CURRENT_LAYER_STATE'; export const ROLLBACK_TO_TRACKED_LAYER_STATE = 'ROLLBACK_TO_TRACKED_LAYER_STATE'; export const REMOVE_TRACKED_LAYER_STATE = 'REMOVE_TRACKED_LAYER_STATE'; -export const SET_TOOLTIP_STATE = 'SET_TOOLTIP_STATE'; +export const SET_OPEN_TOOLTIPS = 'SET_OPEN_TOOLTIPS'; export const UPDATE_DRAW_STATE = 'UPDATE_DRAW_STATE'; export const SET_SCROLL_ZOOM = 'SET_SCROLL_ZOOM'; export const SET_MAP_INIT_ERROR = 'SET_MAP_INIT_ERROR'; @@ -221,34 +223,36 @@ function setLayerDataLoadErrorStatus(layerId, errorMessage) { export function cleanTooltipStateForLayer(layerId, layerFeatures = []) { return (dispatch, getState) => { - const tooltipState = getTooltipState(getState()); - - if (!tooltipState) { - return; - } - - const nextTooltipFeatures = tooltipState.features.filter(tooltipFeature => { - if (tooltipFeature.layerId !== layerId) { - // feature from another layer, keep it - return true; - } - - // Keep feature if it is still in layer - return layerFeatures.some(layerFeature => { - return layerFeature.properties[FEATURE_ID_PROPERTY_NAME] === tooltipFeature.id; + let featuresRemoved = false; + const openTooltips = getOpenTooltips(getState()) + .map(tooltipState => { + const nextFeatures = tooltipState.features.filter(tooltipFeature => { + if (tooltipFeature.layerId !== layerId) { + // feature from another layer, keep it + return true; + } + + // Keep feature if it is still in layer + return layerFeatures.some(layerFeature => { + return layerFeature.properties[FEATURE_ID_PROPERTY_NAME] === tooltipFeature.id; + }); + }); + + if (tooltipState.features.length !== nextFeatures.length) { + featuresRemoved = true; + } + + return { ...tooltipState, features: nextFeatures }; + }) + .filter(tooltipState => { + return tooltipState.features.length > 0; }); - }); - - if (tooltipState.features.length === nextTooltipFeatures.length) { - // no features got removed, nothing to update - return; - } - if (nextTooltipFeatures.length === 0) { - // all features removed from tooltip, close tooltip - dispatch(setTooltipState(null)); - } else { - dispatch(setTooltipState({ ...tooltipState, features: nextTooltipFeatures })); + if (featuresRemoved) { + dispatch({ + type: SET_OPEN_TOOLTIPS, + openTooltips, + }); } }; } @@ -412,10 +416,61 @@ export function mapExtentChanged(newMapConstants) { }; } -export function setTooltipState(tooltipState) { +export function closeOnClickTooltip(tooltipId) { + return (dispatch, getState) => { + dispatch({ + type: SET_OPEN_TOOLTIPS, + openTooltips: getOpenTooltips(getState()).filter(({ id }) => { + return tooltipId !== id; + }), + }); + }; +} + +export function openOnClickTooltip(tooltipState) { + return (dispatch, getState) => { + const openTooltips = getOpenTooltips(getState()).filter(({ features, location, isLocked }) => { + return ( + isLocked && + !_.isEqual(location, tooltipState.location) && + !_.isEqual(features, tooltipState.features) + ); + }); + + openTooltips.push({ + ...tooltipState, + isLocked: true, + id: uuid(), + }); + + dispatch({ + type: SET_OPEN_TOOLTIPS, + openTooltips, + }); + }; +} + +export function closeOnHoverTooltip() { + return (dispatch, getState) => { + if (getOpenTooltips(getState()).length) { + dispatch({ + type: SET_OPEN_TOOLTIPS, + openTooltips: [], + }); + } + }; +} + +export function openOnHoverTooltip(tooltipState) { return { - type: 'SET_TOOLTIP_STATE', - tooltipState: tooltipState, + type: SET_OPEN_TOOLTIPS, + openTooltips: [ + { + ...tooltipState, + isLocked: false, + id: uuid(), + }, + ], }; } @@ -826,9 +881,9 @@ export function setJoinsForLayer(layer, joins) { } export function updateDrawState(drawState) { - return async dispatch => { + return dispatch => { if (drawState !== null) { - await dispatch(setTooltipState(null)); //tooltips just get in the way + dispatch({ type: SET_OPEN_TOOLTIPS, openTooltips: [] }); // tooltips just get in the way } dispatch({ type: UPDATE_DRAW_STATE, diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js index 0274f849daf3d..9148fbdfd2d1e 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js @@ -16,7 +16,6 @@ import { setMapInitError, } from '../../../actions/map_actions'; import { - getTooltipState, getLayerList, getMapReady, getGoto, @@ -33,7 +32,6 @@ function mapStateToProps(state = {}) { layerList: getLayerList(state), goto: getGoto(state), inspectorAdapters: getInspectorAdapters(state), - tooltipState: getTooltipState(state), scrollZoom: getScrollZoom(state), disableInteractive: isInteractiveDisabled(state), disableTooltipControl: isTooltipControlDisabled(state), diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap index 7e8feeec01bbd..cffa441d04ff5 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap @@ -1,117 +1,97 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`TooltipControl render tooltipState is not provided should not render tooltip popover when tooltipState is not provided 1`] = `""`; +exports[`TooltipControl render should not render tooltips when there are no open tooltips 1`] = `""`; -exports[`TooltipControl render tooltipState is provided should render tooltip popover with custom tooltip content when renderTooltipContent provided 1`] = ` -<EuiPopover - anchorPosition="upCenter" - button={ - <div - style={ - Object { - "background": "transparent", - "height": "26px", - "width": "26px", - } - } - /> +exports[`TooltipControl render should render hover tooltip 1`] = ` +<TooltipPopover + addFilters={[Function]} + closeTooltip={[Function]} + features={ + Array [ + Object { + "geometry": Object {}, + "id": 1, + "layerId": "tfi3f", + }, + ] + } + geoFields={ + Array [ + Object {}, + ] + } + index={0} + isLocked={false} + key="1" + layerList={ + Array [ + Object { + "canShowTooltip": [Function], + "getId": [Function], + "getMbLayerIds": [Function], + }, + ] } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="mapTooltip" - isOpen={true} - ownFocus={false} - panelPaddingSize="m" - style={ + location={ + Array [ + -120, + 30, + ] + } + mbMap={ Object { - "pointerEvents": "none", - "transform": "translate(11987px, 2987px)", + "getLayer": [Function], + "off": [Function], + "on": [Function], + "queryRenderedFeatures": [Function], } } -> - <div - addFilters={[Function]} - closeTooltip={[Function]} - features={ - Array [ - Object { - "geometry": Object {}, - "id": 1, - "layerId": "tfi3f", - }, - ] - } - getLayerName={[Function]} - isLocked={false} - loadFeatureGeometry={[Function]} - loadFeatureProperties={[Function]} - > - Custom tooltip content - </div> -</EuiPopover> +/> `; -exports[`TooltipControl render tooltipState is provided should render tooltip popover with features tooltip content 1`] = ` -<EuiPopover - anchorPosition="upCenter" - button={ - <div - style={ - Object { - "background": "transparent", - "height": "26px", - "width": "26px", - } - } - /> +exports[`TooltipControl render should render locked tooltip 1`] = ` +<TooltipPopover + addFilters={[Function]} + closeTooltip={[Function]} + features={ + Array [ + Object { + "geometry": Object {}, + "id": 1, + "layerId": "tfi3f", + }, + ] } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="mapTooltip" - isOpen={true} - ownFocus={false} - panelPaddingSize="m" - style={ - Object { - "pointerEvents": "none", - "transform": "translate(11987px, 2987px)", - } + geoFields={ + Array [ + Object {}, + ] } -> - <EuiText - size="xs" - style={ + index={0} + isLocked={true} + key="2" + layerList={ + Array [ Object { - "maxWidth": "425px", - } + "canShowTooltip": [Function], + "getId": [Function], + "getMbLayerIds": [Function], + }, + ] + } + location={ + Array [ + -120, + 30, + ] + } + mbMap={ + Object { + "getLayer": [Function], + "off": [Function], + "on": [Function], + "queryRenderedFeatures": [Function], } - > - <FeaturesTooltip - addFilters={[Function]} - closeTooltip={[Function]} - features={ - Array [ - Object { - "geometry": Object {}, - "id": 1, - "layerId": "tfi3f", - }, - ] - } - findLayerById={[Function]} - geoFields={ - Array [ - Object {}, - ] - } - getLayerName={[Function]} - isLocked={false} - loadFeatureGeometry={[Function]} - loadFeatureProperties={[Function]} - loadPreIndexedShape={[Function]} - /> - </EuiText> -</EuiPopover> + } +/> `; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_popover.test.js.snap b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_popover.test.js.snap new file mode 100644 index 0000000000000..d95a418988ae7 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_popover.test.js.snap @@ -0,0 +1,115 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TooltipPopover render should render tooltip popover 1`] = ` +<EuiPopover + anchorPosition="upCenter" + button={ + <div + style={ + Object { + "background": "none", + "height": "26px", + "width": "26px", + } + } + /> + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="mapTooltip" + isOpen={true} + ownFocus={false} + panelPaddingSize="m" + style={ + Object { + "pointerEvents": "none", + "transform": "translate(NaNpx, 2987px)", + } + } +> + <EuiText + size="xs" + style={ + Object { + "maxWidth": "425px", + } + } + > + <FeaturesTooltip + addFilters={[Function]} + closeTooltip={[Function]} + features={ + Array [ + Object { + "geometry": Object {}, + "id": 1, + "layerId": "tfi3f", + }, + ] + } + findLayerById={[Function]} + geoFields={ + Array [ + Object {}, + ] + } + getLayerName={[Function]} + isLocked={false} + loadFeatureGeometry={[Function]} + loadFeatureProperties={[Function]} + loadPreIndexedShape={[Function]} + /> + </EuiText> +</EuiPopover> +`; + +exports[`TooltipPopover render should render tooltip popover with custom tooltip content when renderTooltipContent provided 1`] = ` +<EuiPopover + anchorPosition="upCenter" + button={ + <div + style={ + Object { + "background": "none", + "height": "26px", + "width": "26px", + } + } + /> + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="mapTooltip" + isOpen={true} + ownFocus={false} + panelPaddingSize="m" + style={ + Object { + "pointerEvents": "none", + "transform": "translate(NaNpx, 2987px)", + } + } +> + <div + addFilters={[Function]} + closeTooltip={[Function]} + features={ + Array [ + Object { + "geometry": Object {}, + "id": 1, + "layerId": "tfi3f", + }, + ] + } + getLayerName={[Function]} + isLocked={false} + loadFeatureGeometry={[Function]} + loadFeatureProperties={[Function]} + > + Custom tooltip content + </div> +</EuiPopover> +`; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js index 6bc9511c6c580..d3cdbfeca3e57 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js @@ -6,28 +6,41 @@ import { connect } from 'react-redux'; import { TooltipControl } from './tooltip_control'; -import { setTooltipState } from '../../../../actions/map_actions'; +import { + closeOnClickTooltip, + openOnClickTooltip, + closeOnHoverTooltip, + openOnHoverTooltip, +} from '../../../../actions/map_actions'; import { getLayerList, - getTooltipState, + getOpenTooltips, + getHasLockedTooltips, isDrawingFilter, } from '../../../../selectors/map_selectors'; function mapStateToProps(state = {}) { return { layerList: getLayerList(state), - tooltipState: getTooltipState(state), + hasLockedTooltips: getHasLockedTooltips(state), isDrawingFilter: isDrawingFilter(state), + openTooltips: getOpenTooltips(state), }; } function mapDispatchToProps(dispatch) { return { - setTooltipState(tooltipState) { - dispatch(setTooltipState(tooltipState)); + closeOnClickTooltip(tooltipId) { + dispatch(closeOnClickTooltip(tooltipId)); + }, + openOnClickTooltip(tooltipState) { + dispatch(openOnClickTooltip(tooltipState)); + }, + closeOnHoverTooltip() { + dispatch(closeOnHoverTooltip()); }, - clearTooltipState() { - dispatch(setTooltipState(null)); + openOnHoverTooltip(tooltipState) { + dispatch(openOnHoverTooltip(tooltipState)); }, }; } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js index cfb92a8677455..329d2b7fd2985 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js @@ -6,16 +6,8 @@ import _ from 'lodash'; import React from 'react'; -import { FEATURE_ID_PROPERTY_NAME, LAT_INDEX, LON_INDEX } from '../../../../../common/constants'; -import { FeaturesTooltip } from '../../features_tooltip/features_tooltip'; -import { EuiPopover, EuiText } from '@elastic/eui'; - -export const TOOLTIP_TYPE = { - HOVER: 'HOVER', - LOCKED: 'LOCKED', -}; - -const noop = () => {}; +import { FEATURE_ID_PROPERTY_NAME, LON_INDEX } from '../../../../../common/constants'; +import { TooltipPopover } from './tooltip_popover'; function justifyAnchorLocation(mbLngLat, targetFeature) { let popupAnchorLocation = [mbLngLat.lng, mbLngLat.lat]; // default popup location to mouse location @@ -35,80 +27,23 @@ function justifyAnchorLocation(mbLngLat, targetFeature) { } export class TooltipControl extends React.Component { - state = { - x: undefined, - y: undefined, - }; - - constructor(props) { - super(props); - this._popoverRef = React.createRef(); - } - - static getDerivedStateFromProps(nextProps, prevState) { - if (nextProps.tooltipState) { - const nextPoint = nextProps.mbMap.project(nextProps.tooltipState.location); - if (nextPoint.x !== prevState.x || nextPoint.y !== prevState.y) { - return { - x: nextPoint.x, - y: nextPoint.y, - }; - } - } - - return null; - } - componentDidMount() { this.props.mbMap.on('mouseout', this._onMouseout); this.props.mbMap.on('mousemove', this._updateHoverTooltipState); - this.props.mbMap.on('move', this._updatePopoverPosition); this.props.mbMap.on('click', this._lockTooltip); } - componentDidUpdate() { - if (this.props.tooltipState && this._popoverRef.current) { - this._popoverRef.current.positionPopoverFluid(); - } - } - componentWillUnmount() { this.props.mbMap.off('mouseout', this._onMouseout); this.props.mbMap.off('mousemove', this._updateHoverTooltipState); - this.props.mbMap.off('move', this._updatePopoverPosition); this.props.mbMap.off('click', this._lockTooltip); } _onMouseout = () => { this._updateHoverTooltipState.cancel(); - if (this.props.tooltipState && this.props.tooltipState.type !== TOOLTIP_TYPE.LOCKED) { - this.props.clearTooltipState(); - } - }; - - _updatePopoverPosition = () => { - if (!this.props.tooltipState) { - return; + if (!this.props.hasLockedTooltips) { + this.props.closeOnHoverTooltip(); } - - const lat = this.props.tooltipState.location[LAT_INDEX]; - const lon = this.props.tooltipState.location[LON_INDEX]; - const bounds = this.props.mbMap.getBounds(); - if ( - lat > bounds.getNorth() || - lat < bounds.getSouth() || - lon < bounds.getWest() || - lon > bounds.getEast() - ) { - this.props.clearTooltipState(); - return; - } - - const nextPoint = this.props.mbMap.project(this.props.tooltipState.location); - this.setState({ - x: nextPoint.x, - y: nextPoint.y, - }); }; _getLayerByMbLayerId(mbLayerId) { @@ -148,7 +83,7 @@ export class TooltipControl extends React.Component { _lockTooltip = e => { if (this.props.isDrawingFilter) { - //ignore click events when in draw mode + // ignore click events when in draw mode return; } @@ -156,7 +91,7 @@ export class TooltipControl extends React.Component { const mbFeatures = this._getFeaturesUnderPointer(e.point); if (!mbFeatures.length) { - this.props.clearTooltipState(); + // No features at click location so there is no tooltip to open return; } @@ -164,42 +99,36 @@ export class TooltipControl extends React.Component { const popupAnchorLocation = justifyAnchorLocation(e.lngLat, targetMbFeataure); const features = this._getIdsForFeatures(mbFeatures); - this.props.setTooltipState({ - type: TOOLTIP_TYPE.LOCKED, + this.props.openOnClickTooltip({ features: features, location: popupAnchorLocation, }); }; _updateHoverTooltipState = _.debounce(e => { - if (this.props.isDrawingFilter) { - //ignore hover events when in draw mode - return; - } - - if (this.props.tooltipState && this.props.tooltipState.type === TOOLTIP_TYPE.LOCKED) { - //ignore hover events when tooltip is locked + if (this.props.isDrawingFilter || this.props.hasLockedTooltips) { + // ignore hover events when in draw mode or when there are locked tooltips return; } const mbFeatures = this._getFeaturesUnderPointer(e.point); if (!mbFeatures.length) { - this.props.clearTooltipState(); + this.props.closeOnHoverTooltip(); return; } const targetMbFeature = mbFeatures[0]; - if (this.props.tooltipState) { - const firstFeature = this.props.tooltipState.features[0]; + if (this.props.openTooltips[0]) { + const firstFeature = this.props.openTooltips[0].features[0]; if (targetMbFeature.properties[FEATURE_ID_PROPERTY_NAME] === firstFeature.id) { + // ignore hover events when hover tooltip is all ready opened for feature return; } } const popupAnchorLocation = justifyAnchorLocation(e.lngLat, targetMbFeature); const features = this._getIdsForFeatures(mbFeatures); - this.props.setTooltipState({ - type: TOOLTIP_TYPE.HOVER, + this.props.openOnHoverTooltip({ features: features, location: popupAnchorLocation, }); @@ -240,114 +169,32 @@ export class TooltipControl extends React.Component { return this.props.mbMap.queryRenderedFeatures(mbBbox, { layers: mbLayerIds }); } - // Must load original geometry instead of using geometry from mapbox feature. - // Mapbox feature geometry is from vector tile and is not the same as the original geometry. - _loadFeatureGeometry = ({ layerId, featureId }) => { - const tooltipLayer = this._findLayerById(layerId); - if (!tooltipLayer) { - return null; - } - - const targetFeature = tooltipLayer.getFeatureById(featureId); - if (!targetFeature) { - return null; - } - - return targetFeature.geometry; - }; - - _loadFeatureProperties = async ({ layerId, featureId }) => { - const tooltipLayer = this._findLayerById(layerId); - if (!tooltipLayer) { - return []; - } - - const targetFeature = tooltipLayer.getFeatureById(featureId); - if (!targetFeature) { - return []; - } - return await tooltipLayer.getPropertiesForTooltip(targetFeature.properties); - }; - - _loadPreIndexedShape = async ({ layerId, featureId }) => { - const tooltipLayer = this._findLayerById(layerId); - if (!tooltipLayer) { - return null; - } - - const targetFeature = tooltipLayer.getFeatureById(featureId); - if (!targetFeature) { - return null; - } - - return await tooltipLayer.getSource().getPreIndexedShape(targetFeature.properties); - }; - - _findLayerById = layerId => { - return this.props.layerList.find(layer => { - return layer.getId() === layerId; - }); - }; - - _getLayerName = async layerId => { - const layer = this._findLayerById(layerId); - if (!layer) { + render() { + if (this.props.openTooltips.length === 0) { return null; } - return layer.getDisplayName(); - }; - - _renderTooltipContent = () => { - const publicProps = { - addFilters: this.props.addFilters, - closeTooltip: this.props.clearTooltipState, - features: this.props.tooltipState.features, - isLocked: this.props.tooltipState.type === TOOLTIP_TYPE.LOCKED, - loadFeatureProperties: this._loadFeatureProperties, - loadFeatureGeometry: this._loadFeatureGeometry, - getLayerName: this._getLayerName, - }; - - if (this.props.renderTooltipContent) { - return this.props.renderTooltipContent(publicProps); - } - - return ( - <EuiText size="xs" style={{ maxWidth: '425px' }}> - <FeaturesTooltip - {...publicProps} - findLayerById={this._findLayerById} + return this.props.openTooltips.map(({ features, location, id, isLocked }, index) => { + const closeTooltip = isLocked + ? () => { + this.props.closeOnClickTooltip(id); + } + : this.props.closeOnHoverTooltip; + return ( + <TooltipPopover + key={id} + mbMap={this.props.mbMap} + layerList={this.props.layerList} + addFilters={this.props.addFilters} + renderTooltipContent={this.props.renderTooltipContent} geoFields={this.props.geoFields} - loadPreIndexedShape={this._loadPreIndexedShape} + features={features} + location={location} + closeTooltip={closeTooltip} + isLocked={isLocked} + index={index} /> - </EuiText> - ); - }; - - render() { - if (!this.props.tooltipState) { - return null; - } - - const tooltipAnchor = ( - <div style={{ height: '26px', width: '26px', background: 'transparent' }} /> - ); - return ( - <EuiPopover - id="mapTooltip" - button={tooltipAnchor} - anchorPosition="upCenter" - isOpen - closePopover={noop} - ref={this._popoverRef} - style={{ - pointerEvents: 'none', - transform: `translate(${this.state.x - 13}px, ${this.state.y - 13}px)`, - }} - > - {this._renderTooltipContent()} - </EuiPopover> - ); + ); + }); } } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js index b9dc668cfb016..620d7cb9ff756 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js @@ -4,21 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../../features_tooltip/features_tooltip', () => ({ - FeaturesTooltip: () => { - return <div>mockFeaturesTooltip</div>; +jest.mock('./tooltip_popover', () => ({ + TooltipPopover: () => { + return <div>mockTooltipPopover</div>; }, })); import sinon from 'sinon'; import React from 'react'; import { mount, shallow } from 'enzyme'; -import { TooltipControl, TOOLTIP_TYPE } from './tooltip_control'; +import { TooltipControl } from './tooltip_control'; // mutable map state let featuresAtLocation; -let mapCenter; -let mockMbMapBounds; const layerId = 'tfi3f'; const mbLayerId = 'tfi3f_circle'; @@ -32,48 +30,16 @@ const mockLayer = { canShowTooltip: () => { return true; }, - getFeatureById: () => { - return { - geometry: { - type: 'Point', - coordinates: [102.0, 0.5], - }, - }; - }, }; const mockMbMapHandlers = {}; const mockMBMap = { - project: lonLatArray => { - const lonDistanceFromCenter = Math.abs(lonLatArray[0] - mapCenter[0]); - const latDistanceFromCenter = Math.abs(lonLatArray[1] - mapCenter[1]); - return { - x: lonDistanceFromCenter * 100, - y: latDistanceFromCenter * 100, - }; - }, on: (eventName, callback) => { mockMbMapHandlers[eventName] = callback; }, off: eventName => { delete mockMbMapHandlers[eventName]; }, - getBounds: () => { - return { - getNorth: () => { - return mockMbMapBounds.north; - }, - getSouth: () => { - return mockMbMapBounds.south; - }, - getWest: () => { - return mockMbMapBounds.west; - }, - getEast: () => { - return mockMbMapBounds.east; - }, - }; - }, getLayer: () => {}, queryRenderedFeatures: () => { return featuresAtLocation; @@ -82,16 +48,21 @@ const mockMBMap = { const defaultProps = { mbMap: mockMBMap, - clearTooltipState: () => {}, - setTooltipState: () => {}, + closeOnClickTooltip: () => {}, + openOnClickTooltip: () => {}, + closeOnHoverTooltip: () => {}, + openOnHoverTooltip: () => {}, layerList: [mockLayer], isDrawingFilter: false, addFilters: () => {}, geoFields: [{}], + openTooltips: [], + hasLockedTooltips: false, }; const hoverTooltipState = { - type: TOOLTIP_TYPE.HOVER, + id: '1', + isLocked: false, location: [-120, 30], features: [ { @@ -103,7 +74,8 @@ const hoverTooltipState = { }; const lockedTooltipState = { - type: TOOLTIP_TYPE.LOCKED, + id: '2', + isLocked: true, location: [-120, 30], features: [ { @@ -117,82 +89,79 @@ const lockedTooltipState = { describe('TooltipControl', () => { beforeEach(() => { featuresAtLocation = []; - mapCenter = [0, 0]; - mockMbMapBounds = { - west: -180, - east: 180, - north: 90, - south: -90, - }; }); describe('render', () => { - describe('tooltipState is not provided', () => { - test('should not render tooltip popover when tooltipState is not provided', () => { - const component = shallow(<TooltipControl {...defaultProps} />); + test('should not render tooltips when there are no open tooltips', () => { + const component = shallow(<TooltipControl {...defaultProps} />); - expect(component).toMatchSnapshot(); - }); + expect(component).toMatchSnapshot(); }); - describe('tooltipState is provided', () => { - test('should render tooltip popover with features tooltip content', () => { - const component = shallow( - <TooltipControl {...defaultProps} tooltipState={hoverTooltipState} /> - ); + test('should render hover tooltip', () => { + const component = shallow( + <TooltipControl {...defaultProps} openTooltips={[hoverTooltipState]} /> + ); - expect(component).toMatchSnapshot(); - }); + expect(component).toMatchSnapshot(); + }); - test('should render tooltip popover with custom tooltip content when renderTooltipContent provided', () => { - const component = shallow( - <TooltipControl - {...defaultProps} - tooltipState={hoverTooltipState} - renderTooltipContent={props => { - return <div {...props}>Custom tooltip content</div>; - }} - /> - ); - - expect(component).toMatchSnapshot(); - }); + test('should render locked tooltip', () => { + const component = shallow( + <TooltipControl + {...defaultProps} + hasLockedTooltips={true} + openTooltips={[lockedTooltipState]} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + test('should un-register all map callbacks on unmount', () => { + const component = mount(<TooltipControl {...defaultProps} />); + + expect(Object.keys(mockMbMapHandlers).length).toBe(3); + + component.unmount(); + expect(Object.keys(mockMbMapHandlers).length).toBe(0); }); }); describe('on mouse out', () => { - const clearTooltipStateStub = sinon.stub(); + const closeOnHoverTooltipStub = sinon.stub(); beforeEach(() => { - clearTooltipStateStub.reset(); + closeOnHoverTooltipStub.reset(); }); test('should clear hover tooltip state', () => { mount( <TooltipControl {...defaultProps} - clearTooltipState={clearTooltipStateStub} - tooltipState={hoverTooltipState} + closeOnHoverTooltip={closeOnHoverTooltipStub} + openTooltips={[hoverTooltipState]} /> ); mockMbMapHandlers.mouseout(); - sinon.assert.calledOnce(clearTooltipStateStub); + sinon.assert.calledOnce(closeOnHoverTooltipStub); }); test('should not clear locked tooltip state', () => { mount( <TooltipControl {...defaultProps} - clearTooltipState={clearTooltipStateStub} - tooltipState={lockedTooltipState} + closeOnHoverTooltip={closeOnHoverTooltipStub} + hasLockedTooltips={true} + openTooltips={[lockedTooltipState]} /> ); mockMbMapHandlers.mouseout(); - sinon.assert.notCalled(clearTooltipStateStub); + sinon.assert.notCalled(closeOnHoverTooltipStub); }); }); @@ -201,44 +170,44 @@ describe('TooltipControl', () => { point: { x: 0, y: 0 }, lngLat: { lng: 0, lat: 0 }, }; - const setTooltipStateStub = sinon.stub(); - const clearTooltipStateStub = sinon.stub(); + const openOnClickTooltipStub = sinon.stub(); + const closeOnClickTooltipStub = sinon.stub(); beforeEach(() => { - setTooltipStateStub.reset(); - clearTooltipStateStub.reset(); + openOnClickTooltipStub.reset(); + closeOnClickTooltipStub.reset(); }); test('should ignore clicks when map is in drawing mode', () => { mount( <TooltipControl {...defaultProps} - clearTooltipState={clearTooltipStateStub} - setTooltipState={setTooltipStateStub} + closeOnClickTooltip={closeOnClickTooltipStub} + openOnClickTooltip={openOnClickTooltipStub} isDrawingFilter={true} /> ); mockMbMapHandlers.click(mockMapMouseEvent); - sinon.assert.notCalled(clearTooltipStateStub); - sinon.assert.notCalled(setTooltipStateStub); + sinon.assert.notCalled(closeOnClickTooltipStub); + sinon.assert.notCalled(openOnClickTooltipStub); }); - test('should clear tooltip state when there are no features at clicked location', () => { + test('should not open tooltip when there are no features at clicked location', () => { featuresAtLocation = []; mount( <TooltipControl {...defaultProps} - clearTooltipState={clearTooltipStateStub} - setTooltipState={setTooltipStateStub} + closeOnClickTooltip={closeOnClickTooltipStub} + openOnClickTooltip={openOnClickTooltipStub} /> ); mockMbMapHandlers.click(mockMapMouseEvent); - sinon.assert.calledOnce(clearTooltipStateStub); - sinon.assert.notCalled(setTooltipStateStub); + sinon.assert.notCalled(closeOnClickTooltipStub); + sinon.assert.notCalled(openOnClickTooltipStub); }); test('should set tooltip state when there are features at clicked location and remove duplicate features', () => { @@ -258,93 +227,18 @@ describe('TooltipControl', () => { mount( <TooltipControl {...defaultProps} - clearTooltipState={clearTooltipStateStub} - setTooltipState={setTooltipStateStub} + closeOnClickTooltip={closeOnClickTooltipStub} + openOnClickTooltip={openOnClickTooltipStub} /> ); mockMbMapHandlers.click(mockMapMouseEvent); - sinon.assert.notCalled(clearTooltipStateStub); - sinon.assert.calledWith(setTooltipStateStub, { + sinon.assert.notCalled(closeOnClickTooltipStub); + sinon.assert.calledWith(openOnClickTooltipStub, { features: [{ id: 1, layerId: 'tfi3f' }], location: [100, 30], - type: 'LOCKED', }); }); }); - - describe('on map move', () => { - const clearTooltipStateStub = sinon.stub(); - - beforeEach(() => { - clearTooltipStateStub.reset(); - }); - - test('should safely handle map move when there is no tooltip location', () => { - const component = mount( - <TooltipControl {...defaultProps} clearTooltipState={clearTooltipStateStub} /> - ); - - mockMbMapHandlers.move(); - component.update(); - - sinon.assert.notCalled(clearTooltipStateStub); - }); - - test('should update popover location', () => { - const component = mount( - <TooltipControl - {...defaultProps} - tooltipState={hoverTooltipState} - clearTooltipState={clearTooltipStateStub} - /> - ); - - // ensure x and y set from original tooltipState.location - expect(component.state('x')).toBe(12000); - expect(component.state('y')).toBe(3000); - - mapCenter = [25, -15]; - mockMbMapHandlers.move(); - component.update(); - - // ensure x and y updated from new map center with same tooltipState.location - expect(component.state('x')).toBe(14500); - expect(component.state('y')).toBe(4500); - - sinon.assert.notCalled(clearTooltipStateStub); - }); - - test('should clear tooltip state if tooltip location is outside map bounds', () => { - const component = mount( - <TooltipControl - {...defaultProps} - tooltipState={hoverTooltipState} - clearTooltipState={clearTooltipStateStub} - /> - ); - - // move map bounds outside of hoverTooltipState.location, which is [-120, 30] - mockMbMapBounds = { - west: -180, - east: -170, - north: 90, - south: 80, - }; - mockMbMapHandlers.move(); - component.update(); - - sinon.assert.calledOnce(clearTooltipStateStub); - }); - }); - - test('should un-register all map callbacks on unmount', () => { - const component = mount(<TooltipControl {...defaultProps} />); - - expect(Object.keys(mockMbMapHandlers).length).toBe(4); - - component.unmount(); - expect(Object.keys(mockMbMapHandlers).length).toBe(0); - }); }); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js new file mode 100644 index 0000000000000..867c779bc4dba --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js @@ -0,0 +1,169 @@ +/* + * 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, { Component } from 'react'; +import { LAT_INDEX, LON_INDEX } from '../../../../../common/constants'; +import { FeaturesTooltip } from '../../features_tooltip/features_tooltip'; +import { EuiPopover, EuiText } from '@elastic/eui'; + +const noop = () => {}; + +export class TooltipPopover extends Component { + state = { + x: undefined, + y: undefined, + isVisible: true, + }; + + constructor(props) { + super(props); + this._popoverRef = React.createRef(); + } + + componentDidMount() { + this._updatePopoverPosition(); + this.props.mbMap.on('move', this._updatePopoverPosition); + } + + componentDidUpdate() { + if (this._popoverRef.current) { + this._popoverRef.current.positionPopoverFluid(); + } + } + + componentWillUnmount() { + this.props.mbMap.off('move', this._updatePopoverPosition); + } + + _updatePopoverPosition = () => { + const nextPoint = this.props.mbMap.project(this.props.location); + const lat = this.props.location[LAT_INDEX]; + const lon = this.props.location[LON_INDEX]; + const bounds = this.props.mbMap.getBounds(); + this.setState({ + x: nextPoint.x, + y: nextPoint.y, + isVisible: + lat < bounds.getNorth() && + lat > bounds.getSouth() && + lon > bounds.getWest() && + lon < bounds.getEast(), + }); + }; + + // Must load original geometry instead of using geometry from mapbox feature. + // Mapbox feature geometry is from vector tile and is not the same as the original geometry. + _loadFeatureGeometry = ({ layerId, featureId }) => { + const tooltipLayer = this._findLayerById(layerId); + if (!tooltipLayer) { + return null; + } + + const targetFeature = tooltipLayer.getFeatureById(featureId); + if (!targetFeature) { + return null; + } + + return targetFeature.geometry; + }; + + _loadFeatureProperties = async ({ layerId, featureId }) => { + const tooltipLayer = this._findLayerById(layerId); + if (!tooltipLayer) { + return []; + } + + const targetFeature = tooltipLayer.getFeatureById(featureId); + if (!targetFeature) { + return []; + } + return await tooltipLayer.getPropertiesForTooltip(targetFeature.properties); + }; + + _loadPreIndexedShape = async ({ layerId, featureId }) => { + const tooltipLayer = this._findLayerById(layerId); + if (!tooltipLayer) { + return null; + } + + const targetFeature = tooltipLayer.getFeatureById(featureId); + if (!targetFeature) { + return null; + } + + return await tooltipLayer.getSource().getPreIndexedShape(targetFeature.properties); + }; + + _findLayerById = layerId => { + return this.props.layerList.find(layer => { + return layer.getId() === layerId; + }); + }; + + _getLayerName = async layerId => { + const layer = this._findLayerById(layerId); + if (!layer) { + return null; + } + + return layer.getDisplayName(); + }; + + _renderTooltipContent = () => { + const publicProps = { + addFilters: this.props.addFilters, + closeTooltip: this.props.closeTooltip, + features: this.props.features, + isLocked: this.props.isLocked, + loadFeatureProperties: this._loadFeatureProperties, + loadFeatureGeometry: this._loadFeatureGeometry, + getLayerName: this._getLayerName, + }; + + if (this.props.renderTooltipContent) { + return this.props.renderTooltipContent(publicProps); + } + + return ( + <EuiText size="xs" style={{ maxWidth: '425px' }}> + <FeaturesTooltip + {...publicProps} + findLayerById={this._findLayerById} + geoFields={this.props.geoFields} + loadPreIndexedShape={this._loadPreIndexedShape} + /> + </EuiText> + ); + }; + + render() { + if (!this.state.isVisible) { + return null; + } + + const tooltipAnchor = <div style={{ height: '26px', width: '26px', background: 'none' }} />; + // Although tooltip anchors are not visible, they take up horizontal space. + // This horizontal spacing needs to be accounted for in the translate function, + // otherwise the anchors get increasingly pushed to the right away from the actual location. + const offset = this.props.index * 26; + return ( + <EuiPopover + id="mapTooltip" + button={tooltipAnchor} + anchorPosition="upCenter" + isOpen + closePopover={noop} + ref={this._popoverRef} + style={{ + pointerEvents: 'none', + transform: `translate(${this.state.x - 13 - offset}px, ${this.state.y - 13}px)`, + }} + > + {this._renderTooltipContent()} + </EuiPopover> + ); + } +} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.test.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.test.js new file mode 100644 index 0000000000000..bcef03c205b2b --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.test.js @@ -0,0 +1,142 @@ +/* + * 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. + */ + +jest.mock('../../features_tooltip/features_tooltip', () => ({ + FeaturesTooltip: () => { + return <div>mockFeaturesTooltip</div>; + }, +})); + +import sinon from 'sinon'; +import React from 'react'; +import { mount, shallow } from 'enzyme'; +import { TooltipPopover } from './tooltip_popover'; + +// mutable map state +let mapCenter; +let mockMbMapBounds; + +const layerId = 'tfi3f'; + +const mockMbMapHandlers = {}; +const mockMBMap = { + project: lonLatArray => { + const lonDistanceFromCenter = Math.abs(lonLatArray[0] - mapCenter[0]); + const latDistanceFromCenter = Math.abs(lonLatArray[1] - mapCenter[1]); + return { + x: lonDistanceFromCenter * 100, + y: latDistanceFromCenter * 100, + }; + }, + on: (eventName, callback) => { + mockMbMapHandlers[eventName] = callback; + }, + off: eventName => { + delete mockMbMapHandlers[eventName]; + }, + getBounds: () => { + return { + getNorth: () => { + return mockMbMapBounds.north; + }, + getSouth: () => { + return mockMbMapBounds.south; + }, + getWest: () => { + return mockMbMapBounds.west; + }, + getEast: () => { + return mockMbMapBounds.east; + }, + }; + }, +}; + +const defaultProps = { + mbMap: mockMBMap, + closeTooltip: () => {}, + layerList: [], + isDrawingFilter: false, + addFilters: () => {}, + geoFields: [{}], + location: [-120, 30], + features: [ + { + id: 1, + layerId: layerId, + geometry: {}, + }, + ], + isLocked: false, +}; + +describe('TooltipPopover', () => { + beforeEach(() => { + mapCenter = [0, 0]; + mockMbMapBounds = { + west: -180, + east: 180, + north: 90, + south: -90, + }; + }); + + describe('render', () => { + test('should render tooltip popover', () => { + const component = shallow(<TooltipPopover {...defaultProps} />); + + expect(component).toMatchSnapshot(); + }); + + test('should render tooltip popover with custom tooltip content when renderTooltipContent provided', () => { + const component = shallow( + <TooltipPopover + {...defaultProps} + renderTooltipContent={props => { + return <div {...props}>Custom tooltip content</div>; + }} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + test('should un-register all map callbacks on unmount', () => { + const component = mount(<TooltipPopover {...defaultProps} />); + + expect(Object.keys(mockMbMapHandlers).length).toBe(1); + + component.unmount(); + expect(Object.keys(mockMbMapHandlers).length).toBe(0); + }); + }); + + describe('on map move', () => { + const closeTooltipStub = sinon.stub(); + + beforeEach(() => { + closeTooltipStub.reset(); + }); + + test('should update popover location', () => { + const component = mount(<TooltipPopover {...defaultProps} closeTooltip={closeTooltipStub} />); + + // ensure x and y set from original tooltipState.location + expect(component.state('x')).toBe(12000); + expect(component.state('y')).toBe(3000); + + mapCenter = [25, -15]; + mockMbMapHandlers.move(); + component.update(); + + // ensure x and y updated from new map center with same tooltipState.location + expect(component.state('x')).toBe(14500); + expect(component.state('y')).toBe(4500); + + sinon.assert.notCalled(closeTooltipStub); + }); + }); +}); diff --git a/x-pack/legacy/plugins/maps/public/kibana_services.js b/x-pack/legacy/plugins/maps/public/kibana_services.js index 60fda398b4f3e..a1b1c9ec1518e 100644 --- a/x-pack/legacy/plugins/maps/public/kibana_services.js +++ b/x-pack/legacy/plugins/maps/public/kibana_services.js @@ -14,6 +14,7 @@ import { npStart } from 'ui/new_platform'; export const SPATIAL_FILTER_TYPE = esFilters.FILTERS.SPATIAL_FILTER; export { SearchSource } from '../../../../../src/plugins/data/public'; export const indexPatternService = npStart.plugins.data.indexPatterns; +export const autocompleteService = npStart.plugins.data.autocomplete; let licenseId; export const setLicenseId = latestLicenseId => (licenseId = latestLicenseId); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js index b8644adddcf7e..93ef40162a584 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js @@ -29,6 +29,31 @@ import { loadIndexSettings } from './load_index_settings'; import { DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants'; import { ESDocField } from '../../fields/es_doc_field'; +function getField(indexPattern, fieldName) { + const field = indexPattern.fields.getByName(fieldName); + if (!field) { + throw new Error( + i18n.translate('xpack.maps.source.esSearch.fieldNotFoundMsg', { + defaultMessage: `Unable to find '{fieldName}' in index-pattern '{indexPatternTitle}'.`, + values: { fieldName, indexPatternTitle: indexPattern.title }, + }) + ); + } + return field; +} + +function addFieldToDSL(dsl, field) { + return !field.scripted + ? { ...dsl, field: field.name } + : { + ...dsl, + script: { + source: field.script, + lang: field.lang, + }, + }; +} + export class ESSearchSource extends AbstractESSource { static type = ES_SEARCH; static title = i18n.translate('xpack.maps.source.esSearchTitle', { @@ -242,7 +267,7 @@ export class ESSearchSource extends AbstractESSource { } async _getTopHits(layerName, searchFilters, registerCancelCallback) { - const { topHitsSplitField, topHitsSize } = this._descriptor; + const { topHitsSplitField: topHitsSplitFieldName, topHitsSize } = this._descriptor; const indexPattern = await this.getIndexPattern(); const geoField = await this._getGeoField(); @@ -279,20 +304,20 @@ export class ESSearchSource extends AbstractESSource { }; } + const topHitsSplitField = getField(indexPattern, topHitsSplitFieldName); + const cardinalityAgg = { precision_threshold: 1 }; + const termsAgg = { + size: DEFAULT_MAX_BUCKETS_LIMIT, + shard_size: DEFAULT_MAX_BUCKETS_LIMIT, + }; + const searchSource = await this._makeSearchSource(searchFilters, 0); searchSource.setField('aggs', { totalEntities: { - cardinality: { - field: topHitsSplitField, - precision_threshold: 1, - }, + cardinality: addFieldToDSL(cardinalityAgg, topHitsSplitField), }, entitySplit: { - terms: { - field: topHitsSplitField, - size: DEFAULT_MAX_BUCKETS_LIMIT, - shard_size: DEFAULT_MAX_BUCKETS_LIMIT, - }, + terms: addFieldToDSL(termsAgg, topHitsSplitField), aggs: { entityHits: { top_hits: topHits, diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js index 26cc7ece66753..d78d3038f870d 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js @@ -6,6 +6,7 @@ import { AbstractVectorSource } from './vector_source'; import { + autocompleteService, fetchSearchSourceAndRecordWithInspector, indexPatternService, SearchSource, @@ -344,4 +345,25 @@ export class AbstractESSource extends AbstractVectorSource { return resp.aggregations; } + + getValueSuggestions = async (fieldName, query) => { + if (!fieldName) { + return []; + } + + try { + const indexPattern = await this.getIndexPattern(); + const field = indexPattern.fields.getByName(fieldName); + return await autocompleteService.getValueSuggestions({ + indexPattern, + field, + query, + }); + } catch (error) { + console.warn( + `Unable to fetch suggestions for field: ${fieldName}, query: ${query}, error: ${error.message}` + ); + return []; + } + }; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/source.js b/x-pack/legacy/plugins/maps/public/layers/sources/source.js index cc5d62bbdfeef..3c6ddb74bedeb 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/source.js @@ -139,4 +139,8 @@ export class AbstractSource { async loadStylePropsMeta() { throw new Error(`Source#loadStylePropsMeta not implemented`); } + + async getValueSuggestions(/* fieldName, query */) { + return []; + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js index fde088ab4475e..e8d5754ef4206 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js @@ -72,6 +72,8 @@ export class ColorMapSelect extends Component { <EuiSpacer size="s" /> <ColorStopsCategorical colorStops={this.state.customColorMap} + field={this.props.styleProperty.getField()} + getValueSuggestions={this.props.styleProperty.getValueSuggestions} onChange={this._onCustomColorMapChange} /> </Fragment> diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js index 6b403ff61532d..47c2d037e0c79 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops.js @@ -59,26 +59,23 @@ export const ColorStops = ({ onChange, colorStops, isStopsInvalid, - sanitizeStopInput, getStopError, renderStopInput, addNewRow, canDeleteStop, }) => { function getStopInput(stop, index) { - const onStopChange = e => { + const onStopChange = newStopValue => { const newColorStops = _.cloneDeep(colorStops); - newColorStops[index].stop = sanitizeStopInput(e.target.value); - const invalid = isStopsInvalid(newColorStops); + newColorStops[index].stop = newStopValue; onChange({ colorStops: newColorStops, - isInvalid: invalid, + isInvalid: isStopsInvalid(newColorStops), }); }; - const error = getStopError(stop, index); return { - stopError: error, + stopError: getStopError(stop, index), stopInput: renderStopInput(stop, onStopChange, index), }; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js index d52c3dbcfa1df..124c2bf0cff55 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js @@ -17,18 +17,17 @@ import { import { i18n } from '@kbn/i18n'; import { ColorStops } from './color_stops'; import { getOtherCategoryLabel } from '../../style_util'; +import { StopInput } from '../stop_input'; export const ColorStopsCategorical = ({ colorStops = [ { stop: null, color: DEFAULT_CUSTOM_COLOR }, //first stop is the "other" color { stop: '', color: DEFAULT_NEXT_COLOR }, ], + field, onChange, + getValueSuggestions, }) => { - const sanitizeStopInput = value => { - return value; - }; - const getStopError = (stop, index) => { let count = 0; for (let i = 1; i < colorStops.length; i++) { @@ -49,34 +48,23 @@ export const ColorStopsCategorical = ({ if (index === 0) { return ( <EuiFieldText - aria-label={i18n.translate( - 'xpack.maps.styles.colorStops.categoricalStop.defaultCategoryAriaLabel', - { - defaultMessage: 'Default stop', - } - )} - value={stopValue} + aria-label={getOtherCategoryLabel()} placeholder={getOtherCategoryLabel()} disabled - onChange={onStopChange} - compressed - /> - ); - } else { - return ( - <EuiFieldText - aria-label={i18n.translate( - 'xpack.maps.styles.colorStops.categoricalStop.categoryAriaLabel', - { - defaultMessage: 'Category', - } - )} - value={stopValue} - onChange={onStopChange} compressed /> ); } + + return ( + <StopInput + key={field.getName()} // force new component instance when field changes + field={field} + getValueSuggestions={getValueSuggestions} + value={stopValue} + onChange={onStopChange} + /> + ); }; const canDeleteStop = (colorStops, index) => { @@ -88,7 +76,6 @@ export const ColorStopsCategorical = ({ onChange={onChange} colorStops={colorStops} isStopsInvalid={isCategoricalStopsInvalid} - sanitizeStopInput={sanitizeStopInput} getStopError={getStopError} renderStopInput={renderStopInput} canDeleteStop={canDeleteStop} @@ -114,4 +101,8 @@ ColorStopsCategorical.propTypes = { * Callback for when the color stops changes. Called with { colorStops, isInvalid } */ onChange: PropTypes.func.isRequired, + /** + * Callback for fetching stop value suggestions. Called with query. + */ + getValueSuggestions: PropTypes.func.isRequired, }; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js index 61fbb376ad601..0f6a0583d3dbc 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js @@ -21,11 +21,6 @@ export const ColorStopsOrdinal = ({ colorStops = [{ stop: 0, color: DEFAULT_CUSTOM_COLOR }], onChange, }) => { - const sanitizeStopInput = value => { - const sanitizedValue = parseFloat(value); - return isNaN(sanitizedValue) ? '' : sanitizedValue; - }; - const getStopError = (stop, index) => { let error; if (isOrdinalStopInvalid(stop)) { @@ -44,13 +39,18 @@ export const ColorStopsOrdinal = ({ }; const renderStopInput = (stop, onStopChange) => { + function handleOnChangeEvent(event) { + const sanitizedValue = parseFloat(event.target.value); + const newStopValue = isNaN(sanitizedValue) ? '' : sanitizedValue; + onStopChange(newStopValue); + } return ( <EuiFieldNumber aria-label={i18n.translate('xpack.maps.styles.colorStops.ordinalStop.stopLabel', { defaultMessage: 'Stop', })} value={stop} - onChange={onStopChange} + onChange={handleOnChangeEvent} compressed /> ); @@ -65,7 +65,6 @@ export const ColorStopsOrdinal = ({ onChange={onChange} colorStops={colorStops} isStopsInvalid={isOrdinalStopsInvalid} - sanitizeStopInput={sanitizeStopInput} getStopError={getStopError} renderStopInput={renderStopInput} canDeleteStop={canDeleteStop} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js index 5491d5d567f84..af5e5b37f5467 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js @@ -67,7 +67,7 @@ export function DynamicColorForm({ color={styleOptions.color} customColorMap={styleOptions.customColorRamp} useCustomColorMap={_.get(styleOptions, 'useCustomColorRamp', false)} - compressed + styleProperty={styleProperty} /> ); } @@ -83,7 +83,7 @@ export function DynamicColorForm({ color={styleOptions.colorCategory} customColorMap={styleOptions.customColorPalette} useCustomColorMap={_.get(styleOptions, 'useCustomColorPalette', false)} - compressed + styleProperty={styleProperty} /> ); }; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/__snapshots__/vector_icon.test.js.snap b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/__snapshots__/vector_icon.test.js.snap index 5837a80ec3083..f7dea92a8a0b7 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/__snapshots__/vector_icon.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/__snapshots__/vector_icon.test.js.snap @@ -38,8 +38,8 @@ exports[`Renders PolygonIcon 1`] = ` exports[`Renders SymbolIcon 1`] = ` <SymbolIcon fill="#ff0000" + key="airfield-15#ff0000rgb(106,173,213)" stroke="rgb(106,173,213)" - strokeWidth="1px" symbolId="airfield-15" /> `; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/symbol_icon.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/symbol_icon.js index 301d64e676703..ea3886c600be9 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/symbol_icon.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/symbol_icon.js @@ -12,62 +12,30 @@ import { getMakiSymbolSvg, styleSvg, buildSrcUrl } from '../../symbol_utils'; export class SymbolIcon extends Component { state = { imgDataUrl: undefined, - prevSymbolId: undefined, - prevFill: undefined, - prevStroke: undefined, - prevStrokeWidth: undefined, }; componentDidMount() { this._isMounted = true; - this._loadSymbol( - this.props.symbolId, - this.props.fill, - this.props.stroke, - this.props.strokeWidth - ); - } - - componentDidUpdate() { - this._loadSymbol( - this.props.symbolId, - this.props.fill, - this.props.stroke, - this.props.strokeWidth - ); + this._loadSymbol(); } componentWillUnmount() { this._isMounted = false; } - async _loadSymbol(nextSymbolId, nextFill, nextStroke, nextStrokeWidth) { - if ( - nextSymbolId === this.state.prevSymbolId && - nextFill === this.state.prevFill && - nextStroke === this.state.prevStroke && - nextStrokeWidth === this.state.prevStrokeWidth - ) { - return; - } - + async _loadSymbol() { let imgDataUrl; try { - const svg = getMakiSymbolSvg(nextSymbolId); - const styledSvg = await styleSvg(svg, nextFill, nextStroke, nextStrokeWidth); + const svg = getMakiSymbolSvg(this.props.symbolId); + const styledSvg = await styleSvg(svg, this.props.fill, this.props.stroke); imgDataUrl = buildSrcUrl(styledSvg); } catch (error) { // ignore failures - component will just not display an icon + return; } if (this._isMounted) { - this.setState({ - imgDataUrl, - prevSymbolId: nextSymbolId, - prevFill: nextFill, - prevStroke: nextStroke, - prevStrokeWidth: nextStrokeWidth, - }); + this.setState({ imgDataUrl }); } } @@ -80,7 +48,6 @@ export class SymbolIcon extends Component { symbolId, // eslint-disable-line no-unused-vars fill, // eslint-disable-line no-unused-vars stroke, // eslint-disable-line no-unused-vars - strokeWidth, // eslint-disable-line no-unused-vars ...rest } = this.props; @@ -98,7 +65,6 @@ export class SymbolIcon extends Component { SymbolIcon.propTypes = { symbolId: PropTypes.string.isRequired, - fill: PropTypes.string.isRequired, - stroke: PropTypes.string.isRequired, - strokeWidth: PropTypes.string.isRequired, + fill: PropTypes.string, + stroke: PropTypes.string, }; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_icon.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_icon.js index 29429b5b29aff..e255dceda856e 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_icon.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_icon.js @@ -37,10 +37,10 @@ export function VectorIcon({ fillColor, isPointsOnly, isLinesOnly, strokeColor, return ( <SymbolIcon + key={`${symbolId}${fillColor}${strokeColor}`} symbolId={symbolId} - fill={style.fill} - stroke={style.stroke} - strokeWidth={style.strokeWidth} + fill={fillColor} + stroke={strokeColor} /> ); } @@ -49,6 +49,6 @@ VectorIcon.propTypes = { fillColor: PropTypes.string, isPointsOnly: PropTypes.bool.isRequired, isLinesOnly: PropTypes.bool.isRequired, - strokeColor: PropTypes.string.isRequired, + strokeColor: PropTypes.string, symbolId: PropTypes.string, }; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/stop_input.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/stop_input.js new file mode 100644 index 0000000000000..d12a3d77d0b29 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/stop_input.js @@ -0,0 +1,148 @@ +/* + * 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 _ from 'lodash'; +import React, { Component } from 'react'; + +import { EuiComboBox, EuiFieldText } from '@elastic/eui'; + +export class StopInput extends Component { + constructor(props) { + super(props); + this.state = { + suggestions: [], + isLoadingSuggestions: false, + hasPrevFocus: false, + fieldDataType: undefined, + localFieldTextValue: props.value, + }; + } + + componentDidMount() { + this._isMounted = true; + this._loadFieldDataType(); + } + + componentWillUnmount() { + this._isMounted = false; + this._loadSuggestions.cancel(); + } + + async _loadFieldDataType() { + const fieldDataType = await this.props.field.getDataType(); + if (this._isMounted) { + this.setState({ fieldDataType }); + } + } + + _onFocus = () => { + if (!this.state.hasPrevFocus) { + this.setState({ hasPrevFocus: true }); + this._onSearchChange(''); + } + }; + + _onChange = selectedOptions => { + this.props.onChange(_.get(selectedOptions, '[0].label', '')); + }; + + _onCreateOption = newValue => { + this.props.onChange(newValue); + }; + + _onSearchChange = async searchValue => { + this.setState( + { + isLoadingSuggestions: true, + searchValue, + }, + () => { + this._loadSuggestions(searchValue); + } + ); + }; + + _loadSuggestions = _.debounce(async searchValue => { + let suggestions = []; + try { + suggestions = await this.props.getValueSuggestions(searchValue); + } catch (error) { + // ignore suggestions error + } + + if (this._isMounted && searchValue === this.state.searchValue) { + this.setState({ + isLoadingSuggestions: false, + suggestions, + }); + } + }, 300); + + _onFieldTextChange = event => { + this.setState({ localFieldTextValue: event.target.value }); + // onChange can cause UI lag, ensure smooth input typing by debouncing onChange + this._debouncedOnFieldTextChange(); + }; + + _debouncedOnFieldTextChange = _.debounce(() => { + this.props.onChange(this.state.localFieldTextValue); + }, 500); + + _renderSuggestionInput() { + const suggestionOptions = this.state.suggestions.map(suggestion => { + return { label: `${suggestion}` }; + }); + + const selectedOptions = []; + if (this.props.value) { + let option = suggestionOptions.find(({ label }) => { + return label === this.props.value; + }); + if (!option) { + option = { label: this.props.value }; + suggestionOptions.unshift(option); + } + selectedOptions.push(option); + } + + return ( + <EuiComboBox + options={suggestionOptions} + selectedOptions={selectedOptions} + singleSelection={{ asPlainText: true }} + onChange={this._onChange} + onSearchChange={this._onSearchChange} + onCreateOption={this._onCreateOption} + isClearable={false} + isLoading={this.state.isLoadingSuggestions} + onFocus={this._onFocus} + compressed + /> + ); + } + + _renderTextInput() { + return ( + <EuiFieldText + value={this.state.localFieldTextValue} + onChange={this._onFieldTextChange} + compressed + /> + ); + } + + render() { + if (!this.state.fieldDataType) { + return null; + } + + // autocomplete service can not provide suggestions for non string fields (and boolean) because it uses + // term aggregation include parameter. Include paramerter uses a regular expressions that only supports string type + return this.state.fieldDataType === 'string' || this.state.fieldDataType === 'boolean' + ? this._renderSuggestionInput() + : this._renderTextInput(); + } +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/__snapshots__/icon_select.test.js.snap b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/__snapshots__/icon_select.test.js.snap index b4b7a3fcf28fa..706dc0763b7ca 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/__snapshots__/icon_select.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/__snapshots__/icon_select.test.js.snap @@ -25,8 +25,6 @@ exports[`Should render icon select 1`] = ` <SymbolIcon className="mapIconSelectSymbol__inputButton" fill="rgb(52, 55, 65)" - stroke="rgb(0, 0, 0)" - strokeWidth="1px" symbolId="symbol1" /> } @@ -53,8 +51,6 @@ exports[`Should render icon select 1`] = ` "label": "symbol1", "prepend": <SymbolIcon fill="rgb(52, 55, 65)" - stroke="rgb(0, 0, 0)" - strokeWidth="1px" symbolId="symbol1" />, "value": "symbol1", @@ -63,8 +59,6 @@ exports[`Should render icon select 1`] = ` "label": "symbol2", "prepend": <SymbolIcon fill="rgb(52, 55, 65)" - stroke="rgb(0, 0, 0)" - strokeWidth="1px" symbolId="symbol2" />, "value": "symbol2", diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/dynamic_icon_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/dynamic_icon_form.js index 9a0d73cef616c..afa11daf45217 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/dynamic_icon_form.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/dynamic_icon_form.js @@ -43,6 +43,7 @@ export function DynamicIconForm({ return ( <IconMapSelect {...styleOptions} + styleProperty={styleProperty} onChange={onIconMapChange} isDarkMode={isDarkMode} symbolOptions={symbolOptions} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/icon_map_select.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/icon_map_select.js index a8bb94d1d9ce4..08f5dfe4f4ba0 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/icon_map_select.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/icon_map_select.js @@ -16,6 +16,7 @@ export function IconMapSelect({ iconPaletteId, isDarkMode, onChange, + styleProperty, symbolOptions, useCustomIconMap, }) { @@ -30,6 +31,8 @@ export function IconMapSelect({ function renderCustomIconStopsInput(onCustomMapChange) { return ( <IconStops + field={styleProperty.getField()} + getValueSuggestions={styleProperty.getValueSuggestions} iconStops={customIconStops} isDarkMode={isDarkMode} onChange={onCustomMapChange} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/icon_select.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/icon_select.js index 03cd1ac14a013..68f7a30b22862 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/icon_select.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/icon_select.js @@ -80,11 +80,10 @@ export class IconSelect extends Component { fullWidth prepend={ <SymbolIcon + key={value} className="mapIconSelectSymbol__inputButton" symbolId={value} fill={isDarkMode ? 'rgb(223, 229, 239)' : 'rgb(52, 55, 65)'} - stroke={isDarkMode ? 'rgb(255, 255, 255)' : 'rgb(0, 0, 0)'} - strokeWidth={'1px'} /> } /> @@ -100,10 +99,9 @@ export class IconSelect extends Component { label, prepend: ( <SymbolIcon + key={value} symbolId={value} fill={isDarkMode ? 'rgb(223, 229, 239)' : 'rgb(52, 55, 65)'} - stroke={isDarkMode ? 'rgb(255, 255, 255)' : 'rgb(0, 0, 0)'} - strokeWidth={'1px'} /> ), }; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/icon_stops.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/icon_stops.js index a655a4434ddaa..4475af65be2e2 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/icon_stops.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/symbol/icon_stops.js @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import { getOtherCategoryLabel } from '../../style_util'; import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiFieldText } from '@elastic/eui'; import { IconSelect } from './icon_select'; +import { StopInput } from '../stop_input'; function isDuplicateStop(targetStop, iconStops) { const stops = iconStops.filter(({ stop }) => { @@ -23,7 +24,14 @@ const DEFAULT_ICON_STOPS = [ { stop: '', icon: DEFAULT_ICON }, ]; -export function IconStops({ iconStops = DEFAULT_ICON_STOPS, isDarkMode, onChange, symbolOptions }) { +export function IconStops({ + field, + getValueSuggestions, + iconStops = DEFAULT_ICON_STOPS, + isDarkMode, + onChange, + symbolOptions, +}) { return iconStops.map(({ stop, icon }, index) => { const onIconSelect = selectedIconId => { const newIconStops = [...iconStops]; @@ -33,8 +41,7 @@ export function IconStops({ iconStops = DEFAULT_ICON_STOPS, isDarkMode, onChange }; onChange({ customMapStops: newIconStops }); }; - const onStopChange = e => { - const newStopValue = e.target.value; + const onStopChange = newStopValue => { const newIconStops = [...iconStops]; newIconStops[index] = { ...iconStops[index], @@ -83,7 +90,24 @@ export function IconStops({ iconStops = DEFAULT_ICON_STOPS, isDarkMode, onChange const errors = []; // TODO check for duplicate values and add error messages here - const isOtherCategoryRow = index === 0; + const stopInput = + index === 0 ? ( + <EuiFieldText + aria-label={getOtherCategoryLabel()} + placeholder={getOtherCategoryLabel()} + disabled + compressed + /> + ) : ( + <StopInput + key={field.getName()} // force new component instance when field changes + field={field} + getValueSuggestions={getValueSuggestions} + value={stop} + onChange={onStopChange} + /> + ); + return ( <EuiFormRow key={index} @@ -94,18 +118,7 @@ export function IconStops({ iconStops = DEFAULT_ICON_STOPS, isDarkMode, onChange > <div> <EuiFlexGroup responsive={false} alignItems="center" gutterSize="xs"> - <EuiFlexItem> - <EuiFieldText - aria-label={i18n.translate('xpack.maps.styles.iconStops.stopInputAriaLabel', { - defaultMessage: 'Icon stop', - })} - value={isOtherCategoryRow ? null : stop} - placeholder={isOtherCategoryRow ? getOtherCategoryLabel() : null} - disabled={isOtherCategoryRow} - onChange={onStopChange} - compressed - /> - </EuiFlexItem> + <EuiFlexItem>{stopInput}</EuiFlexItem> <EuiFlexItem> <IconSelect isDarkMode={isDarkMode} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js index 9636dab406a44..7daf85b68dd8e 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js @@ -146,11 +146,6 @@ export class VectorStyleEditor extends Component { this.props.handlePropertyChange(propertyName, styleDescriptor); }; - _hasBorder() { - const width = this.props.styleProperties[VECTOR_STYLES.LINE_WIDTH]; - return width.isDynamic() ? width.isComplete() : width.getOptions().size !== 0; - } - _hasMarkerOrIcon() { const iconSize = this.props.styleProperties[VECTOR_STYLES.ICON_SIZE]; return iconSize.isDynamic() || iconSize.getOptions().size > 0; @@ -192,7 +187,7 @@ export class VectorStyleEditor extends Component { const disabledByIconSize = isPointBorderColor && !this._hasMarkerOrIcon(); return ( <VectorStyleColorEditor - disabled={disabledByIconSize || !this._hasBorder()} + disabled={disabledByIconSize || !this.props.hasBorder} disabledBy={disabledByIconSize ? VECTOR_STYLES.ICON_SIZE : VECTOR_STYLES.LINE_WIDTH} swatches={DEFAULT_LINE_COLORS} onStaticStyleChange={this._onStaticStyleChange} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js index dfc5c530cc90f..e137e15730827 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js @@ -43,8 +43,16 @@ function getSymbolSizeIcons() { } export class DynamicSizeProperty extends DynamicStyleProperty { - constructor(options, styleName, field, getFieldMeta, getFieldFormatter, isSymbolizedAsIcon) { - super(options, styleName, field, getFieldMeta, getFieldFormatter); + constructor( + options, + styleName, + field, + getFieldMeta, + getFieldFormatter, + getValueSuggestions, + isSymbolizedAsIcon + ) { + super(options, styleName, field, getFieldMeta, getFieldFormatter, getValueSuggestions); this._isSymbolizedAsIcon = isSymbolizedAsIcon; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js index 37db54389866d..ef19e9b23b10d 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js @@ -17,13 +17,19 @@ import { OrdinalFieldMetaOptionsPopover } from '../components/ordinal_field_meta export class DynamicStyleProperty extends AbstractStyleProperty { static type = STYLE_TYPE.DYNAMIC; - constructor(options, styleName, field, getFieldMeta, getFieldFormatter) { + constructor(options, styleName, field, getFieldMeta, getFieldFormatter, source) { super(options, styleName); this._field = field; this._getFieldMeta = getFieldMeta; this._getFieldFormatter = getFieldFormatter; + this._source = source; } + getValueSuggestions = query => { + const fieldName = this.getFieldName(); + return this._source && fieldName ? this._source.getValueSuggestions(fieldName, query) : []; + }; + getFieldMeta() { return this._getFieldMeta && this._field ? this._getFieldMeta(this._field.getName()) : null; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/symbol_utils.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/symbol_utils.js index b577d4080b879..affb9c1805170 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/symbol_utils.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/symbol_utils.js @@ -69,7 +69,7 @@ export function buildSrcUrl(svgString) { return domUrl.createObjectURL(svg); } -export async function styleSvg(svgString, fill, stroke, strokeWidth) { +export async function styleSvg(svgString, fill, stroke) { const svgXml = await parseXmlString(svgString); let style = ''; if (fill) { @@ -77,9 +77,7 @@ export async function styleSvg(svgString, fill, stroke, strokeWidth) { } if (stroke) { style += `stroke:${stroke};`; - } - if (strokeWidth) { - style += `stroke-width:${strokeWidth};`; + style += `stroke-width:1;`; } if (style) svgXml.svg.$.style = style; const builder = new xml2js.Builder(); @@ -119,8 +117,6 @@ export function getIconPaletteOptions(isDarkMode) { className="mapIcon" symbolId={iconId} fill={isDarkMode ? 'rgb(223, 229, 239)' : 'rgb(52, 55, 65)'} - stroke={isDarkMode ? 'rgb(255, 255, 255)' : 'rgb(0, 0, 0)'} - strokeWidth={'1px'} /> </div> ); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/symbol_utils.test.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/symbol_utils.test.js index ed59b1d5513a0..1d3b3608cb2d9 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/symbol_utils.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/symbol_utils.test.js @@ -32,21 +32,12 @@ describe('styleSvg', () => { ); }); - it('Should add stroke style property to svg element', async () => { + it('Should add stroke and stroke-wdth style properties to svg element', async () => { const unstyledSvgString = '<svg version="1.1" width="11px" height="11px" viewBox="0 0 11 11"><path/></svg>'; const styledSvg = await styleSvg(unstyledSvgString, 'red', 'white'); expect(styledSvg.split('\n')[1]).toBe( - '<svg version="1.1" width="11px" height="11px" viewBox="0 0 11 11" style="fill:red;stroke:white;">' - ); - }); - - it('Should add stroke-width style property to svg element', async () => { - const unstyledSvgString = - '<svg version="1.1" width="11px" height="11px" viewBox="0 0 11 11"><path/></svg>'; - const styledSvg = await styleSvg(unstyledSvgString, 'red', 'white', '2px'); - expect(styledSvg.split('\n')[1]).toBe( - '<svg version="1.1" width="11px" height="11px" viewBox="0 0 11 11" style="fill:red;stroke:white;stroke-width:2px;">' + '<svg version="1.1" width="11px" height="11px" viewBox="0 0 11 11" style="fill:red;stroke:white;stroke-width:1;">' ); }); }); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js index 97259a908f1e4..62651fdd702d6 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js @@ -138,6 +138,12 @@ export class VectorStyle extends AbstractStyle { ]; } + _hasBorder() { + return this._lineWidthStyleProperty.isDynamic() + ? this._lineWidthStyleProperty.isComplete() + : this._lineWidthStyleProperty.getOptions().size !== 0; + } + renderEditor({ layer, onStyleDescriptorChange }) { const rawProperties = this.getRawProperties(); const handlePropertyChange = (propertyName, settings) => { @@ -170,6 +176,7 @@ export class VectorStyle extends AbstractStyle { onIsTimeAwareChange={onIsTimeAwareChange} isTimeAware={this.isTimeAware()} showIsTimeAware={propertiesWithFieldMeta.length > 0} + hasBorder={this._hasBorder()} /> ); } @@ -423,12 +430,18 @@ export class VectorStyle extends AbstractStyle { getIcon = () => { const isLinesOnly = this._getIsLinesOnly(); - const strokeColor = isLinesOnly - ? extractColorFromStyleProperty(this._descriptor.properties[VECTOR_STYLES.LINE_COLOR], 'grey') - : extractColorFromStyleProperty( - this._descriptor.properties[VECTOR_STYLES.LINE_COLOR], - 'none' - ); + let strokeColor; + if (isLinesOnly) { + strokeColor = extractColorFromStyleProperty( + this._descriptor.properties[VECTOR_STYLES.LINE_COLOR], + 'grey' + ); + } else if (this._hasBorder()) { + strokeColor = extractColorFromStyleProperty( + this._descriptor.properties[VECTOR_STYLES.LINE_COLOR], + 'none' + ); + } const fillColor = isLinesOnly ? null : extractColorFromStyleProperty( @@ -612,6 +625,7 @@ export class VectorStyle extends AbstractStyle { field, this._getFieldMeta, this._getFieldFormatter, + this._source, isSymbolizedAsIcon ); } else { @@ -631,7 +645,8 @@ export class VectorStyle extends AbstractStyle { styleName, field, this._getFieldMeta, - this._getFieldFormatter + this._getFieldFormatter, + this._source ); } else { throw new Error(`${descriptor} not implemented`); @@ -663,7 +678,8 @@ export class VectorStyle extends AbstractStyle { VECTOR_STYLES.LABEL_TEXT, field, this._getFieldMeta, - this._getFieldFormatter + this._getFieldFormatter, + this._source ); } else { throw new Error(`${descriptor} not implemented`); @@ -682,7 +698,8 @@ export class VectorStyle extends AbstractStyle { VECTOR_STYLES.ICON, field, this._getFieldMeta, - this._getFieldFormatter + this._getFieldFormatter, + this._source ); } else { throw new Error(`${descriptor} not implemented`); diff --git a/x-pack/legacy/plugins/maps/public/reducers/map.js b/x-pack/legacy/plugins/maps/public/reducers/map.js index 234584d08a311..7e81fb03dd85b 100644 --- a/x-pack/legacy/plugins/maps/public/reducers/map.js +++ b/x-pack/legacy/plugins/maps/public/reducers/map.js @@ -37,7 +37,7 @@ import { ROLLBACK_TO_TRACKED_LAYER_STATE, REMOVE_TRACKED_LAYER_STATE, UPDATE_SOURCE_DATA_REQUEST, - SET_TOOLTIP_STATE, + SET_OPEN_TOOLTIPS, SET_SCROLL_ZOOM, SET_MAP_INIT_ERROR, UPDATE_DRAW_STATE, @@ -97,7 +97,7 @@ const INITIAL_STATE = { ready: false, mapInitError: null, goto: null, - tooltipState: null, + openTooltips: [], mapState: { zoom: null, // setting this value does not adjust map zoom, read only value used to store current map zoom for persisting between sessions center: null, // setting this value does not adjust map view, read only value used to store current map center for persisting between sessions @@ -138,10 +138,10 @@ export function map(state = INITIAL_STATE, action) { return trackCurrentLayerState(state, action.layerId); case ROLLBACK_TO_TRACKED_LAYER_STATE: return rollbackTrackedLayerState(state, action.layerId); - case SET_TOOLTIP_STATE: + case SET_OPEN_TOOLTIPS: return { ...state, - tooltipState: action.tooltipState, + openTooltips: action.openTooltips, }; case SET_MOUSE_COORDINATES: return { diff --git a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js index 4b3d1355e4264..d1048a759beca 100644 --- a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js +++ b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js @@ -42,8 +42,14 @@ function createSourceInstance(sourceDescriptor, inspectorAdapters) { return new Source(sourceDescriptor, inspectorAdapters); } -export const getTooltipState = ({ map }) => { - return map.tooltipState; +export const getOpenTooltips = ({ map }) => { + return map && map.openTooltips ? map.openTooltips : []; +}; + +export const getHasLockedTooltips = state => { + return getOpenTooltips(state).some(({ isLocked }) => { + return isLocked; + }); }; export const getMapReady = ({ map }) => map && map.ready; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx index 57c96064a8b97..45e2f340d52b6 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx @@ -87,6 +87,7 @@ export const ActionsPanel: FC<Props> = ({ indexPattern }) => { })} onClick={openAdvancedJobWizard} href={`#/jobs/new_job/advanced?index=${indexPattern}`} + data-test-subj="mlDataVisualizerCreateAdvancedJobCard" /> </div> ); diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx index 3ca23998d5b75..e00ff0333bb73 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx @@ -4,12 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse } from 'query-string'; import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { decode } from 'rison-node'; - -// @ts-ignore -import queryString from 'query-string'; import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; @@ -36,7 +34,8 @@ export const analyticsJobExplorationRoute: MlRoute = { const PageWrapper: FC<PageProps> = ({ location, deps }) => { const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); - const { _g } = queryString.parse(location.search); + const { _g }: Record<string, any> = parse(location.search, { sort: false }); + let globalState: any = null; try { globalState = decode(_g); diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx index fa4745f19e3b4..74ab916cb443f 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse } from 'query-string'; import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; - -// @ts-ignore -import queryString from 'query-string'; import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { Page } from '../../../datavisualizer/index_based'; @@ -37,7 +35,7 @@ export const indexBasedRoute: MlRoute = { }; const PageWrapper: FC<PageProps> = ({ location, deps }) => { - const { index, savedSearchId } = queryString.parse(location.search); + const { index, savedSearchId }: Record<string, any> = parse(location.search, { sort: false }); const { context } = useResolver(index, savedSearchId, deps.config, { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/job_type.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/job_type.tsx index c2e87f065116e..f0a25d880a082 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/job_type.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/job_type.tsx @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse } from 'query-string'; import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; - -// @ts-ignore -import queryString from 'query-string'; import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; @@ -33,7 +31,7 @@ export const jobTypeRoute: MlRoute = { }; const PageWrapper: FC<PageProps> = ({ location, deps }) => { - const { index, savedSearchId } = queryString.parse(location.search); + const { index, savedSearchId }: Record<string, any> = parse(location.search, { sort: false }); const { context } = useResolver(index, savedSearchId, deps.config, basicResolvers(deps)); return ( <PageLoader context={context}> diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/recognize.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/recognize.tsx index 78f72a7b7a39b..12687fd71edc5 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/recognize.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/recognize.tsx @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse } from 'query-string'; import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; -// @ts-ignore -import queryString from 'query-string'; - import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; @@ -41,7 +39,7 @@ export const checkViewOrCreateRoute: MlRoute = { }; const PageWrapper: FC<PageProps> = ({ location, deps }) => { - const { id, index, savedSearchId } = queryString.parse(location.search); + const { id, index, savedSearchId }: Record<string, any> = parse(location.search, { sort: false }); const { context, results } = useResolver(index, savedSearchId, deps.config, { ...basicResolvers(deps), existingJobsAndGroups: mlJobService.getJobAndGroupIds, @@ -55,7 +53,10 @@ const PageWrapper: FC<PageProps> = ({ location, deps }) => { }; const CheckViewOrCreateWrapper: FC<PageProps> = ({ location, deps }) => { - const { id: moduleId, index: indexPatternId } = queryString.parse(location.search); + const { id: moduleId, index: indexPatternId }: Record<string, any> = parse(location.search, { + sort: false, + }); + // the single resolver checkViewOrCreateJobs redirects only. so will always reject useResolver(undefined, undefined, deps.config, { checkViewOrCreateJobs: () => checkViewOrCreateJobs(moduleId, indexPatternId), diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/wizard.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/wizard.tsx index 230d96456427c..b1256e21888d9 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/wizard.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/wizard.tsx @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse } from 'query-string'; import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; -// @ts-ignore -import queryString from 'query-string'; import { basicResolvers } from '../../resolvers'; import { MlRoute, PageLoader, PageProps } from '../../router'; @@ -113,7 +112,7 @@ export const categorizationRoute: MlRoute = { }; const PageWrapper: FC<WizardPageProps> = ({ location, jobType, deps }) => { - const { index, savedSearchId } = queryString.parse(location.search); + const { index, savedSearchId }: Record<string, any> = parse(location.search, { sort: false }); const { context, results } = useResolver(index, savedSearchId, deps.config, { ...basicResolvers(deps), privileges: checkCreateJobsPrivilege, diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx index 2bf3d50c3678c..5bc2435db078c 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -8,8 +8,6 @@ import { isEqual } from 'lodash'; import React, { FC, useCallback, useEffect, useState } from 'react'; import { usePrevious } from 'react-use'; import moment from 'moment'; -// @ts-ignore -import queryString from 'query-string'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js index cc9593d946bd1..1ac391c7f84ae 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/jobs.js @@ -21,7 +21,7 @@ export const jobs = { jobsWithTimerange(dateFormatTz) { return http({ - url: `${basePath()}/jobs/jobs_with_timerange`, + url: `${basePath()}/jobs/jobs_with_time_range`, method: 'POST', data: { dateFormatTz, diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index 3c639239757db..bafb12de068bb 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -225,8 +225,8 @@ class TimeseriesChartIntl extends Component { this.renderFocusChart(); } - componentDidUpdate() { - if (this.props.renderFocusChartOnly === false) { + componentDidUpdate(prevProps) { + if (this.props.renderFocusChartOnly === false || prevProps.svgWidth !== this.props.svgWidth) { this.renderChart(); this.drawContextChartSelection(); } @@ -424,11 +424,8 @@ class TimeseriesChartIntl extends Component { } focusLoadTo = Math.min(focusLoadTo, contextXMax); - const brushVisibility = focusLoadFrom !== contextXMin || focusLoadTo !== contextXMax; - this.setBrushVisibility(brushVisibility); - if (focusLoadFrom !== contextXMin || focusLoadTo !== contextXMax) { - this.setContextBrushExtent(new Date(focusLoadFrom), new Date(focusLoadTo), true); + this.setContextBrushExtent(new Date(focusLoadFrom), new Date(focusLoadTo)); const newSelectedBounds = { min: moment(new Date(focusLoadFrom)), max: moment(focusLoadFrom), @@ -442,6 +439,10 @@ class TimeseriesChartIntl extends Component { }; if (!_.isEqual(newSelectedBounds, this.selectedBounds)) { this.selectedBounds = newSelectedBounds; + this.setContextBrushExtent( + new Date(contextXScaleDomain[0]), + new Date(contextXScaleDomain[1]) + ); if (this.contextChartInitialized === false) { this.contextChartInitialized = true; contextChartSelected({ from: contextXScaleDomain[0], to: contextXScaleDomain[1] }); @@ -1178,36 +1179,29 @@ class TimeseriesChartIntl extends Component { '<div class="brush-handle-inner brush-handle-inner-right"><i class="fa fa-caret-right"></i></div>' ); - const showBrush = show => { - if (show === true) { - const brushExtent = brush.extent(); - mask.reveal(brushExtent); - leftHandle.attr('x', contextXScale(brushExtent[0]) - 10); - rightHandle.attr('x', contextXScale(brushExtent[1]) + 0); - - topBorder.attr('x', contextXScale(brushExtent[0]) + 1); - // Use Math.max(0, ...) to make sure we don't end up - // with a negative width which would cause an SVG error. - topBorder.attr( - 'width', - Math.max(0, contextXScale(brushExtent[1]) - contextXScale(brushExtent[0]) - 2) - ); - } - - this.setBrushVisibility(show); - }; - - showBrush(!brush.empty()); - function brushing() { + const brushExtent = brush.extent(); + mask.reveal(brushExtent); + leftHandle.attr('x', contextXScale(brushExtent[0]) - 10); + rightHandle.attr('x', contextXScale(brushExtent[1]) + 0); + + topBorder.attr('x', contextXScale(brushExtent[0]) + 1); + // Use Math.max(0, ...) to make sure we don't end up + // with a negative width which would cause an SVG error. + const topBorderWidth = Math.max( + 0, + contextXScale(brushExtent[1]) - contextXScale(brushExtent[0]) - 2 + ); + topBorder.attr('width', topBorderWidth); + const isEmpty = brush.empty(); - showBrush(!isEmpty); + d3.selectAll('.brush-handle').style('visibility', isEmpty ? 'hidden' : 'visible'); } + brushing(); const that = this; function brushed() { const isEmpty = brush.empty(); - const selectedBounds = isEmpty ? contextXScale.domain() : brush.extent(); const selectionMin = selectedBounds[0].getTime(); const selectionMax = selectedBounds[1].getTime(); @@ -1221,8 +1215,6 @@ class TimeseriesChartIntl extends Component { return; } - showBrush(!isEmpty); - // Set the color of the swimlane cells according to whether they are inside the selection. contextGroup.selectAll('.swimlane-cell').style('fill', d => { const cellMs = d.date.getTime(); @@ -1238,26 +1230,6 @@ class TimeseriesChartIntl extends Component { } }; - setBrushVisibility = show => { - const mask = this.mask; - - if (mask !== undefined) { - const visibility = show ? 'visible' : 'hidden'; - mask.style('visibility', visibility); - - d3.selectAll('.brush').style('visibility', visibility); - - const brushHandles = d3.selectAll('.brush-handle-inner'); - brushHandles.style('visibility', visibility); - - const topBorder = d3.selectAll('.top-border'); - topBorder.style('visibility', visibility); - - const border = d3.selectAll('.chart-border-highlight'); - border.style('visibility', visibility); - } - }; - drawSwimlane = (swlGroup, swlWidth, swlHeight) => { const { contextAggregationInterval, swimlaneData } = this.props; @@ -1368,21 +1340,18 @@ class TimeseriesChartIntl extends Component { // Sets the extent of the brush on the context chart to the // supplied from and to Date objects. - setContextBrushExtent = (from, to, fireEvent) => { + setContextBrushExtent = (from, to) => { const brush = this.brush; const brushExtent = brush.extent(); const newExtent = [from, to]; - if ( - newExtent[0].getTime() === brushExtent[0].getTime() && - newExtent[1].getTime() === brushExtent[1].getTime() - ) { - fireEvent = false; - } - brush.extent(newExtent); brush(d3.select('.brush')); - if (fireEvent) { + + if ( + newExtent[0].getTime() !== brushExtent[0].getTime() || + newExtent[1].getTime() !== brushExtent[1].getTime() + ) { brush.event(d3.select('.brush')); } }; @@ -1403,7 +1372,7 @@ class TimeseriesChartIntl extends Component { to = Math.min(minBoundsMs + millis, maxBoundsMs); } - this.setContextBrushExtent(new Date(from), new Date(to), true); + this.setContextBrushExtent(new Date(from), new Date(to)); } showFocusChartTooltip(marker, circle) { diff --git a/x-pack/legacy/plugins/ml/public/application/util/url_state.ts b/x-pack/legacy/plugins/ml/public/application/util/url_state.ts index e7d5a94e2694f..b0699116895d4 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/url_state.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/url_state.ts @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse, stringify } from 'query-string'; import { useCallback } from 'react'; import { isEqual } from 'lodash'; -// @ts-ignore -import queryString from 'query-string'; import { decode, encode } from 'rison-node'; import { useHistory, useLocation } from 'react-router-dom'; @@ -33,12 +32,12 @@ function isRisonSerializationRequired(queryParam: string): boolean { export function getUrlState(search: string): Dictionary<any> { const urlState: Dictionary<any> = {}; - const parsedQueryString = queryString.parse(search); + const parsedQueryString = parse(search, { sort: false }); try { Object.keys(parsedQueryString).forEach(a => { if (isRisonSerializationRequired(a)) { - urlState[a] = decode(parsedQueryString[a]) as Dictionary<any>; + urlState[a] = decode(parsedQueryString[a] as string); } else { urlState[a] = parsedQueryString[a]; } @@ -64,7 +63,7 @@ export const useUrlState = (accessor: string): UrlState => { const setUrlState = useCallback( (attribute: string | Dictionary<any>, value?: any) => { const urlState = getUrlState(search); - const parsedQueryString = queryString.parse(search); + const parsedQueryString = parse(search, { sort: false }); if (!Object.prototype.hasOwnProperty.call(urlState, accessor)) { urlState[accessor] = {}; @@ -84,7 +83,7 @@ export const useUrlState = (accessor: string): UrlState => { } try { - const oldLocationSearch = queryString.stringify(parsedQueryString, { encode: false }); + const oldLocationSearch = stringify(parsedQueryString, { sort: false, encode: false }); Object.keys(urlState).forEach(a => { if (isRisonSerializationRequired(a)) { @@ -93,11 +92,11 @@ export const useUrlState = (accessor: string): UrlState => { parsedQueryString[a] = urlState[a]; } }); - const newLocationSearch = queryString.stringify(parsedQueryString, { encode: false }); + const newLocationSearch = stringify(parsedQueryString, { sort: false, encode: false }); if (oldLocationSearch !== newLocationSearch) { history.push({ - search: queryString.stringify(parsedQueryString), + search: stringify(parsedQueryString, { sort: false }), }); } } catch (error) { diff --git a/x-pack/legacy/plugins/ml/server/models/calendar/calendar_manager.ts b/x-pack/legacy/plugins/ml/server/models/calendar/calendar_manager.ts index 2487943b5efc0..61f21c316be23 100644 --- a/x-pack/legacy/plugins/ml/server/models/calendar/calendar_manager.ts +++ b/x-pack/legacy/plugins/ml/server/models/calendar/calendar_manager.ts @@ -6,6 +6,7 @@ import { difference } from 'lodash'; import Boom from 'boom'; +import { IScopedClusterClient } from 'src/core/server'; import { EventManager, CalendarEvent } from './event_manager'; interface BasicCalendar { @@ -23,13 +24,12 @@ export interface FormCalendar extends BasicCalendar { } export class CalendarManager { - private _client: any; + private _client: IScopedClusterClient['callAsCurrentUser']; private _eventManager: any; - constructor(isLegacy: boolean, client: any) { - const actualClient = isLegacy === true ? client : client.ml!.mlClient.callAsCurrentUser; - this._client = actualClient; - this._eventManager = new EventManager(actualClient); + constructor(client: any) { + this._client = client; + this._eventManager = new EventManager(client); } async getCalendar(calendarId: string) { diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/groups.js b/x-pack/legacy/plugins/ml/server/models/job_service/groups.js index 58237b2a8a730..91f82f04a9a0c 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/groups.js +++ b/x-pack/legacy/plugins/ml/server/models/job_service/groups.js @@ -7,7 +7,7 @@ import { CalendarManager } from '../calendar'; export function groupsProvider(callWithRequest) { - const calMngr = new CalendarManager(true, callWithRequest); + const calMngr = new CalendarManager(callWithRequest); async function getAllGroups() { const groups = {}; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/index.js b/x-pack/legacy/plugins/ml/server/models/job_service/index.js index 5c0eff3112a53..6f409e70e68b8 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/index.js +++ b/x-pack/legacy/plugins/ml/server/models/job_service/index.js @@ -14,14 +14,14 @@ import { topCategoriesProvider, } from './new_job'; -export function jobServiceProvider(callWithRequest, request) { +export function jobServiceProvider(callAsCurrentUser) { return { - ...datafeedsProvider(callWithRequest), - ...jobsProvider(callWithRequest), - ...groupsProvider(callWithRequest), - ...newJobCapsProvider(callWithRequest, request), - ...newJobChartsProvider(callWithRequest, request), - ...categorizationExamplesProvider(callWithRequest, request), - ...topCategoriesProvider(callWithRequest, request), + ...datafeedsProvider(callAsCurrentUser), + ...jobsProvider(callAsCurrentUser), + ...groupsProvider(callAsCurrentUser), + ...newJobCapsProvider(callAsCurrentUser), + ...newJobChartsProvider(callAsCurrentUser), + ...categorizationExamplesProvider(callAsCurrentUser), + ...topCategoriesProvider(callAsCurrentUser), }; } diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/jobs.js b/x-pack/legacy/plugins/ml/server/models/job_service/jobs.js index e60593c9f0ed5..b4b476c1f926e 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/jobs.js +++ b/x-pack/legacy/plugins/ml/server/models/job_service/jobs.js @@ -22,7 +22,7 @@ export function jobsProvider(callWithRequest) { const { forceDeleteDatafeed, getDatafeedIdsByJobId } = datafeedsProvider(callWithRequest); const { getAuditMessagesSummary } = jobAuditMessagesProvider(callWithRequest); const { getLatestBucketTimestampByJob } = resultsServiceProvider(callWithRequest); - const calMngr = new CalendarManager(true, callWithRequest); + const calMngr = new CalendarManager(callWithRequest); async function forceDeleteJob(jobId) { return callWithRequest('ml.deleteJob', { jobId, force: true }); diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts index 3cfb552189062..5827201a63661 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts @@ -5,7 +5,7 @@ */ import { cloneDeep } from 'lodash'; -import { Request } from 'src/legacy/server/kbn_server'; +import { SavedObjectsClientContract } from 'kibana/server'; import { Field, Aggregation, @@ -40,22 +40,27 @@ export function fieldServiceProvider( indexPattern: string, isRollup: boolean, callWithRequest: any, - request: Request + savedObjectsClient: SavedObjectsClientContract ) { - return new FieldsService(indexPattern, isRollup, callWithRequest, request); + return new FieldsService(indexPattern, isRollup, callWithRequest, savedObjectsClient); } class FieldsService { private _indexPattern: string; private _isRollup: boolean; private _callWithRequest: any; - private _request: Request; + private _savedObjectsClient: SavedObjectsClientContract; - constructor(indexPattern: string, isRollup: boolean, callWithRequest: any, request: Request) { + constructor( + indexPattern: string, + isRollup: boolean, + callWithRequest: any, + savedObjectsClient: any + ) { this._indexPattern = indexPattern; this._isRollup = isRollup; this._callWithRequest = callWithRequest; - this._request = request; + this._savedObjectsClient = savedObjectsClient; } private async loadFieldCaps(): Promise<any> { @@ -104,7 +109,7 @@ class FieldsService { const rollupService = await rollupServiceProvider( this._indexPattern, this._callWithRequest, - this._request + this._savedObjectsClient ); const rollupConfigs: RollupJob[] | null = await rollupService.getRollupJobs(); diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts index 2c8f8a8f82fb8..f1af7614b4232 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.test.ts @@ -18,7 +18,7 @@ import cloudwatchJobCaps from './__mocks__/results/cloudwatch_rollup_job_caps.js describe('job_service - job_caps', () => { let callWithRequestNonRollupMock: jest.Mock; let callWithRequestRollupMock: jest.Mock; - let requestMock: any; + let savedObjectsClientMock: any; beforeEach(() => { callWithRequestNonRollupMock = jest.fn((action: string) => { @@ -37,14 +37,10 @@ describe('job_service - job_caps', () => { } }); - requestMock = { - getSavedObjectsClient: jest.fn(() => { - return { - async find() { - return Promise.resolve(kibanaSavedObjects); - }, - }; - }), + savedObjectsClientMock = { + async find() { + return Promise.resolve(kibanaSavedObjects); + }, }; }); @@ -52,8 +48,8 @@ describe('job_service - job_caps', () => { it('can get job caps for index pattern', async done => { const indexPattern = 'farequote-*'; const isRollup = false; - const { newJobCaps } = newJobCapsProvider(callWithRequestNonRollupMock, requestMock); - const response = await newJobCaps(indexPattern, isRollup); + const { newJobCaps } = newJobCapsProvider(callWithRequestNonRollupMock); + const response = await newJobCaps(indexPattern, isRollup, savedObjectsClientMock); expect(response).toEqual(farequoteJobCaps); done(); }); @@ -61,8 +57,8 @@ describe('job_service - job_caps', () => { it('can get rollup job caps for non rollup index pattern', async done => { const indexPattern = 'farequote-*'; const isRollup = true; - const { newJobCaps } = newJobCapsProvider(callWithRequestNonRollupMock, requestMock); - const response = await newJobCaps(indexPattern, isRollup); + const { newJobCaps } = newJobCapsProvider(callWithRequestNonRollupMock); + const response = await newJobCaps(indexPattern, isRollup, savedObjectsClientMock); expect(response).toEqual(farequoteJobCapsEmpty); done(); }); @@ -72,8 +68,8 @@ describe('job_service - job_caps', () => { it('can get rollup job caps for rollup index pattern', async done => { const indexPattern = 'cloud_roll_index'; const isRollup = true; - const { newJobCaps } = newJobCapsProvider(callWithRequestRollupMock, requestMock); - const response = await newJobCaps(indexPattern, isRollup); + const { newJobCaps } = newJobCapsProvider(callWithRequestRollupMock); + const response = await newJobCaps(indexPattern, isRollup, savedObjectsClientMock); expect(response).toEqual(cloudwatchJobCaps); done(); }); @@ -81,8 +77,8 @@ describe('job_service - job_caps', () => { it('can get non rollup job caps for rollup index pattern', async done => { const indexPattern = 'cloud_roll_index'; const isRollup = false; - const { newJobCaps } = newJobCapsProvider(callWithRequestRollupMock, requestMock); - const response = await newJobCaps(indexPattern, isRollup); + const { newJobCaps } = newJobCapsProvider(callWithRequestRollupMock); + const response = await newJobCaps(indexPattern, isRollup, savedObjectsClientMock); expect(response).not.toEqual(cloudwatchJobCaps); done(); }); diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts index cbb249be09aa0..3a9d979ccb22c 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/new_job_caps.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Request } from 'src/legacy/server/kbn_server'; +import { SavedObjectsClientContract } from 'kibana/server'; import { Aggregation, Field, NewJobCaps } from '../../../../common/types/fields'; import { fieldServiceProvider } from './field_service'; @@ -12,12 +12,18 @@ interface NewJobCapsResponse { [indexPattern: string]: NewJobCaps; } -export function newJobCapsProvider(callWithRequest: any, request: Request) { +export function newJobCapsProvider(callWithRequest: any) { async function newJobCaps( indexPattern: string, - isRollup: boolean = false + isRollup: boolean = false, + savedObjectsClient: SavedObjectsClientContract ): Promise<NewJobCapsResponse> { - const fieldService = fieldServiceProvider(indexPattern, isRollup, callWithRequest, request); + const fieldService = fieldServiceProvider( + indexPattern, + isRollup, + callWithRequest, + savedObjectsClient + ); const { aggs, fields } = await fieldService.getData(); convertForStringify(aggs, fields); diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts index 5f8d8ae5c1f25..11b0802192e1f 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/rollup.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Request } from 'src/legacy/server/kbn_server'; import { SavedObject } from 'src/core/server'; +import { SavedObjectsClientContract } from 'kibana/server'; import { FieldId } from '../../../../common/types/fields'; import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types'; @@ -21,9 +21,9 @@ export interface RollupJob { export async function rollupServiceProvider( indexPattern: string, callWithRequest: any, - request: Request + savedObjectsClient: SavedObjectsClientContract ) { - const rollupIndexPatternObject = await loadRollupIndexPattern(indexPattern, request); + const rollupIndexPatternObject = await loadRollupIndexPattern(indexPattern, savedObjectsClient); let jobIndexPatterns: string[] = [indexPattern]; async function getRollupJobs(): Promise<RollupJob[] | null> { @@ -57,9 +57,8 @@ export async function rollupServiceProvider( async function loadRollupIndexPattern( indexPattern: string, - request: Request + savedObjectsClient: SavedObjectsClientContract ): Promise<SavedObject | null> { - const savedObjectsClient = request.getSavedObjectsClient(); const resp = await savedObjectsClient.find({ type: 'index-pattern', fields: ['title', 'type', 'typeMeta'], diff --git a/x-pack/legacy/plugins/ml/server/new_platform/anomaly_detectors_schema.ts b/x-pack/legacy/plugins/ml/server/new_platform/anomaly_detectors_schema.ts index 392d3bfd84768..d728fbf312d76 100644 --- a/x-pack/legacy/plugins/ml/server/new_platform/anomaly_detectors_schema.ts +++ b/x-pack/legacy/plugins/ml/server/new_platform/anomaly_detectors_schema.ts @@ -6,6 +6,18 @@ import { schema } from '@kbn/config-schema'; +const customRulesSchema = schema.maybe( + schema.arrayOf( + schema.maybe( + schema.object({ + actions: schema.arrayOf(schema.string()), + conditions: schema.arrayOf(schema.any()), + scope: schema.maybe(schema.any()), + }) + ) + ) +); + const detectorSchema = schema.object({ identifier: schema.maybe(schema.string()), function: schema.string(), @@ -14,6 +26,7 @@ const detectorSchema = schema.object({ over_field_name: schema.maybe(schema.string()), partition_field_name: schema.maybe(schema.string()), detector_description: schema.maybe(schema.string()), + custom_rules: customRulesSchema, }); const customUrlSchema = { @@ -34,15 +47,8 @@ export const anomalyDetectionUpdateJobSchema = { schema.maybe( schema.object({ detector_index: schema.number(), - custom_rules: schema.arrayOf( - schema.maybe( - schema.object({ - actions: schema.arrayOf(schema.string()), - conditions: schema.arrayOf(schema.any()), - scope: schema.maybe(schema.any()), - }) - ) - ), + description: schema.maybe(schema.string()), + custom_rules: customRulesSchema, }) ) ) diff --git a/x-pack/legacy/plugins/ml/server/new_platform/job_service_schema.ts b/x-pack/legacy/plugins/ml/server/new_platform/job_service_schema.ts new file mode 100644 index 0000000000000..b37fcba737802 --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/new_platform/job_service_schema.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +const analyzerSchema = { + tokenizer: schema.string(), + filter: schema.maybe( + schema.arrayOf( + schema.object({ + type: schema.string(), + stopwords: schema.arrayOf(schema.maybe(schema.string())), + }) + ) + ), +}; + +export const categorizationFieldExamplesSchema = { + indexPatternTitle: schema.string(), + query: schema.any(), + size: schema.number(), + field: schema.string(), + timeField: schema.maybe(schema.string()), + start: schema.number(), + end: schema.number(), + analyzer: schema.object(analyzerSchema), +}; + +export const chartSchema = { + indexPatternTitle: schema.string(), + timeField: schema.maybe(schema.string()), + start: schema.maybe(schema.number()), + end: schema.maybe(schema.number()), + intervalMs: schema.number(), + query: schema.any(), + aggFieldNamePairs: schema.arrayOf(schema.any()), + splitFieldName: schema.maybe(schema.nullable(schema.string())), + splitFieldValue: schema.maybe(schema.nullable(schema.string())), +}; + +export const datafeedIdsSchema = { datafeedIds: schema.arrayOf(schema.maybe(schema.string())) }; + +export const forceStartDatafeedSchema = { + datafeedIds: schema.arrayOf(schema.maybe(schema.string())), + start: schema.maybe(schema.number()), + end: schema.maybe(schema.number()), +}; + +export const jobIdsSchema = { + jobIds: schema.maybe( + schema.oneOf([schema.string(), schema.arrayOf(schema.maybe(schema.string()))]) + ), +}; + +export const jobsWithTimerangeSchema = { dateFormatTz: schema.maybe(schema.string()) }; + +export const lookBackProgressSchema = { + jobId: schema.string(), + start: schema.maybe(schema.number()), + end: schema.maybe(schema.number()), +}; + +export const topCategoriesSchema = { jobId: schema.string(), count: schema.number() }; + +export const updateGroupsSchema = { + jobs: schema.maybe( + schema.arrayOf( + schema.object({ + job_id: schema.maybe(schema.string()), + groups: schema.arrayOf(schema.maybe(schema.string())), + }) + ) + ), +}; diff --git a/x-pack/legacy/plugins/ml/server/routes/apidoc.json b/x-pack/legacy/plugins/ml/server/routes/apidoc.json index 919592f8ed62a..3fac715fef85a 100644 --- a/x-pack/legacy/plugins/ml/server/routes/apidoc.json +++ b/x-pack/legacy/plugins/ml/server/routes/apidoc.json @@ -50,6 +50,25 @@ "Annotations", "GetAnnotations", "IndexAnnotations", - "DeleteAnnotation" + "DeleteAnnotation", + "JobService", + "ForceStartDatafeeds", + "StopDatafeeds", + "DeleteJobs", + "CloseJobs", + "JobsSummary", + "JobsWithTimerange", + "CreateFullJobsList", + "GetAllGroups", + "UpdateGroups", + "DeletingJobTasks", + "JobsExist", + "NewJobCaps", + "NewJobLineChart", + "NewJobPopulationChart", + "GetAllJobAndGroupIds", + "GetLookBackProgress", + "ValidateCategoryExamples", + "TopCategories" ] } diff --git a/x-pack/legacy/plugins/ml/server/routes/calendars.ts b/x-pack/legacy/plugins/ml/server/routes/calendars.ts index 19d614a4e6a22..8e4e1c4c14751 100644 --- a/x-pack/legacy/plugins/ml/server/routes/calendars.ts +++ b/x-pack/legacy/plugins/ml/server/routes/calendars.ts @@ -13,32 +13,32 @@ import { calendarSchema } from '../new_platform/calendars_schema'; import { CalendarManager, Calendar, FormCalendar } from '../models/calendar'; function getAllCalendars(context: RequestHandlerContext) { - const cal = new CalendarManager(false, context); + const cal = new CalendarManager(context.ml!.mlClient.callAsCurrentUser); return cal.getAllCalendars(); } function getCalendar(context: RequestHandlerContext, calendarId: string) { - const cal = new CalendarManager(false, context); + const cal = new CalendarManager(context.ml!.mlClient.callAsCurrentUser); return cal.getCalendar(calendarId); } function newCalendar(context: RequestHandlerContext, calendar: FormCalendar) { - const cal = new CalendarManager(false, context); + const cal = new CalendarManager(context.ml!.mlClient.callAsCurrentUser); return cal.newCalendar(calendar); } function updateCalendar(context: RequestHandlerContext, calendarId: string, calendar: Calendar) { - const cal = new CalendarManager(false, context); + const cal = new CalendarManager(context.ml!.mlClient.callAsCurrentUser); return cal.updateCalendar(calendarId, calendar); } function deleteCalendar(context: RequestHandlerContext, calendarId: string) { - const cal = new CalendarManager(false, context); + const cal = new CalendarManager(context.ml!.mlClient.callAsCurrentUser); return cal.deleteCalendar(calendarId); } function getCalendarsByIds(context: RequestHandlerContext, calendarIds: string) { - const cal = new CalendarManager(false, context); + const cal = new CalendarManager(context.ml!.mlClient.callAsCurrentUser); return cal.getCalendarsByIds(calendarIds); } diff --git a/x-pack/legacy/plugins/ml/server/routes/job_service.js b/x-pack/legacy/plugins/ml/server/routes/job_service.js deleted file mode 100644 index a83b4fa403f65..0000000000000 --- a/x-pack/legacy/plugins/ml/server/routes/job_service.js +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { callWithRequestFactory } from '../client/call_with_request_factory'; -import { wrapError } from '../client/errors'; -import { jobServiceProvider } from '../models/job_service'; - -export function jobServiceRoutes({ commonRouteConfig, elasticsearchPlugin, route }) { - route({ - method: 'POST', - path: '/api/ml/jobs/force_start_datafeeds', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { forceStartDatafeeds } = jobServiceProvider(callWithRequest); - const { datafeedIds, start, end } = request.payload; - return forceStartDatafeeds(datafeedIds, start, end).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/stop_datafeeds', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { stopDatafeeds } = jobServiceProvider(callWithRequest); - const { datafeedIds } = request.payload; - return stopDatafeeds(datafeedIds).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/delete_jobs', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { deleteJobs } = jobServiceProvider(callWithRequest); - const { jobIds } = request.payload; - return deleteJobs(jobIds).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/close_jobs', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { closeJobs } = jobServiceProvider(callWithRequest); - const { jobIds } = request.payload; - return closeJobs(jobIds).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/jobs_summary', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { jobsSummary } = jobServiceProvider(callWithRequest); - const { jobIds } = request.payload; - return jobsSummary(jobIds).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/jobs_with_timerange', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { jobsWithTimerange } = jobServiceProvider(callWithRequest); - const { dateFormatTz } = request.payload; - return jobsWithTimerange(dateFormatTz).catch(resp => { - wrapError(resp); - }); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/jobs', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { createFullJobsList } = jobServiceProvider(callWithRequest); - const { jobIds } = request.payload; - return createFullJobsList(jobIds).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'GET', - path: '/api/ml/jobs/groups', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { getAllGroups } = jobServiceProvider(callWithRequest); - return getAllGroups().catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/update_groups', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { updateGroups } = jobServiceProvider(callWithRequest); - const { jobs } = request.payload; - return updateGroups(jobs).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'GET', - path: '/api/ml/jobs/deleting_jobs_tasks', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { deletingJobTasks } = jobServiceProvider(callWithRequest); - return deletingJobTasks().catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/jobs_exist', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { jobsExist } = jobServiceProvider(callWithRequest); - const { jobIds } = request.payload; - return jobsExist(jobIds).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'GET', - path: '/api/ml/jobs/new_job_caps/{indexPattern}', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { indexPattern } = request.params; - const isRollup = request.query.rollup === 'true'; - const { newJobCaps } = jobServiceProvider(callWithRequest, request); - return newJobCaps(indexPattern, isRollup).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/new_job_line_chart', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { - indexPatternTitle, - timeField, - start, - end, - intervalMs, - query, - aggFieldNamePairs, - splitFieldName, - splitFieldValue, - } = request.payload; - const { newJobLineChart } = jobServiceProvider(callWithRequest, request); - return newJobLineChart( - indexPatternTitle, - timeField, - start, - end, - intervalMs, - query, - aggFieldNamePairs, - splitFieldName, - splitFieldValue - ).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/new_job_population_chart', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { - indexPatternTitle, - timeField, - start, - end, - intervalMs, - query, - aggFieldNamePairs, - splitFieldName, - } = request.payload; - const { newJobPopulationChart } = jobServiceProvider(callWithRequest, request); - return newJobPopulationChart( - indexPatternTitle, - timeField, - start, - end, - intervalMs, - query, - aggFieldNamePairs, - splitFieldName - ).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'GET', - path: '/api/ml/jobs/all_jobs_and_group_ids', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { getAllJobAndGroupIds } = jobServiceProvider(callWithRequest); - return getAllJobAndGroupIds().catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/look_back_progress', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { getLookBackProgress } = jobServiceProvider(callWithRequest); - const { jobId, start, end } = request.payload; - return getLookBackProgress(jobId, start, end).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/categorization_field_examples', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { validateCategoryExamples } = jobServiceProvider(callWithRequest); - const { - indexPatternTitle, - timeField, - query, - size, - field, - start, - end, - analyzer, - } = request.payload; - return validateCategoryExamples( - indexPatternTitle, - query, - size, - field, - timeField, - start, - end, - analyzer - ).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); - - route({ - method: 'POST', - path: '/api/ml/jobs/top_categories', - handler(request) { - const callWithRequest = callWithRequestFactory(elasticsearchPlugin, request); - const { topCategories } = jobServiceProvider(callWithRequest); - const { jobId, count } = request.payload; - return topCategories(jobId, count).catch(resp => wrapError(resp)); - }, - config: { - ...commonRouteConfig, - }, - }); -} diff --git a/x-pack/legacy/plugins/ml/server/routes/job_service.ts b/x-pack/legacy/plugins/ml/server/routes/job_service.ts new file mode 100644 index 0000000000000..3af651c92353b --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/routes/job_service.ts @@ -0,0 +1,610 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { licensePreRoutingFactory } from '../new_platform/licence_check_pre_routing_factory'; +import { wrapError } from '../client/error_wrapper'; +import { RouteInitialization } from '../new_platform/plugin'; +import { + categorizationFieldExamplesSchema, + chartSchema, + datafeedIdsSchema, + forceStartDatafeedSchema, + jobIdsSchema, + jobsWithTimerangeSchema, + lookBackProgressSchema, + topCategoriesSchema, + updateGroupsSchema, +} from '../new_platform/job_service_schema'; +// @ts-ignore no declaration module +import { jobServiceProvider } from '../models/job_service'; + +/** + * Routes for job service + */ +export function jobServiceRoutes({ xpackMainPlugin, router }: RouteInitialization) { + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/force_start_datafeeds + * @apiName ForceStartDatafeeds + * @apiDescription Starts one or more datafeeds + */ + router.post( + { + path: '/api/ml/jobs/force_start_datafeeds', + validate: { + body: schema.object(forceStartDatafeedSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { forceStartDatafeeds } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { datafeedIds, start, end } = request.body; + const resp = await forceStartDatafeeds(datafeedIds, start, end); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/stop_datafeeds + * @apiName StopDatafeeds + * @apiDescription Stops one or more datafeeds + */ + router.post( + { + path: '/api/ml/jobs/stop_datafeeds', + validate: { + body: schema.object(datafeedIdsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { stopDatafeeds } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { datafeedIds } = request.body; + const resp = await stopDatafeeds(datafeedIds); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/delete_jobs + * @apiName DeleteJobs + * @apiDescription Deletes an existing anomaly detection job + */ + router.post( + { + path: '/api/ml/jobs/delete_jobs', + validate: { + body: schema.object(jobIdsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { deleteJobs } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobIds } = request.body; + const resp = await deleteJobs(jobIds); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/close_jobs + * @apiName CloseJobs + * @apiDescription Closes one or more anomaly detection jobs + */ + router.post( + { + path: '/api/ml/jobs/close_jobs', + validate: { + body: schema.object(jobIdsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { closeJobs } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobIds } = request.body; + const resp = await closeJobs(jobIds); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/jobs_summary + * @apiName JobsSummary + * @apiDescription Creates a summary jobs list. Jobs include job stats, datafeed stats, and calendars. + */ + router.post( + { + path: '/api/ml/jobs/jobs_summary', + validate: { + body: schema.object(jobIdsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { jobsSummary } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobIds } = request.body; + const resp = await jobsSummary(jobIds); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/jobs_with_time_range + * @apiName JobsWithTimerange + * @apiDescription Creates a list of jobs with data about the job's timerange + */ + router.post( + { + path: '/api/ml/jobs/jobs_with_time_range', + validate: { + body: schema.object(jobsWithTimerangeSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { jobsWithTimerange } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { dateFormatTz } = request.body; + const resp = await jobsWithTimerange(dateFormatTz); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/jobs + * @apiName CreateFullJobsList + * @apiDescription Creates a list of jobs + */ + router.post( + { + path: '/api/ml/jobs/jobs', + validate: { + body: schema.object(jobIdsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { createFullJobsList } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobIds } = request.body; + const resp = await createFullJobsList(jobIds); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {get} /api/ml/jobs/groups + * @apiName GetAllGroups + * @apiDescription Returns array of group objects with job ids listed for each group + */ + router.get( + { + path: '/api/ml/jobs/groups', + validate: false, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { getAllGroups } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const resp = await getAllGroups(); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/update_groups + * @apiName UpdateGroups + * @apiDescription Updates 'groups' property of an anomaly detection job + */ + router.post( + { + path: '/api/ml/jobs/update_groups', + validate: { + body: schema.object(updateGroupsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { updateGroups } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobs } = request.body; + const resp = await updateGroups(jobs); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {get} /api/ml/jobs/deleting_jobs_tasks + * @apiName DeletingJobTasks + * @apiDescription Gets the ids of deleting anomaly detection jobs + */ + router.get( + { + path: '/api/ml/jobs/deleting_jobs_tasks', + validate: false, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { deletingJobTasks } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const resp = await deletingJobTasks(); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/jobs_exist + * @apiName JobsExist + * @apiDescription Checks if each of the jobs in the specified list of IDs exist + */ + router.post( + { + path: '/api/ml/jobs/jobs_exist', + validate: { + body: schema.object(jobIdsSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { jobsExist } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobIds } = request.body; + const resp = await jobsExist(jobIds); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {get} /api/ml/jobs/new_job_caps/:indexPattern + * @apiName NewJobCaps + * @apiDescription Retrieve the capabilities of fields for indices + */ + router.get( + { + path: '/api/ml/jobs/new_job_caps/{indexPattern}', + validate: { + params: schema.object({ indexPattern: schema.string() }), + query: schema.maybe(schema.object({ rollup: schema.maybe(schema.string()) })), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { indexPattern } = request.params; + const isRollup = request.query.rollup === 'true'; + const savedObjectsClient = context.core.savedObjects.client; + const { newJobCaps } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const resp = await newJobCaps(indexPattern, isRollup, savedObjectsClient); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/new_job_line_chart + * @apiName NewJobLineChart + * @apiDescription Returns line chart data for anomaly detection job + */ + router.post( + { + path: '/api/ml/jobs/new_job_line_chart', + validate: { + body: schema.object(chartSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName, + splitFieldValue, + } = request.body; + + const { newJobLineChart } = jobServiceProvider( + context.ml!.mlClient.callAsCurrentUser, + request + ); + const resp = await newJobLineChart( + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName, + splitFieldValue + ); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/new_job_population_chart + * @apiName NewJobPopulationChart + * @apiDescription Returns population job chart data + */ + router.post( + { + path: '/api/ml/jobs/new_job_population_chart', + validate: { + body: schema.object(chartSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName, + } = request.body; + + const { newJobPopulationChart } = jobServiceProvider( + context.ml!.mlClient.callAsCurrentUser + ); + const resp = await newJobPopulationChart( + indexPatternTitle, + timeField, + start, + end, + intervalMs, + query, + aggFieldNamePairs, + splitFieldName + ); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {get} /api/ml/jobs/all_jobs_and_group_ids + * @apiName GetAllJobAndGroupIds + * @apiDescription Returns a list of all job IDs and all group IDs + */ + router.get( + { + path: '/api/ml/jobs/all_jobs_and_group_ids', + validate: false, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { getAllJobAndGroupIds } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const resp = await getAllJobAndGroupIds(); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/look_back_progress + * @apiName GetLookBackProgress + * @apiDescription Returns current progress of anomaly detection job + */ + router.post( + { + path: '/api/ml/jobs/look_back_progress', + validate: { + body: schema.object(lookBackProgressSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { getLookBackProgress } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobId, start, end } = request.body; + const resp = await getLookBackProgress(jobId, start, end); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/categorization_field_examples + * @apiName ValidateCategoryExamples + * @apiDescription Validates category examples + */ + router.post( + { + path: '/api/ml/jobs/categorization_field_examples', + validate: { + body: schema.object(categorizationFieldExamplesSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { validateCategoryExamples } = jobServiceProvider( + context.ml!.mlClient.callAsCurrentUser + ); + const { + indexPatternTitle, + timeField, + query, + size, + field, + start, + end, + analyzer, + } = request.body; + + const resp = await validateCategoryExamples( + indexPatternTitle, + query, + size, + field, + timeField, + start, + end, + analyzer + ); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup JobService + * + * @api {post} /api/ml/jobs/top_categories + * @apiName TopCategories + * @apiDescription Returns list of top categories + */ + router.post( + { + path: '/api/ml/jobs/top_categories', + validate: { + body: schema.object(topCategoriesSchema), + }, + }, + licensePreRoutingFactory(xpackMainPlugin, async (context, request, response) => { + try { + const { topCategories } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); + const { jobId, count } = request.body; + const resp = await topCategories(jobId, count); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); +} diff --git a/x-pack/legacy/plugins/monitoring/common/formatting.js b/x-pack/legacy/plugins/monitoring/common/formatting.js index a3b3ce07c8c76..ed5d68f942dfd 100644 --- a/x-pack/legacy/plugins/monitoring/common/formatting.js +++ b/x-pack/legacy/plugins/monitoring/common/formatting.js @@ -13,14 +13,16 @@ export const SMALL_BYTES = '0.0 b'; export const LARGE_ABBREVIATED = '0,0.[0]a'; /** - * Format the {@code date} in the user's expected date/time format using their <em>guessed</em> local time zone. + * Format the {@code date} in the user's expected date/time format using their <em>dateFormat:tz</em> defined time zone. * @param date Either a numeric Unix timestamp or a {@code Date} object * @returns The date formatted using 'LL LTS' */ -export function formatDateTimeLocal(date, useUTC = false) { - return useUTC - ? moment.utc(date).format('LL LTS') - : moment.tz(date, moment.tz.guess()).format('LL LTS'); +export function formatDateTimeLocal(date, timezone) { + if (timezone === 'Browser') { + timezone = moment.tz.guess() || 'utc'; + } + + return moment.tz(date, timezone).format('LL LTS'); } /** diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/alerts.js b/x-pack/legacy/plugins/monitoring/public/components/alerts/alerts.js index 4c2f3b027bc8a..11fcef73a4b97 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/alerts.js +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/alerts.js @@ -5,6 +5,7 @@ */ import React from 'react'; +import chrome from '../../np_imports/ui/chrome'; import { capitalize } from 'lodash'; import { formatDateTimeLocal } from '../../../common/formatting'; import { formatTimestampToDuration } from '../../../common'; @@ -21,7 +22,7 @@ const linkToCategories = { 'kibana/instances': 'Kibana Instances', 'logstash/instances': 'Logstash Nodes', }; -const getColumns = (kbnUrl, scope) => [ +const getColumns = (kbnUrl, scope, timezone) => [ { name: i18n.translate('xpack.monitoring.alerts.statusColumnTitle', { defaultMessage: 'Status', @@ -126,7 +127,7 @@ const getColumns = (kbnUrl, scope) => [ }), field: 'update_timestamp', sortable: true, - render: timestamp => formatDateTimeLocal(timestamp), + render: timestamp => formatDateTimeLocal(timestamp, timezone), }, { name: i18n.translate('xpack.monitoring.alerts.triggeredColumnTitle', { @@ -151,11 +152,14 @@ export const Alerts = ({ alerts, angular, sorting, pagination, onTableChange }) category: alert.metadata.link, })); + const injector = chrome.dangerouslyGetActiveInjector(); + const timezone = injector.get('config').get('dateFormat:tz'); + return ( <EuiMonitoringTable className="alertsTable" rows={alertsFlattened} - columns={getColumns(angular.kbnUrl, angular.scope)} + columns={getColumns(angular.kbnUrl, angular.scope, timezone)} sorting={{ ...sorting, sort: { diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/get_chart_options.js b/x-pack/legacy/plugins/monitoring/public/components/chart/get_chart_options.js index 6f26abeadb3a0..661d51e068201 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/chart/get_chart_options.js +++ b/x-pack/legacy/plugins/monitoring/public/components/chart/get_chart_options.js @@ -9,7 +9,7 @@ import { merge } from 'lodash'; import { CHART_LINE_COLOR, CHART_TEXT_COLOR } from '../../../common/constants'; export async function getChartOptions(axisOptions) { - const $injector = await chrome.dangerouslyGetActiveInjector(); + const $injector = chrome.dangerouslyGetActiveInjector(); const timezone = $injector.get('config').get('dateFormat:tz'); const opts = { legend: { diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js index a8001638f4399..8455fb8cf3088 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js +++ b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js @@ -6,6 +6,7 @@ import React, { Fragment } from 'react'; import moment from 'moment-timezone'; +import chrome from '../../../np_imports/ui/chrome'; import { FormattedAlert } from 'plugins/monitoring/components/alerts/formatted_alert'; import { mapSeverity } from 'plugins/monitoring/components/alerts/map_severity'; import { formatTimestampToDuration } from '../../../../common/format_timestamp_to_duration'; @@ -57,6 +58,9 @@ export function AlertsPanel({ alerts, changeUrl }) { severityIcon.iconType = 'check'; } + const injector = chrome.dangerouslyGetActiveInjector(); + const timezone = injector.get('config').get('dateFormat:tz'); + return ( <EuiCallOut key={`alert-item-${index}`} @@ -79,7 +83,7 @@ export function AlertsPanel({ alerts, changeUrl }) { id="xpack.monitoring.cluster.overview.alertsPanel.lastCheckedTimeText" defaultMessage="Last checked {updateDateTime} (triggered {duration} ago)" values={{ - updateDateTime: formatDateTimeLocal(item.update_timestamp), + updateDateTime: formatDateTimeLocal(item.update_timestamp, timezone), duration: formatTimestampToDuration(item.timestamp, CALCULATE_DURATION_SINCE), }} /> diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap index 366c23135cc76..e55f9c84b51fe 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap +++ b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap @@ -147,7 +147,7 @@ exports[`CcrShard that it renders normally 1`] = ` size="s" > <h2> - September 27, 2018 9:32:09 AM + September 27, 2018 1:32:09 PM </h2> </EuiTitle> <EuiHorizontalRule /> diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js index 68c87b386da49..af0ff323b7ba8 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js +++ b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js @@ -5,6 +5,7 @@ */ import React, { Fragment, PureComponent } from 'react'; +import chrome from '../../../np_imports/ui/chrome'; import { EuiPage, EuiPageBody, @@ -92,6 +93,8 @@ export class CcrShard extends PureComponent { renderLatestStat() { const { stat, timestamp } = this.props; + const injector = chrome.dangerouslyGetActiveInjector(); + const timezone = injector.get('config').get('dateFormat:tz'); return ( <EuiAccordion @@ -110,7 +113,7 @@ export class CcrShard extends PureComponent { > <Fragment> <EuiTitle size="s"> - <h2>{formatDateTimeLocal(timestamp)}</h2> + <h2>{formatDateTimeLocal(timestamp, timezone)}</h2> </EuiTitle> <EuiHorizontalRule /> <EuiCodeBlock language="json">{JSON.stringify(stat, null, 2)}</EuiCodeBlock> diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js index 17caa8429a275..b950c2ca0a6d2 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js +++ b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js @@ -11,6 +11,7 @@ import { CcrShard } from './ccr_shard'; jest.mock('../../../np_imports/ui/chrome', () => { return { getBasePath: () => '', + dangerouslyGetActiveInjector: () => ({ get: () => ({ get: () => 'utc' }) }), }; }); diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js index 692025631f3b8..133b520947b1b 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js +++ b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import chrome from '../../../np_imports/ui/chrome'; import { capitalize } from 'lodash'; import { formatMetric } from 'plugins/monitoring/lib/format_number'; import { formatDateTimeLocal } from '../../../../common/formatting'; @@ -38,13 +39,15 @@ export const parseProps = props => { } = props; const { files, size } = index; + const injector = chrome.dangerouslyGetActiveInjector(); + const timezone = injector.get('config').get('dateFormat:tz'); return { name: indexName || index.name, shard: `${id} / ${isPrimary ? 'Primary' : 'Replica'}`, relocationType: type === 'PRIMARY_RELOCATION' ? 'Primary Relocation' : normalizeString(type), stage: normalizeString(stage), - startTime: formatDateTimeLocal(startTimeInMillis), + startTime: formatDateTimeLocal(startTimeInMillis, timezone), totalTime: formatMetric(Math.floor(totalTimeInMillis / 1000), '00:00:00'), isCopiedFromPrimary: !isPrimary || type === 'PRIMARY_RELOCATION', sourceName: source.name === undefined ? 'n/a' : source.name, diff --git a/x-pack/legacy/plugins/monitoring/public/components/logs/logs.js b/x-pack/legacy/plugins/monitoring/public/components/logs/logs.js index 926f5cdda26a7..744ebb5a7ceb4 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/logs/logs.js +++ b/x-pack/legacy/plugins/monitoring/public/components/logs/logs.js @@ -14,6 +14,12 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { Reason } from './reason'; import { capabilities } from '../../np_imports/ui/capabilities'; +const getFormattedDateTimeLocal = timestamp => { + const injector = chrome.dangerouslyGetActiveInjector(); + const timezone = injector.get('config').get('dateFormat:tz'); + return formatDateTimeLocal(timestamp, timezone); +}; + const columnTimestampTitle = i18n.translate('xpack.monitoring.logs.listing.timestampTitle', { defaultMessage: 'Timestamp', }); @@ -43,7 +49,7 @@ const columns = [ field: 'timestamp', name: columnTimestampTitle, width: '12%', - render: timestamp => formatDateTimeLocal(timestamp, true), + render: timestamp => getFormattedDateTimeLocal(timestamp), }, { field: 'level', @@ -73,7 +79,7 @@ const clusterColumns = [ field: 'timestamp', name: columnTimestampTitle, width: '12%', - render: timestamp => formatDateTimeLocal(timestamp, true), + render: timestamp => getFormattedDateTimeLocal(timestamp), }, { field: 'level', diff --git a/x-pack/legacy/plugins/monitoring/public/views/license/controller.js b/x-pack/legacy/plugins/monitoring/public/views/license/controller.js index dcd3ca76ceffd..ce6e9c8fb74cd 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/license/controller.js +++ b/x-pack/legacy/plugins/monitoring/public/views/license/controller.js @@ -54,11 +54,13 @@ export class LicenseViewController { } renderReact($scope) { + const injector = chrome.dangerouslyGetActiveInjector(); + const timezone = injector.get('config').get('dateFormat:tz'); $scope.$evalAsync(() => { const { isPrimaryCluster, license, isExpired, uploadLicensePath } = this; let expiryDate = license.expiry_date_in_millis; if (license.expiry_date_in_millis !== undefined) { - expiryDate = formatDateTimeLocal(license.expiry_date_in_millis); + expiryDate = formatDateTimeLocal(license.expiry_date_in_millis, timezone); } // Mount the React component to the template diff --git a/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.test.ts b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.test.ts index ec00ece9e6ee2..38b4e6c60ca48 100644 --- a/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.test.ts +++ b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.test.ts @@ -102,7 +102,7 @@ describe('getLicenseExpiration', () => { it('should have the right id and actionGroups', () => { const alert = getLicenseExpiration(server, getMonitoringCluster, getLogger, ccrEnabled); expect(alert.id).toBe(ALERT_TYPE_LICENSE_EXPIRATION); - expect(alert.actionGroups).toEqual(['default']); + expect(alert.actionGroups).toEqual([{ id: 'default', name: 'Default' }]); }); it('should return the state if no license is provided', async () => { diff --git a/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts index 197c5c9cdcbc7..8688a2b08efc4 100644 --- a/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts +++ b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts @@ -8,6 +8,7 @@ import moment from 'moment-timezone'; import { get } from 'lodash'; import { Legacy } from 'kibana'; import { Logger } from 'src/core/server'; +import { i18n } from '@kbn/i18n'; import { ALERT_TYPE_LICENSE_EXPIRATION, INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; import { AlertType } from '../../../alerting'; import { fetchLicenses } from '../lib/alerts/fetch_licenses'; @@ -45,7 +46,14 @@ export const getLicenseExpiration = ( return { id: ALERT_TYPE_LICENSE_EXPIRATION, name: 'Monitoring Alert - License Expiration', - actionGroups: ['default'], + actionGroups: [ + { + id: 'default', + name: i18n.translate('xpack.monitoring.alerts.licenseExpiration.actionGroups.default', { + defaultMessage: 'Default', + }), + }, + ], async executor({ services, params, diff --git a/x-pack/legacy/plugins/remote_clusters/public/app/services/query_params.js b/x-pack/legacy/plugins/remote_clusters/public/app/services/query_params.js index f0871d62976ed..af462bfeffcf5 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/app/services/query_params.js +++ b/x-pack/legacy/plugins/remote_clusters/public/app/services/query_params.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { parse } from 'querystring'; +import { parse } from 'query-string'; export function extractQueryParams(queryString) { const hrefSplit = queryString.split('?'); @@ -12,5 +12,5 @@ export function extractQueryParams(queryString) { return {}; } - return parse(hrefSplit[1]); + return parse(hrefSplit[1], { sort: false }); } diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/encode_uri_query.js b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/encode_uri_query.js deleted file mode 100644 index ce2346b0f28dc..0000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/encode_uri_query.js +++ /dev/null @@ -1,39 +0,0 @@ -/* eslint-disable @kbn/eslint/require-license-header */ - -// This function was extracted from angular v1.3 - -/* @notice - * This product includes code that was extracted from angular@1.3. - * Original license: - * The MIT License - * - * Copyright (c) 2010-2014 Google, Inc. http://angularjs.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -export function encodeUriQuery(val, pctEncodeSpaces) { - return encodeURIComponent(val) - .replace(/%40/gi, '@') - .replace(/%3A/gi, ':') - .replace(/%24/g, '$') - .replace(/%2C/gi, ',') - .replace(/%3B/gi, ';') - .replace(/%20/g, pctEncodeSpaces ? '%20' : '+'); -} diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/uri_encode.js b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/uri_encode.js index 5b93461bfaffb..f764271c22a2d 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/uri_encode.js +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/uri_encode.js @@ -5,20 +5,20 @@ */ import { forEach, isArray } from 'lodash'; -import { encodeUriQuery } from './encode_uri_query'; +import { url } from '../../../../../../../../src/plugins/kibana_utils/server'; function toKeyValue(obj) { const parts = []; forEach(obj, function(value, key) { if (isArray(value)) { forEach(value, function(arrayValue) { - const keyStr = encodeUriQuery(key, true); - const valStr = arrayValue === true ? '' : '=' + encodeUriQuery(arrayValue, true); + const keyStr = url.encodeUriQuery(key, true); + const valStr = arrayValue === true ? '' : '=' + url.encodeUriQuery(arrayValue, true); parts.push(keyStr + valStr); }); } else { - const keyStr = encodeUriQuery(key, true); - const valStr = value === true ? '' : '=' + encodeUriQuery(value, true); + const keyStr = url.encodeUriQuery(key, true); + const valStr = value === true ? '' : '=' + url.encodeUriQuery(value, true); parts.push(keyStr + valStr); } }); @@ -27,5 +27,5 @@ function toKeyValue(obj) { export const uriEncode = { stringify: toKeyValue, - string: encodeUriQuery, + string: url.encodeUriQuery, }; diff --git a/x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts b/x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts index 9056c7967b4a8..d471dc57fc9e1 100644 --- a/x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts +++ b/x-pack/legacy/plugins/reporting/public/lib/reporting_client.ts @@ -3,16 +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 { stringify } from 'query-string'; import { npStart } from 'ui/new_platform'; -import querystring from 'querystring'; - -const { core } = npStart; - // @ts-ignore import rison from 'rison-node'; import { add } from './job_completion_notifications'; +const { core } = npStart; const API_BASE_URL = '/api/reporting/generate'; interface JobParams { @@ -20,7 +17,7 @@ interface JobParams { } export const getReportingJobPath = (exportType: string, jobParams: JobParams) => { - const params = querystring.stringify({ jobParams: rison.encode(jobParams) }); + const params = stringify({ jobParams: rison.encode(jobParams) }); return `${core.http.basePath.prepend(API_BASE_URL)}/${exportType}?${params}`; }; diff --git a/x-pack/legacy/plugins/searchprofiler/index.ts b/x-pack/legacy/plugins/searchprofiler/index.ts deleted file mode 100644 index fab2e43847348..0000000000000 --- a/x-pack/legacy/plugins/searchprofiler/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { resolve } from 'path'; - -// TODO: -// Until we can process SCSS in new platform, this part of Searchprofiler -// legacy must remain here. - -export const searchprofiler = (kibana: any) => { - const publicSrc = resolve(__dirname, 'public'); - - return new kibana.Plugin({ - require: ['elasticsearch', 'xpack_main'], - id: 'searchprofiler', - configPrefix: 'xpack.searchprofiler', - publicDir: publicSrc, - - uiExports: { - styleSheetPaths: `${publicSrc}/index.scss`, - }, - init() {}, - }); -}; diff --git a/x-pack/legacy/plugins/searchprofiler/public/index.scss b/x-pack/legacy/plugins/searchprofiler/public/index.scss deleted file mode 100644 index e04e81c023196..0000000000000 --- a/x-pack/legacy/plugins/searchprofiler/public/index.scss +++ /dev/null @@ -1,12 +0,0 @@ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; - -// Search profiler plugin styles - -// Prefix all styles with "prfDevTool" to avoid conflicts. -// Examples -// prfDevTool__ -// prfDevTool__cell -// prfDevTool__shardDetails - -@import 'styles/index'; diff --git a/x-pack/legacy/plugins/security/index.js b/x-pack/legacy/plugins/security/index.js index fd89c40f010b7..18b815fb429cb 100644 --- a/x-pack/legacy/plugins/security/index.js +++ b/x-pack/legacy/plugins/security/index.js @@ -115,9 +115,6 @@ export const security = kibana => const xpackInfo = server.plugins.xpack_main.info; securityPlugin.__legacyCompat.registerLegacyAPI({ auditLogger: new AuditLogger(server, 'security', config, xpackInfo), - isSystemAPIRequest: server.plugins.kibana.systemApi.isSystemApiRequest.bind( - server.plugins.kibana.systemApi - ), }); // Legacy xPack Info endpoint returns whatever we return in a callback for `registerLicenseCheckResultsGenerator` diff --git a/x-pack/legacy/plugins/security/public/index.scss b/x-pack/legacy/plugins/security/public/index.scss index 187ad5231534d..0050d01a52493 100644 --- a/x-pack/legacy/plugins/security/public/index.scss +++ b/x-pack/legacy/plugins/security/public/index.scss @@ -7,14 +7,9 @@ // secChart__legend--small // secChart__legend-isLoading -$secFormWidth: 460px; - // Public components @import './components/index'; // Public views @import './views/index'; -// Styles of Kibana Platform plugin -@import '../../../../plugins/security/public/index'; - diff --git a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/__snapshots__/basic_login_form.test.tsx.snap b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/__snapshots__/basic_login_form.test.tsx.snap index 9814a2a7784fc..b09f398ed5ed9 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/__snapshots__/basic_login_form.test.tsx.snap +++ b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/__snapshots__/basic_login_form.test.tsx.snap @@ -64,6 +64,7 @@ exports[`BasicLoginForm renders as expected 1`] = ` > <EuiFieldText aria-required={true} + autoComplete="off" data-test-subj="loginPassword" disabled={false} id="password" diff --git a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx index c263381fbdb56..d5658cc297c26 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx @@ -75,6 +75,7 @@ class BasicLoginFormUI extends Component<Props, State> { } > <EuiFieldText + autoComplete="off" id="password" name="password" data-test-subj="loginPassword" diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/pagination/pagination.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/pagination/pagination.spec.ts index 3853e703a7c07..d8ad75322b889 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/pagination/pagination.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/pagination/pagination.spec.ts @@ -4,107 +4,77 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HOSTS_PAGE_TAB_URLS } from '../../lib/urls'; -import { - AUTHENTICATIONS_TABLE, - getDraggableField, - getPageButtonSelector, - NAVIGATION_AUTHENTICATIONS, - NAVIGATION_UNCOMMON_PROCESSES, - NUMBERED_PAGINATION, - SUPER_DATE_PICKER_APPLY_BUTTON, - UNCOMMON_PROCCESSES_TABLE, -} from '../../lib/pagination/selectors'; -import { DEFAULT_TIMEOUT, loginAndWaitForPage, waitForTableLoad } from '../../lib/util/helpers'; +import { HOSTS_PAGE_TAB_URLS } from '../../../urls/navigation'; +import { loginAndWaitForPage } from '../../../tasks/login'; +import { refreshPage } from '../../../tasks/header'; +import { goToFirstPage, goToThirdPage } from '../../../tasks/pagination'; +import { FIRST_PAGE_SELECTOR, THIRD_PAGE_SELECTOR } from '../../../screens/pagination'; +import { PROCESS_NAME_FIELD } from '../../../screens/uncommon_processes'; +import { waitForUncommonProcessesToBeLoaded } from '../../../tasks/uncommon_processes'; +import { waitForAuthenticationsToBeLoaded } from '../../../tasks/authentications'; +import { openAuthentications, openUncommonProcesses } from '../../../tasks/hosts/main'; describe('Pagination', () => { before(() => { loginAndWaitForPage(HOSTS_PAGE_TAB_URLS.uncommonProcesses); - waitForTableLoad(UNCOMMON_PROCCESSES_TABLE); + waitForUncommonProcessesToBeLoaded(); }); afterEach(() => { - cy.get(getPageButtonSelector(0)).click({ force: true }); + goToFirstPage(); }); it('pagination updates results and page number', () => { - cy.get(getPageButtonSelector(0)).should('have.class', 'euiPaginationButton-isActive'); + cy.get(FIRST_PAGE_SELECTOR).should('have.class', 'euiPaginationButton-isActive'); - cy.get(getDraggableField('process.name')) + cy.get(PROCESS_NAME_FIELD) .first() .invoke('text') - .then(text1 => { - cy.get(getPageButtonSelector(2)).click({ force: true }); - // wait for table to be done loading - waitForTableLoad(UNCOMMON_PROCCESSES_TABLE); - cy.get(getDraggableField('process.name')) + .then(processNameFirstPage => { + goToThirdPage(); + waitForUncommonProcessesToBeLoaded(); + cy.get(PROCESS_NAME_FIELD) .first() .invoke('text') - .should(text2 => { - expect(text1).not.to.eq(text2); + .should(processNameSecondPage => { + expect(processNameFirstPage).not.to.eq(processNameSecondPage); }); }); - cy.get(getPageButtonSelector(0)).should('not.have.class', 'euiPaginationButton-isActive'); - cy.get(getPageButtonSelector(2)).should('have.class', 'euiPaginationButton-isActive'); + cy.get(FIRST_PAGE_SELECTOR).should('not.have.class', 'euiPaginationButton-isActive'); + cy.get(THIRD_PAGE_SELECTOR).should('have.class', 'euiPaginationButton-isActive'); }); it('pagination keeps track of page results when tabs change', () => { - cy.get(getPageButtonSelector(0)).should('have.class', 'euiPaginationButton-isActive'); - let thirdPageResult: string; - cy.get(getPageButtonSelector(2)).click({ force: true }); - // wait for table to be done loading - waitForTableLoad(UNCOMMON_PROCCESSES_TABLE); + cy.get(FIRST_PAGE_SELECTOR).should('have.class', 'euiPaginationButton-isActive'); + goToThirdPage(); + waitForUncommonProcessesToBeLoaded(); - cy.get(getDraggableField('process.name')) + cy.get(PROCESS_NAME_FIELD) .first() .invoke('text') - .then(text2 => { - thirdPageResult = `${text2}`; - }); - cy.get(NAVIGATION_AUTHENTICATIONS).click({ force: true }); - waitForTableLoad(AUTHENTICATIONS_TABLE); - // check authentications table starts at 1 - cy.get(getPageButtonSelector(0)).should('have.class', 'euiPaginationButton-isActive'); - - cy.get(NAVIGATION_UNCOMMON_PROCESSES).click({ force: true }); - waitForTableLoad(UNCOMMON_PROCCESSES_TABLE); - // check uncommon processes table picks up at 3 - cy.get(getPageButtonSelector(2)).should('have.class', 'euiPaginationButton-isActive'); - cy.get(getDraggableField('process.name')) - .first() - .invoke('text') - .should(text1 => { - expect(text1).to.eq(thirdPageResult); + .then(expectedThirdPageResult => { + openAuthentications(); + waitForAuthenticationsToBeLoaded(); + cy.get(FIRST_PAGE_SELECTOR).should('have.class', 'euiPaginationButton-isActive'); + openUncommonProcesses(); + waitForUncommonProcessesToBeLoaded(); + cy.get(THIRD_PAGE_SELECTOR).should('have.class', 'euiPaginationButton-isActive'); + cy.get(PROCESS_NAME_FIELD) + .first() + .invoke('text') + .should(actualThirdPageResult => { + expect(expectedThirdPageResult).to.eq(actualThirdPageResult); + }); }); }); - /* - * We only want to comment this code/test for now because it can be nondeterministic - * when we figure out a way to really mock the data, we should come back to it - */ it('pagination resets results and page number to first page when refresh is clicked', () => { - cy.get(NUMBERED_PAGINATION, { timeout: DEFAULT_TIMEOUT }); - cy.get(getPageButtonSelector(0)).should('have.class', 'euiPaginationButton-isActive'); - // let firstResult: string; - // cy.get(getDraggableField('user.name')) - // .first() - // .invoke('text') - // .then(text1 => { - // firstResult = `${text1}`; - // }); - cy.get(getPageButtonSelector(2)).click({ force: true }); - waitForTableLoad(UNCOMMON_PROCCESSES_TABLE); - cy.get(getPageButtonSelector(0)).should('not.have.class', 'euiPaginationButton-isActive'); - cy.get(SUPER_DATE_PICKER_APPLY_BUTTON) - .last() - .click({ force: true }); - waitForTableLoad(UNCOMMON_PROCCESSES_TABLE); - cy.get(getPageButtonSelector(2)).should('have.class', 'euiPaginationButton-isActive'); - // cy.get(getDraggableField('user.name')) - // .first() - // .invoke('text') - // .should(text1 => { - // expect(text1).to.eq(firstResult); - // }); + cy.get(FIRST_PAGE_SELECTOR).should('have.class', 'euiPaginationButton-isActive'); + goToThirdPage(); + waitForUncommonProcessesToBeLoaded(); + cy.get(FIRST_PAGE_SELECTOR).should('not.have.class', 'euiPaginationButton-isActive'); + refreshPage(); + waitForUncommonProcessesToBeLoaded(); + cy.get(FIRST_PAGE_SELECTOR).should('have.class', 'euiPaginationButton-isActive'); }); }); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts index 24c1974cf8343..3d251c1c6bcac 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/timeline/data_providers.spec.ts @@ -4,30 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ +import { HOSTS_PAGE } from '../../../urls/navigation'; import { + waitForAllHostsToBeLoaded, + dragAndDropFirstHostToTimeline, + dragFirstHostToTimeline, + dragFirstHostToEmptyTimelineDataProviders, +} from '../../../tasks/hosts/all_hosts'; +import { HOSTS_NAMES } from '../../../screens/hosts/all_hosts'; +import { DEFAULT_TIMEOUT, loginAndWaitForPage } from '../../../tasks/login'; +import { openTimeline, createNewTimeline } from '../../../tasks/timeline/main'; +import { + TIMELINE_DATA_PROVIDERS_EMPTY, TIMELINE_DATA_PROVIDERS, TIMELINE_DROPPED_DATA_PROVIDERS, - TIMELINE_DATA_PROVIDERS_EMPTY, -} from '../../lib/timeline/selectors'; -import { - createNewTimeline, - dragFromAllHostsToTimeline, - toggleTimelineVisibility, -} from '../../lib/timeline/helpers'; -import { ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS } from '../../lib/hosts/selectors'; -import { HOSTS_PAGE } from '../../lib/urls'; -import { waitForAllHostsWidget } from '../../lib/hosts/helpers'; -import { DEFAULT_TIMEOUT, loginAndWaitForPage } from '../../lib/util/helpers'; -import { drag, dragWithoutDrop } from '../../lib/drag_n_drop/helpers'; +} from '../../../screens/timeline/main'; describe('timeline data providers', () => { before(() => { loginAndWaitForPage(HOSTS_PAGE); - waitForAllHostsWidget(); + waitForAllHostsToBeLoaded(); }); beforeEach(() => { - toggleTimelineVisibility(); + openTimeline(); }); afterEach(() => { @@ -35,16 +35,13 @@ describe('timeline data providers', () => { }); it('renders the data provider of a host dragged from the All Hosts widget on the hosts page', () => { - dragFromAllHostsToTimeline(); + dragAndDropFirstHostToTimeline(); - cy.get(TIMELINE_DROPPED_DATA_PROVIDERS, { - timeout: DEFAULT_TIMEOUT + 10 * 1000, - }) + cy.get(TIMELINE_DROPPED_DATA_PROVIDERS, { timeout: DEFAULT_TIMEOUT }) .first() .invoke('text') .then(dataProviderText => { - // verify the data provider displays the same `host.name` as the host dragged from the `All Hosts` widget - cy.get(ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS) + cy.get(HOSTS_NAMES) .first() .invoke('text') .should(hostname => { @@ -54,9 +51,7 @@ describe('timeline data providers', () => { }); it('sets the background to euiColorSuccess with a 10% alpha channel when the user starts dragging a host, but is not hovering over the data providers', () => { - cy.get(ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS) - .first() - .then(host => drag(host)); + dragFirstHostToTimeline(); cy.get(TIMELINE_DATA_PROVIDERS).should( 'have.css', @@ -65,30 +60,14 @@ describe('timeline data providers', () => { ); }); - it('sets the background to euiColorSuccess with a 20% alpha channel when the user starts dragging a host AND is hovering over the data providers', () => { - cy.get(ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS) - .first() - .then(host => drag(host)); - - cy.get(TIMELINE_DATA_PROVIDERS_EMPTY).then(dataProvidersDropArea => - dragWithoutDrop(dataProvidersDropArea) - ); + it('sets the background to euiColorSuccess with a 20% alpha channel and renders the dashed border color as euiColorSuccess when the user starts dragging a host AND is hovering over the data providers', () => { + dragFirstHostToEmptyTimelineDataProviders(); cy.get(TIMELINE_DATA_PROVIDERS_EMPTY).should( 'have.css', 'background', 'rgba(1, 125, 115, 0.2) none repeat scroll 0% 0% / auto padding-box border-box' ); - }); - - it('renders the dashed border color as euiColorSuccess when hovering over the data providers', () => { - cy.get(ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS) - .first() - .then(host => drag(host)); - - cy.get(TIMELINE_DATA_PROVIDERS_EMPTY).then(dataProvidersDropArea => - dragWithoutDrop(dataProvidersDropArea) - ); cy.get(TIMELINE_DATA_PROVIDERS).should( 'have.css', diff --git a/x-pack/legacy/plugins/siem/cypress/screens/authentications.ts b/x-pack/legacy/plugins/siem/cypress/screens/authentications.ts new file mode 100644 index 0000000000000..839fa82933d6d --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/screens/authentications.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const AUTHENTICATIONS_TABLE = '[data-test-subj="table-authentications-loading-false"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/header.ts b/x-pack/legacy/plugins/siem/cypress/screens/header.ts index 6e4f5fc0e35cb..344fa1829bdec 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/header.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/header.ts @@ -13,3 +13,5 @@ export const NETWORK = '[data-test-subj="navigation-network"]'; export const OVERVIEW = '[data-test-subj="navigation-overview"]'; export const TIMELINES = '[data-test-subj="navigation-timelines"]'; + +export const REFRESH_BUTTON = '[data-test-subj="querySubmitButton"]'; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/selectors.ts b/x-pack/legacy/plugins/siem/cypress/screens/hosts/all_hosts.ts similarity index 60% rename from x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/selectors.ts rename to x-pack/legacy/plugins/siem/cypress/screens/hosts/all_hosts.ts index 6ffcebc3f41aa..f316356580814 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/selectors.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/hosts/all_hosts.ts @@ -4,6 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EndpointListState } from './types'; +export const ALL_HOSTS_TABLE = '[data-test-subj="table-allHosts-loading-false"]'; -export const endpointListData = (state: EndpointListState) => state.endpoints; +export const HOSTS_NAMES = '[data-test-subj="draggable-content-host.name"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/hosts/main.ts b/x-pack/legacy/plugins/siem/cypress/screens/hosts/main.ts index e80ecdac272cd..2187ca40a38a4 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/hosts/main.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/hosts/main.ts @@ -6,4 +6,8 @@ export const EVENTS_TAB = '[data-test-subj="navigation-events"]'; +export const AUTHENTICATIONS_TAB = '[data-test-subj="navigation-authentications"]'; + +export const UNCOMMON_PROCESSES_TAB = '[data-test-subj="navigation-uncommonProcesses"]'; + export const KQL_SEARCH_BAR = '[data-test-subj="queryInput"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/hosts/uncommon_processes.ts b/x-pack/legacy/plugins/siem/cypress/screens/hosts/uncommon_processes.ts new file mode 100644 index 0000000000000..9e15bea79eae0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/screens/hosts/uncommon_processes.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const PROCESS_NAME = '[data-test-subj="draggable-content-process.name"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/pagination.ts b/x-pack/legacy/plugins/siem/cypress/screens/pagination.ts new file mode 100644 index 0000000000000..d2c6b7381656f --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/screens/pagination.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const FIRST_PAGE_SELECTOR = '[data-test-subj="pagination-button-0"]'; +export const THIRD_PAGE_SELECTOR = '[data-test-subj="pagination-button-2"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/timeline/main.ts b/x-pack/legacy/plugins/siem/cypress/screens/timeline/main.ts index ca11f48932263..60c9c2ab44372 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/timeline/main.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/timeline/main.ts @@ -20,3 +20,15 @@ export const SERVER_SIDE_EVENT_COUNT = '[data-test-subj="server-side-event-count export const TIMELINE_SETTINGS_ICON = '[data-test-subj="settings-gear"]'; export const TIMELINE_INSPECT_BUTTON = '[data-test-subj="inspect-empty-button"]'; + +export const CLOSE_TIMELINE_BTN = '[data-test-subj="close-timeline"]'; + +export const CREATE_NEW_TIMELINE = '[data-test-subj="timeline-new"]'; + +export const TIMELINE_DATA_PROVIDERS = '[data-test-subj="dataProviders"]'; + +export const TIMELINE_DATA_PROVIDERS_EMPTY = + '[data-test-subj="dataProviders"] [data-test-subj="empty"]'; + +export const TIMELINE_DROPPED_DATA_PROVIDERS = + '[data-test-subj="dataProviders"] [data-test-subj="providerContainer"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/uncommon_processes.ts b/x-pack/legacy/plugins/siem/cypress/screens/uncommon_processes.ts new file mode 100644 index 0000000000000..71abaa21bf6bd --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/screens/uncommon_processes.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const PROCESS_NAME_FIELD = '[data-test-subj="draggable-content-process.name"]'; +export const UNCOMMON_PROCESSES_TABLE = '[data-test-subj="table-uncommonProcesses-loading-false"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/authentications.ts b/x-pack/legacy/plugins/siem/cypress/tasks/authentications.ts new file mode 100644 index 0000000000000..6fa4bf72ca2b2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/tasks/authentications.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AUTHENTICATIONS_TABLE } from '../screens/authentications'; +import { DEFAULT_TIMEOUT } from '../tasks/login'; + +export const waitForAuthenticationsToBeLoaded = () => { + cy.get(AUTHENTICATIONS_TABLE, { timeout: DEFAULT_TIMEOUT }).should('exist'); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/common.ts b/x-pack/legacy/plugins/siem/cypress/tasks/common.ts new file mode 100644 index 0000000000000..39a61401c15b3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/tasks/common.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +const primaryButton = 0; + +/** + * To overcome the React Beautiful DND sloppy click detection threshold: + * https://github.com/atlassian/react-beautiful-dnd/blob/67b96c8d04f64af6b63ae1315f74fc02b5db032b/docs/sensors/mouse.md#sloppy-clicks-and-click-prevention- + */ +const dndSloppyClickDetectionThreshold = 5; + +/** Starts dragging the subject */ +export const drag = (subject: JQuery<HTMLElement>) => { + const subjectLocation = subject[0].getBoundingClientRect(); + + cy.wrap(subject) + .trigger('mousedown', { + button: primaryButton, + clientX: subjectLocation.left, + clientY: subjectLocation.top, + force: true, + }) + .wait(1) + .trigger('mousemove', { + button: primaryButton, + clientX: subjectLocation.left + dndSloppyClickDetectionThreshold, + clientY: subjectLocation.top, + force: true, + }) + .wait(1); +}; + +/** "Drops" the subject being dragged on the specified drop target */ +export const drop = (dropTarget: JQuery<HTMLElement>) => { + cy.wrap(dropTarget) + .trigger('mousemove', { button: primaryButton, force: true }) + .wait(1) + .trigger('mouseup', { force: true }) + .wait(1); +}; + +/** Drags the subject being dragged on the specified drop target, but does not drop it */ +export const dragWithoutDrop = (dropTarget: JQuery<HTMLElement>) => { + cy.wrap(dropTarget).trigger('mousemove', 'center', { + button: primaryButton, + }); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/header.ts b/x-pack/legacy/plugins/siem/cypress/tasks/header.ts index 1405f4bd81848..1c2f21c40dfba 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/header.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/header.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KQL_INPUT } from '../screens/header'; import { DEFAULT_TIMEOUT } from '../tasks/login'; +import { REFRESH_BUTTON, KQL_INPUT } from '../screens/header'; export const navigateFromHeaderTo = (page: string) => { cy.get(page).click({ force: true }); @@ -16,3 +16,10 @@ export const clearSearchBar = () => { .clear() .type('{enter}'); }; + +export const refreshPage = () => { + cy.get(REFRESH_BUTTON) + .click({ force: true }) + .invoke('text', { timeout: DEFAULT_TIMEOUT }) + .should('not.equal', 'Updating'); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/hosts/all_hosts.ts b/x-pack/legacy/plugins/siem/cypress/tasks/hosts/all_hosts.ts new file mode 100644 index 0000000000000..43e2a7e1bef11 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/tasks/hosts/all_hosts.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ALL_HOSTS_TABLE, HOSTS_NAMES } from '../../screens/hosts/all_hosts'; +import { + TIMELINE_DATA_PROVIDERS, + TIMELINE_DATA_PROVIDERS_EMPTY, +} from '../../screens/timeline/main'; +import { DEFAULT_TIMEOUT } from '../../tasks/login'; +import { drag, drop, dragWithoutDrop } from '../../tasks/common'; + +export const waitForAllHostsToBeLoaded = () => { + cy.get(ALL_HOSTS_TABLE, { timeout: DEFAULT_TIMEOUT }).should('exist'); +}; + +export const dragAndDropFirstHostToTimeline = () => { + cy.get(HOSTS_NAMES) + .first() + .then(firstHost => drag(firstHost)); + cy.get(TIMELINE_DATA_PROVIDERS).then(dataProvidersDropArea => drop(dataProvidersDropArea)); +}; + +export const dragFirstHostToTimeline = () => { + cy.get(HOSTS_NAMES) + .first() + .then(host => drag(host)); +}; + +export const dragFirstHostToEmptyTimelineDataProviders = () => { + cy.get(HOSTS_NAMES) + .first() + .then(host => drag(host)); + + cy.get(TIMELINE_DATA_PROVIDERS_EMPTY).then(dataProvidersDropArea => + dragWithoutDrop(dataProvidersDropArea) + ); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/hosts/main.ts b/x-pack/legacy/plugins/siem/cypress/tasks/hosts/main.ts index d95ae837a3de6..11cd0c8405f26 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/hosts/main.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/hosts/main.ts @@ -6,8 +6,14 @@ import { DEFAULT_TIMEOUT } from '../../integration/lib/util/helpers'; -import { EVENTS_TAB } from '../../screens/hosts/main'; +import { EVENTS_TAB, AUTHENTICATIONS_TAB, UNCOMMON_PROCESSES_TAB } from '../../screens/hosts/main'; /** Clicks the Events tab on the hosts page */ export const openEvents = () => cy.get(EVENTS_TAB, { timeout: DEFAULT_TIMEOUT }).click({ force: true }); + +export const openAuthentications = () => + cy.get(AUTHENTICATIONS_TAB, { timeout: DEFAULT_TIMEOUT }).click({ force: true }); + +export const openUncommonProcesses = () => + cy.get(UNCOMMON_PROCESSES_TAB, { timeout: DEFAULT_TIMEOUT }).click({ force: true }); diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/pagination.ts b/x-pack/legacy/plugins/siem/cypress/tasks/pagination.ts new file mode 100644 index 0000000000000..6b65d5181a7dd --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/tasks/pagination.ts @@ -0,0 +1,15 @@ +/* + * 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 { FIRST_PAGE_SELECTOR, THIRD_PAGE_SELECTOR } from '../screens/pagination'; + +export const goToFirstPage = () => { + cy.get(FIRST_PAGE_SELECTOR).click({ force: true }); +}; + +export const goToThirdPage = () => { + cy.get(THIRD_PAGE_SELECTOR).click({ force: true }); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/timeline/main.ts b/x-pack/legacy/plugins/siem/cypress/tasks/timeline/main.ts index ae2a863092907..068b6dd9f8bd4 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/timeline/main.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/timeline/main.ts @@ -13,6 +13,8 @@ import { SERVER_SIDE_EVENT_COUNT, TIMELINE_SETTINGS_ICON, TIMELINE_INSPECT_BUTTON, + CREATE_NEW_TIMELINE, + CLOSE_TIMELINE_BTN, } from '../../screens/timeline/main'; export const hostExistsQuery = 'host.name: *'; @@ -44,3 +46,9 @@ export const openTimelineInspectButton = () => { cy.get(TIMELINE_INSPECT_BUTTON, { timeout: DEFAULT_TIMEOUT }).should('not.be.disabled'); cy.get(TIMELINE_INSPECT_BUTTON).trigger('click', { force: true }); }; + +export const createNewTimeline = () => { + cy.get(TIMELINE_SETTINGS_ICON).click({ force: true }); + cy.get(CREATE_NEW_TIMELINE).click(); + cy.get(CLOSE_TIMELINE_BTN).click({ force: true }); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/uncommon_processes.ts b/x-pack/legacy/plugins/siem/cypress/tasks/uncommon_processes.ts new file mode 100644 index 0000000000000..007a20c770ca0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/cypress/tasks/uncommon_processes.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UNCOMMON_PROCESSES_TABLE } from '../screens/uncommon_processes'; +import { DEFAULT_TIMEOUT } from '../tasks/login'; + +export const waitForUncommonProcessesToBeLoaded = () => { + cy.get(UNCOMMON_PROCESSES_TABLE, { timeout: DEFAULT_TIMEOUT }).should('exist'); +}; diff --git a/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts b/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts index 35db3003ac436..164a117b82475 100644 --- a/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts +++ b/x-pack/legacy/plugins/siem/cypress/urls/navigation.ts @@ -6,3 +6,11 @@ export const TIMELINES_PAGE = '/app/siem#/timelines'; export const OVERVIEW_PAGE = '/app/siem#/overview'; +export const HOSTS_PAGE = '/app/siem#/hosts/allHosts'; +export const HOSTS_PAGE_TAB_URLS = { + allHosts: '/app/siem#/hosts/allHosts', + anomalies: '/app/siem#/hosts/anomalies', + authentications: '/app/siem#/hosts/authentications', + events: '/app/siem#/hosts/events', + uncommonProcesses: '/app/siem#/hosts/uncommonProcesses', +}; diff --git a/x-pack/legacy/plugins/siem/index.ts b/x-pack/legacy/plugins/siem/index.ts index 0a3e447ac64a1..c786dad61c09d 100644 --- a/x-pack/legacy/plugins/siem/index.ts +++ b/x-pack/legacy/plugins/siem/index.ts @@ -5,12 +5,10 @@ */ import { i18n } from '@kbn/i18n'; -import { get } from 'lodash/fp'; import { resolve } from 'path'; import { Server } from 'hapi'; import { Root } from 'joi'; -import { PluginInitializerContext } from '../../../../src/core/server'; import { plugin } from './server'; import { savedObjectMappings } from './server/saved_objects'; @@ -32,7 +30,6 @@ import { SIGNALS_INDEX_KEY, } from './common/constants'; import { defaultIndexPattern } from './default_index_pattern'; -import { initServerWithKibana } from './server/kibana.index'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -151,27 +148,20 @@ export const siem = (kibana: any) => { mappings: savedObjectMappings, }, init(server: Server) { - const { config, newPlatform, plugins, route } = server; - const { coreContext, env, setup } = newPlatform; - const initializerContext = { ...coreContext, env } as PluginInitializerContext; - const serverFacade = { - config, - usingEphemeralEncryptionKey: - get('usingEphemeralEncryptionKey', newPlatform.setup.plugins.encryptedSavedObjects) ?? - false, - plugins: { - alerting: plugins.alerting, - actions: newPlatform.start.plugins.actions, - elasticsearch: plugins.elasticsearch, - spaces: plugins.spaces, - savedObjects: server.savedObjects.SavedObjectsClient, - }, - route: route.bind(server), + const { coreContext, env, setup, start } = server.newPlatform; + const initializerContext = { ...coreContext, env }; + const __legacy = { + config: server.config, + alerting: server.plugins.alerting, + route: server.route.bind(server), }; - // @ts-ignore-next-line: setup.plugins is too loosely typed - plugin(initializerContext).setup(setup.core, setup.plugins); - initServerWithKibana(initializerContext, serverFacade); + // @ts-ignore-next-line: NewPlatform shim is too loosely typed + const pluginInstance = plugin(initializerContext); + // @ts-ignore-next-line: NewPlatform shim is too loosely typed + pluginInstance.setup(setup.core, setup.plugins, __legacy); + // @ts-ignore-next-line: NewPlatform shim is too loosely typed + pluginInstance.start(start.core, start.plugins); }, config(Joi: Root) { // See x-pack/plugins/siem/server/config.ts if you're adding another diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/histogram_configs.ts b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/histogram_configs.ts new file mode 100644 index 0000000000000..fbcf4c6ed039b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/histogram_configs.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as i18n from './translations'; +import { MatrixHistogramOption, MatrixHisrogramConfigs } from '../matrix_histogram/types'; +import { HistogramType } from '../../graphql/types'; + +export const alertsStackByOptions: MatrixHistogramOption[] = [ + { + text: 'event.category', + value: 'event.category', + }, + { + text: 'event.module', + value: 'event.module', + }, +]; + +const DEFAULT_STACK_BY = 'event.module'; + +export const histogramConfigs: MatrixHisrogramConfigs = { + defaultStackByOption: + alertsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[1], + errorMessage: i18n.ERROR_FETCHING_ALERTS_DATA, + histogramType: HistogramType.alerts, + stackByOptions: alertsStackByOptions, + subtitle: undefined, + title: i18n.ALERTS_GRAPH_TITLE, +}; diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx index a8c2f429040ea..587002c24d526 100644 --- a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx @@ -3,30 +3,18 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { noop } from 'lodash/fp'; -import React, { useEffect, useCallback } from 'react'; +import React, { useEffect, useCallback, useMemo } from 'react'; import numeral from '@elastic/numeral'; import { AlertsComponentsQueryProps } from './types'; import { AlertsTable } from './alerts_table'; import * as i18n from './translations'; -import { MatrixHistogramOption } from '../matrix_histogram/types'; -import { MatrixHistogramContainer } from '../../containers/matrix_histogram'; -import { MatrixHistogramGqlQuery } from '../../containers/matrix_histogram/index.gql_query'; import { useUiSetting$ } from '../../lib/kibana'; import { DEFAULT_NUMBER_FORMAT } from '../../../common/constants'; +import { MatrixHistogramContainer } from '../matrix_histogram'; +import { histogramConfigs } from './histogram_configs'; +import { MatrixHisrogramConfigs } from '../matrix_histogram/types'; const ID = 'alertsOverTimeQuery'; -export const alertsStackByOptions: MatrixHistogramOption[] = [ - { - text: 'event.category', - value: 'event.category', - }, - { - text: 'event.module', - value: 'event.module', - }, -]; -const dataKey = 'AlertsHistogram'; export const AlertsView = ({ deleteQuery, @@ -34,21 +22,10 @@ export const AlertsView = ({ filterQuery, pageFilters, setQuery, - skip, startDate, type, - updateDateRange = noop, }: AlertsComponentsQueryProps) => { const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); - - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: ID }); - } - }; - }, []); - const getSubtitle = useCallback( (totalCount: number) => `${i18n.SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${i18n.UNIT( @@ -56,27 +33,32 @@ export const AlertsView = ({ )}`, [] ); + const alertsHistogramConfigs: MatrixHisrogramConfigs = useMemo( + () => ({ + ...histogramConfigs, + subtitle: getSubtitle, + }), + [getSubtitle] + ); + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: ID }); + } + }; + }, [deleteQuery]); return ( <> <MatrixHistogramContainer - dataKey={dataKey} - defaultStackByOption={alertsStackByOptions[1]} endDate={endDate} - errorMessage={i18n.ERROR_FETCHING_ALERTS_DATA} filterQuery={filterQuery} id={ID} - isAlertsHistogram={true} - query={MatrixHistogramGqlQuery} setQuery={setQuery} - skip={skip} sourceId="default" - stackByOptions={alertsStackByOptions} startDate={startDate} - subtitle={getSubtitle} - title={i18n.ALERTS_GRAPH_TITLE} type={type} - updateDateRange={updateDateRange} + {...alertsHistogramConfigs} /> <AlertsTable endDate={endDate} startDate={startDate} pageFilters={pageFilters} /> </> diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/types.ts b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/types.ts index e6d6fdf273ec8..a24c66e31e670 100644 --- a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/types.ts @@ -13,14 +13,7 @@ type CommonQueryProps = HostsComponentsQueryProps | NetworkComponentQueryProps; export interface AlertsComponentsQueryProps extends Pick< CommonQueryProps, - | 'deleteQuery' - | 'endDate' - | 'filterQuery' - | 'skip' - | 'setQuery' - | 'startDate' - | 'type' - | 'updateDateRange' + 'deleteQuery' | 'endDate' | 'filterQuery' | 'skip' | 'setQuery' | 'startDate' | 'type' > { pageFilters: Filter[]; stackByOptions?: MatrixHistogramOption[]; diff --git a/x-pack/legacy/plugins/siem/public/components/charts/common.tsx b/x-pack/legacy/plugins/siem/public/components/charts/common.tsx index 62f1ac56890ca..03b412f575646 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/common.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/common.tsx @@ -28,10 +28,10 @@ const chartDefaultRendering: Rendering = 'canvas'; export type UpdateDateRange = (min: number, max: number) => void; export interface ChartData { - x: number | string | null; - y: number | string | null; + x?: number | string | null; + y?: number | string | null; y0?: number; - g?: number | string; + g?: number | string | null; } export interface ChartSeriesConfigs { diff --git a/x-pack/legacy/plugins/siem/public/components/header_page_new/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/header_page_new/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..1b792503cf1c6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/header_page_new/__snapshots__/index.test.tsx.snap @@ -0,0 +1,47 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HeaderPage it renders 1`] = ` +<Header + border={true} +> + <EuiFlexGroup + alignItems="center" + justifyContent="spaceBetween" + > + <FlexItem + grow={false} + > + <EuiTitle + size="l" + > + <h1 + data-test-subj="header-page-title" + > + Test title + + <StyledEuiBetaBadge + label="Beta" + tooltipContent="Test tooltip" + tooltipPosition="bottom" + /> + </h1> + </EuiTitle> + <Subtitle + data-test-subj="header-page-subtitle" + items="Test subtitle" + /> + <Subtitle + data-test-subj="header-page-subtitle-2" + items="Test subtitle 2" + /> + </FlexItem> + <FlexItem + data-test-subj="header-page-supplements" + > + <p> + Test supplement + </p> + </FlexItem> + </EuiFlexGroup> +</Header> +`; diff --git a/x-pack/legacy/plugins/siem/public/components/header_page_new/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/header_page_new/index.test.tsx new file mode 100644 index 0000000000000..83a70fd90d82b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/header_page_new/index.test.tsx @@ -0,0 +1,224 @@ +/* + * 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 euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { shallow } from 'enzyme'; +import React from 'react'; + +import { TestProviders } from '../../mock'; +import { HeaderPage } from './index'; +import { useMountAppended } from '../../utils/use_mount_appended'; + +describe('HeaderPage', () => { + const mount = useMountAppended(); + + test('it renders', () => { + const wrapper = shallow( + <HeaderPage + badgeOptions={{ beta: true, text: 'Beta', tooltip: 'Test tooltip' }} + border + subtitle="Test subtitle" + subtitle2="Test subtitle 2" + title="Test title" + > + <p>{'Test supplement'}</p> + </HeaderPage> + ); + + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders the title', () => { + const wrapper = mount( + <TestProviders> + <HeaderPage title="Test title" /> + </TestProviders> + ); + + expect( + wrapper + .find('[data-test-subj="header-page-title"]') + .first() + .exists() + ).toBe(true); + }); + + test('it renders the back link when provided', () => { + const wrapper = mount( + <TestProviders> + <HeaderPage backOptions={{ href: '#', text: 'Test link' }} title="Test title" /> + </TestProviders> + ); + + expect( + wrapper + .find('.siemHeaderPage__linkBack') + .first() + .exists() + ).toBe(true); + }); + + test('it DOES NOT render the back link when not provided', () => { + const wrapper = mount( + <TestProviders> + <HeaderPage title="Test title" /> + </TestProviders> + ); + + expect( + wrapper + .find('.siemHeaderPage__linkBack') + .first() + .exists() + ).toBe(false); + }); + + test('it renders the first subtitle when provided', () => { + const wrapper = mount( + <TestProviders> + <HeaderPage subtitle="Test subtitle" title="Test title" /> + </TestProviders> + ); + + expect( + wrapper + .find('[data-test-subj="header-page-subtitle"]') + .first() + .exists() + ).toBe(true); + }); + + test('it DOES NOT render the first subtitle when not provided', () => { + const wrapper = mount( + <TestProviders> + <HeaderPage title="Test title" /> + </TestProviders> + ); + + expect( + wrapper + .find('[data-test-subj="header-section-subtitle"]') + .first() + .exists() + ).toBe(false); + }); + + test('it renders the second subtitle when provided', () => { + const wrapper = mount( + <TestProviders> + <HeaderPage subtitle2="Test subtitle 2" title="Test title" /> + </TestProviders> + ); + + expect( + wrapper + .find('[data-test-subj="header-page-subtitle-2"]') + .first() + .exists() + ).toBe(true); + }); + + test('it DOES NOT render the second subtitle when not provided', () => { + const wrapper = mount( + <TestProviders> + <HeaderPage title="Test title" /> + </TestProviders> + ); + + expect( + wrapper + .find('[data-test-subj="header-section-subtitle-2"]') + .first() + .exists() + ).toBe(false); + }); + + test('it renders supplements when children provided', () => { + const wrapper = mount( + <TestProviders> + <HeaderPage title="Test title"> + <p>{'Test supplement'}</p> + </HeaderPage> + </TestProviders> + ); + + expect( + wrapper + .find('[data-test-subj="header-page-supplements"]') + .first() + .exists() + ).toBe(true); + }); + + test('it DOES NOT render supplements when children not provided', () => { + const wrapper = mount( + <TestProviders> + <HeaderPage title="Test title" /> + </TestProviders> + ); + + expect( + wrapper + .find('[data-test-subj="header-page-supplements"]') + .first() + .exists() + ).toBe(false); + }); + + test('it applies border styles when border is true', () => { + const wrapper = mount( + <TestProviders> + <HeaderPage border title="Test title" /> + </TestProviders> + ); + const siemHeaderPage = wrapper.find('.siemHeaderPage').first(); + + expect(siemHeaderPage).toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin); + expect(siemHeaderPage).toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.l); + }); + + test('it DOES NOT apply border styles when border is false', () => { + const wrapper = mount( + <TestProviders> + <HeaderPage title="Test title" /> + </TestProviders> + ); + const siemHeaderPage = wrapper.find('.siemHeaderPage').first(); + + expect(siemHeaderPage).not.toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin); + expect(siemHeaderPage).not.toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.l); + }); + + test('it renders as a draggable when arguments provided', () => { + const wrapper = mount( + <TestProviders> + <HeaderPage draggableArguments={{ field: 'neat', value: 'cool' }} title="Test title" /> + </TestProviders> + ); + + expect( + wrapper + .find('[data-test-subj="header-page-draggable"]') + .first() + .exists() + ).toBe(true); + }); + + test('it DOES NOT render as a draggable when arguments not provided', () => { + const wrapper = mount( + <TestProviders> + <HeaderPage title="Test title" /> + </TestProviders> + ); + + expect( + wrapper + .find('[data-test-subj="header-page-draggable"]') + .first() + .exists() + ).toBe(false); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/header_page_new/index.tsx b/x-pack/legacy/plugins/siem/public/components/header_page_new/index.tsx new file mode 100644 index 0000000000000..7e486c78fb9b9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/header_page_new/index.tsx @@ -0,0 +1,220 @@ +/* + * 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 { + EuiBadge, + EuiBetaBadge, + EuiButton, + EuiButtonEmpty, + EuiButtonIcon, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiProgress, + EuiTitle, +} from '@elastic/eui'; +import React from 'react'; +import styled, { css } from 'styled-components'; + +import { DefaultDraggable } from '../draggables'; +import { LinkIcon, LinkIconProps } from '../link_icon'; +import { Subtitle, SubtitleProps } from '../subtitle'; +import * as i18n from './translations'; + +interface HeaderProps { + border?: boolean; + isLoading?: boolean; +} + +const Header = styled.header.attrs({ + className: 'siemHeaderPage', +})<HeaderProps>` + ${({ border, theme }) => css` + margin-bottom: ${theme.eui.euiSizeL}; + + ${border && + css` + border-bottom: ${theme.eui.euiBorderThin}; + padding-bottom: ${theme.eui.paddingSizes.l}; + .euiProgress { + top: ${theme.eui.paddingSizes.l}; + } + `} + `} +`; +Header.displayName = 'Header'; + +const FlexItem = styled(EuiFlexItem)` + display: block; +`; +FlexItem.displayName = 'FlexItem'; + +const LinkBack = styled.div.attrs({ + className: 'siemHeaderPage__linkBack', +})` + ${({ theme }) => css` + font-size: ${theme.eui.euiFontSizeXS}; + line-height: ${theme.eui.euiLineHeight}; + margin-bottom: ${theme.eui.euiSizeS}; + `} +`; +LinkBack.displayName = 'LinkBack'; + +const Badge = styled(EuiBadge)` + letter-spacing: 0; +`; +Badge.displayName = 'Badge'; + +const StyledEuiBetaBadge = styled(EuiBetaBadge)` + vertical-align: middle; +`; + +StyledEuiBetaBadge.displayName = 'StyledEuiBetaBadge'; + +const StyledEuiButtonIcon = styled(EuiButtonIcon)` + ${({ theme }) => css` + margin-left: ${theme.eui.euiSize}; + `} +`; + +StyledEuiButtonIcon.displayName = 'StyledEuiButtonIcon'; + +interface BackOptions { + href: LinkIconProps['href']; + text: LinkIconProps['children']; +} + +interface BadgeOptions { + beta?: boolean; + text: string; + tooltip?: string; +} + +interface DraggableArguments { + field: string; + value: string; +} +interface IconAction { + 'aria-label': string; + iconType: string; + onChange: (a: string) => void; + onClick: (b: boolean) => void; + onSubmit: () => void; +} + +export interface HeaderPageProps extends HeaderProps { + backOptions?: BackOptions; + badgeOptions?: BadgeOptions; + children?: React.ReactNode; + draggableArguments?: DraggableArguments; + isEditTitle?: boolean; + iconAction?: IconAction; + subtitle2?: SubtitleProps['items']; + subtitle?: SubtitleProps['items']; + title: string | React.ReactNode; +} + +const HeaderPageComponent: React.FC<HeaderPageProps> = ({ + backOptions, + badgeOptions, + border, + children, + draggableArguments, + isEditTitle, + iconAction, + isLoading, + subtitle, + subtitle2, + title, + ...rest +}) => ( + <Header border={border} {...rest}> + <EuiFlexGroup alignItems="center" justifyContent="spaceBetween"> + <FlexItem grow={false}> + {backOptions && ( + <LinkBack> + <LinkIcon href={backOptions.href} iconType="arrowLeft"> + {backOptions.text} + </LinkIcon> + </LinkBack> + )} + + {isEditTitle && iconAction ? ( + <EuiFlexGroup alignItems="center" gutterSize="m" justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <EuiFieldText + onChange={e => iconAction.onChange(e.target.value)} + value={`${title}`} + /> + </EuiFlexItem> + <EuiFlexGroup gutterSize="none" responsive={false} wrap={true}> + <EuiFlexItem grow={false}> + <EuiButton + fill + isDisabled={isLoading} + isLoading={isLoading} + onClick={iconAction.onSubmit} + > + {i18n.SUBMIT} + </EuiButton> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonEmpty onClick={() => iconAction.onClick(false)}> + {i18n.CANCEL} + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexItem /> + </EuiFlexGroup> + ) : ( + <EuiTitle size="l"> + <h1 data-test-subj="header-page-title"> + {!draggableArguments ? ( + title + ) : ( + <DefaultDraggable + data-test-subj="header-page-draggable" + id={`header-page-draggable-${draggableArguments.field}-${draggableArguments.value}`} + field={draggableArguments.field} + value={`${draggableArguments.value}`} + /> + )} + {badgeOptions && ( + <> + {' '} + {badgeOptions.beta ? ( + <StyledEuiBetaBadge + label={badgeOptions.text} + tooltipContent={badgeOptions.tooltip} + tooltipPosition="bottom" + /> + ) : ( + <Badge color="hollow">{badgeOptions.text}</Badge> + )} + </> + )} + {iconAction && ( + <StyledEuiButtonIcon + aria-label={iconAction['aria-label']} + iconType={iconAction.iconType} + onClick={() => iconAction.onClick(true)} + /> + )} + </h1> + </EuiTitle> + )} + + {subtitle && <Subtitle data-test-subj="header-page-subtitle" items={subtitle} />} + {subtitle2 && <Subtitle data-test-subj="header-page-subtitle-2" items={subtitle2} />} + {border && isLoading && <EuiProgress size="xs" color="accent" />} + </FlexItem> + + {children && <FlexItem data-test-subj="header-page-supplements">{children}</FlexItem>} + </EuiFlexGroup> + </Header> +); + +export const HeaderPage = React.memo(HeaderPageComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/header_page_new/translations.ts b/x-pack/legacy/plugins/siem/public/components/header_page_new/translations.ts new file mode 100644 index 0000000000000..57b2cda0b0b01 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/header_page_new/translations.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const SUBMIT = i18n.translate('xpack.siem.case.casePage.title.submit', { + defaultMessage: 'Submit', +}); + +export const CANCEL = i18n.translate('xpack.siem.case.casePage.title.cancel', { + defaultMessage: 'Cancel', +}); diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/index.ts b/x-pack/legacy/plugins/siem/public/components/link_to/index.ts index ad6147e5aad76..c93b415e017bb 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/index.ts +++ b/x-pack/legacy/plugins/siem/public/components/link_to/index.ts @@ -13,3 +13,10 @@ export { getOverviewUrl, RedirectToOverviewPage } from './redirect_to_overview'; export { getHostDetailsUrl, getHostsUrl } from './redirect_to_hosts'; export { getNetworkUrl, getIPDetailsUrl, RedirectToNetworkPage } from './redirect_to_network'; export { getTimelinesUrl, RedirectToTimelinesPage } from './redirect_to_timelines'; +export { + getCaseDetailsUrl, + getCaseUrl, + getCreateCaseUrl, + RedirectToCasePage, + RedirectToCreatePage, +} from './redirect_to_case'; diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx index dc8c696301611..c08b429dc4625 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx @@ -20,6 +20,7 @@ import { RedirectToHostsPage, RedirectToHostDetailsPage } from './redirect_to_ho import { RedirectToNetworkPage } from './redirect_to_network'; import { RedirectToOverviewPage } from './redirect_to_overview'; import { RedirectToTimelinesPage } from './redirect_to_timelines'; +import { RedirectToCasePage, RedirectToCreatePage } from './redirect_to_case'; import { DetectionEngineTab } from '../../pages/detection_engine/types'; interface LinkToPageProps { @@ -32,6 +33,20 @@ export const LinkToPage = React.memo<LinkToPageProps>(({ match }) => ( component={RedirectToOverviewPage} path={`${match.url}/:pageName(${SiemPageName.overview})`} /> + <Route + exact + component={RedirectToCasePage} + path={`${match.url}/:pageName(${SiemPageName.case})`} + /> + <Route + exact + component={RedirectToCreatePage} + path={`${match.url}/:pageName(${SiemPageName.case})/create`} + /> + <Route + component={RedirectToCasePage} + path={`${match.url}/:pageName(${SiemPageName.case})/:detailName`} + /> <Route component={RedirectToHostsPage} exact diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_case.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_case.tsx new file mode 100644 index 0000000000000..39e9f6b64b1d3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_case.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { RedirectWrapper } from './redirect_wrapper'; +import { SiemPageName } from '../../pages/home/types'; + +export type CaseComponentProps = RouteComponentProps<{ + detailName: string; +}>; + +export const RedirectToCasePage = ({ + match: { + params: { detailName }, + }, +}: CaseComponentProps) => ( + <RedirectWrapper + to={detailName ? `/${SiemPageName.case}/${detailName}` : `/${SiemPageName.case}`} + /> +); + +export const RedirectToCreatePage = () => <RedirectWrapper to={`/${SiemPageName.case}/create`} />; + +const baseCaseUrl = `#/link-to/${SiemPageName.case}`; + +export const getCaseUrl = () => baseCaseUrl; +export const getCaseDetailsUrl = (detailName: string) => `${baseCaseUrl}/${detailName}`; +export const getCreateCaseUrl = () => `${baseCaseUrl}/create`; diff --git a/x-pack/legacy/plugins/siem/public/components/links/index.tsx b/x-pack/legacy/plugins/siem/public/components/links/index.tsx index e122b3e235a9e..4f74f9ff2f5d6 100644 --- a/x-pack/legacy/plugins/siem/public/components/links/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/links/index.tsx @@ -8,7 +8,12 @@ import { EuiLink } from '@elastic/eui'; import React from 'react'; import { encodeIpv6 } from '../../lib/helpers'; -import { getHostDetailsUrl, getIPDetailsUrl } from '../link_to'; +import { + getCaseDetailsUrl, + getHostDetailsUrl, + getIPDetailsUrl, + getCreateCaseUrl, +} from '../link_to'; import { FlowTarget, FlowTargetSourceDest } from '../../graphql/types'; // Internal Links @@ -35,6 +40,23 @@ const IPDetailsLinkComponent: React.FC<{ export const IPDetailsLink = React.memo(IPDetailsLinkComponent); +const CaseDetailsLinkComponent: React.FC<{ children?: React.ReactNode; detailName: string }> = ({ + children, + detailName, +}) => ( + <EuiLink href={getCaseDetailsUrl(encodeURIComponent(detailName))}> + {children ? children : detailName} + </EuiLink> +); +export const CaseDetailsLink = React.memo(CaseDetailsLinkComponent); +CaseDetailsLink.displayName = 'CaseDetailsLink'; + +export const CreateCaseLink = React.memo<{ children: React.ReactNode }>(({ children }) => ( + <EuiLink href={getCreateCaseUrl()}>{children}</EuiLink> +)); + +CreateCaseLink.displayName = 'CreateCaseLink'; + // External Links export const GoogleLink = React.memo<{ children?: React.ReactNode; link: string }>( ({ children, link }) => ( diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..0e518e48e2e88 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matrix Histogram Component not initial load it renders no MatrixLoader 1`] = `"<div class=\\"sc-AykKF jbBKkl\\"><div class=\\"euiPanel euiPanel--paddingMedium sc-AykKC sc-AykKH iNPult\\" data-test-subj=\\"mockIdPanel\\" height=\\"300\\"><div class=\\"headerSection\\"></div><div class=\\"barchart\\"></div></div></div><div class=\\"euiSpacer euiSpacer--l\\"></div>"`; + +exports[`Matrix Histogram Component on initial load it renders MatrixLoader 1`] = `"<div class=\\"sc-AykKF hneqJM\\"><div class=\\"euiPanel euiPanel--paddingMedium sc-AykKC sc-AykKH iNPult\\" data-test-subj=\\"mockIdPanel\\" height=\\"300\\"><div class=\\"headerSection\\"></div><div class=\\"matrixLoader\\"></div></div></div><div class=\\"euiSpacer euiSpacer--l\\"></div>"`; diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx index a44efed47372d..db5b1f7f03ee3 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx @@ -6,17 +6,19 @@ /* eslint-disable react/display-name */ -import { shallow } from 'enzyme'; +import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; import { MatrixHistogram } from '.'; -import { MatrixHistogramGqlQuery as mockQuery } from '../../containers/matrix_histogram/index.gql_query'; - +import { useQuery } from '../../containers/matrix_histogram'; +import { HistogramType } from '../../graphql/types'; jest.mock('../../lib/kibana'); -jest.mock('../loader', () => { +jest.mock('./matrix_loader', () => { return { - Loader: () => <div className="loader" />, + MatrixLoader: () => { + return <div className="matrixLoader" />; + }, }; }); @@ -32,17 +34,31 @@ jest.mock('../charts/barchart', () => { }; }); +jest.mock('../../containers/matrix_histogram', () => { + return { + useQuery: jest.fn(), + }; +}); + +jest.mock('../../components/matrix_histogram/utils', () => { + return { + getBarchartConfigs: jest.fn(), + getCustomChartData: jest.fn().mockReturnValue(true), + }; +}); + describe('Matrix Histogram Component', () => { + let wrapper: ReactWrapper; + const mockMatrixOverTimeHistogramProps = { - dataKey: 'mockDataKey', defaultIndex: ['defaultIndex'], defaultStackByOption: { text: 'text', value: 'value' }, endDate: new Date('2019-07-18T20:00:00.000Z').valueOf(), errorMessage: 'error', + histogramType: HistogramType.alerts, id: 'mockId', isInspected: false, isPtrIncluded: false, - query: mockQuery, setQuery: jest.fn(), skip: false, sourceId: 'default', @@ -52,36 +68,56 @@ describe('Matrix Histogram Component', () => { subtitle: 'mockSubtitle', totalCount: -1, title: 'mockTitle', - updateDateRange: jest.fn(), + dispatchSetAbsoluteRangeDatePicker: jest.fn(), }; - describe('rendering', () => { - test('it renders EuiLoadingContent on initialLoad', () => { - const wrapper = shallow(<MatrixHistogram {...mockMatrixOverTimeHistogramProps} />); - expect(wrapper.find(`[data-test-subj="initialLoadingPanelMatrixOverTime"]`)).toBeTruthy(); + beforeAll(() => { + (useQuery as jest.Mock).mockReturnValue({ + data: null, + loading: false, + inspect: false, + totalCount: null, }); - - test('it renders Loader while fetching data if visited before', () => { - const mockProps = { - ...mockMatrixOverTimeHistogramProps, - data: [{ x: new Date('2019-09-16T02:20:00.000Z').valueOf(), y: 3787, g: 'config_change' }], - totalCount: 10, - loading: true, - }; - const wrapper = shallow(<MatrixHistogram {...mockProps} />); - expect(wrapper.find('.loader')).toBeTruthy(); + wrapper = mount(<MatrixHistogram {...mockMatrixOverTimeHistogramProps} />); + }); + describe('on initial load', () => { + test('it renders MatrixLoader', () => { + expect(wrapper.html()).toMatchSnapshot(); + expect(wrapper.find('MatrixLoader').exists()).toBe(true); }); + }); - test('it renders BarChart if data available', () => { - const mockProps = { - ...mockMatrixOverTimeHistogramProps, - data: [{ x: new Date('2019-09-16T02:20:00.000Z').valueOf(), y: 3787, g: 'config_change' }], - totalCount: 10, + describe('not initial load', () => { + beforeAll(() => { + (useQuery as jest.Mock).mockReturnValue({ + data: [ + { x: 1, y: 2, g: 'g1' }, + { x: 2, y: 4, g: 'g1' }, + { x: 3, y: 6, g: 'g1' }, + { x: 1, y: 1, g: 'g2' }, + { x: 2, y: 3, g: 'g2' }, + { x: 3, y: 5, g: 'g2' }, + ], loading: false, - }; - const wrapper = shallow(<MatrixHistogram {...mockProps} />); + inspect: false, + totalCount: 1, + }); + wrapper.setProps({ endDate: 100 }); + wrapper.update(); + }); + test('it renders no MatrixLoader', () => { + expect(wrapper.html()).toMatchSnapshot(); + expect(wrapper.find(`MatrixLoader`).exists()).toBe(false); + }); + + test('it shows BarChart if data available', () => { + expect(wrapper.find(`.barchart`).exists()).toBe(true); + }); + }); - expect(wrapper.find(`.barchart`)).toBeTruthy(); + describe('select dropdown', () => { + test('should be hidden if only one option is provided', () => { + expect(wrapper.find('EuiSelect').exists()).toBe(false); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx index 04b988f8270f3..cb9afde899cf8 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx @@ -4,19 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect, useCallback } from 'react'; -import { ScaleType } from '@elastic/charts'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import { Position } from '@elastic/charts'; import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSelect, EuiSpacer } from '@elastic/eui'; import { noop } from 'lodash/fp'; +import { compose } from 'redux'; +import { connect } from 'react-redux'; import * as i18n from './translations'; import { BarChart } from '../charts/barchart'; import { HeaderSection } from '../header_section'; import { MatrixLoader } from './matrix_loader'; import { Panel } from '../panel'; -import { getBarchartConfigs, getCustomChartData } from './utils'; -import { useQuery } from '../../containers/matrix_histogram/utils'; +import { getBarchartConfigs, getCustomChartData } from '../../components/matrix_histogram/utils'; +import { useQuery } from '../../containers/matrix_histogram'; import { MatrixHistogramProps, MatrixHistogramOption, @@ -26,6 +28,35 @@ import { import { ChartSeriesData } from '../charts/common'; import { InspectButtonContainer } from '../inspect'; +import { State, inputsSelectors, hostsModel, networkModel } from '../../store'; + +import { + MatrixHistogramMappingTypes, + GetTitle, + GetSubTitle, +} from '../../components/matrix_histogram/types'; +import { SetQuery } from '../../pages/hosts/navigation/types'; +import { QueryTemplateProps } from '../../containers/query_template'; +import { setAbsoluteRangeDatePicker } from '../../store/inputs/actions'; +import { HistogramType } from '../../graphql/types'; + +export interface OwnProps extends QueryTemplateProps { + defaultStackByOption: MatrixHistogramOption; + errorMessage: string; + headerChildren?: React.ReactNode; + hideHistogramIfEmpty?: boolean; + histogramType: HistogramType; + id: string; + legendPosition?: Position; + mapping?: MatrixHistogramMappingTypes; + setQuery: SetQuery; + showLegend?: boolean; + stackByOptions: MatrixHistogramOption[]; + subtitle?: string | GetSubTitle; + title: string | GetTitle; + type: hostsModel.HostsType | networkModel.NetworkType; +} + const DEFAULT_PANEL_HEIGHT = 300; const HeaderChildrenFlexItem = styled(EuiFlexItem)` @@ -41,45 +72,50 @@ const HistogramPanel = styled(Panel)<{ height?: number }>` export const MatrixHistogramComponent: React.FC<MatrixHistogramProps & MatrixHistogramQueryProps> = ({ chartHeight, - dataKey, defaultStackByOption, endDate, errorMessage, filterQuery, headerChildren, + histogramType, hideHistogramIfEmpty = false, id, - isAlertsHistogram, - isAnomaliesHistogram, - isAuthenticationsHistogram, - isDnsHistogram, - isEventsHistogram, isInspected, - legendPosition = 'right', + legendPosition, mapping, panelHeight = DEFAULT_PANEL_HEIGHT, - query, - scaleType = ScaleType.Time, setQuery, - showLegend = true, - skip, + showLegend, stackByOptions, startDate, subtitle, title, - updateDateRange, + dispatchSetAbsoluteRangeDatePicker, yTickFormatter, }) => { - const barchartConfigs = getBarchartConfigs({ - chartHeight, - from: startDate, - legendPosition, - to: endDate, - onBrushEnd: updateDateRange, - scaleType, - yTickFormatter, - showLegend, - }); + const barchartConfigs = useMemo( + () => + getBarchartConfigs({ + chartHeight, + from: startDate, + legendPosition, + to: endDate, + onBrushEnd: (min: number, max: number) => { + dispatchSetAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + }, + yTickFormatter, + showLegend, + }), + [ + chartHeight, + startDate, + legendPosition, + endDate, + dispatchSetAbsoluteRangeDatePicker, + yTickFormatter, + showLegend, + ] + ); const [isInitialLoading, setIsInitialLoading] = useState(true); const [selectedStackByOption, setSelectedStackByOption] = useState<MatrixHistogramOption>( defaultStackByOption @@ -100,19 +136,11 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps & const { data, loading, inspect, totalCount, refetch = noop } = useQuery<{}, HistogramAggregation>( { - dataKey, endDate, errorMessage, filterQuery, - query, - skip, + histogramType, startDate, - title, - isAlertsHistogram, - isAnomaliesHistogram, - isAuthenticationsHistogram, - isDnsHistogram, - isEventsHistogram, isInspected, stackByField: selectedStackByOption.value, } @@ -129,7 +157,6 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps & } else { setHideHistogram(false); } - setBarChartData(getCustomChartData(data, mapping)); setQuery({ id, inspect, loading, refetch }); @@ -145,8 +172,11 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps & setQuery, hideHistogramIfEmpty, totalCount, + id, + inspect, isInspected, loading, + refetch, data, refetch, isInitialLoading, @@ -174,7 +204,7 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps & <HeaderSection id={id} title={titleWithStackByField} - subtitle={!loading && (totalCount >= 0 ? subtitleWithCounts : null)} + subtitle={!isInitialLoading && (totalCount >= 0 ? subtitleWithCounts : null)} > <EuiFlexGroup alignItems="center" gutterSize="none"> <EuiFlexItem grow={false}> @@ -197,7 +227,10 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps & <HeaderSection id={id} title={titleWithStackByField} - subtitle={!isInitialLoading && (totalCount >= 0 ? subtitleWithCounts : null)} + subtitle={ + !isInitialLoading && + (totalCount != null && totalCount >= 0 ? subtitleWithCounts : null) + } > <EuiFlexGroup alignItems="center" gutterSize="none"> <EuiFlexItem grow={false}> @@ -224,3 +257,20 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramProps & }; export const MatrixHistogram = React.memo(MatrixHistogramComponent); + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { type, id }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +export const MatrixHistogramContainer = compose<React.ComponentClass<OwnProps>>( + connect(makeMapStateToProps, { + dispatchSetAbsoluteRangeDatePicker: setAbsoluteRangeDatePicker, + }) +)(MatrixHistogram); diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts index 88f8f1ff28fa9..fda4f5d15d95c 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts @@ -4,20 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ScaleType, Position } from '@elastic/charts'; -import { SetStateAction } from 'react'; -import { DocumentNode } from 'graphql'; -import { - MatrixOverTimeHistogramData, - MatrixOverOrdinalHistogramData, - NetworkDnsSortField, - PaginationInputPaginated, -} from '../../graphql/types'; -import { UpdateDateRange } from '../charts/common'; +import { ScaleType, Position, TickFormatter } from '@elastic/charts'; +import { ActionCreator } from 'redux'; import { ESQuery } from '../../../common/typed_json'; import { SetQuery } from '../../pages/hosts/navigation/types'; +import { InputsModelId } from '../../store/inputs/constants'; +import { HistogramType } from '../../graphql/types'; +import { UpdateDateRange } from '../charts/common'; -export type MatrixHistogramDataTypes = MatrixOverTimeHistogramData | MatrixOverOrdinalHistogramData; export type MatrixHistogramMappingTypes = Record< string, { key: string; value: null; color?: string | undefined } @@ -30,10 +24,27 @@ export interface MatrixHistogramOption { export type GetSubTitle = (count: number) => string; export type GetTitle = (matrixHistogramOption: MatrixHistogramOption) => string; -export interface MatrixHistogramBasicProps { +export interface MatrixHisrogramConfigs { + defaultStackByOption: MatrixHistogramOption; + errorMessage: string; + hideHistogramIfEmpty?: boolean; + histogramType: HistogramType; + legendPosition?: Position; + mapping?: MatrixHistogramMappingTypes; + stackByOptions: MatrixHistogramOption[]; + subtitle?: string | GetSubTitle; + title: string | GetTitle; +} + +interface MatrixHistogramBasicProps { chartHeight?: number; defaultIndex: string[]; defaultStackByOption: MatrixHistogramOption; + dispatchSetAbsoluteRangeDatePicker: ActionCreator<{ + id: InputsModelId; + from: number; + to: number; + }>; endDate: number; headerChildren?: React.ReactNode; hideHistogramIfEmpty?: boolean; @@ -42,35 +53,20 @@ export interface MatrixHistogramBasicProps { mapping?: MatrixHistogramMappingTypes; panelHeight?: number; setQuery: SetQuery; - sourceId: string; startDate: number; stackByOptions: MatrixHistogramOption[]; subtitle?: string | GetSubTitle; - title?: string; - updateDateRange: UpdateDateRange; + title?: string | GetTitle; } export interface MatrixHistogramQueryProps { - activePage?: number; - dataKey: string; endDate: number; errorMessage: string; filterQuery?: ESQuery | string | undefined; - limit?: number; - query: DocumentNode; - sort?: NetworkDnsSortField; stackByField: string; - skip: boolean; startDate: number; - title: string | GetTitle; - isAlertsHistogram?: boolean; - isAnomaliesHistogram?: boolean; - isAuthenticationsHistogram?: boolean; - isDnsHistogram?: boolean; - isEventsHistogram?: boolean; isInspected: boolean; - isPtrIncluded?: boolean; - pagination?: PaginationInputPaginated; + histogramType: HistogramType; } export interface MatrixHistogramProps extends MatrixHistogramBasicProps { @@ -98,31 +94,38 @@ export interface HistogramAggregation { }; } -export interface SignalsResponse { - took: number; - timeout: boolean; -} - -export interface SignalSearchResponse<Hit = {}, Aggregations = {} | undefined> - extends SignalsResponse { - _shards: { - total: number; - successful: number; - skipped: number; - failed: number; +export interface BarchartConfigs { + series: { + xScaleType: ScaleType; + yScaleType: ScaleType; + stackAccessors: string[]; }; - aggregations?: Aggregations; - hits: { - total: { - value: number; - relation: string; + axis: { + xTickFormatter: TickFormatter; + yTickFormatter: TickFormatter; + tickSize: number; + }; + settings: { + legendPosition: Position; + onBrushEnd: UpdateDateRange; + showLegend: boolean; + theme: { + scales: { + barsPadding: number; + }; + chartMargins: { + left: number; + right: number; + top: number; + bottom: number; + }; + chartPaddings: { + left: number; + right: number; + top: number; + bottom: number; + }; }; - hits: Hit[]; }; + customHeight: number; } - -export type Return<Hit, Aggs> = [ - boolean, - SignalSearchResponse<Hit, Aggs> | null, - React.Dispatch<SetStateAction<string>> -]; diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.test.ts b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.test.ts new file mode 100644 index 0000000000000..2c34a307bfded --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.test.ts @@ -0,0 +1,139 @@ +/* + * 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 { + getBarchartConfigs, + DEFAULT_CHART_HEIGHT, + DEFAULT_Y_TICK_FORMATTER, + formatToChartDataItem, + getCustomChartData, +} from './utils'; +import { UpdateDateRange } from '../charts/common'; +import { Position } from '@elastic/charts'; +import { MatrixOverTimeHistogramData } from '../../graphql/types'; +import { BarchartConfigs } from './types'; + +describe('utils', () => { + describe('getBarchartConfigs', () => { + describe('it should get correct default values', () => { + let configs: BarchartConfigs; + beforeAll(() => { + configs = getBarchartConfigs({ + from: 0, + to: 0, + onBrushEnd: jest.fn() as UpdateDateRange, + }); + }); + + test('it should set default chartHeight', () => { + expect(configs.customHeight).toEqual(DEFAULT_CHART_HEIGHT); + }); + + test('it should show legend by default', () => { + expect(configs.settings.showLegend).toEqual(true); + }); + + test('it should put legend on the right', () => { + expect(configs.settings.legendPosition).toEqual(Position.Right); + }); + + test('it should format Y tick to local string', () => { + expect(configs.axis.yTickFormatter).toEqual(DEFAULT_Y_TICK_FORMATTER); + }); + }); + + describe('it should set custom configs', () => { + let configs: BarchartConfigs; + const mockYTickFormatter = jest.fn(); + const mockChartHeight = 100; + + beforeAll(() => { + configs = getBarchartConfigs({ + chartHeight: mockChartHeight, + from: 0, + to: 0, + onBrushEnd: jest.fn() as UpdateDateRange, + yTickFormatter: mockYTickFormatter, + showLegend: false, + }); + }); + + test('it should set custom chart height', () => { + expect(configs.customHeight).toEqual(mockChartHeight); + }); + + test('it should hide legend', () => { + expect(configs.settings.showLegend).toEqual(false); + }); + + test('it should format y tick with custom formatter', () => { + expect(configs.axis.yTickFormatter).toEqual(mockYTickFormatter); + }); + }); + }); + + describe('formatToChartDataItem', () => { + test('it should format data correctly', () => { + const data: [string, MatrixOverTimeHistogramData[]] = [ + 'g1', + [ + { x: 1, y: 2, g: 'g1' }, + { x: 2, y: 4, g: 'g1' }, + { x: 3, y: 6, g: 'g1' }, + ], + ]; + const result = formatToChartDataItem(data); + expect(result).toEqual({ + key: 'g1', + value: [ + { x: 1, y: 2, g: 'g1' }, + { x: 2, y: 4, g: 'g1' }, + { x: 3, y: 6, g: 'g1' }, + ], + }); + }); + }); + + describe('getCustomChartData', () => { + test('should handle the case when no data provided', () => { + const data = null; + const result = getCustomChartData(data); + + expect(result).toEqual([]); + }); + + test('shoule format data correctly', () => { + const data = [ + { x: 1, y: 2, g: 'g1' }, + { x: 2, y: 4, g: 'g1' }, + { x: 3, y: 6, g: 'g1' }, + { x: 1, y: 1, g: 'g2' }, + { x: 2, y: 3, g: 'g2' }, + { x: 3, y: 5, g: 'g2' }, + ]; + const result = getCustomChartData(data); + + expect(result).toEqual([ + { + key: 'g1', + value: [ + { x: 1, y: 2, g: 'g1' }, + { x: 2, y: 4, g: 'g1' }, + { x: 3, y: 6, g: 'g1' }, + ], + }, + { + key: 'g2', + value: [ + { x: 1, y: 1, g: 'g2' }, + { x: 2, y: 3, g: 'g2' }, + { x: 3, y: 5, g: 'g2' }, + ], + }, + ]); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts index 95b1cd806cf6c..ccd1b03eb5474 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts @@ -7,7 +7,8 @@ import { ScaleType, Position } from '@elastic/charts'; import { get, groupBy, map, toPairs } from 'lodash/fp'; import { UpdateDateRange, ChartSeriesData } from '../charts/common'; -import { MatrixHistogramDataTypes, MatrixHistogramMappingTypes } from './types'; +import { MatrixHistogramMappingTypes, BarchartConfigs } from './types'; +import { MatrixOverTimeHistogramData } from '../../graphql/types'; import { histogramDateTimeFormatter } from '../utils'; interface GetBarchartConfigsProps { @@ -15,40 +16,35 @@ interface GetBarchartConfigsProps { from: number; legendPosition?: Position; to: number; - scaleType: ScaleType; onBrushEnd: UpdateDateRange; yTickFormatter?: (value: number) => string; showLegend?: boolean; } export const DEFAULT_CHART_HEIGHT = 174; +export const DEFAULT_Y_TICK_FORMATTER = (value: string | number): string => value.toLocaleString(); export const getBarchartConfigs = ({ chartHeight, from, legendPosition, to, - scaleType, onBrushEnd, yTickFormatter, showLegend, -}: GetBarchartConfigsProps) => ({ +}: GetBarchartConfigsProps): BarchartConfigs => ({ series: { - xScaleType: scaleType || ScaleType.Time, + xScaleType: ScaleType.Time, yScaleType: ScaleType.Linear, stackAccessors: ['g'], }, axis: { - xTickFormatter: - scaleType === ScaleType.Time ? histogramDateTimeFormatter([from, to]) : undefined, - yTickFormatter: - yTickFormatter != null - ? yTickFormatter - : (value: string | number): string => value.toLocaleString(), + xTickFormatter: histogramDateTimeFormatter([from, to]), + yTickFormatter: yTickFormatter != null ? yTickFormatter : DEFAULT_Y_TICK_FORMATTER, tickSize: 8, }, settings: { - legendPosition: legendPosition ?? Position.Bottom, + legendPosition: legendPosition ?? Position.Right, onBrushEnd, showLegend: showLegend ?? true, theme: { @@ -74,14 +70,14 @@ export const getBarchartConfigs = ({ export const formatToChartDataItem = ([key, value]: [ string, - MatrixHistogramDataTypes[] + MatrixOverTimeHistogramData[] ]): ChartSeriesData => ({ key, value, }); export const getCustomChartData = ( - data: MatrixHistogramDataTypes[] | null, + data: MatrixOverTimeHistogramData[] | null, mapping?: MatrixHistogramMappingTypes ): ChartSeriesData[] => { if (!data) return []; @@ -92,7 +88,7 @@ export const getCustomChartData = ( if (mapping) return map((item: ChartSeriesData) => { const mapItem = get(item.key, mapping); - return { ...item, color: mapItem.color }; + return { ...item, color: mapItem?.color }; }, formattedChartData); else return formattedChartData; }; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/ml_host_conditional_container.tsx b/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/ml_host_conditional_container.tsx index 8bd97304a7e21..b5aacdf664c67 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/ml_host_conditional_container.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/ml_host_conditional_container.tsx @@ -4,16 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse, stringify } from 'query-string'; import React from 'react'; import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; -import { QueryString } from 'ui/utils/query_string'; import { addEntitiesToKql } from './add_entities_to_kql'; import { replaceKQLParts } from './replace_kql_parts'; import { emptyEntity, multipleEntities, getMultipleEntities } from './entity_helpers'; import { SiemPageName } from '../../../pages/home/types'; import { HostsTableType } from '../../../store/hosts/model'; +import { url as urlUtils } from '../../../../../../../../src/plugins/kibana_utils/public'; + interface QueryStringType { '?_g': string; query: string | null; @@ -29,13 +31,17 @@ export const MlHostConditionalContainer = React.memo<MlHostConditionalProps>(({ exact path={url} render={({ location }) => { - const queryStringDecoded: QueryStringType = QueryString.decode( - location.search.substring(1) - ); + const queryStringDecoded = parse(location.search.substring(1), { + sort: false, + }) as Required<QueryStringType>; + if (queryStringDecoded.query != null) { queryStringDecoded.query = replaceKQLParts(queryStringDecoded.query); } - const reEncoded = QueryString.encode(queryStringDecoded); + const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), { + sort: false, + encode: false, + }); return <Redirect to={`/${SiemPageName.hosts}?${reEncoded}`} />; }} /> @@ -47,14 +53,19 @@ export const MlHostConditionalContainer = React.memo<MlHostConditionalProps>(({ params: { hostName }, }, }) => { - const queryStringDecoded: QueryStringType = QueryString.decode( - location.search.substring(1) - ); + const queryStringDecoded = parse(location.search.substring(1), { + sort: false, + }) as Required<QueryStringType>; + if (queryStringDecoded.query != null) { queryStringDecoded.query = replaceKQLParts(queryStringDecoded.query); } if (emptyEntity(hostName)) { - const reEncoded = QueryString.encode(queryStringDecoded); + const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), { + sort: false, + encode: false, + }); + return ( <Redirect to={`/${SiemPageName.hosts}/${HostsTableType.anomalies}?${reEncoded}`} /> ); @@ -65,12 +76,20 @@ export const MlHostConditionalContainer = React.memo<MlHostConditionalProps>(({ hosts, queryStringDecoded.query || '' ); - const reEncoded = QueryString.encode(queryStringDecoded); + const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), { + sort: false, + encode: false, + }); + return ( <Redirect to={`/${SiemPageName.hosts}/${HostsTableType.anomalies}?${reEncoded}`} /> ); } else { - const reEncoded = QueryString.encode(queryStringDecoded); + const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), { + sort: false, + encode: false, + }); + return ( <Redirect to={`/${SiemPageName.hosts}/${hostName}/${HostsTableType.anomalies}?${reEncoded}`} diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/ml_network_conditional_container.tsx b/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/ml_network_conditional_container.tsx index 8778818829cf7..e27e9dc084c57 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/ml_network_conditional_container.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/ml_network_conditional_container.tsx @@ -4,15 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse, stringify } from 'query-string'; import React from 'react'; import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; -import { QueryString } from 'ui/utils/query_string'; import { addEntitiesToKql } from './add_entities_to_kql'; import { replaceKQLParts } from './replace_kql_parts'; import { emptyEntity, getMultipleEntities, multipleEntities } from './entity_helpers'; import { SiemPageName } from '../../../pages/home/types'; +import { url as urlUtils } from '../../../../../../../../src/plugins/kibana_utils/public'; + interface QueryStringType { '?_g': string; query: string | null; @@ -28,13 +30,19 @@ export const MlNetworkConditionalContainer = React.memo<MlNetworkConditionalProp exact path={url} render={({ location }) => { - const queryStringDecoded: QueryStringType = QueryString.decode( - location.search.substring(1) - ); + const queryStringDecoded = parse(location.search.substring(1), { + sort: false, + }) as Required<QueryStringType>; + if (queryStringDecoded.query != null) { queryStringDecoded.query = replaceKQLParts(queryStringDecoded.query); } - const reEncoded = QueryString.encode(queryStringDecoded); + + const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), { + sort: false, + encode: false, + }); + return <Redirect to={`/${SiemPageName.network}?${reEncoded}`} />; }} /> @@ -46,14 +54,20 @@ export const MlNetworkConditionalContainer = React.memo<MlNetworkConditionalProp params: { ip }, }, }) => { - const queryStringDecoded: QueryStringType = QueryString.decode( - location.search.substring(1) - ); + const queryStringDecoded = parse(location.search.substring(1), { + sort: false, + }) as Required<QueryStringType>; + if (queryStringDecoded.query != null) { queryStringDecoded.query = replaceKQLParts(queryStringDecoded.query); } + if (emptyEntity(ip)) { - const reEncoded = QueryString.encode(queryStringDecoded); + const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), { + sort: false, + encode: false, + }); + return <Redirect to={`/${SiemPageName.network}?${reEncoded}`} />; } else if (multipleEntities(ip)) { const ips: string[] = getMultipleEntities(ip); @@ -62,10 +76,16 @@ export const MlNetworkConditionalContainer = React.memo<MlNetworkConditionalProp ips, queryStringDecoded.query || '' ); - const reEncoded = QueryString.encode(queryStringDecoded); + const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), { + sort: false, + encode: false, + }); return <Redirect to={`/${SiemPageName.network}?${reEncoded}`} />; } else { - const reEncoded = QueryString.encode(queryStringDecoded); + const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), { + sort: false, + encode: false, + }); return <Redirect to={`/${SiemPageName.network}/ip/${ip}?${reEncoded}`} />; } }} diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts index e8d5032fd7548..e25fb4374bb14 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts +++ b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts @@ -11,6 +11,7 @@ import { APP_NAME } from '../../../../common/constants'; import { StartServices } from '../../../plugin'; import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../pages/hosts/details/utils'; import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../pages/network/ip_details'; +import { getBreadcrumbs as getCaseDetailsBreadcrumbs } from '../../../pages/case/utils'; import { getBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../pages/detection_engine/rules/utils'; import { SiemPageName } from '../../../pages/home/types'; import { RouteSpyState, HostRouteSpyState, NetworkRouteSpyState } from '../../../utils/route/types'; @@ -43,6 +44,9 @@ const isNetworkRoutes = (spyState: RouteSpyState): spyState is NetworkRouteSpySt const isHostsRoutes = (spyState: RouteSpyState): spyState is HostRouteSpyState => spyState != null && spyState.pageName === SiemPageName.hosts; +const isCaseRoutes = (spyState: RouteSpyState): spyState is RouteSpyState => + spyState != null && spyState.pageName === SiemPageName.case; + const isDetectionsRoutes = (spyState: RouteSpyState) => spyState != null && spyState.pageName === SiemPageName.detections; @@ -102,6 +106,9 @@ export const getBreadcrumbsForRoute = ( ), ]; } + if (isCaseRoutes(spyState) && object.navTabs) { + return [...siemRootBreadcrumb, ...getCaseDetailsBreadcrumbs(spyState)]; + } if ( spyState != null && object.navTabs && diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx index ac7a4a0ee52b7..8eb08bd3d62f0 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx @@ -66,6 +66,13 @@ describe('SIEM Navigation', () => { { detailName: undefined, navTabs: { + case: { + disabled: true, + href: '#/link-to/case', + id: 'case', + name: 'Case', + urlKey: 'case', + }, detections: { disabled: false, href: '#/link-to/detections', @@ -152,6 +159,13 @@ describe('SIEM Navigation', () => { detailName: undefined, filters: [], navTabs: { + case: { + disabled: true, + href: '#/link-to/case', + id: 'case', + name: 'Case', + urlKey: 'case', + }, detections: { disabled: false, href: '#/link-to/detections', diff --git a/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.tsx b/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.tsx index 5a3439d53dd89..15e58f3efd21e 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.tsx +++ b/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.tsx @@ -32,8 +32,6 @@ const TextArea = styled(EuiTextArea)<{ height: number }>` TextArea.displayName = 'TextArea'; -TextArea.displayName = 'TextArea'; - /** An input for entering a new note */ export const NewNote = React.memo<{ noteInputHeight: number; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts b/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts index 22e8f99658f8d..b6ef3c8ccd4e9 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts @@ -6,6 +6,8 @@ export enum CONSTANTS { appQuery = 'query', + caseDetails = 'case.details', + casePage = 'case.page', detectionsPage = 'detections.page', filters = 'filters', hostsDetails = 'hosts.details', @@ -14,10 +16,10 @@ export enum CONSTANTS { networkPage = 'network.page', overviewPage = 'overview.page', savedQuery = 'savedQuery', + timeline = 'timeline', timelinePage = 'timeline.page', timerange = 'timerange', - timeline = 'timeline', unknown = 'unknown', } -export type UrlStateType = 'detections' | 'host' | 'network' | 'overview' | 'timeline'; +export type UrlStateType = 'case' | 'detections' | 'host' | 'network' | 'overview' | 'timeline'; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts index 34f1ea156eee7..05329621aa97a 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse, stringify } from 'query-string'; import { decode, encode } from 'rison-node'; import * as H from 'history'; -import { QueryString } from 'ui/utils/query_string'; import { Query, Filter } from 'src/plugins/data/public'; import { isEmpty } from 'lodash/fp'; @@ -24,6 +24,8 @@ import { UpdateUrlStateString, } from './types'; +import { url } from '../../../../../../../src/plugins/kibana_utils/public'; + export const decodeRisonUrlState = <T>(value: string | undefined): T | null => { try { return value ? ((decode(value) as unknown) as T) : null; @@ -40,30 +42,35 @@ export const encodeRisonUrlState = (state: any) => encode(state); export const getQueryStringFromLocation = (search: string) => search.substring(1); -export const getParamFromQueryString = (queryString: string, key: string): string | undefined => { - const queryParam = QueryString.decode(queryString)[key]; +export const getParamFromQueryString = (queryString: string, key: string) => { + const parsedQueryString = parse(queryString, { sort: false }); + const queryParam = parsedQueryString[key]; + return Array.isArray(queryParam) ? queryParam[0] : queryParam; }; export const replaceStateKeyInQueryString = <T>(stateKey: string, urlState: T) => ( queryString: string ): string => { - const previousQueryValues = QueryString.decode(queryString); + const previousQueryValues = parse(queryString, { sort: false }); if (urlState == null || (typeof urlState === 'string' && urlState === '')) { delete previousQueryValues[stateKey]; - return QueryString.encode({ - ...previousQueryValues, - }); + + return stringify(url.encodeQuery(previousQueryValues), { sort: false, encode: false }); } // ಠ_ಠ Code was copied from x-pack/legacy/plugins/infra/public/utils/url_state.tsx ಠ_ಠ // Remove this if these utilities are promoted to kibana core const encodedUrlState = typeof urlState !== 'undefined' ? encodeRisonUrlState(urlState) : undefined; - return QueryString.encode({ - ...previousQueryValues, - [stateKey]: encodedUrlState, - }); + + return stringify( + url.encodeQuery({ + ...previousQueryValues, + [stateKey]: encodedUrlState, + }), + { sort: false, encode: false } + ); }; export const replaceQueryStringInLocation = ( @@ -91,6 +98,8 @@ export const getUrlType = (pageName: string): UrlStateType => { return 'detections'; } else if (pageName === SiemPageName.timelines) { return 'timeline'; + } else if (pageName === SiemPageName.case) { + return 'case'; } return 'overview'; }; @@ -124,6 +133,11 @@ export const getCurrentLocation = ( return CONSTANTS.detectionsPage; } else if (pageName === SiemPageName.timelines) { return CONSTANTS.timelinePage; + } else if (pageName === SiemPageName.case) { + if (detailName != null) { + return CONSTANTS.caseDetails; + } + return CONSTANTS.casePage; } return CONSTANTS.unknown; }; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/index_mocked.test.tsx b/x-pack/legacy/plugins/siem/public/components/url_state/index_mocked.test.tsx index 6995bc8bf1d40..4adc17b32e189 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/index_mocked.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/url_state/index_mocked.test.tsx @@ -147,7 +147,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => hash: '', pathname: '/network', search: - '?timeline=(id:hello_timeline_id,isOpen:!t)&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + '?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))&timeline=(id:hello_timeline_id,isOpen:!t)', state: '', }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts index fea1bc016fd49..97979e514aeaf 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts @@ -60,9 +60,12 @@ export const URL_STATE_KEYS: Record<UrlStateType, KeyUrlState[]> = { CONSTANTS.timeline, ], timeline: [CONSTANTS.timeline, CONSTANTS.timerange], + case: [], }; export type LocationTypes = + | CONSTANTS.caseDetails + | CONSTANTS.casePage | CONSTANTS.detectionsPage | CONSTANTS.hostsDetails | CONSTANTS.hostsPage diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts new file mode 100644 index 0000000000000..f63349d3e573a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as i18n from './translations'; +import { + MatrixHistogramOption, + MatrixHisrogramConfigs, +} from '../../../components/matrix_histogram/types'; +import { HistogramType } from '../../../graphql/types'; + +export const anomaliesStackByOptions: MatrixHistogramOption[] = [ + { + text: i18n.ANOMALIES_STACK_BY_JOB_ID, + value: 'job_id', + }, +]; + +const DEFAULT_STACK_BY = i18n.ANOMALIES_STACK_BY_JOB_ID; + +export const histogramConfigs: MatrixHisrogramConfigs = { + defaultStackByOption: + anomaliesStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? anomaliesStackByOptions[0], + errorMessage: i18n.ERROR_FETCHING_ANOMALIES_DATA, + hideHistogramIfEmpty: true, + histogramType: HistogramType.anomalies, + stackByOptions: anomaliesStackByOptions, + subtitle: undefined, + title: i18n.ANOMALIES_TITLE, +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx index e34832aa88c93..85e19248f2eb5 100644 --- a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx @@ -5,23 +5,14 @@ */ import React, { useEffect } from 'react'; -import * as i18n from './translations'; import { AnomaliesQueryTabBodyProps } from './types'; import { getAnomaliesFilterQuery } from './utils'; import { useSiemJobs } from '../../../components/ml_popover/hooks/use_siem_jobs'; import { useUiSetting$ } from '../../../lib/kibana'; import { DEFAULT_ANOMALY_SCORE } from '../../../../common/constants'; -import { MatrixHistogramContainer } from '../../matrix_histogram'; -import { MatrixHistogramOption } from '../../../components/matrix_histogram/types'; -import { MatrixHistogramGqlQuery } from '../../matrix_histogram/index.gql_query'; - +import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; +import { histogramConfigs } from './histogram_configs'; const ID = 'anomaliesOverTimeQuery'; -const anomaliesStackByOptions: MatrixHistogramOption[] = [ - { - text: i18n.ANOMALIES_STACK_BY_JOB_ID, - value: 'job_id', - }, -]; export const AnomaliesQueryTabBody = ({ deleteQuery, @@ -33,7 +24,6 @@ export const AnomaliesQueryTabBody = ({ narrowDateRange, filterQuery, anomaliesFilterQuery, - updateDateRange = () => {}, AnomaliesTableComponent, flowTarget, ip, @@ -61,23 +51,14 @@ export const AnomaliesQueryTabBody = ({ return ( <> <MatrixHistogramContainer - isAnomaliesHistogram={true} - dataKey="AnomaliesHistogram" - defaultStackByOption={anomaliesStackByOptions[0]} endDate={endDate} - errorMessage={i18n.ERROR_FETCHING_ANOMALIES_DATA} filterQuery={mergedFilterQuery} - hideHistogramIfEmpty={true} id={ID} - query={MatrixHistogramGqlQuery} setQuery={setQuery} - skip={skip} sourceId="default" - stackByOptions={anomaliesStackByOptions} startDate={startDate} - title={i18n.ANOMALIES_TITLE} type={type} - updateDateRange={updateDateRange} + {...histogramConfigs} /> <AnomaliesTableComponent startDate={startDate} diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts new file mode 100644 index 0000000000000..830e00c70975e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts @@ -0,0 +1,73 @@ +/* + * 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 { KibanaServices } from '../../lib/kibana'; +import { AllCases, FetchCasesProps, Case, NewCase, SortFieldCase } from './types'; +import { Direction } from '../../graphql/types'; +import { throwIfNotOk } from '../../hooks/api/api'; +import { CASES_URL } from './constants'; + +export const getCase = async (caseId: string, includeComments: boolean) => { + const response = await KibanaServices.get().http.fetch(`${CASES_URL}/${caseId}`, { + method: 'GET', + asResponse: true, + query: { + includeComments, + }, + }); + await throwIfNotOk(response.response); + return response.body!; +}; + +export const getCases = async ({ + filterOptions = { + search: '', + tags: [], + }, + queryParams = { + page: 1, + perPage: 20, + sortField: SortFieldCase.createdAt, + sortOrder: Direction.desc, + }, +}: FetchCasesProps): Promise<AllCases> => { + const tags = [...(filterOptions.tags?.map(t => `case-workflow.attributes.tags: ${t}`) ?? [])]; + const query = { + ...queryParams, + filter: tags.join(' AND '), + search: filterOptions.search, + }; + const response = await KibanaServices.get().http.fetch(`${CASES_URL}`, { + method: 'GET', + query, + asResponse: true, + }); + await throwIfNotOk(response.response); + return response.body!; +}; + +export const createCase = async (newCase: NewCase): Promise<Case> => { + const response = await KibanaServices.get().http.fetch(`${CASES_URL}`, { + method: 'POST', + asResponse: true, + body: JSON.stringify(newCase), + }); + await throwIfNotOk(response.response); + return response.body!; +}; + +export const updateCaseProperty = async ( + caseId: string, + updatedCase: Partial<Case> +): Promise<Partial<Case>> => { + const response = await KibanaServices.get().http.fetch(`${CASES_URL}/${caseId}`, { + method: 'PATCH', + asResponse: true, + body: JSON.stringify(updatedCase), + }); + await throwIfNotOk(response.response); + return response.body!; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts new file mode 100644 index 0000000000000..c8d668527ae32 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const CASES_URL = `/api/cases`; +export const DEFAULT_TABLE_ACTIVE_PAGE = 1; +export const DEFAULT_TABLE_LIMIT = 5; +export const FETCH_FAILURE = 'FETCH_FAILURE'; +export const FETCH_INIT = 'FETCH_INIT'; +export const FETCH_SUCCESS = 'FETCH_SUCCESS'; +export const POST_NEW_CASE = 'POST_NEW_CASE'; +export const UPDATE_CASE_PROPERTY = 'UPDATE_CASE_PROPERTY'; +export const UPDATE_FILTER_OPTIONS = 'UPDATE_FILTER_OPTIONS'; +export const UPDATE_QUERY_PARAMS = 'UPDATE_QUERY_PARAMS'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/translations.ts b/x-pack/legacy/plugins/siem/public/containers/case/translations.ts new file mode 100644 index 0000000000000..0c8b896e2b426 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/translations.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_TITLE = i18n.translate('xpack.siem.containers.case.errorTitle', { + defaultMessage: 'Error fetching data', +}); + +export const TAG_FETCH_FAILURE = i18n.translate( + 'xpack.siem.containers.case.tagFetchFailDescription', + { + defaultMessage: 'Failed to fetch Tags', + } +); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts new file mode 100644 index 0000000000000..0f80b2327a30c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Direction } from '../../graphql/types'; +interface FormData { + isNew?: boolean; +} + +export interface NewCase extends FormData { + description: string; + tags: string[]; + title: string; +} + +export interface Case { + case_id: string; + created_at: string; + created_by: ElasticUser; + description: string; + state: string; + tags: string[]; + title: string; + updated_at: string; +} + +export interface QueryParams { + page: number; + perPage: number; + sortField: SortFieldCase; + sortOrder: Direction; +} + +export interface FilterOptions { + search: string; + tags: string[]; +} + +export interface AllCases { + cases: Case[]; + page: number; + per_page: number; + total: number; +} +export enum SortFieldCase { + createdAt = 'created_at', + state = 'state', + updatedAt = 'updated_at', +} + +export interface ElasticUser { + readonly username: string; + readonly full_name?: string; +} + +export interface FetchCasesProps { + queryParams?: QueryParams; + filterOptions?: FilterOptions; +} diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx new file mode 100644 index 0000000000000..8cc961c68fdf0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useReducer } from 'react'; + +import { Case } from './types'; +import { FETCH_INIT, FETCH_FAILURE, FETCH_SUCCESS } from './constants'; +import { getTypedPayload } from './utils'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import * as i18n from './translations'; +import { useStateToaster } from '../../components/toasters'; +import { getCase } from './api'; + +interface CaseState { + data: Case; + isLoading: boolean; + isError: boolean; +} +interface Action { + type: string; + payload?: Case; +} + +const dataFetchReducer = (state: CaseState, action: Action): CaseState => { + switch (action.type) { + case FETCH_INIT: + return { + ...state, + isLoading: true, + isError: false, + }; + case FETCH_SUCCESS: + return { + ...state, + isLoading: false, + isError: false, + data: getTypedPayload<Case>(action.payload), + }; + case FETCH_FAILURE: + return { + ...state, + isLoading: false, + isError: true, + }; + default: + throw new Error(); + } +}; +const initialData: Case = { + case_id: '', + created_at: '', + created_by: { + username: '', + }, + description: '', + state: '', + tags: [], + title: '', + updated_at: '', +}; + +export const useGetCase = (caseId: string): [CaseState] => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: true, + isError: false, + data: initialData, + }); + const [, dispatchToaster] = useStateToaster(); + + const callFetch = () => { + let didCancel = false; + const fetchData = async () => { + dispatch({ type: FETCH_INIT }); + try { + const response = await getCase(caseId, false); + if (!didCancel) { + dispatch({ type: FETCH_SUCCESS, payload: response }); + } + } catch (error) { + if (!didCancel) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: FETCH_FAILURE }); + } + } + }; + fetchData(); + return () => { + didCancel = true; + }; + }; + + useEffect(() => { + callFetch(); + }, [caseId]); + return [state]; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx new file mode 100644 index 0000000000000..db9c07747ba04 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -0,0 +1,154 @@ +/* + * 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 { Dispatch, SetStateAction, useEffect, useReducer, useState } from 'react'; +import { isEqual } from 'lodash/fp'; +import { + DEFAULT_TABLE_ACTIVE_PAGE, + DEFAULT_TABLE_LIMIT, + FETCH_FAILURE, + FETCH_INIT, + FETCH_SUCCESS, + UPDATE_QUERY_PARAMS, + UPDATE_FILTER_OPTIONS, +} from './constants'; +import { AllCases, SortFieldCase, FilterOptions, QueryParams } from './types'; +import { getTypedPayload } from './utils'; +import { Direction } from '../../graphql/types'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import { useStateToaster } from '../../components/toasters'; +import * as i18n from './translations'; +import { getCases } from './api'; + +export interface UseGetCasesState { + data: AllCases; + isLoading: boolean; + isError: boolean; + queryParams: QueryParams; + filterOptions: FilterOptions; +} + +export interface QueryArgs { + page?: number; + perPage?: number; + sortField?: SortFieldCase; + sortOrder?: Direction; +} + +export interface Action { + type: string; + payload?: AllCases | QueryArgs | FilterOptions; +} +const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesState => { + switch (action.type) { + case FETCH_INIT: + return { + ...state, + isLoading: true, + isError: false, + }; + case FETCH_SUCCESS: + return { + ...state, + isLoading: false, + isError: false, + data: getTypedPayload<AllCases>(action.payload), + }; + case FETCH_FAILURE: + return { + ...state, + isLoading: false, + isError: true, + }; + case UPDATE_QUERY_PARAMS: + return { + ...state, + queryParams: { + ...state.queryParams, + ...action.payload, + }, + }; + case UPDATE_FILTER_OPTIONS: + return { + ...state, + filterOptions: getTypedPayload<FilterOptions>(action.payload), + }; + default: + throw new Error(); + } +}; + +const initialData: AllCases = { + page: 0, + per_page: 0, + total: 0, + cases: [], +}; +export const useGetCases = (): [ + UseGetCasesState, + Dispatch<SetStateAction<QueryArgs>>, + Dispatch<SetStateAction<FilterOptions>> +] => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: false, + isError: false, + data: initialData, + filterOptions: { + search: '', + tags: [], + }, + queryParams: { + page: DEFAULT_TABLE_ACTIVE_PAGE, + perPage: DEFAULT_TABLE_LIMIT, + sortField: SortFieldCase.createdAt, + sortOrder: Direction.desc, + }, + }); + const [queryParams, setQueryParams] = useState(state.queryParams as QueryArgs); + const [filterQuery, setFilters] = useState(state.filterOptions as FilterOptions); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + if (!isEqual(queryParams, state.queryParams)) { + dispatch({ type: UPDATE_QUERY_PARAMS, payload: queryParams }); + } + }, [queryParams, state.queryParams]); + + useEffect(() => { + if (!isEqual(filterQuery, state.filterOptions)) { + dispatch({ type: UPDATE_FILTER_OPTIONS, payload: filterQuery }); + } + }, [filterQuery, state.filterOptions]); + + useEffect(() => { + let didCancel = false; + const fetchData = async () => { + dispatch({ type: FETCH_INIT }); + try { + const response = await getCases({ + filterOptions: state.filterOptions, + queryParams: state.queryParams, + }); + if (!didCancel) { + dispatch({ + type: FETCH_SUCCESS, + payload: response, + }); + } + } catch (error) { + if (!didCancel) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: FETCH_FAILURE }); + } + } + }; + fetchData(); + return () => { + didCancel = true; + }; + }, [state.queryParams, state.filterOptions]); + return [state, setQueryParams, setFilters]; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx new file mode 100644 index 0000000000000..f796ae550c9ec --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useReducer } from 'react'; +import chrome from 'ui/chrome'; +import { useStateToaster } from '../../components/toasters'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import * as i18n from './translations'; +import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; +import { throwIfNotOk } from '../../hooks/api/api'; + +interface TagsState { + data: string[]; + isLoading: boolean; + isError: boolean; +} +interface Action { + type: string; + payload?: string[]; +} + +const dataFetchReducer = (state: TagsState, action: Action): TagsState => { + switch (action.type) { + case FETCH_INIT: + return { + ...state, + isLoading: true, + isError: false, + }; + case FETCH_SUCCESS: + const getTypedPayload = (a: Action['payload']) => a as string[]; + return { + ...state, + isLoading: false, + isError: false, + data: getTypedPayload(action.payload), + }; + case FETCH_FAILURE: + return { + ...state, + isLoading: false, + isError: true, + }; + default: + throw new Error(); + } +}; +const initialData: string[] = []; + +export const useGetTags = (): [TagsState] => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: false, + isError: false, + data: initialData, + }); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + let didCancel = false; + const fetchData = async () => { + dispatch({ type: FETCH_INIT }); + try { + const response = await fetch(`${chrome.getBasePath()}/api/cases/tags`, { + method: 'GET', + credentials: 'same-origin', + headers: { + 'content-type': 'application/json', + 'kbn-system-api': 'true', + }, + }); + if (!didCancel) { + await throwIfNotOk(response); + const responseJson = await response.json(); + dispatch({ type: FETCH_SUCCESS, payload: responseJson }); + } + } catch (error) { + if (!didCancel) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: FETCH_FAILURE }); + } + } + }; + fetchData(); + return () => { + didCancel = true; + }; + }, []); + return [state]; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx new file mode 100644 index 0000000000000..5cf99701977d2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Dispatch, SetStateAction, useEffect, useReducer, useState } from 'react'; +import { useStateToaster } from '../../components/toasters'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import * as i18n from './translations'; +import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS, POST_NEW_CASE } from './constants'; +import { Case, NewCase } from './types'; +import { createCase } from './api'; +import { getTypedPayload } from './utils'; + +interface NewCaseState { + data: NewCase; + newCase?: Case; + isLoading: boolean; + isError: boolean; +} +interface Action { + type: string; + payload?: NewCase | Case; +} + +const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => { + switch (action.type) { + case FETCH_INIT: + return { + ...state, + isLoading: true, + isError: false, + }; + case POST_NEW_CASE: + return { + ...state, + isLoading: false, + isError: false, + data: getTypedPayload<NewCase>(action.payload), + }; + case FETCH_SUCCESS: + return { + ...state, + isLoading: false, + isError: false, + newCase: getTypedPayload<Case>(action.payload), + }; + case FETCH_FAILURE: + return { + ...state, + isLoading: false, + isError: true, + }; + default: + throw new Error(); + } +}; +const initialData: NewCase = { + description: '', + isNew: false, + tags: [], + title: '', +}; + +export const usePostCase = (): [NewCaseState, Dispatch<SetStateAction<NewCase>>] => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: false, + isError: false, + data: initialData, + }); + const [formData, setFormData] = useState(initialData); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + dispatch({ type: POST_NEW_CASE, payload: formData }); + }, [formData]); + + useEffect(() => { + const postCase = async () => { + dispatch({ type: FETCH_INIT }); + try { + const dataWithoutIsNew = state.data; + delete dataWithoutIsNew.isNew; + const response = await createCase(dataWithoutIsNew); + dispatch({ type: FETCH_SUCCESS, payload: response }); + } catch (error) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: FETCH_FAILURE }); + } + }; + if (state.data.isNew) { + postCase(); + } + }, [state.data.isNew]); + return [state, setFormData]; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx new file mode 100644 index 0000000000000..68592c17e58dc --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useReducer } from 'react'; +import { useStateToaster } from '../../components/toasters'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import * as i18n from './translations'; +import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS, UPDATE_CASE_PROPERTY } from './constants'; +import { Case } from './types'; +import { updateCaseProperty } from './api'; +import { getTypedPayload } from './utils'; + +type UpdateKey = keyof Case; + +interface NewCaseState { + data: Case; + isLoading: boolean; + isError: boolean; + updateKey?: UpdateKey | null; +} + +interface UpdateByKey { + updateKey: UpdateKey; + updateValue: Case[UpdateKey]; +} + +interface Action { + type: string; + payload?: Partial<Case> | UpdateByKey; +} + +const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => { + switch (action.type) { + case FETCH_INIT: + return { + ...state, + isLoading: true, + isError: false, + updateKey: null, + }; + case UPDATE_CASE_PROPERTY: + const { updateKey, updateValue } = getTypedPayload<UpdateByKey>(action.payload); + return { + ...state, + isLoading: false, + isError: false, + data: { + ...state.data, + [updateKey]: updateValue, + }, + updateKey, + }; + case FETCH_SUCCESS: + return { + ...state, + isLoading: false, + isError: false, + data: { + ...state.data, + ...getTypedPayload<Case>(action.payload), + }, + }; + case FETCH_FAILURE: + return { + ...state, + isLoading: false, + isError: true, + }; + default: + throw new Error(); + } +}; + +export const useUpdateCase = ( + caseId: string, + initialData: Case +): [{ data: Case }, (updates: UpdateByKey) => void] => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isLoading: false, + isError: false, + data: initialData, + }); + const [, dispatchToaster] = useStateToaster(); + + const dispatchUpdateCaseProperty = ({ updateKey, updateValue }: UpdateByKey) => { + dispatch({ + type: UPDATE_CASE_PROPERTY, + payload: { updateKey, updateValue }, + }); + }; + + useEffect(() => { + const updateData = async (updateKey: keyof Case) => { + dispatch({ type: FETCH_INIT }); + try { + const response = await updateCaseProperty(caseId, { [updateKey]: state.data[updateKey] }); + dispatch({ type: FETCH_SUCCESS, payload: response }); + } catch (error) { + errorToToaster({ title: i18n.ERROR_TITLE, error, dispatchToaster }); + dispatch({ type: FETCH_FAILURE }); + } + }; + if (state.updateKey) { + updateData(state.updateKey); + } + }, [state.updateKey]); + + return [{ data: state.data }, dispatchUpdateCaseProperty]; +}; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/index.ts b/x-pack/legacy/plugins/siem/public/containers/case/utils.ts similarity index 79% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/index.ts rename to x-pack/legacy/plugins/siem/public/containers/case/utils.ts index f75dce9b7507f..8e6eaca1a8f0c 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/index.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/utils.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './plugin'; +export const getTypedPayload = <T>(a: unknown): T => a as T; diff --git a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.gql_query.ts index e21d4c6e34ff8..6fb729ca7e9a0 100644 --- a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.gql_query.ts +++ b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.gql_query.ts @@ -8,94 +8,23 @@ import gql from 'graphql-tag'; export const MatrixHistogramGqlQuery = gql` query GetMatrixHistogramQuery( - $isAlertsHistogram: Boolean! - $isAnomaliesHistogram: Boolean! - $isAuthenticationsHistogram: Boolean! - $isDnsHistogram: Boolean! $defaultIndex: [String!]! - $isEventsHistogram: Boolean! $filterQuery: String + $histogramType: HistogramType! $inspect: Boolean! $sourceId: ID! - $stackByField: String + $stackByField: String! $timerange: TimerangeInput! ) { source(id: $sourceId) { id - AlertsHistogram( + MatrixHistogram( timerange: $timerange filterQuery: $filterQuery defaultIndex: $defaultIndex stackByField: $stackByField - ) @include(if: $isAlertsHistogram) { - matrixHistogramData { - x - y - g - } - totalCount - inspect @include(if: $inspect) { - dsl - response - } - } - AnomaliesHistogram( - timerange: $timerange - filterQuery: $filterQuery - defaultIndex: $defaultIndex - stackByField: $stackByField - ) @include(if: $isAnomaliesHistogram) { - matrixHistogramData { - x - y - g - } - totalCount - inspect @include(if: $inspect) { - dsl - response - } - } - AuthenticationsHistogram( - timerange: $timerange - filterQuery: $filterQuery - defaultIndex: $defaultIndex - stackByField: $stackByField - ) @include(if: $isAuthenticationsHistogram) { - matrixHistogramData { - x - y - g - } - totalCount - inspect @include(if: $inspect) { - dsl - response - } - } - EventsHistogram( - timerange: $timerange - filterQuery: $filterQuery - defaultIndex: $defaultIndex - stackByField: $stackByField - ) @include(if: $isEventsHistogram) { - matrixHistogramData { - x - y - g - } - totalCount - inspect @include(if: $inspect) { - dsl - response - } - } - NetworkDnsHistogram( - timerange: $timerange - filterQuery: $filterQuery - defaultIndex: $defaultIndex - stackByField: $stackByField - ) @include(if: $isDnsHistogram) { + histogramType: $histogramType + ) { matrixHistogramData { x y diff --git a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.test.tsx b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.test.tsx new file mode 100644 index 0000000000000..06367ab8657a8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.test.tsx @@ -0,0 +1,154 @@ +/* + * 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 { useQuery } from '.'; +import { mount } from 'enzyme'; +import React from 'react'; +import { useApolloClient } from '../../utils/apollo_context'; +import { errorToToaster } from '../../components/ml/api/error_to_toaster'; +import { MatrixOverTimeHistogramData, HistogramType } from '../../graphql/types'; +import { InspectQuery, Refetch } from '../../store/inputs/model'; + +const mockQuery = jest.fn().mockResolvedValue({ + data: { + source: { + MatrixHistogram: { + matrixHistogramData: [{}], + totalCount: 1, + inspect: false, + }, + }, + }, +}); + +const mockRejectQuery = jest.fn().mockRejectedValue(new Error()); +jest.mock('../../utils/apollo_context', () => ({ + useApolloClient: jest.fn(), +})); + +jest.mock('../../lib/kibana', () => { + return { + useUiSetting$: jest.fn().mockReturnValue(['mockDefaultIndex']), + }; +}); + +jest.mock('./index.gql_query', () => { + return { + MatrixHistogramGqlQuery: 'mockGqlQuery', + }; +}); + +jest.mock('../../components/ml/api/error_to_toaster'); + +describe('useQuery', () => { + let result: { + data: MatrixOverTimeHistogramData[] | null; + loading: boolean; + inspect: InspectQuery | null; + totalCount: number; + refetch: Refetch | undefined; + }; + describe('happy path', () => { + beforeAll(() => { + (useApolloClient as jest.Mock).mockReturnValue({ + query: mockQuery, + }); + const TestComponent = () => { + result = useQuery({ + endDate: 100, + errorMessage: 'fakeErrorMsg', + filterQuery: '', + histogramType: HistogramType.alerts, + isInspected: false, + stackByField: 'fakeField', + startDate: 0, + }); + + return <div />; + }; + + mount(<TestComponent />); + }); + + test('should set variables', () => { + expect(mockQuery).toBeCalledWith({ + query: 'mockGqlQuery', + fetchPolicy: 'network-only', + variables: { + filterQuery: '', + sourceId: 'default', + timerange: { + interval: '12h', + from: 0, + to: 100, + }, + defaultIndex: 'mockDefaultIndex', + inspect: false, + stackByField: 'fakeField', + histogramType: 'alerts', + }, + context: { + fetchOptions: { + abortSignal: new AbortController().signal, + }, + }, + }); + }); + + test('should setData', () => { + expect(result.data).toEqual([{}]); + }); + + test('should set total count', () => { + expect(result.totalCount).toEqual(1); + }); + + test('should set inspect', () => { + expect(result.inspect).toEqual(false); + }); + }); + + describe('failure path', () => { + beforeAll(() => { + mockQuery.mockClear(); + (useApolloClient as jest.Mock).mockReset(); + (useApolloClient as jest.Mock).mockReturnValue({ + query: mockRejectQuery, + }); + const TestComponent = () => { + result = useQuery({ + endDate: 100, + errorMessage: 'fakeErrorMsg', + filterQuery: '', + histogramType: HistogramType.alerts, + isInspected: false, + stackByField: 'fakeField', + startDate: 0, + }); + + return <div />; + }; + + mount(<TestComponent />); + }); + + test('should setData', () => { + expect(result.data).toEqual(null); + }); + + test('should set total count', () => { + expect(result.totalCount).toEqual(-1); + }); + + test('should set inspect', () => { + expect(result.inspect).toEqual(null); + }); + + test('should set error to toster', () => { + expect(errorToToaster).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/utils.ts b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.ts similarity index 61% rename from x-pack/legacy/plugins/siem/public/containers/matrix_histogram/utils.ts rename to x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.ts index 1df1aec76627c..683d5b68c305b 100644 --- a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/utils.ts +++ b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.ts @@ -3,12 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { getOr } from 'lodash/fp'; -import { useEffect, useRef, useState } from 'react'; -import { - MatrixHistogramDataTypes, - MatrixHistogramQueryProps, -} from '../../components/matrix_histogram/types'; +import { useEffect, useState, useRef } from 'react'; +import { MatrixHistogramQueryProps } from '../../components/matrix_histogram/types'; import { DEFAULT_INDEX_KEY } from '../../../common/constants'; import { useStateToaster } from '../../components/toasters'; import { errorToToaster } from '../../components/ml/api/error_to_toaster'; @@ -16,20 +12,15 @@ import { useUiSetting$ } from '../../lib/kibana'; import { createFilter } from '../helpers'; import { useApolloClient } from '../../utils/apollo_context'; import { inputsModel } from '../../store'; -import { GetMatrixHistogramQuery } from '../../graphql/types'; +import { MatrixHistogramGqlQuery } from './index.gql_query'; +import { GetMatrixHistogramQuery, MatrixOverTimeHistogramData } from '../../graphql/types'; export const useQuery = <Hit, Aggs, TCache = object>({ - dataKey, endDate, errorMessage, filterQuery, - isAlertsHistogram = false, - isAnomaliesHistogram = false, - isAuthenticationsHistogram = false, - isEventsHistogram = false, - isDnsHistogram = false, + histogramType, isInspected, - query, stackByField, startDate, }: MatrixHistogramQueryProps) => { @@ -37,30 +28,25 @@ export const useQuery = <Hit, Aggs, TCache = object>({ const [, dispatchToaster] = useStateToaster(); const refetch = useRef<inputsModel.Refetch>(); const [loading, setLoading] = useState<boolean>(false); - const [data, setData] = useState<MatrixHistogramDataTypes[] | null>(null); + const [data, setData] = useState<MatrixOverTimeHistogramData[] | null>(null); const [inspect, setInspect] = useState<inputsModel.InspectQuery | null>(null); - const [totalCount, setTotalCount] = useState(-1); + const [totalCount, setTotalCount] = useState<number>(-1); const apolloClient = useApolloClient(); - const matrixHistogramVariables: GetMatrixHistogramQuery.Variables = { - filterQuery: createFilter(filterQuery), - sourceId: 'default', - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - defaultIndex, - inspect: isInspected, - stackByField, - isAlertsHistogram, - isAnomaliesHistogram, - isAuthenticationsHistogram, - isDnsHistogram, - isEventsHistogram, - }; - useEffect(() => { + const matrixHistogramVariables: GetMatrixHistogramQuery.Variables = { + filterQuery: createFilter(filterQuery), + sourceId: 'default', + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + defaultIndex, + inspect: isInspected, + stackByField, + histogramType, + }; let isSubscribed = true; const abortCtrl = new AbortController(); const abortSignal = abortCtrl.signal; @@ -70,7 +56,7 @@ export const useQuery = <Hit, Aggs, TCache = object>({ setLoading(true); return apolloClient .query<GetMatrixHistogramQuery.Query, GetMatrixHistogramQuery.Variables>({ - query, + query: MatrixHistogramGqlQuery, fetchPolicy: 'network-only', variables: matrixHistogramVariables, context: { @@ -82,13 +68,10 @@ export const useQuery = <Hit, Aggs, TCache = object>({ .then( result => { if (isSubscribed) { - const isDataKeyAnArray = Array.isArray(dataKey); - const rootDataKey = isDataKeyAnArray ? dataKey[0] : `${dataKey}`; - const histogramDataKey = isDataKeyAnArray ? dataKey[1] : `matrixHistogramData`; - const source = getOr({}, `data.source.${rootDataKey}`, result); - setData(getOr([], histogramDataKey, source)); - setTotalCount(getOr(-1, 'totalCount', source)); - setInspect(getOr(null, 'inspect', source)); + const source = result?.data?.source?.MatrixHistogram ?? {}; + setData(source?.matrixHistogramData ?? []); + setTotalCount(source?.totalCount ?? -1); + setInspect(source?.inspect ?? null); setLoading(false); } }, @@ -97,8 +80,8 @@ export const useQuery = <Hit, Aggs, TCache = object>({ setData(null); setTotalCount(-1); setInspect(null); - errorToToaster({ title: errorMessage, error, dispatchToaster }); setLoading(false); + errorToToaster({ title: errorMessage, error, dispatchToaster }); } } ); @@ -111,13 +94,14 @@ export const useQuery = <Hit, Aggs, TCache = object>({ }; }, [ defaultIndex, - query, + errorMessage, filterQuery, + histogramType, isInspected, - isDnsHistogram, stackByField, startDate, endDate, + data, ]); return { data, loading, inspect, totalCount, refetch: refetch.current }; diff --git a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.tsx b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.tsx deleted file mode 100644 index 9e0b1579a7b65..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Position } from '@elastic/charts'; -import React from 'react'; -import { compose } from 'redux'; - -import { connect } from 'react-redux'; -import { State, inputsSelectors, hostsModel, networkModel } from '../../store'; -import { QueryTemplateProps } from '../query_template'; - -import { Maybe } from '../../graphql/types'; -import { MatrixHistogram } from '../../components/matrix_histogram'; -import { - MatrixHistogramOption, - MatrixHistogramMappingTypes, - GetTitle, - GetSubTitle, -} from '../../components/matrix_histogram/types'; -import { UpdateDateRange } from '../../components/charts/common'; -import { SetQuery } from '../../pages/hosts/navigation/types'; - -export interface OwnProps extends QueryTemplateProps { - chartHeight?: number; - dataKey: string | string[]; - defaultStackByOption: MatrixHistogramOption; - errorMessage: string; - headerChildren?: React.ReactNode; - hideHistogramIfEmpty?: boolean; - isAlertsHistogram?: boolean; - isAnomaliesHistogram?: boolean; - isAuthenticationsHistogram?: boolean; - id: string; - isDnsHistogram?: boolean; - isEventsHistogram?: boolean; - legendPosition?: Position; - mapping?: MatrixHistogramMappingTypes; - panelHeight?: number; - query: Maybe<string>; - setQuery: SetQuery; - showLegend?: boolean; - sourceId: string; - stackByOptions: MatrixHistogramOption[]; - subtitle?: string | GetSubTitle; - title: string | GetTitle; - type: hostsModel.HostsType | networkModel.NetworkType; - updateDateRange: UpdateDateRange; -} - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { type, id }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -export const MatrixHistogramContainer = compose<React.ComponentClass<OwnProps>>( - connect(makeMapStateToProps) -)(MatrixHistogram); diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/legacy/plugins/siem/public/graphql/introspection.json index b356b67b75c7b..9802a5f5bd3bf 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json +++ b/x-pack/legacy/plugins/siem/public/graphql/introspection.json @@ -666,112 +666,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "AlertsHistogram", - "description": "", - "args": [ - { - "name": "filterQuery", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "defaultIndex", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "defaultValue": null - }, - { - "name": "timerange", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "stackByField", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "AlertsOverTimeData", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "AnomaliesHistogram", - "description": "", - "args": [ - { - "name": "timerange", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "filterQuery", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "defaultIndex", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "defaultValue": null - }, - { - "name": "stackByField", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "AnomaliesOverTimeData", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "Authentications", "description": "Gets Authentication success and failures based on a timerange", @@ -833,59 +727,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "AuthenticationsHistogram", - "description": "", - "args": [ - { - "name": "timerange", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "filterQuery", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "defaultIndex", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "defaultValue": null - }, - { - "name": "stackByField", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "AuthenticationsOverTimeData", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "Timeline", "description": "", @@ -1075,59 +916,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "EventsHistogram", - "description": "", - "args": [ - { - "name": "timerange", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "filterQuery", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "defaultIndex", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "defaultValue": null - }, - { - "name": "stackByField", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "EventsOverTimeData", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "Hosts", "description": "Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified", @@ -1610,6 +1398,73 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "MatrixHistogram", + "description": "", + "args": [ + { + "name": "filterQuery", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "defaultIndex", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + } + }, + "defaultValue": null + }, + { + "name": "timerange", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "stackByField", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "histogramType", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "ENUM", "name": "HistogramType", "ofType": null } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "MatrixHistogramOverTimeData", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "NetworkTopCountries", "description": "", @@ -2607,211 +2462,17 @@ }, { "name": "description", - "description": "Description of the field", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "format", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "TimerangeInput", - "description": "", - "fields": null, - "inputFields": [ - { - "name": "interval", - "description": "The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan.", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "to", - "description": "The end of the timerange", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "from", - "description": "The beginning of the timerange", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "AlertsOverTimeData", - "description": "", - "fields": [ - { - "name": "inspect", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "matrixHistogramData", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "MatrixOverTimeHistogramData", - "ofType": null - } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "totalCount", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Inspect", - "description": "", - "fields": [ - { - "name": "dsl", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "response", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "MatrixOverTimeHistogramData", - "description": "", - "fields": [ - { - "name": "x", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "y", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, + "description": "Description of the field", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "g", + "name": "format", "description": "", "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null } @@ -2822,57 +2483,43 @@ "possibleTypes": null }, { - "kind": "OBJECT", - "name": "AnomaliesOverTimeData", + "kind": "INPUT_OBJECT", + "name": "TimerangeInput", "description": "", - "fields": [ + "fields": null, + "inputFields": [ { - "name": "inspect", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null + "name": "interval", + "description": "The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "defaultValue": null }, { - "name": "matrixHistogramData", - "description": "", - "args": [], + "name": "to", + "description": "The end of the timerange", "type": { "kind": "NON_NULL", "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "MatrixOverTimeHistogramData", - "ofType": null - } - } - } + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } }, - "isDeprecated": false, - "deprecationReason": null + "defaultValue": null }, { - "name": "totalCount", - "description": "", - "args": [], + "name": "from", + "description": "The beginning of the timerange", "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } }, - "isDeprecated": false, - "deprecationReason": null + "defaultValue": null } ], - "inputFields": null, - "interfaces": [], + "interfaces": null, "enumValues": null, "possibleTypes": null }, @@ -3587,19 +3234,11 @@ }, { "kind": "OBJECT", - "name": "AuthenticationsOverTimeData", + "name": "Inspect", "description": "", "fields": [ { - "name": "inspect", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "matrixHistogramData", + "name": "dsl", "description": "", "args": [], "type": { @@ -3611,11 +3250,7 @@ "ofType": { "kind": "NON_NULL", "name": null, - "ofType": { - "kind": "OBJECT", - "name": "MatrixOverTimeHistogramData", - "ofType": null - } + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } } } }, @@ -3623,13 +3258,21 @@ "deprecationReason": null }, { - "name": "totalCount", + "name": "response", "description": "", "args": [], "type": { "kind": "NON_NULL", "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + } }, "isDeprecated": false, "deprecationReason": null @@ -6639,61 +6282,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "OBJECT", - "name": "EventsOverTimeData", - "description": "", - "fields": [ - { - "name": "inspect", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "matrixHistogramData", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "MatrixOverTimeHistogramData", - "ofType": null - } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "totalCount", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, { "kind": "INPUT_OBJECT", "name": "HostsSortField", @@ -7844,6 +7432,122 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "ENUM", + "name": "HistogramType", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "authentications", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "anomalies", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "events", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "alerts", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "dns", "description": "", "isDeprecated": false, "deprecationReason": null } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "MatrixHistogramOverTimeData", + "description": "", + "fields": [ + { + "name": "inspect", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "matrixHistogramData", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "MatrixOverTimeHistogramData", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalCount", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "MatrixOverTimeHistogramData", + "description": "", + "fields": [ + { + "name": "x", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "y", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "g", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "ENUM", "name": "FlowTargetSourceDest", diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts index 0103713a8c8a2..3528ee6e13a38 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/public/graphql/types.ts @@ -301,6 +301,14 @@ export enum FlowTarget { source = 'source', } +export enum HistogramType { + authentications = 'authentications', + anomalies = 'anomalies', + events = 'events', + alerts = 'alerts', + dns = 'dns', +} + export enum FlowTargetSourceDest { destination = 'destination', source = 'source', @@ -460,22 +468,14 @@ export interface Source { configuration: SourceConfiguration; /** The status of the source */ status: SourceStatus; - - AlertsHistogram: AlertsOverTimeData; - - AnomaliesHistogram: AnomaliesOverTimeData; /** Gets Authentication success and failures based on a timerange */ Authentications: AuthenticationsData; - AuthenticationsHistogram: AuthenticationsOverTimeData; - Timeline: TimelineData; TimelineDetails: TimelineDetailsData; LastEventTime: LastEventTimeData; - - EventsHistogram: EventsOverTimeData; /** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */ Hosts: HostsData; @@ -493,6 +493,8 @@ export interface Source { KpiHostDetails: KpiHostDetailsData; + MatrixHistogram: MatrixHistogramOverTimeData; + NetworkTopCountries: NetworkTopCountriesData; NetworkTopNFlow: NetworkTopNFlowData; @@ -566,36 +568,6 @@ export interface IndexField { format?: Maybe<string>; } -export interface AlertsOverTimeData { - inspect?: Maybe<Inspect>; - - matrixHistogramData: MatrixOverTimeHistogramData[]; - - totalCount: number; -} - -export interface Inspect { - dsl: string[]; - - response: string[]; -} - -export interface MatrixOverTimeHistogramData { - x: number; - - y: number; - - g: string; -} - -export interface AnomaliesOverTimeData { - inspect?: Maybe<Inspect>; - - matrixHistogramData: MatrixOverTimeHistogramData[]; - - totalCount: number; -} - export interface AuthenticationsData { edges: AuthenticationsEdges[]; @@ -730,12 +702,10 @@ export interface PageInfoPaginated { showMorePagesIndicator: boolean; } -export interface AuthenticationsOverTimeData { - inspect?: Maybe<Inspect>; - - matrixHistogramData: MatrixOverTimeHistogramData[]; +export interface Inspect { + dsl: string[]; - totalCount: number; + response: string[]; } export interface TimelineData { @@ -1390,14 +1360,6 @@ export interface LastEventTimeData { inspect?: Maybe<Inspect>; } -export interface EventsOverTimeData { - inspect?: Maybe<Inspect>; - - matrixHistogramData: MatrixOverTimeHistogramData[]; - - totalCount: number; -} - export interface HostsData { edges: HostsEdges[]; @@ -1598,6 +1560,22 @@ export interface KpiHostDetailsData { inspect?: Maybe<Inspect>; } +export interface MatrixHistogramOverTimeData { + inspect?: Maybe<Inspect>; + + matrixHistogramData: MatrixOverTimeHistogramData[]; + + totalCount: number; +} + +export interface MatrixOverTimeHistogramData { + x?: Maybe<number>; + + y?: Maybe<number>; + + g?: Maybe<string>; +} + export interface NetworkTopCountriesData { edges: NetworkTopCountriesEdges[]; @@ -2241,24 +2219,6 @@ export interface GetAllTimelineQueryArgs { onlyUserFavorite?: Maybe<boolean>; } -export interface AlertsHistogramSourceArgs { - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - timerange: TimerangeInput; - - stackByField?: Maybe<string>; -} -export interface AnomaliesHistogramSourceArgs { - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - stackByField?: Maybe<string>; -} export interface AuthenticationsSourceArgs { timerange: TimerangeInput; @@ -2268,15 +2228,6 @@ export interface AuthenticationsSourceArgs { defaultIndex: string[]; } -export interface AuthenticationsHistogramSourceArgs { - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - stackByField?: Maybe<string>; -} export interface TimelineSourceArgs { pagination: PaginationInput; @@ -2306,15 +2257,6 @@ export interface LastEventTimeSourceArgs { defaultIndex: string[]; } -export interface EventsHistogramSourceArgs { - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - stackByField?: Maybe<string>; -} export interface HostsSourceArgs { id?: Maybe<string>; @@ -2397,6 +2339,17 @@ export interface KpiHostDetailsSourceArgs { defaultIndex: string[]; } +export interface MatrixHistogramSourceArgs { + filterQuery?: Maybe<string>; + + defaultIndex: string[]; + + timerange: TimerangeInput; + + stackByField: string; + + histogramType: HistogramType; +} export interface NetworkTopCountriesSourceArgs { id?: Maybe<string>; @@ -3330,16 +3283,12 @@ export namespace GetKpiNetworkQuery { export namespace GetMatrixHistogramQuery { export type Variables = { - isAlertsHistogram: boolean; - isAnomaliesHistogram: boolean; - isAuthenticationsHistogram: boolean; - isDnsHistogram: boolean; defaultIndex: string[]; - isEventsHistogram: boolean; filterQuery?: Maybe<string>; + histogramType: HistogramType; inspect: boolean; sourceId: string; - stackByField?: Maybe<string>; + stackByField: string; timerange: TimerangeInput; }; @@ -3354,19 +3303,11 @@ export namespace GetMatrixHistogramQuery { id: string; - AlertsHistogram: AlertsHistogram; - - AnomaliesHistogram: AnomaliesHistogram; - - AuthenticationsHistogram: AuthenticationsHistogram; - - EventsHistogram: EventsHistogram; - - NetworkDnsHistogram: NetworkDnsHistogram; + MatrixHistogram: MatrixHistogram; }; - export type AlertsHistogram = { - __typename?: 'AlertsOverTimeData'; + export type MatrixHistogram = { + __typename?: 'MatrixHistogramOverTimeData'; matrixHistogramData: MatrixHistogramData[]; @@ -3378,11 +3319,11 @@ export namespace GetMatrixHistogramQuery { export type MatrixHistogramData = { __typename?: 'MatrixOverTimeHistogramData'; - x: number; + x: Maybe<number>; - y: number; + y: Maybe<number>; - g: string; + g: Maybe<string>; }; export type Inspect = { @@ -3392,118 +3333,6 @@ export namespace GetMatrixHistogramQuery { response: string[]; }; - - export type AnomaliesHistogram = { - __typename?: 'AnomaliesOverTimeData'; - - matrixHistogramData: _MatrixHistogramData[]; - - totalCount: number; - - inspect: Maybe<_Inspect>; - }; - - export type _MatrixHistogramData = { - __typename?: 'MatrixOverTimeHistogramData'; - - x: number; - - y: number; - - g: string; - }; - - export type _Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; - - export type AuthenticationsHistogram = { - __typename?: 'AuthenticationsOverTimeData'; - - matrixHistogramData: __MatrixHistogramData[]; - - totalCount: number; - - inspect: Maybe<__Inspect>; - }; - - export type __MatrixHistogramData = { - __typename?: 'MatrixOverTimeHistogramData'; - - x: number; - - y: number; - - g: string; - }; - - export type __Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; - - export type EventsHistogram = { - __typename?: 'EventsOverTimeData'; - - matrixHistogramData: ___MatrixHistogramData[]; - - totalCount: number; - - inspect: Maybe<___Inspect>; - }; - - export type ___MatrixHistogramData = { - __typename?: 'MatrixOverTimeHistogramData'; - - x: number; - - y: number; - - g: string; - }; - - export type ___Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; - - export type NetworkDnsHistogram = { - __typename?: 'NetworkDsOverTimeData'; - - matrixHistogramData: ____MatrixHistogramData[]; - - totalCount: number; - - inspect: Maybe<____Inspect>; - }; - - export type ____MatrixHistogramData = { - __typename?: 'MatrixOverTimeHistogramData'; - - x: number; - - y: number; - - g: string; - }; - - export type ____Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; } export namespace GetNetworkDnsQuery { diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx new file mode 100644 index 0000000000000..1206ec950deed --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EuiButton, EuiFlexGroup } from '@elastic/eui'; +import { HeaderPage } from '../../components/header_page'; +import { WrapperPage } from '../../components/wrapper_page'; +import { AllCases } from './components/all_cases'; +import { SpyRoute } from '../../utils/route/spy_routes'; +import * as i18n from './translations'; +import { getCreateCaseUrl } from '../../components/link_to'; + +const badgeOptions = { + beta: true, + text: i18n.PAGE_BADGE_LABEL, + tooltip: i18n.PAGE_BADGE_TOOLTIP, +}; + +export const CasesPage = React.memo(() => ( + <> + <WrapperPage> + <HeaderPage badgeOptions={badgeOptions} subtitle={i18n.PAGE_SUBTITLE} title={i18n.PAGE_TITLE}> + <EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap={true}> + <EuiButton fill href={getCreateCaseUrl()} iconType="plusInCircle"> + {i18n.CREATE_TITLE} + </EuiButton> + </EuiFlexGroup> + </HeaderPage> + <AllCases /> + </WrapperPage> + <SpyRoute /> + </> +)); + +CasesPage.displayName = 'CasesPage'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx new file mode 100644 index 0000000000000..890df91c8560e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx @@ -0,0 +1,26 @@ +/* + * 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 { useParams } from 'react-router-dom'; + +import { CaseView } from './components/case_view'; +import { SpyRoute } from '../../utils/route/spy_routes'; + +export const CaseDetailsPage = React.memo(() => { + const { detailName: caseId } = useParams(); + if (!caseId) { + return null; + } + return ( + <> + <CaseView caseId={caseId} /> + <SpyRoute /> + </> + ); +}); + +CaseDetailsPage.displayName = 'CaseDetailsPage'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx new file mode 100644 index 0000000000000..92cd16fd2000e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { EuiBadge, EuiTableFieldDataColumnType, EuiTableComputedColumnType } from '@elastic/eui'; +import { getEmptyTagValue } from '../../../../components/empty_value'; +import { Case } from '../../../../containers/case/types'; +import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; +import { CaseDetailsLink } from '../../../../components/links'; +import { TruncatableText } from '../../../../components/truncatable_text'; +import * as i18n from './translations'; + +export type CasesColumns = EuiTableFieldDataColumnType<Case> | EuiTableComputedColumnType<Case>; + +const renderStringField = (field: string) => (field != null ? field : getEmptyTagValue()); + +export const getCasesColumns = (): CasesColumns[] => [ + { + name: i18n.CASE_TITLE, + render: (theCase: Case) => { + if (theCase.case_id != null && theCase.title != null) { + return <CaseDetailsLink detailName={theCase.case_id}>{theCase.title}</CaseDetailsLink>; + } + return getEmptyTagValue(); + }, + }, + { + field: 'tags', + name: i18n.TAGS, + render: (tags: Case['tags']) => { + if (tags != null && tags.length > 0) { + return ( + <TruncatableText> + {tags.map((tag: string, i: number) => ( + <EuiBadge color="hollow" key={`${tag}-${i}`}> + {tag} + </EuiBadge> + ))} + </TruncatableText> + ); + } + return getEmptyTagValue(); + }, + truncateText: true, + }, + { + field: 'created_at', + name: i18n.CREATED_AT, + sortable: true, + render: (createdAt: Case['created_at']) => { + if (createdAt != null) { + return <FormattedRelativePreferenceDate value={createdAt} />; + } + return getEmptyTagValue(); + }, + }, + { + field: 'created_by.username', + name: i18n.REPORTER, + render: (createdBy: Case['created_by']['username']) => renderStringField(createdBy), + }, + { + field: 'updated_at', + name: i18n.LAST_UPDATED, + sortable: true, + render: (updatedAt: Case['updated_at']) => { + if (updatedAt != null) { + return <FormattedRelativePreferenceDate value={updatedAt} />; + } + return getEmptyTagValue(); + }, + }, + { + field: 'state', + name: i18n.STATE, + sortable: true, + render: (state: Case['state']) => renderStringField(state), + }, +]; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx new file mode 100644 index 0000000000000..b1dd39c95e191 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -0,0 +1,147 @@ +/* + * 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, { useCallback, useMemo } from 'react'; +import { + EuiBasicTable, + EuiButton, + EuiEmptyPrompt, + EuiLoadingContent, + EuiTableSortingType, +} from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import * as i18n from './translations'; + +import { getCasesColumns } from './columns'; +import { SortFieldCase, Case, FilterOptions } from '../../../../containers/case/types'; + +import { Direction } from '../../../../graphql/types'; +import { useGetCases } from '../../../../containers/case/use_get_cases'; +import { EuiBasicTableOnChange } from '../../../detection_engine/rules/types'; +import { Panel } from '../../../../components/panel'; +import { HeaderSection } from '../../../../components/header_section'; +import { CasesTableFilters } from './table_filters'; + +import { + UtilityBar, + UtilityBarGroup, + UtilityBarSection, + UtilityBarText, +} from '../../../../components/detection_engine/utility_bar'; +import { getCreateCaseUrl } from '../../../../components/link_to'; + +export const AllCases = React.memo(() => { + const [ + { data, isLoading, queryParams, filterOptions }, + setQueryParams, + setFilters, + ] = useGetCases(); + + const tableOnChangeCallback = useCallback( + ({ page, sort }: EuiBasicTableOnChange) => { + let newQueryParams = queryParams; + if (sort) { + let newSort; + switch (sort.field) { + case 'state': + newSort = SortFieldCase.state; + break; + case 'created_at': + newSort = SortFieldCase.createdAt; + break; + case 'updated_at': + newSort = SortFieldCase.updatedAt; + break; + default: + newSort = SortFieldCase.createdAt; + } + newQueryParams = { + ...newQueryParams, + sortField: newSort, + sortOrder: sort.direction as Direction, + }; + } + if (page) { + newQueryParams = { + ...newQueryParams, + page: page.index + 1, + perPage: page.size, + }; + } + setQueryParams(newQueryParams); + }, + [setQueryParams, queryParams] + ); + + const onFilterChangedCallback = useCallback( + (newFilterOptions: Partial<FilterOptions>) => { + setFilters({ ...filterOptions, ...newFilterOptions }); + }, + [filterOptions, setFilters] + ); + + const memoizedGetCasesColumns = useMemo(() => getCasesColumns(), []); + const memoizedPagination = useMemo( + () => ({ + pageIndex: queryParams.page - 1, + pageSize: queryParams.perPage, + totalItemCount: data.total, + pageSizeOptions: [5, 10, 20, 50, 100, 200, 300], + }), + [data, queryParams] + ); + + const sorting: EuiTableSortingType<Case> = { + sort: { field: queryParams.sortField, direction: queryParams.sortOrder }, + }; + + return ( + <Panel loading={isLoading}> + <HeaderSection split title={i18n.ALL_CASES}> + <CasesTableFilters + onFilterChanged={onFilterChangedCallback} + initial={{ search: filterOptions.search, tags: filterOptions.tags }} + /> + </HeaderSection> + {isLoading && isEmpty(data.cases) && ( + <EuiLoadingContent data-test-subj="initialLoadingPanelAllCases" lines={10} /> + )} + {!isLoading && !isEmpty(data.cases) && ( + <> + <UtilityBar border> + <UtilityBarSection> + <UtilityBarGroup> + <UtilityBarText>{i18n.SHOWING_CASES(data.total ?? 0)}</UtilityBarText> + </UtilityBarGroup> + </UtilityBarSection> + </UtilityBar> + <EuiBasicTable + columns={memoizedGetCasesColumns} + itemId="id" + items={data.cases} + noItemsMessage={ + <EuiEmptyPrompt + title={<h3>{i18n.NO_CASES}</h3>} + titleSize="xs" + body={i18n.NO_CASES_BODY} + actions={ + <EuiButton fill size="s" href={getCreateCaseUrl()} iconType="plusInCircle"> + {i18n.ADD_NEW_CASE} + </EuiButton> + } + /> + } + onChange={tableOnChangeCallback} + pagination={memoizedPagination} + sorting={sorting} + /> + </> + )} + </Panel> + ); +}); + +AllCases.displayName = 'AllCases'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx new file mode 100644 index 0000000000000..e593623788046 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx @@ -0,0 +1,90 @@ +/* + * 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, { useCallback, useState } from 'react'; +import { isEqual } from 'lodash/fp'; +import { EuiFieldSearch, EuiFilterGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import * as i18n from './translations'; + +import { FilterOptions } from '../../../../containers/case/types'; +import { useGetTags } from '../../../../containers/case/use_get_tags'; +import { TagsFilterPopover } from '../../../../pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover'; + +interface Initial { + search: string; + tags: string[]; +} +interface CasesTableFiltersProps { + onFilterChanged: (filterOptions: Partial<FilterOptions>) => void; + initial: Initial; +} + +/** + * Collection of filters for filtering data within the CasesTable. Contains search bar, + * and tag selection + * + * @param onFilterChanged change listener to be notified on filter changes + */ + +const CasesTableFiltersComponent = ({ + onFilterChanged, + initial = { search: '', tags: [] }, +}: CasesTableFiltersProps) => { + const [search, setSearch] = useState(initial.search); + const [selectedTags, setSelectedTags] = useState(initial.tags); + const [{ isLoading, data }] = useGetTags(); + + const handleSelectedTags = useCallback( + newTags => { + if (!isEqual(newTags, selectedTags)) { + setSelectedTags(newTags); + onFilterChanged({ search, tags: newTags }); + } + }, + [search, selectedTags] + ); + const handleOnSearch = useCallback( + newSearch => { + const trimSearch = newSearch.trim(); + if (!isEqual(trimSearch, search)) { + setSearch(trimSearch); + onFilterChanged({ tags: selectedTags, search: trimSearch }); + } + }, + [search, selectedTags] + ); + + return ( + <EuiFlexGroup gutterSize="m" justifyContent="flexEnd"> + <EuiFlexItem grow={true}> + <EuiFieldSearch + aria-label={i18n.SEARCH_CASES} + fullWidth + incremental={false} + placeholder={i18n.SEARCH_PLACEHOLDER} + onSearch={handleOnSearch} + /> + </EuiFlexItem> + + <EuiFlexItem grow={false}> + <EuiFilterGroup> + <TagsFilterPopover + isLoading={isLoading} + onSelectedTagsChanged={handleSelectedTags} + selectedTags={selectedTags} + tags={data} + /> + </EuiFilterGroup> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; + +CasesTableFiltersComponent.displayName = 'CasesTableFiltersComponent'; + +export const CasesTableFilters = React.memo(CasesTableFiltersComponent); + +CasesTableFilters.displayName = 'CasesTableFilters'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts new file mode 100644 index 0000000000000..ab8e22ebcf1be --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export * from '../../translations'; + +export const ALL_CASES = i18n.translate('xpack.siem.case.caseTable.title', { + defaultMessage: 'All Cases', +}); +export const NO_CASES = i18n.translate('xpack.siem.case.caseTable.noCases.title', { + defaultMessage: 'No Cases', +}); +export const NO_CASES_BODY = i18n.translate('xpack.siem.case.caseTable.noCases.body', { + defaultMessage: 'Create a new case to see it displayed in the case workflow table.', +}); +export const ADD_NEW_CASE = i18n.translate('xpack.siem.case.caseTable.addNewCase', { + defaultMessage: 'Add New Case', +}); + +export const SHOWING_CASES = (totalRules: number) => + i18n.translate('xpack.siem.case.caseTable.showingCasesTitle', { + values: { totalRules }, + defaultMessage: 'Showing {totalRules} {totalRules, plural, =1 {case} other {cases}}', + }); + +export const UNIT = (totalCount: number) => + i18n.translate('xpack.siem.case.caseTable.unit', { + values: { totalCount }, + defaultMessage: `{totalCount, plural, =1 {case} other {cases}}`, + }); + +export const SEARCH_CASES = i18n.translate( + 'xpack.siem.detectionEngine.case.caseTable.searchAriaLabel', + { + defaultMessage: 'Search cases', + } +); + +export const SEARCH_PLACEHOLDER = i18n.translate( + 'xpack.siem.detectionEngine.case.caseTable.searchPlaceholder', + { + defaultMessage: 'e.g. case name', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx new file mode 100644 index 0000000000000..4f43a6edeeac6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -0,0 +1,312 @@ +/* + * 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, { useCallback, useEffect, useState } from 'react'; +import { + EuiBadge, + EuiButton, + EuiButtonEmpty, + EuiButtonToggle, + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, +} from '@elastic/eui'; + +import styled, { css } from 'styled-components'; +import * as i18n from './translations'; +import { DescriptionMarkdown } from '../description_md_editor'; +import { Case } from '../../../../containers/case/types'; +import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; +import { getCaseUrl } from '../../../../components/link_to'; +import { HeaderPage } from '../../../../components/header_page_new'; +import { Markdown } from '../../../../components/markdown'; +import { PropertyActions } from '../property_actions'; +import { TagList } from '../tag_list'; +import { useGetCase } from '../../../../containers/case/use_get_case'; +import { UserActionTree } from '../user_action_tree'; +import { UserList } from '../user_list'; +import { useUpdateCase } from '../../../../containers/case/use_update_case'; +import { WrapperPage } from '../../../../components/wrapper_page'; + +interface Props { + caseId: string; +} + +const MyDescriptionList = styled(EuiDescriptionList)` + ${({ theme }) => css` + & { + padding-right: ${theme.eui.euiSizeL}; + border-right: ${theme.eui.euiBorderThin}; + } + `} +`; + +const MyWrapper = styled(WrapperPage)` + padding-bottom: 0; +`; +const BackgroundWrapper = styled.div` + ${({ theme }) => css` + background-color: ${theme.eui.euiColorEmptyShade}; + border-top: ${theme.eui.euiBorderThin}; + height: 100%; + `} +`; + +interface CasesProps { + caseId: string; + initialData: Case; + isLoading: boolean; +} + +export const Cases = React.memo<CasesProps>(({ caseId, initialData, isLoading }) => { + const [{ data }, dispatchUpdateCaseProperty] = useUpdateCase(caseId, initialData); + const [isEditDescription, setIsEditDescription] = useState(false); + const [isEditTitle, setIsEditTitle] = useState(false); + const [isEditTags, setIsEditTags] = useState(false); + const [isCaseOpen, setIsCaseOpen] = useState(data.state === 'open'); + const [description, setDescription] = useState(data.description); + const [title, setTitle] = useState(data.title); + const [tags, setTags] = useState(data.tags); + + const onUpdateField = useCallback( + async (updateKey: keyof Case, updateValue: string | string[]) => { + switch (updateKey) { + case 'title': + if (updateValue.length > 0) { + dispatchUpdateCaseProperty({ + updateKey: 'title', + updateValue, + }); + setIsEditTitle(false); + } + break; + case 'description': + if (updateValue.length > 0) { + dispatchUpdateCaseProperty({ + updateKey: 'description', + updateValue, + }); + setIsEditDescription(false); + } + break; + case 'tags': + setTags(updateValue as string[]); + if (updateValue.length > 0) { + dispatchUpdateCaseProperty({ + updateKey: 'tags', + updateValue, + }); + setIsEditTags(false); + } + break; + default: + return null; + } + }, + [dispatchUpdateCaseProperty, title] + ); + + const onSetIsCaseOpen = useCallback(() => setIsCaseOpen(!isCaseOpen), [ + isCaseOpen, + setIsCaseOpen, + ]); + + useEffect(() => { + const caseState = isCaseOpen ? 'open' : 'closed'; + if (data.state !== caseState) { + dispatchUpdateCaseProperty({ + updateKey: 'state', + updateValue: caseState, + }); + } + }, [isCaseOpen]); + + // TO DO refactor each of these const's into their own components + const propertyActions = [ + { + iconType: 'documentEdit', + label: 'Edit description', + onClick: () => setIsEditDescription(true), + }, + { + iconType: 'securitySignalResolved', + label: 'Close case', + onClick: () => null, + }, + { + iconType: 'trash', + label: 'Delete case', + onClick: () => null, + }, + { + iconType: 'importAction', + label: 'Push as ServiceNow incident', + onClick: () => null, + }, + { + iconType: 'popout', + label: 'View ServiceNow incident', + onClick: () => null, + }, + { + iconType: 'importAction', + label: 'Update ServiceNow incident', + onClick: () => null, + }, + ]; + const userActions = [ + { + avatarName: data.created_by.username, + title: ( + <EuiFlexGroup alignItems="baseline" gutterSize="none" justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <p> + <strong>{`${data.created_by.username}`}</strong> + {` ${i18n.ADDED_DESCRIPTION} `}{' '} + <FormattedRelativePreferenceDate value={data.created_at} /> + {/* STEPH FIX come back and add label `on` */} + </p> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <PropertyActions propertyActions={propertyActions} /> + </EuiFlexItem> + </EuiFlexGroup> + ), + children: isEditDescription ? ( + <> + <DescriptionMarkdown + descriptionInputHeight={200} + initialDescription={data.description} + isLoading={isLoading} + onChange={updatedDescription => setDescription(updatedDescription)} + /> + + <EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap={true}> + <EuiFlexItem grow={false}> + <EuiButton + fill + isDisabled={isLoading} + isLoading={isLoading} + onClick={() => onUpdateField('description', description)} + > + {i18n.SUBMIT} + </EuiButton> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonEmpty onClick={() => setIsEditDescription(false)}> + {i18n.CANCEL} + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + </> + ) : ( + <Markdown raw={data.description} /> + ), + }, + ]; + return ( + <> + <MyWrapper> + <HeaderPage + backOptions={{ + href: getCaseUrl(), + text: i18n.BACK_TO_ALL, + }} + iconAction={{ + 'aria-label': title, + iconType: 'pencil', + onChange: newTitle => setTitle(newTitle), + onSubmit: () => onUpdateField('title', title), + onClick: isEdit => setIsEditTitle(isEdit), + }} + isEditTitle={isEditTitle} + title={title} + > + <EuiFlexGroup gutterSize="l" justifyContent="flexEnd"> + <EuiFlexItem grow={false}> + <MyDescriptionList compressed> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiDescriptionListTitle>{i18n.STATUS}</EuiDescriptionListTitle> + <EuiDescriptionListDescription> + <EuiBadge color={isCaseOpen ? 'secondary' : 'danger'}>{data.state}</EuiBadge> + </EuiDescriptionListDescription> + </EuiFlexItem> + <EuiFlexItem> + <EuiDescriptionListTitle>{i18n.CASE_OPENED}</EuiDescriptionListTitle> + <EuiDescriptionListDescription> + <FormattedRelativePreferenceDate value={data.created_at} /> + </EuiDescriptionListDescription> + </EuiFlexItem> + </EuiFlexGroup> + </MyDescriptionList> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFlexGroup gutterSize="l" alignItems="center"> + <EuiFlexItem> + <EuiButtonToggle + label={isCaseOpen ? 'Close case' : 'Reopen case'} + iconType={isCaseOpen ? 'checkInCircleFilled' : 'magnet'} + onChange={onSetIsCaseOpen} + isSelected={isCaseOpen} + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <PropertyActions propertyActions={propertyActions} /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + </HeaderPage> + </MyWrapper> + <BackgroundWrapper> + <MyWrapper> + <EuiFlexGroup> + <EuiFlexItem grow={6}> + <UserActionTree userActions={userActions} /> + </EuiFlexItem> + <EuiFlexItem grow={2}> + <UserList headline={i18n.REPORTER} users={[data.created_by]} /> + <TagList + tags={tags} + iconAction={{ + 'aria-label': title, + iconType: 'pencil', + onSubmit: newTags => onUpdateField('tags', newTags), + onClick: isEdit => setIsEditTags(isEdit), + }} + isEditTags={isEditTags} + /> + </EuiFlexItem> + </EuiFlexGroup> + </MyWrapper> + </BackgroundWrapper> + </> + ); +}); + +export const CaseView = React.memo(({ caseId }: Props) => { + const [{ data, isLoading, isError }] = useGetCase(caseId); + if (isError) { + return null; + } + if (isLoading) { + return ( + <EuiFlexGroup justifyContent="center" alignItems="center"> + <EuiFlexItem grow={false}> + <EuiLoadingSpinner size="xl" /> + </EuiFlexItem> + </EuiFlexGroup> + ); + } + + return <Cases caseId={caseId} initialData={data} isLoading={isLoading} />; +}); + +CaseView.displayName = 'CaseView'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts new file mode 100644 index 0000000000000..f45c52533d2e7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export * from '../../translations'; + +export const SHOWING_CASES = (actionDate: string, actionName: string, userName: string) => + i18n.translate('xpack.siem.case.caseView.actionHeadline', { + values: { + actionDate, + actionName, + userName, + }, + defaultMessage: '{userName} {actionName} on {actionDate}', + }); + +export const ADDED_DESCRIPTION = i18n.translate( + 'xpack.siem.case.caseView.actionLabel.addDescription', + { + defaultMessage: 'added description', + } +); + +export const EDITED_DESCRIPTION = i18n.translate( + 'xpack.siem.case.caseView.actionLabel.editDescription', + { + defaultMessage: 'edited description', + } +); + +export const ADDED_COMMENT = i18n.translate('xpack.siem.case.caseView.actionLabel.addComment', { + defaultMessage: 'added comment', +}); + +export const STATUS = i18n.translate('xpack.siem.case.caseView.statusLabel', { + defaultMessage: 'Status', +}); + +export const CASE_OPENED = i18n.translate('xpack.siem.case.caseView.caseOpened', { + defaultMessage: 'Case opened', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/form_options.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/create/form_options.ts new file mode 100644 index 0000000000000..7bc43e23a72c5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/form_options.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const stateOptions = [ + { + value: 'open', + inputDisplay: 'Open', + }, + { + value: 'closed', + inputDisplay: 'Closed', + }, +]; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx new file mode 100644 index 0000000000000..9fd1525003b0b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useCallback } from 'react'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiLoadingSpinner, + EuiPanel, +} from '@elastic/eui'; +import styled from 'styled-components'; +import { Redirect } from 'react-router-dom'; +import { Field, Form, getUseField, useForm } from '../../../shared_imports'; +import { NewCase } from '../../../../containers/case/types'; +import { usePostCase } from '../../../../containers/case/use_post_case'; +import { schema } from './schema'; +import * as i18n from '../../translations'; +import { SiemPageName } from '../../../home/types'; +import { DescriptionMarkdown } from '../description_md_editor'; + +export const CommonUseField = getUseField({ component: Field }); + +const TagContainer = styled.div` + margin-top: 16px; +`; +const MySpinner = styled(EuiLoadingSpinner)` + position: absolute; + top: 50%; + left: 50%; +`; + +export const Create = React.memo(() => { + const [{ data, isLoading, newCase }, setFormData] = usePostCase(); + const { form } = useForm({ + defaultValue: data, + options: { stripEmptyFields: false }, + schema, + }); + + const onSubmit = useCallback(async () => { + const { isValid, data: newData } = await form.submit(); + if (isValid) { + setFormData({ ...newData, isNew: true } as NewCase); + } + }, [form]); + + if (newCase && newCase.case_id) { + return <Redirect to={`/${SiemPageName.case}/${newCase.case_id}`} />; + } + return ( + <EuiPanel> + {isLoading && <MySpinner size="xl" />} + <Form form={form}> + <CommonUseField + path="title" + componentProps={{ + idAria: 'caseTitle', + 'data-test-subj': 'caseTitle', + euiFieldProps: { + fullWidth: false, + }, + isDisabled: isLoading, + }} + /> + <DescriptionMarkdown + descriptionInputHeight={200} + formHook={true} + initialDescription={data.description} + isLoading={isLoading} + onChange={description => setFormData({ ...data, description })} + /> + <TagContainer> + <CommonUseField + path="tags" + componentProps={{ + idAria: 'caseTags', + 'data-test-subj': 'caseTags', + euiFieldProps: { + fullWidth: true, + placeholder: '', + }, + isDisabled: isLoading, + }} + /> + </TagContainer> + </Form> + <> + <EuiHorizontalRule margin="m" /> + <EuiFlexGroup + alignItems="center" + justifyContent="flexEnd" + gutterSize="xs" + responsive={false} + > + <EuiFlexItem grow={false}> + <EuiButton fill isDisabled={isLoading} isLoading={isLoading} onClick={onSubmit}> + {i18n.SUBMIT} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </> + </EuiPanel> + ); +}); + +Create.displayName = 'Create'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/optional_field_label/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/optional_field_label/index.tsx new file mode 100644 index 0000000000000..b86198e09ceac --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/optional_field_label/index.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiText } from '@elastic/eui'; +import React from 'react'; + +import * as i18n from '../../../translations'; + +export const OptionalFieldLabel = ( + <EuiText color="subdued" size="xs"> + {i18n.OPTIONAL} + </EuiText> +); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx new file mode 100644 index 0000000000000..1b5df72a6671c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../shared_imports'; +import { OptionalFieldLabel } from './optional_field_label'; +import * as i18n from '../../translations'; + +const { emptyField } = fieldValidators; + +export const schema: FormSchema = { + title: { + type: FIELD_TYPES.TEXT, + label: i18n.CASE_TITLE, + validations: [ + { + validator: emptyField(i18n.TITLE_REQUIRED), + }, + ], + }, + description: { + type: FIELD_TYPES.TEXTAREA, + validations: [ + { + validator: emptyField(i18n.DESCRIPTION_REQUIRED), + }, + ], + }, + tags: { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.TAGS, + helpText: i18n.TAGS_HELP, + labelAppend: OptionalFieldLabel, + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx new file mode 100644 index 0000000000000..44062a5a1d589 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/description_md_editor/index.tsx @@ -0,0 +1,111 @@ +/* + * 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 { EuiFlexItem, EuiPanel, EuiTabbedContent, EuiTextArea } from '@elastic/eui'; +import React, { useState } from 'react'; +import styled from 'styled-components'; + +import { Markdown } from '../../../../components/markdown'; +import * as i18n from '../../translations'; +import { MarkdownHint } from '../../../../components/markdown/markdown_hint'; +import { CommonUseField } from '../create'; + +const TextArea = styled(EuiTextArea)<{ height: number }>` + min-height: ${({ height }) => `${height}px`}; + width: 100%; +`; + +TextArea.displayName = 'TextArea'; + +const DescriptionContainer = styled.div` + margin-top: 15px; + margin-bottom: 15px; +`; + +const DescriptionMarkdownTabs = styled(EuiTabbedContent)` + width: 100%; +`; + +DescriptionMarkdownTabs.displayName = 'DescriptionMarkdownTabs'; + +const MarkdownContainer = styled(EuiPanel)<{ height: number }>` + height: ${({ height }) => height}px; + overflow: auto; +`; + +MarkdownContainer.displayName = 'MarkdownContainer'; + +/** An input for entering a new case description */ +export const DescriptionMarkdown = React.memo<{ + descriptionInputHeight: number; + initialDescription: string; + isLoading: boolean; + formHook?: boolean; + onChange: (description: string) => void; +}>(({ initialDescription, isLoading, descriptionInputHeight, onChange, formHook = false }) => { + const [description, setDescription] = useState(initialDescription); + const tabs = [ + { + id: 'description', + name: i18n.DESCRIPTION, + content: formHook ? ( + <CommonUseField + path="description" + onChange={e => { + setDescription(e as string); + onChange(e as string); + }} + componentProps={{ + idAria: 'caseDescription', + 'data-test-subj': 'caseDescription', + isDisabled: isLoading, + spellcheck: false, + }} + /> + ) : ( + <TextArea + onChange={e => { + setDescription(e.target.value); + onChange(e.target.value); + }} + fullWidth={true} + height={descriptionInputHeight} + aria-label={i18n.DESCRIPTION} + disabled={isLoading} + spellCheck={false} + value={description} + /> + ), + }, + { + id: 'preview', + name: i18n.PREVIEW, + content: ( + <MarkdownContainer + data-test-subj="markdown-container" + height={descriptionInputHeight} + paddingSize="s" + > + <Markdown raw={description} /> + </MarkdownContainer> + ), + }, + ]; + return ( + <DescriptionContainer> + <DescriptionMarkdownTabs + data-test-subj="new-description-tabs" + tabs={tabs} + initialSelectedTab={tabs[0]} + /> + <EuiFlexItem grow={true}> + <MarkdownHint show={description.trim().length > 0} /> + </EuiFlexItem> + </DescriptionContainer> + ); +}); + +DescriptionMarkdown.displayName = 'DescriptionMarkdown'; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/index.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/constants.ts similarity index 86% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/index.ts rename to x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/constants.ts index f75dce9b7507f..14e4b46eb83f0 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/index.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/constants.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './plugin'; +export const SET_STATE = 'SET_STATE'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/index.tsx new file mode 100644 index 0000000000000..7fe5b6f5f8794 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/index.tsx @@ -0,0 +1,84 @@ +/* + * 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, { useCallback, useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPopover, EuiButtonIcon, EuiButtonEmpty } from '@elastic/eui'; + +export interface PropertyActionButtonProps { + onClick: () => void; + iconType: string; + label: string; +} + +const PropertyActionButton = React.memo<PropertyActionButtonProps>( + ({ onClick, iconType, label }) => ( + <EuiButtonEmpty + aria-label={label} + color="text" + iconSide="left" + iconType={iconType} + onClick={onClick} + > + {label} + </EuiButtonEmpty> + ) +); + +PropertyActionButton.displayName = 'PropertyActionButton'; + +export interface PropertyActionsProps { + propertyActions: PropertyActionButtonProps[]; +} + +export const PropertyActions = React.memo<PropertyActionsProps>(({ propertyActions }) => { + const [showActions, setShowActions] = useState(false); + + const onButtonClick = useCallback(() => { + setShowActions(!showActions); + }, [showActions]); + + const onClosePopover = useCallback((cb?: () => void) => { + setShowActions(false); + if (cb) { + cb(); + } + }, []); + + return ( + <EuiFlexGroup alignItems="flexStart" data-test-subj="properties-right" gutterSize="none"> + <EuiFlexItem grow={false}> + <EuiPopover + anchorPosition="downRight" + button={ + <EuiButtonIcon + data-test-subj="ellipses" + aria-label="Actions" + iconType="boxesHorizontal" + onClick={onButtonClick} + /> + } + id="settingsPopover" + isOpen={showActions} + closePopover={onClosePopover} + > + <EuiFlexGroup alignItems="flexStart" direction="column" gutterSize="none"> + {propertyActions.map((action, key) => ( + <EuiFlexItem grow={false} key={`${action.label}${key}`}> + <PropertyActionButton + iconType={action.iconType} + label={action.label} + onClick={() => onClosePopover(action.onClick)} + /> + </EuiFlexItem> + ))} + </EuiFlexGroup> + </EuiPopover> + </EuiFlexItem> + </EuiFlexGroup> + ); +}); + +PropertyActions.displayName = 'PropertyActions'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx new file mode 100644 index 0000000000000..6634672cb6a77 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx @@ -0,0 +1,125 @@ +/* + * 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, { useCallback } from 'react'; +import { + EuiText, + EuiHorizontalRule, + EuiFlexGroup, + EuiFlexItem, + EuiBadge, + EuiButton, + EuiButtonEmpty, + EuiButtonIcon, +} from '@elastic/eui'; +import styled, { css } from 'styled-components'; +import * as i18n from '../../translations'; +import { Form, useForm } from '../../../shared_imports'; +import { schema } from './schema'; +import { CommonUseField } from '../create'; + +interface IconAction { + 'aria-label': string; + iconType: string; + onClick: (b: boolean) => void; + onSubmit: (a: string[]) => void; +} + +interface TagListProps { + tags: string[]; + iconAction?: IconAction; + isEditTags?: boolean; +} + +const MyFlexGroup = styled(EuiFlexGroup)` + ${({ theme }) => css` + margin-top: ${theme.eui.euiSizeM}; + p { + font-size: ${theme.eui.euiSizeM}; + } + `} +`; + +export const TagList = React.memo(({ tags, isEditTags, iconAction }: TagListProps) => { + const { form } = useForm({ + defaultValue: { tags }, + options: { stripEmptyFields: false }, + schema, + }); + + const onSubmit = useCallback(async () => { + const { isValid, data: newData } = await form.submit(); + if (isValid && iconAction) { + iconAction.onSubmit(newData.tags); + iconAction.onClick(false); + } + }, [form]); + + const onActionClick = useCallback( + (cb: (b: boolean) => void, onClickBool: boolean) => cb(onClickBool), + [iconAction] + ); + return ( + <EuiText> + <EuiFlexGroup alignItems="center" gutterSize="xs" justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <h4>{i18n.TAGS}</h4> + </EuiFlexItem> + {iconAction && ( + <EuiFlexItem grow={false}> + <EuiButtonIcon + aria-label={iconAction['aria-label']} + iconType={iconAction.iconType} + onClick={() => onActionClick(iconAction.onClick, true)} + /> + </EuiFlexItem> + )} + </EuiFlexGroup> + <EuiHorizontalRule margin="xs" /> + <MyFlexGroup gutterSize="xs"> + {tags.length === 0 && !isEditTags && <p>{i18n.NO_TAGS}</p>} + {tags.length > 0 && + !isEditTags && + tags.map((tag, key) => ( + <EuiFlexItem grow={false} key={`${tag}${key}`}> + <EuiBadge color="hollow">{tag}</EuiBadge> + </EuiFlexItem> + ))} + {isEditTags && iconAction && ( + <EuiFlexGroup direction="column"> + <EuiFlexItem> + <Form form={form}> + <CommonUseField + path="tags" + componentProps={{ + idAria: 'caseTags', + 'data-test-subj': 'caseTags', + euiFieldProps: { + fullWidth: true, + placeholder: '', + }, + }} + /> + </Form> + </EuiFlexItem> + <EuiFlexItem> + <EuiButton fill onClick={onSubmit}> + {i18n.SUBMIT} + </EuiButton> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonEmpty onClick={() => onActionClick(iconAction.onClick, false)}> + {i18n.CANCEL} + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + )} + </MyFlexGroup> + </EuiText> + ); +}); + +TagList.displayName = 'TagList'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/schema.tsx new file mode 100644 index 0000000000000..dfc9c61cd5f0c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/schema.tsx @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FormSchema } from '../../../shared_imports'; +import { schema as createSchema } from '../create/schema'; + +export const schema: FormSchema = { + tags: createSchema.tags, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx new file mode 100644 index 0000000000000..8df98a4cef0e8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx @@ -0,0 +1,82 @@ +/* + * 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, { ReactNode } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiAvatar, EuiPanel, EuiText } from '@elastic/eui'; +import styled, { css } from 'styled-components'; + +export interface UserActionItem { + avatarName: string; + children?: ReactNode; + title: ReactNode; +} + +export interface UserActionTreeProps { + userActions: UserActionItem[]; +} + +const UserAction = styled(EuiFlexGroup)` + ${({ theme }) => css` + & { + background-image: linear-gradient( + to right, + transparent 0, + transparent 15px, + ${theme.eui.euiBorderColor} 15px, + ${theme.eui.euiBorderColor} 17px, + transparent 17px, + transparent 100% + ); + background-repeat: no-repeat; + background-position: left ${theme.eui.euiSizeXXL}; + margin-bottom: ${theme.eui.euiSizeS}; + } + .userAction__panel { + margin-bottom: ${theme.eui.euiSize}; + } + .userAction__circle { + flex-shrink: 0; + margin-right: ${theme.eui.euiSize}; + vertical-align: top; + } + .userAction__title { + padding: ${theme.eui.euiSizeS} ${theme.eui.euiSizeL}; + background: ${theme.eui.euiColorLightestShade}; + border-bottom: ${theme.eui.euiBorderThin}; + border-radius: ${theme.eui.euiBorderRadius} ${theme.eui.euiBorderRadius} 0 0; + } + .userAction__content { + padding: ${theme.eui.euiSizeM} ${theme.eui.euiSizeL}; + } + .euiText--small * { + margin-bottom: 0; + } + `} +`; + +const renderUserActions = (userActions: UserActionItem[]) => { + return userActions.map(({ avatarName, children, title }, key) => ( + <UserAction key={key} gutterSize={'none'}> + <EuiFlexItem grow={false}> + <EuiAvatar className="userAction__circle" name={avatarName} /> + </EuiFlexItem> + <EuiFlexItem> + <EuiPanel className="userAction__panel" paddingSize="none"> + <EuiText size="s" className="userAction__title"> + {title} + </EuiText> + {children && <div className="userAction__content">{children}</div>} + </EuiPanel> + </EuiFlexItem> + </UserAction> + )); +}; + +export const UserActionTree = React.memo(({ userActions }: UserActionTreeProps) => ( + <div>{renderUserActions(userActions)}</div> +)); + +UserActionTree.displayName = 'UserActionTree'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx new file mode 100644 index 0000000000000..b80ee58f8abbf --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { + EuiButtonIcon, + EuiText, + EuiHorizontalRule, + EuiAvatar, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import styled, { css } from 'styled-components'; +import { ElasticUser } from '../../../../containers/case/types'; + +interface UserListProps { + headline: string; + users: ElasticUser[]; +} + +const MyAvatar = styled(EuiAvatar)` + top: -4px; +`; + +const MyFlexGroup = styled(EuiFlexGroup)` + ${({ theme }) => css` + margin-top: ${theme.eui.euiSizeM}; + `} +`; + +const renderUsers = (users: ElasticUser[]) => { + return users.map(({ username }, key) => ( + <MyFlexGroup key={key} justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <EuiFlexGroup gutterSize="xs"> + <EuiFlexItem> + <MyAvatar name={username} /> + </EuiFlexItem> + <EuiFlexItem> + <p> + <strong> + <small>{username}</small> + </strong> + </p> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonIcon + onClick={() => window.alert('Email clicked')} + iconType="email" + aria-label="email" + /> + </EuiFlexItem> + </MyFlexGroup> + )); +}; + +export const UserList = React.memo(({ headline, users }: UserListProps) => { + return ( + <EuiText> + <h4>{headline}</h4> + <EuiHorizontalRule margin="xs" /> + {renderUsers(users)} + </EuiText> + ); +}); + +UserList.displayName = 'UserList'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx new file mode 100644 index 0000000000000..9bc356517cc68 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { WrapperPage } from '../../components/wrapper_page'; +import { Create } from './components/create'; +import { SpyRoute } from '../../utils/route/spy_routes'; +import { HeaderPage } from '../../components/header_page'; +import * as i18n from './translations'; +import { getCaseUrl } from '../../components/link_to'; + +const backOptions = { + href: getCaseUrl(), + text: i18n.BACK_TO_ALL, +}; +const badgeOptions = { + beta: true, + text: i18n.PAGE_BADGE_LABEL, + tooltip: i18n.PAGE_BADGE_TOOLTIP, +}; +export const CreateCasePage = React.memo(() => ( + <> + <WrapperPage> + <HeaderPage backOptions={backOptions} badgeOptions={badgeOptions} title={i18n.CREATE_TITLE} /> + <Create /> + </WrapperPage> + <SpyRoute /> + </> +)); + +CreateCasePage.displayName = 'CreateCasePage'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/index.tsx new file mode 100644 index 0000000000000..9bd91b1c6d62d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/index.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { Route, Switch } from 'react-router-dom'; +import { SiemPageName } from '../home/types'; +import { CaseDetailsPage } from './case_details'; +import { CasesPage } from './case'; +import { CreateCasePage } from './create_case'; + +const casesPagePath = `/:pageName(${SiemPageName.case})`; +const caseDetailsPagePath = `${casesPagePath}/:detailName`; +const createCasePagePath = `${casesPagePath}/create`; + +const CaseContainerComponent: React.FC = () => ( + <Switch> + <Route strict exact path={casesPagePath}> + <CasesPage /> + </Route> + <Route strict exact path={createCasePagePath}> + <CreateCasePage /> + </Route> + <Route strict path={caseDetailsPagePath}> + <CaseDetailsPage /> + </Route> + </Switch> +); + +export const Case = React.memo(CaseContainerComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts new file mode 100644 index 0000000000000..4e878ba58411e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const BACK_TO_ALL = i18n.translate('xpack.siem.case.caseView.backLabel', { + defaultMessage: 'Back to cases', +}); + +export const CANCEL = i18n.translate('xpack.siem.case.caseView.cancel', { + defaultMessage: 'Cancel', +}); + +export const CASE_TITLE = i18n.translate('xpack.siem.case.caseView.caseTitle', { + defaultMessage: 'Case Title', +}); + +export const CREATED_AT = i18n.translate('xpack.siem.case.caseView.createdAt', { + defaultMessage: 'Created at', +}); + +export const REPORTER = i18n.translate('xpack.siem.case.caseView.createdBy', { + defaultMessage: 'Reporter', +}); + +export const CREATE_BC_TITLE = i18n.translate('xpack.siem.case.caseView.breadcrumb', { + defaultMessage: 'Create', +}); + +export const CREATE_TITLE = i18n.translate('xpack.siem.case.caseView.create', { + defaultMessage: 'Create new case', +}); + +export const DESCRIPTION = i18n.translate('xpack.siem.case.caseView.description', { + defaultMessage: 'Description', +}); + +export const DESCRIPTION_REQUIRED = i18n.translate( + 'xpack.siem.case.createCase.descriptionFieldRequiredError', + { + defaultMessage: 'A description is required.', + } +); + +export const EDIT = i18n.translate('xpack.siem.case.caseView.edit', { + defaultMessage: 'Edit', +}); + +export const OPTIONAL = i18n.translate('xpack.siem.case.caseView.optional', { + defaultMessage: 'Optional', +}); + +export const LAST_UPDATED = i18n.translate('xpack.siem.case.caseView.updatedAt', { + defaultMessage: 'Last updated', +}); + +export const PAGE_BADGE_LABEL = i18n.translate('xpack.siem.case.caseView.pageBadgeLabel', { + defaultMessage: 'Beta', +}); + +export const PAGE_BADGE_TOOLTIP = i18n.translate('xpack.siem.case.caseView.pageBadgeTooltip', { + defaultMessage: + 'Case Workflow is still in beta. Please help us improve by reporting issues or bugs in the Kibana repo.', +}); + +export const PAGE_SUBTITLE = i18n.translate('xpack.siem.case.caseView.pageSubtitle', { + defaultMessage: 'Case Workflow Management within the Elastic SIEM', +}); + +export const PAGE_TITLE = i18n.translate('xpack.siem.case.pageTitle', { + defaultMessage: 'Case Workflows', +}); + +export const PREVIEW = i18n.translate('xpack.siem.case.caseView.preview', { + defaultMessage: 'Preview', +}); + +export const STATE = i18n.translate('xpack.siem.case.caseView.state', { + defaultMessage: 'State', +}); + +export const SUBMIT = i18n.translate('xpack.siem.case.caseView.submit', { + defaultMessage: 'Submit', +}); + +export const TAGS = i18n.translate('xpack.siem.case.caseView.tags', { + defaultMessage: 'Tags', +}); + +export const TAGS_HELP = i18n.translate('xpack.siem.case.createCase.fieldTagsHelpText', { + defaultMessage: + 'Type one or more custom identifying tags for this case. Press enter after each tag to begin a new one.', +}); + +export const NO_TAGS = i18n.translate('xpack.siem.case.caseView.noTags', { + defaultMessage: 'No tags are currently assigned to this case.', +}); + +export const TITLE_REQUIRED = i18n.translate('xpack.siem.case.createCase.titleFieldRequiredError', { + defaultMessage: 'A title is required.', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/utils.ts b/x-pack/legacy/plugins/siem/public/pages/case/utils.ts new file mode 100644 index 0000000000000..bd6cb5da5eb01 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/utils.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Breadcrumb } from 'ui/chrome'; +import { getCaseDetailsUrl, getCaseUrl, getCreateCaseUrl } from '../../components/link_to'; +import { RouteSpyState } from '../../utils/route/types'; +import * as i18n from './translations'; + +export const getBreadcrumbs = (params: RouteSpyState): Breadcrumb[] => { + let breadcrumb = [ + { + text: i18n.PAGE_TITLE, + href: getCaseUrl(), + }, + ]; + if (params.detailName === 'create') { + breadcrumb = [ + ...breadcrumb, + { + text: i18n.CREATE_BC_TITLE, + href: getCreateCaseUrl(), + }, + ]; + } else if (params.detailName != null) { + breadcrumb = [ + ...breadcrumb, + { + text: params.detailName, + href: getCaseDetailsUrl(params.detailName), + }, + ]; + } + return breadcrumb; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx index 8a37461746773..8cfcac8fc862b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx @@ -193,7 +193,6 @@ const DetectionEnginePageComponent: React.FC<DetectionEnginePageComponentProps> hideHeaderChildren={true} indexPattern={indexPattern} query={query} - setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker!} setQuery={setQuery} to={to} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx index c54a2e8d49844..fa4f6a874ca5e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx @@ -13,6 +13,7 @@ import { EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; +import { isEqual } from 'lodash/fp'; import * as i18n from '../../translations'; import { FilterOptions } from '../../../../../containers/detection_engine/rules'; @@ -59,6 +60,15 @@ const RulesTableFiltersComponent = ({ setShowElasticRules(false); }, [setShowElasticRules, showCustomRules, setShowCustomRules]); + const handleSelectedTags = useCallback( + newTags => { + if (!isEqual(newTags, selectedTags)) { + setSelectedTags(newTags); + } + }, + [selectedTags] + ); + return ( <EuiFlexGroup gutterSize="m" justifyContent="flexEnd"> <EuiFlexItem grow={true}> @@ -74,9 +84,10 @@ const RulesTableFiltersComponent = ({ <EuiFlexItem grow={false}> <EuiFilterGroup> <TagsFilterPopover - tags={tags} - onSelectedTagsChanged={setSelectedTags} isLoading={isLoadingTags} + onSelectedTagsChanged={handleSelectedTags} + selectedTags={selectedTags} + tags={tags} /> </EuiFilterGroup> </EuiFlexItem> diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx index b9d2c97f063b1..44149a072f5c1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import React, { Dispatch, SetStateAction, useState } from 'react'; import { EuiFilterButton, EuiFilterSelectItem, @@ -19,9 +19,10 @@ import * as i18n from '../../translations'; import { toggleSelectedGroup } from '../../../../../components/ml_popover/jobs_table/filters/toggle_selected_group'; interface TagsFilterPopoverProps { + selectedTags: string[]; tags: string[]; onSelectedTagsChanged: Dispatch<SetStateAction<string[]>>; - isLoading: boolean; + isLoading: boolean; // TO DO reimplement? } const ScrollableDiv = styled.div` @@ -37,14 +38,10 @@ const ScrollableDiv = styled.div` */ export const TagsFilterPopoverComponent = ({ tags, + selectedTags, onSelectedTagsChanged, }: TagsFilterPopoverProps) => { const [isTagPopoverOpen, setIsTagPopoverOpen] = useState(false); - const [selectedTags, setSelectedTags] = useState<string[]>([]); - - useEffect(() => { - onSelectedTagsChanged(selectedTags); - }, [selectedTags.sort().join()]); return ( <EuiPopover @@ -70,7 +67,7 @@ export const TagsFilterPopoverComponent = ({ <EuiFilterSelectItem checked={selectedTags.includes(tag) ? 'on' : undefined} key={`${index}-${tag}`} - onClick={() => toggleSelectedGroup(tag, selectedTags, setSelectedTags)} + onClick={() => toggleSelectedGroup(tag, selectedTags, onSelectedTagsChanged)} > {`${tag}`} </EuiFilterSelectItem> diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx index 0c75da7d8a632..cc5e9b38eb2f8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx @@ -18,7 +18,7 @@ import React, { ChangeEvent, useCallback, useEffect, useState, useRef } from 're import styled from 'styled-components'; import * as RuleI18n from '../../translations'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; interface AddItemProps { addText: string; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx index 09f4c13acbf69..1cc7bba5558db 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx @@ -19,7 +19,7 @@ import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/searc import { useKibana } from '../../../../../lib/kibana'; import { IMitreEnterpriseAttack } from '../../types'; import { FieldValueTimeline } from '../pick_timeline'; -import { FormSchema } from '../shared_imports'; +import { FormSchema } from '../../../../shared_imports'; import { ListItems } from './types'; import { buildQueryBarDescription, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx index d85be053065fc..b49126c8c0fe0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx @@ -20,7 +20,7 @@ import styled from 'styled-components'; import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; import * as Rulei18n from '../../translations'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; import { threatDefault } from '../step_about_rule/default_value'; import { IMitreEnterpriseAttack } from '../../types'; import { MyAddItemButton } from '../add_item_form'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx index f467d0ebede41..56cb02c9ec817 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx @@ -8,7 +8,7 @@ import { EuiFormRow } from '@elastic/eui'; import React, { useCallback, useEffect, useState } from 'react'; import { SearchTimelineSuperSelect } from '../../../../../components/timeline/search_super_select'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; export interface FieldValueTimeline { id: string | null; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx index 7f55d76c6d6b1..88795f9195e68 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx @@ -29,7 +29,7 @@ import { convertKueryToElasticSearchQuery } from '../../../../../lib/keury'; import { useKibana } from '../../../../../lib/kibana'; import { TimelineModel } from '../../../../../store/timeline/model'; import { useSavedQueryServices } from '../../../../../utils/saved_query_services'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; import * as i18n from './translations'; export interface FieldValueQueryBar { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx index 3bde2087f26b1..ffb6c4eda3243 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx @@ -16,7 +16,7 @@ import { isEmpty } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../shared_imports'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; import * as I18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx index 9c351e66c2f04..45da7d081333e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx @@ -23,7 +23,14 @@ import * as RuleI18n from '../../translations'; import { AddItem } from '../add_item_form'; import { StepRuleDescription } from '../description_step'; import { AddMitreThreat } from '../mitre'; -import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports'; +import { + Field, + Form, + FormDataProvider, + getUseField, + UseField, + useForm, +} from '../../../../shared_imports'; import { defaultRiskScoreBySeverity, severityOptions, SeverityValue } from './data'; import { stepAboutDefaultValue } from './default_value'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx index 22033dcf6b0f7..27887bcbbe600 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx @@ -13,7 +13,7 @@ import { FormSchema, ValidationFunc, ERROR_CODE, -} from '../shared_imports'; +} from '../../../../shared_imports'; import { isMitreAttackInvalid } from '../mitre/helpers'; import { OptionalFieldLabel } from '../optional_field_label'; import { isUrlInvalid } from './helpers'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx index 5409a5f161bba..920a9f2dfe56c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx @@ -25,7 +25,14 @@ import { DefineStepRule, RuleStep, RuleStepProps } from '../../types'; import { StepRuleDescription } from '../description_step'; import { QueryBarDefineRule } from '../query_bar'; import { StepContentWrapper } from '../step_content_wrapper'; -import { Field, Form, FormDataProvider, getUseField, UseField, useForm } from '../shared_imports'; +import { + Field, + Form, + FormDataProvider, + getUseField, + UseField, + useForm, +} from '../../../../shared_imports'; import { schema } from './schema'; import * as i18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx index 079ec0dab4c5a..bb178d7197069 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx @@ -17,7 +17,7 @@ import { fieldValidators, FormSchema, ValidationFunc, -} from '../shared_imports'; +} from '../../../../shared_imports'; import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY, INDEX_HELPER_TEXT } from './translations'; const { emptyField } = fieldValidators; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx index 532df628a83ae..cfbb0a622c721 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx @@ -12,7 +12,7 @@ import { setFieldValue } from '../../helpers'; import { RuleStep, RuleStepProps, ScheduleStepRule } from '../../types'; import { StepRuleDescription } from '../description_step'; import { ScheduleItem } from '../schedule_item_form'; -import { Form, UseField, useForm } from '../shared_imports'; +import { Form, UseField, useForm } from '../../../../shared_imports'; import { StepContentWrapper } from '../step_content_wrapper'; import { schema } from './schema'; import * as I18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx index a951c1fab7cc8..9932e4f6ef435 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { OptionalFieldLabel } from '../optional_field_label'; -import { FormSchema } from '../shared_imports'; +import { FormSchema } from '../../../../shared_imports'; export const schema: FormSchema = { interval: { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx index 3adc22329ac4f..c985045b1897b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx @@ -17,7 +17,7 @@ import { displaySuccessToast, useStateToaster } from '../../../../components/toa import { SpyRoute } from '../../../../utils/route/spy_routes'; import { useUserInfo } from '../../components/user_info'; import { AccordionTitle } from '../components/accordion_title'; -import { FormData, FormHook } from '../components/shared_imports'; +import { FormData, FormHook } from '../../../shared_imports'; import { StepAboutRule } from '../components/step_about_rule'; import { StepDefineRule } from '../components/step_define_rule'; import { StepScheduleRule } from '../components/step_schedule_rule'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx index 99fcff6b8d2fd..0fac4641e54a7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx @@ -26,7 +26,7 @@ import { displaySuccessToast, useStateToaster } from '../../../../components/toa import { SpyRoute } from '../../../../utils/route/spy_routes'; import { useUserInfo } from '../../components/user_info'; import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page'; -import { FormHook, FormData } from '../components/shared_imports'; +import { FormHook, FormData } from '../../../shared_imports'; import { StepPanel } from '../components/step_panel'; import { StepAboutRule } from '../components/step_about_rule'; import { StepDefineRule } from '../components/step_define_rule'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx index cfff71851b2e1..3fab456d856ca 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -11,7 +11,7 @@ import { useLocation } from 'react-router-dom'; import { Filter } from '../../../../../../../../src/plugins/data/public'; import { Rule } from '../../../containers/detection_engine/rules'; -import { FormData, FormHook, FormSchema } from './components/shared_imports'; +import { FormData, FormHook, FormSchema } from '../../shared_imports'; import { AboutStepRule, DefineStepRule, IMitreEnterpriseAttack, ScheduleStepRule } from './types'; interface GetStepsData { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts index fc2e3fba24449..55eb45fb5ed9d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts @@ -7,7 +7,7 @@ import { Filter } from '../../../../../../../../src/plugins/data/common'; import { Rule } from '../../../containers/detection_engine/rules'; import { FieldValueQueryBar } from './components/query_bar'; -import { FormData, FormHook } from './components/shared_imports'; +import { FormData, FormHook } from '../../shared_imports'; import { FieldValueTimeline } from './components/pick_timeline'; export interface EuiBasicTableSortTypes { diff --git a/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx b/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx index c0e959c5e97fa..42d333f4f893e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx @@ -10,6 +10,7 @@ import { getNetworkUrl, getTimelinesUrl, getHostsUrl, + getCaseUrl, } from '../../components/link_to'; import * as i18n from './translations'; import { SiemPageName, SiemNavTab } from './types'; @@ -50,4 +51,11 @@ export const navTabs: SiemNavTab = { disabled: false, urlKey: 'timeline', }, + [SiemPageName.case]: { + id: SiemPageName.case, + name: i18n.CASE, + href: getCaseUrl(), + disabled: true, + urlKey: 'case', + }, }; diff --git a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx index 5cfed4121ba77..9ee103f88793d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx @@ -26,6 +26,7 @@ import { DetectionEngineContainer } from '../detection_engine'; import { HostsContainer } from '../hosts'; import { NetworkContainer } from '../network'; import { Overview } from '../overview'; +import { Case } from '../case'; import { Timelines } from '../timelines'; import { navTabs } from './home_navigations'; import { SiemPageName } from './types'; @@ -42,6 +43,11 @@ const WrappedByAutoSizer = styled.div` `; WrappedByAutoSizer.displayName = 'WrappedByAutoSizer'; +const Main = styled.main` + height: 100%; +`; +Main.displayName = 'Main'; + const usersViewing = ['elastic']; // TODO: get the users viewing this timeline from Elasticsearch (persistance) /** the global Kibana navigation at the top of every page */ @@ -61,7 +67,7 @@ export const HomePage: React.FC = () => ( <WrappedByAutoSizer data-test-subj="wrapped-by-auto-sizer" ref={measureRef}> <HeaderGlobal /> - <main data-test-subj="pageContainer"> + <Main data-test-subj="pageContainer"> <WithSource sourceId="default"> {({ browserFields, indexPattern, indicesExist }) => ( <DragDropContextWrapper browserFields={browserFields}> @@ -131,12 +137,15 @@ export const HomePage: React.FC = () => ( <MlNetworkConditionalContainer location={location} url={match.url} /> )} /> + <Route path={`/:pageName(${SiemPageName.case})`}> + <Case /> + </Route> <Route render={() => <NotFoundPage />} /> </Switch> </DragDropContextWrapper> )} </WithSource> - </main> + </Main> <HelpMenu /> diff --git a/x-pack/legacy/plugins/siem/public/pages/home/translations.ts b/x-pack/legacy/plugins/siem/public/pages/home/translations.ts index 80800a3bd4198..581c81d9f98a0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/home/translations.ts @@ -25,3 +25,7 @@ export const DETECTION_ENGINE = i18n.translate('xpack.siem.navigation.detectionE export const TIMELINES = i18n.translate('xpack.siem.navigation.timelines', { defaultMessage: 'Timelines', }); + +export const CASE = i18n.translate('xpack.siem.navigation.case', { + defaultMessage: 'Case', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/home/types.ts b/x-pack/legacy/plugins/siem/public/pages/home/types.ts index 678de6dbcc128..6445ac91d9e13 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/home/types.ts @@ -12,6 +12,7 @@ export enum SiemPageName { network = 'network', detections = 'detections', timelines = 'timelines', + case = 'case', } export type SiemNavTabKey = @@ -19,6 +20,7 @@ export type SiemNavTabKey = | SiemPageName.hosts | SiemPageName.network | SiemPageName.detections - | SiemPageName.timelines; + | SiemPageName.timelines + | SiemPageName.case; export type SiemNavTab = Record<SiemNavTabKey, NavTab>; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx index 2e2986fb632b1..06dffcdb220a9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx @@ -29,6 +29,7 @@ import { useKibana } from '../../lib/kibana'; import { convertToBuildEsQuery } from '../../lib/keury'; import { inputsSelectors, State, hostsModel } from '../../store'; import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; + import { SpyRoute } from '../../utils/route/spy_routes'; import { esQuery } from '../../../../../../../src/plugins/data/public'; import { HostsEmptyPage } from './hosts_empty_page'; @@ -131,11 +132,11 @@ export const HostsComponent = React.memo<HostsComponentProps>( to={to} filterQuery={tabsFilterQuery} isInitializing={isInitializing} + setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} setQuery={setQuery} from={from} type={hostsModel.HostsType.page} indexPattern={indexPattern} - setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} hostsPagePath={hostsPagePath} /> </WrapperPage> diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx index 9c13fc4ac386e..0b83710a13293 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx @@ -52,9 +52,6 @@ const HostsTabs = memo<HostsTabsProps>( to: fromTo.to, }); }, - updateDateRange: (min: number, max: number) => { - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); - }, }; return ( diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx index a6a0344599842..fb083b7a7da2f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx @@ -14,11 +14,12 @@ import { hostsModel } from '../../../store/hosts'; import { MatrixHistogramOption, MatrixHistogramMappingTypes, + MatrixHisrogramConfigs, } from '../../../components/matrix_histogram/types'; -import { MatrixHistogramContainer } from '../../../containers/matrix_histogram'; +import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; import { KpiHostsChartColors } from '../../../components/page/hosts/kpi_hosts/types'; -import { MatrixHistogramGqlQuery } from '../../../containers/matrix_histogram/index.gql_query'; import * as i18n from '../translations'; +import { HistogramType } from '../../../graphql/types'; const AuthenticationTableManage = manageQuery(AuthenticationTable); const ID = 'authenticationsOverTimeQuery'; @@ -28,6 +29,7 @@ const authStackByOptions: MatrixHistogramOption[] = [ value: 'event.type', }, ]; +const DEFAULT_STACK_BY = 'event.type'; enum AuthMatrixDataGroup { authSuccess = 'authentication_success', @@ -47,6 +49,16 @@ export const authMatrixDataMappingFields: MatrixHistogramMappingTypes = { }, }; +const histogramConfigs: MatrixHisrogramConfigs = { + defaultStackByOption: + authStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? authStackByOptions[0], + errorMessage: i18n.ERROR_FETCHING_AUTHENTICATIONS_DATA, + histogramType: HistogramType.authentications, + mapping: authMatrixDataMappingFields, + stackByOptions: authStackByOptions, + title: i18n.NAVIGATION_AUTHENTICATIONS_TITLE, +}; + export const AuthenticationsQueryTabBody = ({ deleteQuery, endDate, @@ -55,7 +67,6 @@ export const AuthenticationsQueryTabBody = ({ setQuery, startDate, type, - updateDateRange = () => {}, }: HostsComponentsQueryProps) => { useEffect(() => { return () => { @@ -64,26 +75,18 @@ export const AuthenticationsQueryTabBody = ({ } }; }, [deleteQuery]); + return ( <> <MatrixHistogramContainer - isAuthenticationsHistogram={true} - dataKey="AuthenticationsHistogram" - defaultStackByOption={authStackByOptions[0]} endDate={endDate} - errorMessage={i18n.ERROR_FETCHING_AUTHENTICATIONS_DATA} filterQuery={filterQuery} id={ID} - mapping={authMatrixDataMappingFields} - query={MatrixHistogramGqlQuery} setQuery={setQuery} - skip={skip} sourceId="default" startDate={startDate} - stackByOptions={authStackByOptions} - title={i18n.NAVIGATION_AUTHENTICATIONS_TITLE} type={hostsModel.HostsType.page} - updateDateRange={updateDateRange} + {...histogramConfigs} /> <AuthenticationsQuery endDate={endDate} diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx index 0ea82ba53b3a2..cb2c19c642bc4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx @@ -9,10 +9,13 @@ import { StatefulEventsViewer } from '../../../components/events_viewer'; import { HostsComponentsQueryProps } from './types'; import { hostsModel } from '../../../store/hosts'; import { eventsDefaultModel } from '../../../components/events_viewer/default_model'; -import { MatrixHistogramOption } from '../../../components/matrix_histogram/types'; -import { MatrixHistogramContainer } from '../../../containers/matrix_histogram'; -import { MatrixHistogramGqlQuery } from '../../../containers/matrix_histogram/index.gql_query'; +import { + MatrixHistogramOption, + MatrixHisrogramConfigs, +} from '../../../components/matrix_histogram/types'; +import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; import * as i18n from '../translations'; +import { HistogramType } from '../../../graphql/types'; const HOSTS_PAGE_TIMELINE_ID = 'hosts-page'; const EVENTS_HISTOGRAM_ID = 'eventsOverTimeQuery'; @@ -32,15 +35,25 @@ export const eventsStackByOptions: MatrixHistogramOption[] = [ }, ]; +const DEFAULT_STACK_BY = 'event.action'; + +export const histogramConfigs: MatrixHisrogramConfigs = { + defaultStackByOption: + eventsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? eventsStackByOptions[0], + errorMessage: i18n.ERROR_FETCHING_EVENTS_DATA, + histogramType: HistogramType.events, + stackByOptions: eventsStackByOptions, + subtitle: undefined, + title: i18n.NAVIGATION_EVENTS_TITLE, +}; + export const EventsQueryTabBody = ({ deleteQuery, endDate, filterQuery, pageFilters, setQuery, - skip, startDate, - updateDateRange = () => {}, }: HostsComponentsQueryProps) => { useEffect(() => { return () => { @@ -49,25 +62,18 @@ export const EventsQueryTabBody = ({ } }; }, [deleteQuery]); + return ( <> <MatrixHistogramContainer - dataKey="EventsHistogram" - defaultStackByOption={eventsStackByOptions[0]} endDate={endDate} - isEventsHistogram={true} - errorMessage={i18n.ERROR_FETCHING_EVENTS_DATA} filterQuery={filterQuery} - query={MatrixHistogramGqlQuery} setQuery={setQuery} - skip={skip} sourceId="default" - stackByOptions={eventsStackByOptions} startDate={startDate} type={hostsModel.HostsType.page} - title={i18n.NAVIGATION_EVENTS_TITLE} - updateDateRange={updateDateRange} id={EVENTS_HISTOGRAM_ID} + {...histogramConfigs} /> <StatefulEventsViewer defaultModel={eventsDefaultModel} diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/types.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/types.ts index 5900937d2108e..e6e2ebb9ac2fe 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/types.ts @@ -20,7 +20,7 @@ export interface HostsComponentReduxProps { filters: Filter[]; } -export interface HostsComponentDispatchProps { +interface HostsComponentDispatchProps { setAbsoluteRangeDatePicker: ActionCreator<{ id: InputsModelId; from: number; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx index b49849b285d8e..fe456afcc7189 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useCallback } from 'react'; +import React, { useEffect, useCallback, useMemo } from 'react'; import { getOr } from 'lodash/fp'; import { NetworkDnsTable } from '../../../components/page/network/network_dns_table'; @@ -14,10 +14,13 @@ import { manageQuery } from '../../../components/page/manage_query'; import { NetworkComponentQueryProps } from './types'; import { networkModel } from '../../../store'; -import { MatrixHistogramOption } from '../../../components/matrix_histogram/types'; +import { + MatrixHistogramOption, + MatrixHisrogramConfigs, +} from '../../../components/matrix_histogram/types'; import * as i18n from '../translations'; -import { MatrixHistogramGqlQuery } from '../../../containers/matrix_histogram/index.gql_query'; -import { MatrixHistogramContainer } from '../../../containers/matrix_histogram'; +import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; +import { HistogramType } from '../../../graphql/types'; const NetworkDnsTableManage = manageQuery(NetworkDnsTable); @@ -28,6 +31,17 @@ const dnsStackByOptions: MatrixHistogramOption[] = [ }, ]; +const DEFAULT_STACK_BY = 'dns.question.registered_domain'; + +export const histogramConfigs: Omit<MatrixHisrogramConfigs, 'title'> = { + defaultStackByOption: + dnsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? dnsStackByOptions[0], + errorMessage: i18n.ERROR_FETCHING_DNS_DATA, + histogramType: HistogramType.dns, + stackByOptions: dnsStackByOptions, + subtitle: undefined, +}; + export const DnsQueryTabBody = ({ deleteQuery, endDate, @@ -36,7 +50,6 @@ export const DnsQueryTabBody = ({ startDate, setQuery, type, - updateDateRange = () => {}, }: NetworkComponentQueryProps) => { useEffect(() => { return () => { @@ -51,24 +64,26 @@ export const DnsQueryTabBody = ({ [] ); + const dnsHistogramConfigs: MatrixHisrogramConfigs = useMemo( + () => ({ + ...histogramConfigs, + title: getTitle, + }), + [getTitle] + ); + return ( <> <MatrixHistogramContainer - dataKey={['NetworkDnsHistogram', 'matrixHistogramData']} - defaultStackByOption={dnsStackByOptions[0]} endDate={endDate} - errorMessage={i18n.ERROR_FETCHING_DNS_DATA} filterQuery={filterQuery} id={HISTOGRAM_ID} - isDnsHistogram={true} - query={MatrixHistogramGqlQuery} setQuery={setQuery} + showLegend={true} sourceId="default" startDate={startDate} - stackByOptions={dnsStackByOptions} - title={getTitle} type={networkModel.NetworkType.page} - updateDateRange={updateDateRange} + {...dnsHistogramConfigs} /> <NetworkDnsQuery endDate={endDate} diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx index acc5d02299f1f..23a619db97ee4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx @@ -45,12 +45,6 @@ export const NetworkRoutes = ({ }, [setAbsoluteRangeDatePicker] ); - const updateDateRange = useCallback( - (min: number, max: number) => { - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); - }, - [setAbsoluteRangeDatePicker] - ); const networkAnomaliesFilterQuery = { bool: { @@ -83,7 +77,6 @@ export const NetworkRoutes = ({ const tabProps = { ...commonProps, indexPattern, - updateDateRange, }; const anomaliesProps = { diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts b/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts index b6063a81f31f6..222a99992917d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts @@ -13,7 +13,6 @@ import { ESTermQuery } from '../../../../common/typed_json'; import { GlobalTimeArgs } from '../../../containers/global_time'; import { SetAbsoluteRangeDatePicker } from '../types'; -import { UpdateDateRange } from '../../../components/charts/common'; import { NarrowDateRange } from '../../../components/ml/types'; interface QueryTabBodyProps extends Pick<GlobalTimeArgs, 'setQuery' | 'deleteQuery'> { @@ -22,7 +21,6 @@ interface QueryTabBodyProps extends Pick<GlobalTimeArgs, 'setQuery' | 'deleteQue startDate: number; endDate: number; filterQuery?: string | ESTermQuery; - updateDateRange?: UpdateDateRange; narrowDateRange?: NarrowDateRange; } diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx index 98ae3f30085a9..f71d83558ae9d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx @@ -6,21 +6,15 @@ import { EuiButton } from '@elastic/eui'; import numeral from '@elastic/numeral'; -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; +import { Position } from '@elastic/charts'; import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; -import { - ERROR_FETCHING_ALERTS_DATA, - SHOWING, - UNIT, -} from '../../../components/alerts_viewer/translations'; -import { alertsStackByOptions } from '../../../components/alerts_viewer'; +import { SHOWING, UNIT } from '../../../components/alerts_viewer/translations'; import { getDetectionEngineAlertUrl } from '../../../components/link_to/redirect_to_detection_engine'; -import { MatrixHistogramContainer } from '../../../containers/matrix_histogram'; -import { MatrixHistogramGqlQuery } from '../../../containers/matrix_histogram/index.gql_query'; +import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; import { useKibana, useUiSetting$ } from '../../../lib/kibana'; import { convertToBuildEsQuery } from '../../../lib/keury'; -import { SetAbsoluteRangeDatePicker } from '../../network/types'; import { Filter, esQuery, @@ -31,6 +25,11 @@ import { inputsModel } from '../../../store'; import { HostsType } from '../../../store/hosts/model'; import * as i18n from '../translations'; +import { + alertsStackByOptions, + histogramConfigs, +} from '../../../components/alerts_viewer/histogram_configs'; +import { MatrixHisrogramConfigs } from '../../../components/matrix_histogram/types'; const ID = 'alertsByCategoryOverview'; @@ -45,7 +44,6 @@ interface Props { hideHeaderChildren?: boolean; indexPattern: IIndexPattern; query?: Query; - setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; setQuery: (params: { id: string; inspect: inputsModel.InspectQuery | null; @@ -62,7 +60,6 @@ const AlertsByCategoryComponent: React.FC<Props> = ({ hideHeaderChildren = false, indexPattern, query = DEFAULT_QUERY, - setAbsoluteRangeDatePicker, setQuery, to, }) => { @@ -77,32 +74,26 @@ const AlertsByCategoryComponent: React.FC<Props> = ({ const kibana = useKibana(); const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); - const updateDateRangeCallback = useCallback( - (min: number, max: number) => { - setAbsoluteRangeDatePicker!({ id: 'global', from: min, to: max }); - }, - [setAbsoluteRangeDatePicker] - ); const alertsCountViewAlertsButton = useMemo( () => <EuiButton href={getDetectionEngineAlertUrl()}>{i18n.VIEW_ALERTS}</EuiButton>, [] ); - const getSubtitle = useCallback( - (totalCount: number) => - `${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`, + const alertsByCategoryHistogramConfigs: MatrixHisrogramConfigs = useMemo( + () => ({ + ...histogramConfigs, + defaultStackByOption: + alertsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[0], + getSubtitle: (totalCount: number) => + `${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`, + legendPosition: Position.Right, + }), [] ); - const defaultStackByOption = - alertsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[0]; - return ( <MatrixHistogramContainer - dataKey="AlertsHistogram" - defaultStackByOption={defaultStackByOption} endDate={to} - errorMessage={ERROR_FETCHING_ALERTS_DATA} filterQuery={convertToBuildEsQuery({ config: esQuery.getEsQueryConfig(kibana.services.uiSettings), indexPattern, @@ -111,17 +102,11 @@ const AlertsByCategoryComponent: React.FC<Props> = ({ })} headerChildren={hideHeaderChildren ? null : alertsCountViewAlertsButton} id={ID} - isAlertsHistogram={true} - legendPosition={'right'} - query={MatrixHistogramGqlQuery} setQuery={setQuery} sourceId="default" - stackByOptions={alertsStackByOptions} startDate={from} - title={i18n.ALERTS_GRAPH_TITLE} - subtitle={getSubtitle} type={HostsType.page} - updateDateRange={updateDateRangeCallback} + {...alertsByCategoryHistogramConfigs} /> ); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx index 5b6ad69bcb15d..315aac5fcae9e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx @@ -6,18 +6,14 @@ import { EuiButton } from '@elastic/eui'; import numeral from '@elastic/numeral'; -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; -import { - ERROR_FETCHING_EVENTS_DATA, - SHOWING, - UNIT, -} from '../../../components/events_viewer/translations'; +import { Position } from '@elastic/charts'; +import { SHOWING, UNIT } from '../../../components/events_viewer/translations'; import { convertToBuildEsQuery } from '../../../lib/keury'; -import { SetAbsoluteRangeDatePicker } from '../../network/types'; import { getTabsOnHostsUrl } from '../../../components/link_to/redirect_to_hosts'; -import { MatrixHistogramContainer } from '../../../containers/matrix_histogram'; -import { MatrixHistogramGqlQuery } from '../../../containers/matrix_histogram/index.gql_query'; +import { histogramConfigs } from '../../../pages/hosts/navigation/events_query_tab_body'; +import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; import { eventsStackByOptions } from '../../hosts/navigation'; import { useKibana, useUiSetting$ } from '../../../lib/kibana'; import { @@ -31,6 +27,7 @@ import { HostsTableType, HostsType } from '../../../store/hosts/model'; import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; import * as i18n from '../translations'; +import { MatrixHisrogramConfigs } from '../../../components/matrix_histogram/types'; const NO_FILTERS: Filter[] = []; const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; @@ -44,7 +41,6 @@ interface Props { from: number; indexPattern: IIndexPattern; query?: Query; - setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; setQuery: (params: { id: string; inspect: inputsModel.InspectQuery | null; @@ -60,7 +56,6 @@ const EventsByDatasetComponent: React.FC<Props> = ({ from, indexPattern, query = DEFAULT_QUERY, - setAbsoluteRangeDatePicker, setQuery, to, }) => { @@ -70,31 +65,16 @@ const EventsByDatasetComponent: React.FC<Props> = ({ deleteQuery({ id: ID }); } }; - }, []); + }, [deleteQuery]); const kibana = useKibana(); const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); - const updateDateRangeCallback = useCallback( - (min: number, max: number) => { - setAbsoluteRangeDatePicker!({ id: 'global', from: min, to: max }); - }, - [setAbsoluteRangeDatePicker] - ); const eventsCountViewEventsButton = useMemo( () => <EuiButton href={getTabsOnHostsUrl(HostsTableType.events)}>{i18n.VIEW_EVENTS}</EuiButton>, [] ); - const getSubtitle = useCallback( - (totalCount: number) => - `${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`, - [] - ); - - const defaultStackByOption = - eventsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? eventsStackByOptions[0]; - const filterQuery = useMemo( () => convertToBuildEsQuery({ @@ -106,26 +86,29 @@ const EventsByDatasetComponent: React.FC<Props> = ({ [kibana, indexPattern, query, filters] ); + const eventsByDatasetHistogramConfigs: MatrixHisrogramConfigs = useMemo( + () => ({ + ...histogramConfigs, + defaultStackByOption: + eventsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? eventsStackByOptions[0], + legendPosition: Position.Right, + subtitle: (totalCount: number) => + `${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`, + }), + [] + ); + return ( <MatrixHistogramContainer - dataKey="EventsHistogram" - defaultStackByOption={defaultStackByOption} endDate={to} - errorMessage={ERROR_FETCHING_EVENTS_DATA} filterQuery={filterQuery} headerChildren={eventsCountViewEventsButton} id={ID} - isEventsHistogram={true} - legendPosition={'right'} - query={MatrixHistogramGqlQuery} setQuery={setQuery} sourceId="default" - stackByOptions={eventsStackByOptions} startDate={from} - title={i18n.EVENTS} - subtitle={getSubtitle} type={HostsType.page} - updateDateRange={updateDateRangeCallback} + {...eventsByDatasetHistogramConfigs} /> ); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx index 6f8446a6b1609..8505b91fe1ff5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx @@ -85,7 +85,6 @@ const OverviewComponent: React.FC<OverviewComponentReduxProps> = ({ from={from} indexPattern={indexPattern} query={query} - setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker!} setQuery={setQuery} to={to} /> @@ -98,7 +97,6 @@ const OverviewComponent: React.FC<OverviewComponentReduxProps> = ({ from={from} indexPattern={indexPattern} query={query} - setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker!} setQuery={setQuery} to={to} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/shared_imports.ts b/x-pack/legacy/plugins/siem/public/pages/shared_imports.ts similarity index 50% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/shared_imports.ts rename to x-pack/legacy/plugins/siem/public/pages/shared_imports.ts index 494da24be706a..a41f121b36926 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/shared_imports.ts +++ b/x-pack/legacy/plugins/siem/public/pages/shared_imports.ts @@ -17,7 +17,7 @@ export { UseField, useForm, ValidationFunc, -} from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; -export { Field } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/components'; -export { fieldValidators } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; -export { ERROR_CODE } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types'; +} from '../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +export { Field } from '../../../../../../src/plugins/es_ui_shared/static/forms/components'; +export { fieldValidators } from '../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; +export { ERROR_CODE } from '../../../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types'; diff --git a/x-pack/legacy/plugins/siem/public/store/model.ts b/x-pack/legacy/plugins/siem/public/store/model.ts index 6f04f22866be5..9e9e663a59fe0 100644 --- a/x-pack/legacy/plugins/siem/public/store/model.ts +++ b/x-pack/legacy/plugins/siem/public/store/model.ts @@ -5,9 +5,9 @@ */ export { appModel } from './app'; -export { inputsModel } from './inputs'; -export { hostsModel } from './hosts'; export { dragAndDropModel } from './drag_and_drop'; +export { hostsModel } from './hosts'; +export { inputsModel } from './inputs'; export { networkModel } from './network'; export type KueryFilterQueryKind = 'kuery' | 'lucene'; diff --git a/x-pack/legacy/plugins/siem/public/utils/use_global_loading.ts b/x-pack/legacy/plugins/siem/public/utils/use_global_loading.ts deleted file mode 100644 index 37abe2f28d310..0000000000000 --- a/x-pack/legacy/plugins/siem/public/utils/use_global_loading.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { useState, useEffect } from 'react'; - -export const useGlobalLoading = () => { - const [isInitializing, setIsInitializing] = useState(true); - useEffect(() => { - if (isInitializing) { - setIsInitializing(false); - } - }); - return isInitializing; -}; diff --git a/x-pack/legacy/plugins/siem/server/graphql/alerts/index.ts b/x-pack/legacy/plugins/siem/server/graphql/alerts/index.ts deleted file mode 100644 index f2beae525ed6b..0000000000000 --- a/x-pack/legacy/plugins/siem/server/graphql/alerts/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { createAlertsResolvers } from './resolvers'; -export { alertsSchema } from './schema.gql'; diff --git a/x-pack/legacy/plugins/siem/server/graphql/alerts/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/alerts/resolvers.ts deleted file mode 100644 index 5a3a50d5c6ec6..0000000000000 --- a/x-pack/legacy/plugins/siem/server/graphql/alerts/resolvers.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Alerts } from '../../lib/alerts'; -import { AppResolverOf, ChildResolverOf } from '../../lib/framework'; -import { createOptions } from '../../utils/build_query/create_options'; -import { QuerySourceResolver } from '../sources/resolvers'; -import { SourceResolvers } from '../types'; - -export interface AlertsResolversDeps { - alerts: Alerts; -} - -type QueryAlertsHistogramResolver = ChildResolverOf< - AppResolverOf<SourceResolvers.AlertsHistogramResolver>, - QuerySourceResolver ->; - -export const createAlertsResolvers = ( - libs: AlertsResolversDeps -): { - Source: { - AlertsHistogram: QueryAlertsHistogramResolver; - }; -} => ({ - Source: { - async AlertsHistogram(source, args, { req }, info) { - const options = { - ...createOptions(source, args, info), - defaultIndex: args.defaultIndex, - stackByField: args.stackByField, - }; - return libs.alerts.getAlertsHistogramData(req, options); - }, - }, -}); diff --git a/x-pack/legacy/plugins/siem/server/graphql/anomalies/index.ts b/x-pack/legacy/plugins/siem/server/graphql/anomalies/index.ts deleted file mode 100644 index 4bfd6be173105..0000000000000 --- a/x-pack/legacy/plugins/siem/server/graphql/anomalies/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { createAnomaliesResolvers } from './resolvers'; -export { anomaliesSchema } from './schema.gql'; diff --git a/x-pack/legacy/plugins/siem/server/graphql/anomalies/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/anomalies/schema.gql.ts deleted file mode 100644 index a0b834f705696..0000000000000 --- a/x-pack/legacy/plugins/siem/server/graphql/anomalies/schema.gql.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import gql from 'graphql-tag'; - -export const anomaliesSchema = gql` - type AnomaliesOverTimeData { - inspect: Inspect - matrixHistogramData: [MatrixOverTimeHistogramData!]! - totalCount: Float! - } - - extend type Source { - AnomaliesHistogram( - timerange: TimerangeInput! - filterQuery: String - defaultIndex: [String!]! - stackByField: String - ): AnomaliesOverTimeData! - } -`; diff --git a/x-pack/legacy/plugins/siem/server/graphql/authentications/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/authentications/resolvers.ts index ce1c86ac8926c..b66ccd9a111b7 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/authentications/resolvers.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/authentications/resolvers.ts @@ -7,7 +7,7 @@ import { SourceResolvers } from '../../graphql/types'; import { Authentications } from '../../lib/authentications'; import { AppResolverOf, ChildResolverOf } from '../../lib/framework'; -import { createOptionsPaginated, createOptions } from '../../utils/build_query/create_options'; +import { createOptionsPaginated } from '../../utils/build_query/create_options'; import { QuerySourceResolver } from '../sources/resolvers'; type QueryAuthenticationsResolver = ChildResolverOf< @@ -15,11 +15,6 @@ type QueryAuthenticationsResolver = ChildResolverOf< QuerySourceResolver >; -type QueryAuthenticationsOverTimeResolver = ChildResolverOf< - AppResolverOf<SourceResolvers.AuthenticationsHistogramResolver>, - QuerySourceResolver ->; - export interface AuthenticationsResolversDeps { authentications: Authentications; } @@ -29,7 +24,6 @@ export const createAuthenticationsResolvers = ( ): { Source: { Authentications: QueryAuthenticationsResolver; - AuthenticationsHistogram: QueryAuthenticationsOverTimeResolver; }; } => ({ Source: { @@ -37,13 +31,5 @@ export const createAuthenticationsResolvers = ( const options = createOptionsPaginated(source, args, info); return libs.authentications.getAuthentications(req, options); }, - async AuthenticationsHistogram(source, args, { req }, info) { - const options = { - ...createOptions(source, args, info), - defaultIndex: args.defaultIndex, - stackByField: args.stackByField, - }; - return libs.authentications.getAuthenticationsOverTime(req, options); - }, }, }); diff --git a/x-pack/legacy/plugins/siem/server/graphql/authentications/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/authentications/schema.gql.ts index 4acc72a5b0b6f..20935ce9ed03f 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/authentications/schema.gql.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/authentications/schema.gql.ts @@ -34,12 +34,6 @@ export const authenticationsSchema = gql` inspect: Inspect } - type AuthenticationsOverTimeData { - inspect: Inspect - matrixHistogramData: [MatrixOverTimeHistogramData!]! - totalCount: Float! - } - extend type Source { "Gets Authentication success and failures based on a timerange" Authentications( @@ -48,11 +42,5 @@ export const authenticationsSchema = gql` filterQuery: String defaultIndex: [String!]! ): AuthenticationsData! - AuthenticationsHistogram( - timerange: TimerangeInput! - filterQuery: String - defaultIndex: [String!]! - stackByField: String - ): AuthenticationsOverTimeData! } `; diff --git a/x-pack/legacy/plugins/siem/server/graphql/events/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/events/resolvers.ts index 335f4c3bf4da3..a9ef6bc682c84 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/events/resolvers.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/events/resolvers.ts @@ -31,12 +31,6 @@ type QueryLastEventTimeResolver = ChildResolverOf< export interface EventsResolversDeps { events: Events; } - -type QueryEventsOverTimeResolver = ChildResolverOf< - AppResolverOf<SourceResolvers.EventsHistogramResolver>, - QuerySourceResolver ->; - export const createEventsResolvers = ( libs: EventsResolversDeps ): { @@ -44,7 +38,6 @@ export const createEventsResolvers = ( Timeline: QueryTimelineResolver; TimelineDetails: QueryTimelineDetailsResolver; LastEventTime: QueryLastEventTimeResolver; - EventsHistogram: QueryEventsOverTimeResolver; }; } => ({ Source: { @@ -71,14 +64,6 @@ export const createEventsResolvers = ( }; return libs.events.getLastEventTimeData(req, options); }, - async EventsHistogram(source, args, { req }, info) { - const options = { - ...createOptions(source, args, info), - defaultIndex: args.defaultIndex, - stackByField: args.stackByField, - }; - return libs.events.getEventsOverTime(req, options); - }, }, }); diff --git a/x-pack/legacy/plugins/siem/server/graphql/events/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/events/schema.gql.ts index 9b321d10614fc..3b71977bc0d47 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/events/schema.gql.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/events/schema.gql.ts @@ -68,18 +68,6 @@ export const eventsSchema = gql` network } - type MatrixOverTimeHistogramData { - x: Float! - y: Float! - g: String! - } - - type EventsOverTimeData { - inspect: Inspect - matrixHistogramData: [MatrixOverTimeHistogramData!]! - totalCount: Float! - } - extend type Source { Timeline( pagination: PaginationInput! @@ -100,11 +88,5 @@ export const eventsSchema = gql` details: LastTimeDetails! defaultIndex: [String!]! ): LastEventTimeData! - EventsHistogram( - timerange: TimerangeInput! - filterQuery: String - defaultIndex: [String!]! - stackByField: String - ): EventsOverTimeData! } `; diff --git a/x-pack/legacy/plugins/siem/server/graphql/index.ts b/x-pack/legacy/plugins/siem/server/graphql/index.ts index 60853e2ce7bed..7e25735707893 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/index.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/index.ts @@ -7,7 +7,6 @@ import { rootSchema } from '../../common/graphql/root'; import { sharedSchema } from '../../common/graphql/shared'; -import { anomaliesSchema } from './anomalies'; import { authenticationsSchema } from './authentications'; import { ecsSchema } from './ecs'; import { eventsSchema } from './events'; @@ -30,10 +29,8 @@ import { timelineSchema } from './timeline'; import { tlsSchema } from './tls'; import { uncommonProcessesSchema } from './uncommon_processes'; import { whoAmISchema } from './who_am_i'; -import { alertsSchema } from './alerts'; +import { matrixHistogramSchema } from './matrix_histogram'; export const schemas = [ - alertsSchema, - anomaliesSchema, authenticationsSchema, ecsSchema, eventsSchema, @@ -46,6 +43,7 @@ export const schemas = [ ...ipDetailsSchemas, kpiNetworkSchema, kpiHostsSchema, + matrixHistogramSchema, networkSchema, noteSchema, overviewSchema, diff --git a/x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/index.ts b/x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/index.ts new file mode 100644 index 0000000000000..1460b6022bb13 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createMatrixHistogramResolvers } from './resolvers'; +export { matrixHistogramSchema } from './schema.gql'; diff --git a/x-pack/legacy/plugins/siem/server/graphql/anomalies/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/resolvers.ts similarity index 55% rename from x-pack/legacy/plugins/siem/server/graphql/anomalies/resolvers.ts rename to x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/resolvers.ts index e7b7a640c58d2..35cebe4777dcf 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/anomalies/resolvers.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/resolvers.ts @@ -4,36 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Anomalies } from '../../lib/anomalies'; +import { MatrixHistogram } from '../../lib/matrix_histogram'; import { AppResolverOf, ChildResolverOf } from '../../lib/framework'; import { createOptions } from '../../utils/build_query/create_options'; import { QuerySourceResolver } from '../sources/resolvers'; import { SourceResolvers } from '../types'; -export interface AnomaliesResolversDeps { - anomalies: Anomalies; +export interface MatrixHistogramResolversDeps { + matrixHistogram: MatrixHistogram; } -type QueryAnomaliesOverTimeResolver = ChildResolverOf< - AppResolverOf<SourceResolvers.AnomaliesHistogramResolver>, +type QueryMatrixHistogramResolver = ChildResolverOf< + AppResolverOf<SourceResolvers.MatrixHistogramResolver>, QuerySourceResolver >; -export const createAnomaliesResolvers = ( - libs: AnomaliesResolversDeps +export const createMatrixHistogramResolvers = ( + libs: MatrixHistogramResolversDeps ): { Source: { - AnomaliesHistogram: QueryAnomaliesOverTimeResolver; + MatrixHistogram: QueryMatrixHistogramResolver; }; } => ({ Source: { - async AnomaliesHistogram(source, args, { req }, info) { + async MatrixHistogram(source, args, { req }, info) { const options = { ...createOptions(source, args, info), - defaultIndex: args.defaultIndex, stackByField: args.stackByField, + histogramType: args.histogramType, }; - return libs.anomalies.getAnomaliesOverTime(req, options); + return libs.matrixHistogram.getMatrixHistogramData(req, options); }, }, }); diff --git a/x-pack/legacy/plugins/siem/server/graphql/alerts/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/schema.gql.ts similarity index 57% rename from x-pack/legacy/plugins/siem/server/graphql/alerts/schema.gql.ts rename to x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/schema.gql.ts index ca91468b1e0f2..deda6dc6e5c1a 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/alerts/schema.gql.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/matrix_histogram/schema.gql.ts @@ -6,19 +6,34 @@ import gql from 'graphql-tag'; -export const alertsSchema = gql` - type AlertsOverTimeData { +export const matrixHistogramSchema = gql` + type MatrixOverTimeHistogramData { + x: Float + y: Float + g: String + } + + type MatrixHistogramOverTimeData { inspect: Inspect matrixHistogramData: [MatrixOverTimeHistogramData!]! totalCount: Float! } + enum HistogramType { + authentications + anomalies + events + alerts + dns + } + extend type Source { - AlertsHistogram( + MatrixHistogram( filterQuery: String defaultIndex: [String!]! timerange: TimerangeInput! - stackByField: String - ): AlertsOverTimeData! + stackByField: String! + histogramType: HistogramType! + ): MatrixHistogramOverTimeData! } `; diff --git a/x-pack/legacy/plugins/siem/server/graphql/network/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/network/resolvers.ts index 06d6b8c516d8b..db15babc42a72 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/network/resolvers.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/network/resolvers.ts @@ -7,7 +7,7 @@ import { SourceResolvers } from '../../graphql/types'; import { AppResolverOf, ChildResolverOf } from '../../lib/framework'; import { Network } from '../../lib/network'; -import { createOptionsPaginated, createOptions } from '../../utils/build_query/create_options'; +import { createOptionsPaginated } from '../../utils/build_query/create_options'; import { QuerySourceResolver } from '../sources/resolvers'; type QueryNetworkTopCountriesResolver = ChildResolverOf< @@ -30,10 +30,6 @@ type QueryDnsResolver = ChildResolverOf< QuerySourceResolver >; -type QueryDnsHistogramResolver = ChildResolverOf< - AppResolverOf<SourceResolvers.NetworkDnsHistogramResolver>, - QuerySourceResolver ->; export interface NetworkResolversDeps { network: Network; } @@ -46,7 +42,6 @@ export const createNetworkResolvers = ( NetworkTopCountries: QueryNetworkTopCountriesResolver; NetworkTopNFlow: QueryNetworkTopNFlowResolver; NetworkDns: QueryDnsResolver; - NetworkDnsHistogram: QueryDnsHistogramResolver; }; } => ({ Source: { @@ -84,12 +79,5 @@ export const createNetworkResolvers = ( }; return libs.network.getNetworkDns(req, options); }, - async NetworkDnsHistogram(source, args, { req }, info) { - const options = { - ...createOptions(source, args, info), - stackByField: args.stackByField, - }; - return libs.network.getNetworkDnsHistogramData(req, options); - }, }, }); diff --git a/x-pack/legacy/plugins/siem/server/graphql/types.ts b/x-pack/legacy/plugins/siem/server/graphql/types.ts index c3fd6e9dde286..f42da48f2c1da 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/types.ts @@ -303,6 +303,14 @@ export enum FlowTarget { source = 'source', } +export enum HistogramType { + authentications = 'authentications', + anomalies = 'anomalies', + events = 'events', + alerts = 'alerts', + dns = 'dns', +} + export enum FlowTargetSourceDest { destination = 'destination', source = 'source', @@ -462,22 +470,14 @@ export interface Source { configuration: SourceConfiguration; /** The status of the source */ status: SourceStatus; - - AlertsHistogram: AlertsOverTimeData; - - AnomaliesHistogram: AnomaliesOverTimeData; /** Gets Authentication success and failures based on a timerange */ Authentications: AuthenticationsData; - AuthenticationsHistogram: AuthenticationsOverTimeData; - Timeline: TimelineData; TimelineDetails: TimelineDetailsData; LastEventTime: LastEventTimeData; - - EventsHistogram: EventsOverTimeData; /** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */ Hosts: HostsData; @@ -495,6 +495,8 @@ export interface Source { KpiHostDetails: KpiHostDetailsData; + MatrixHistogram: MatrixHistogramOverTimeData; + NetworkTopCountries: NetworkTopCountriesData; NetworkTopNFlow: NetworkTopNFlowData; @@ -568,36 +570,6 @@ export interface IndexField { format?: Maybe<string>; } -export interface AlertsOverTimeData { - inspect?: Maybe<Inspect>; - - matrixHistogramData: MatrixOverTimeHistogramData[]; - - totalCount: number; -} - -export interface Inspect { - dsl: string[]; - - response: string[]; -} - -export interface MatrixOverTimeHistogramData { - x: number; - - y: number; - - g: string; -} - -export interface AnomaliesOverTimeData { - inspect?: Maybe<Inspect>; - - matrixHistogramData: MatrixOverTimeHistogramData[]; - - totalCount: number; -} - export interface AuthenticationsData { edges: AuthenticationsEdges[]; @@ -732,12 +704,10 @@ export interface PageInfoPaginated { showMorePagesIndicator: boolean; } -export interface AuthenticationsOverTimeData { - inspect?: Maybe<Inspect>; - - matrixHistogramData: MatrixOverTimeHistogramData[]; +export interface Inspect { + dsl: string[]; - totalCount: number; + response: string[]; } export interface TimelineData { @@ -1392,14 +1362,6 @@ export interface LastEventTimeData { inspect?: Maybe<Inspect>; } -export interface EventsOverTimeData { - inspect?: Maybe<Inspect>; - - matrixHistogramData: MatrixOverTimeHistogramData[]; - - totalCount: number; -} - export interface HostsData { edges: HostsEdges[]; @@ -1600,6 +1562,22 @@ export interface KpiHostDetailsData { inspect?: Maybe<Inspect>; } +export interface MatrixHistogramOverTimeData { + inspect?: Maybe<Inspect>; + + matrixHistogramData: MatrixOverTimeHistogramData[]; + + totalCount: number; +} + +export interface MatrixOverTimeHistogramData { + x?: Maybe<number>; + + y?: Maybe<number>; + + g?: Maybe<string>; +} + export interface NetworkTopCountriesData { edges: NetworkTopCountriesEdges[]; @@ -2243,24 +2221,6 @@ export interface GetAllTimelineQueryArgs { onlyUserFavorite?: Maybe<boolean>; } -export interface AlertsHistogramSourceArgs { - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - timerange: TimerangeInput; - - stackByField?: Maybe<string>; -} -export interface AnomaliesHistogramSourceArgs { - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - stackByField?: Maybe<string>; -} export interface AuthenticationsSourceArgs { timerange: TimerangeInput; @@ -2270,15 +2230,6 @@ export interface AuthenticationsSourceArgs { defaultIndex: string[]; } -export interface AuthenticationsHistogramSourceArgs { - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - stackByField?: Maybe<string>; -} export interface TimelineSourceArgs { pagination: PaginationInput; @@ -2308,15 +2259,6 @@ export interface LastEventTimeSourceArgs { defaultIndex: string[]; } -export interface EventsHistogramSourceArgs { - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - stackByField?: Maybe<string>; -} export interface HostsSourceArgs { id?: Maybe<string>; @@ -2399,6 +2341,17 @@ export interface KpiHostDetailsSourceArgs { defaultIndex: string[]; } +export interface MatrixHistogramSourceArgs { + filterQuery?: Maybe<string>; + + defaultIndex: string[]; + + timerange: TimerangeInput; + + stackByField: string; + + histogramType: HistogramType; +} export interface NetworkTopCountriesSourceArgs { id?: Maybe<string>; @@ -2910,26 +2863,14 @@ export namespace SourceResolvers { configuration?: ConfigurationResolver<SourceConfiguration, TypeParent, TContext>; /** The status of the source */ status?: StatusResolver<SourceStatus, TypeParent, TContext>; - - AlertsHistogram?: AlertsHistogramResolver<AlertsOverTimeData, TypeParent, TContext>; - - AnomaliesHistogram?: AnomaliesHistogramResolver<AnomaliesOverTimeData, TypeParent, TContext>; /** Gets Authentication success and failures based on a timerange */ Authentications?: AuthenticationsResolver<AuthenticationsData, TypeParent, TContext>; - AuthenticationsHistogram?: AuthenticationsHistogramResolver< - AuthenticationsOverTimeData, - TypeParent, - TContext - >; - Timeline?: TimelineResolver<TimelineData, TypeParent, TContext>; TimelineDetails?: TimelineDetailsResolver<TimelineDetailsData, TypeParent, TContext>; LastEventTime?: LastEventTimeResolver<LastEventTimeData, TypeParent, TContext>; - - EventsHistogram?: EventsHistogramResolver<EventsOverTimeData, TypeParent, TContext>; /** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */ Hosts?: HostsResolver<HostsData, TypeParent, TContext>; @@ -2947,6 +2888,8 @@ export namespace SourceResolvers { KpiHostDetails?: KpiHostDetailsResolver<KpiHostDetailsData, TypeParent, TContext>; + MatrixHistogram?: MatrixHistogramResolver<MatrixHistogramOverTimeData, TypeParent, TContext>; + NetworkTopCountries?: NetworkTopCountriesResolver< NetworkTopCountriesData, TypeParent, @@ -2987,36 +2930,6 @@ export namespace SourceResolvers { Parent, TContext >; - export type AlertsHistogramResolver< - R = AlertsOverTimeData, - Parent = Source, - TContext = SiemContext - > = Resolver<R, Parent, TContext, AlertsHistogramArgs>; - export interface AlertsHistogramArgs { - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - timerange: TimerangeInput; - - stackByField?: Maybe<string>; - } - - export type AnomaliesHistogramResolver< - R = AnomaliesOverTimeData, - Parent = Source, - TContext = SiemContext - > = Resolver<R, Parent, TContext, AnomaliesHistogramArgs>; - export interface AnomaliesHistogramArgs { - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - stackByField?: Maybe<string>; - } - export type AuthenticationsResolver< R = AuthenticationsData, Parent = Source, @@ -3032,21 +2945,6 @@ export namespace SourceResolvers { defaultIndex: string[]; } - export type AuthenticationsHistogramResolver< - R = AuthenticationsOverTimeData, - Parent = Source, - TContext = SiemContext - > = Resolver<R, Parent, TContext, AuthenticationsHistogramArgs>; - export interface AuthenticationsHistogramArgs { - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - stackByField?: Maybe<string>; - } - export type TimelineResolver< R = TimelineData, Parent = Source, @@ -3094,21 +2992,6 @@ export namespace SourceResolvers { defaultIndex: string[]; } - export type EventsHistogramResolver< - R = EventsOverTimeData, - Parent = Source, - TContext = SiemContext - > = Resolver<R, Parent, TContext, EventsHistogramArgs>; - export interface EventsHistogramArgs { - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - stackByField?: Maybe<string>; - } - export type HostsResolver<R = HostsData, Parent = Source, TContext = SiemContext> = Resolver< R, Parent, @@ -3241,6 +3124,23 @@ export namespace SourceResolvers { defaultIndex: string[]; } + export type MatrixHistogramResolver< + R = MatrixHistogramOverTimeData, + Parent = Source, + TContext = SiemContext + > = Resolver<R, Parent, TContext, MatrixHistogramArgs>; + export interface MatrixHistogramArgs { + filterQuery?: Maybe<string>; + + defaultIndex: string[]; + + timerange: TimerangeInput; + + stackByField: string; + + histogramType: HistogramType; + } + export type NetworkTopCountriesResolver< R = NetworkTopCountriesData, Parent = Source, @@ -3579,111 +3479,6 @@ export namespace IndexFieldResolvers { > = Resolver<R, Parent, TContext>; } -export namespace AlertsOverTimeDataResolvers { - export interface Resolvers<TContext = SiemContext, TypeParent = AlertsOverTimeData> { - inspect?: InspectResolver<Maybe<Inspect>, TypeParent, TContext>; - - matrixHistogramData?: MatrixHistogramDataResolver< - MatrixOverTimeHistogramData[], - TypeParent, - TContext - >; - - totalCount?: TotalCountResolver<number, TypeParent, TContext>; - } - - export type InspectResolver< - R = Maybe<Inspect>, - Parent = AlertsOverTimeData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; - export type MatrixHistogramDataResolver< - R = MatrixOverTimeHistogramData[], - Parent = AlertsOverTimeData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; - export type TotalCountResolver< - R = number, - Parent = AlertsOverTimeData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; -} - -export namespace InspectResolvers { - export interface Resolvers<TContext = SiemContext, TypeParent = Inspect> { - dsl?: DslResolver<string[], TypeParent, TContext>; - - response?: ResponseResolver<string[], TypeParent, TContext>; - } - - export type DslResolver<R = string[], Parent = Inspect, TContext = SiemContext> = Resolver< - R, - Parent, - TContext - >; - export type ResponseResolver<R = string[], Parent = Inspect, TContext = SiemContext> = Resolver< - R, - Parent, - TContext - >; -} - -export namespace MatrixOverTimeHistogramDataResolvers { - export interface Resolvers<TContext = SiemContext, TypeParent = MatrixOverTimeHistogramData> { - x?: XResolver<number, TypeParent, TContext>; - - y?: YResolver<number, TypeParent, TContext>; - - g?: GResolver<string, TypeParent, TContext>; - } - - export type XResolver< - R = number, - Parent = MatrixOverTimeHistogramData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; - export type YResolver< - R = number, - Parent = MatrixOverTimeHistogramData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; - export type GResolver< - R = string, - Parent = MatrixOverTimeHistogramData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; -} - -export namespace AnomaliesOverTimeDataResolvers { - export interface Resolvers<TContext = SiemContext, TypeParent = AnomaliesOverTimeData> { - inspect?: InspectResolver<Maybe<Inspect>, TypeParent, TContext>; - - matrixHistogramData?: MatrixHistogramDataResolver< - MatrixOverTimeHistogramData[], - TypeParent, - TContext - >; - - totalCount?: TotalCountResolver<number, TypeParent, TContext>; - } - - export type InspectResolver< - R = Maybe<Inspect>, - Parent = AnomaliesOverTimeData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; - export type MatrixHistogramDataResolver< - R = MatrixOverTimeHistogramData[], - Parent = AnomaliesOverTimeData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; - export type TotalCountResolver< - R = number, - Parent = AnomaliesOverTimeData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; -} - export namespace AuthenticationsDataResolvers { export interface Resolvers<TContext = SiemContext, TypeParent = AuthenticationsData> { edges?: EdgesResolver<AuthenticationsEdges[], TypeParent, TContext>; @@ -4129,34 +3924,23 @@ export namespace PageInfoPaginatedResolvers { > = Resolver<R, Parent, TContext>; } -export namespace AuthenticationsOverTimeDataResolvers { - export interface Resolvers<TContext = SiemContext, TypeParent = AuthenticationsOverTimeData> { - inspect?: InspectResolver<Maybe<Inspect>, TypeParent, TContext>; - - matrixHistogramData?: MatrixHistogramDataResolver< - MatrixOverTimeHistogramData[], - TypeParent, - TContext - >; +export namespace InspectResolvers { + export interface Resolvers<TContext = SiemContext, TypeParent = Inspect> { + dsl?: DslResolver<string[], TypeParent, TContext>; - totalCount?: TotalCountResolver<number, TypeParent, TContext>; + response?: ResponseResolver<string[], TypeParent, TContext>; } - export type InspectResolver< - R = Maybe<Inspect>, - Parent = AuthenticationsOverTimeData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; - export type MatrixHistogramDataResolver< - R = MatrixOverTimeHistogramData[], - Parent = AuthenticationsOverTimeData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; - export type TotalCountResolver< - R = number, - Parent = AuthenticationsOverTimeData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; + export type DslResolver<R = string[], Parent = Inspect, TContext = SiemContext> = Resolver< + R, + Parent, + TContext + >; + export type ResponseResolver<R = string[], Parent = Inspect, TContext = SiemContext> = Resolver< + R, + Parent, + TContext + >; } export namespace TimelineDataResolvers { @@ -6343,36 +6127,6 @@ export namespace LastEventTimeDataResolvers { > = Resolver<R, Parent, TContext>; } -export namespace EventsOverTimeDataResolvers { - export interface Resolvers<TContext = SiemContext, TypeParent = EventsOverTimeData> { - inspect?: InspectResolver<Maybe<Inspect>, TypeParent, TContext>; - - matrixHistogramData?: MatrixHistogramDataResolver< - MatrixOverTimeHistogramData[], - TypeParent, - TContext - >; - - totalCount?: TotalCountResolver<number, TypeParent, TContext>; - } - - export type InspectResolver< - R = Maybe<Inspect>, - Parent = EventsOverTimeData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; - export type MatrixHistogramDataResolver< - R = MatrixOverTimeHistogramData[], - Parent = EventsOverTimeData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; - export type TotalCountResolver< - R = number, - Parent = EventsOverTimeData, - TContext = SiemContext - > = Resolver<R, Parent, TContext>; -} - export namespace HostsDataResolvers { export interface Resolvers<TContext = SiemContext, TypeParent = HostsData> { edges?: EdgesResolver<HostsEdges[], TypeParent, TContext>; @@ -7077,6 +6831,62 @@ export namespace KpiHostDetailsDataResolvers { > = Resolver<R, Parent, TContext>; } +export namespace MatrixHistogramOverTimeDataResolvers { + export interface Resolvers<TContext = SiemContext, TypeParent = MatrixHistogramOverTimeData> { + inspect?: InspectResolver<Maybe<Inspect>, TypeParent, TContext>; + + matrixHistogramData?: MatrixHistogramDataResolver< + MatrixOverTimeHistogramData[], + TypeParent, + TContext + >; + + totalCount?: TotalCountResolver<number, TypeParent, TContext>; + } + + export type InspectResolver< + R = Maybe<Inspect>, + Parent = MatrixHistogramOverTimeData, + TContext = SiemContext + > = Resolver<R, Parent, TContext>; + export type MatrixHistogramDataResolver< + R = MatrixOverTimeHistogramData[], + Parent = MatrixHistogramOverTimeData, + TContext = SiemContext + > = Resolver<R, Parent, TContext>; + export type TotalCountResolver< + R = number, + Parent = MatrixHistogramOverTimeData, + TContext = SiemContext + > = Resolver<R, Parent, TContext>; +} + +export namespace MatrixOverTimeHistogramDataResolvers { + export interface Resolvers<TContext = SiemContext, TypeParent = MatrixOverTimeHistogramData> { + x?: XResolver<Maybe<number>, TypeParent, TContext>; + + y?: YResolver<Maybe<number>, TypeParent, TContext>; + + g?: GResolver<Maybe<string>, TypeParent, TContext>; + } + + export type XResolver< + R = Maybe<number>, + Parent = MatrixOverTimeHistogramData, + TContext = SiemContext + > = Resolver<R, Parent, TContext>; + export type YResolver< + R = Maybe<number>, + Parent = MatrixOverTimeHistogramData, + TContext = SiemContext + > = Resolver<R, Parent, TContext>; + export type GResolver< + R = Maybe<string>, + Parent = MatrixOverTimeHistogramData, + TContext = SiemContext + > = Resolver<R, Parent, TContext>; +} + export namespace NetworkTopCountriesDataResolvers { export interface Resolvers<TContext = SiemContext, TypeParent = NetworkTopCountriesData> { edges?: EdgesResolver<NetworkTopCountriesEdges[], TypeParent, TContext>; @@ -9224,10 +9034,6 @@ export type IResolvers<TContext = SiemContext> = { SourceFields?: SourceFieldsResolvers.Resolvers<TContext>; SourceStatus?: SourceStatusResolvers.Resolvers<TContext>; IndexField?: IndexFieldResolvers.Resolvers<TContext>; - AlertsOverTimeData?: AlertsOverTimeDataResolvers.Resolvers<TContext>; - Inspect?: InspectResolvers.Resolvers<TContext>; - MatrixOverTimeHistogramData?: MatrixOverTimeHistogramDataResolvers.Resolvers<TContext>; - AnomaliesOverTimeData?: AnomaliesOverTimeDataResolvers.Resolvers<TContext>; AuthenticationsData?: AuthenticationsDataResolvers.Resolvers<TContext>; AuthenticationsEdges?: AuthenticationsEdgesResolvers.Resolvers<TContext>; AuthenticationItem?: AuthenticationItemResolvers.Resolvers<TContext>; @@ -9240,7 +9046,7 @@ export type IResolvers<TContext = SiemContext> = { OsEcsFields?: OsEcsFieldsResolvers.Resolvers<TContext>; CursorType?: CursorTypeResolvers.Resolvers<TContext>; PageInfoPaginated?: PageInfoPaginatedResolvers.Resolvers<TContext>; - AuthenticationsOverTimeData?: AuthenticationsOverTimeDataResolvers.Resolvers<TContext>; + Inspect?: InspectResolvers.Resolvers<TContext>; TimelineData?: TimelineDataResolvers.Resolvers<TContext>; TimelineEdges?: TimelineEdgesResolvers.Resolvers<TContext>; TimelineItem?: TimelineItemResolvers.Resolvers<TContext>; @@ -9294,7 +9100,6 @@ export type IResolvers<TContext = SiemContext> = { TimelineDetailsData?: TimelineDetailsDataResolvers.Resolvers<TContext>; DetailItem?: DetailItemResolvers.Resolvers<TContext>; LastEventTimeData?: LastEventTimeDataResolvers.Resolvers<TContext>; - EventsOverTimeData?: EventsOverTimeDataResolvers.Resolvers<TContext>; HostsData?: HostsDataResolvers.Resolvers<TContext>; HostsEdges?: HostsEdgesResolvers.Resolvers<TContext>; HostItem?: HostItemResolvers.Resolvers<TContext>; @@ -9315,6 +9120,8 @@ export type IResolvers<TContext = SiemContext> = { KpiHostsData?: KpiHostsDataResolvers.Resolvers<TContext>; KpiHostHistogramData?: KpiHostHistogramDataResolvers.Resolvers<TContext>; KpiHostDetailsData?: KpiHostDetailsDataResolvers.Resolvers<TContext>; + MatrixHistogramOverTimeData?: MatrixHistogramOverTimeDataResolvers.Resolvers<TContext>; + MatrixOverTimeHistogramData?: MatrixOverTimeHistogramDataResolvers.Resolvers<TContext>; NetworkTopCountriesData?: NetworkTopCountriesDataResolvers.Resolvers<TContext>; NetworkTopCountriesEdges?: NetworkTopCountriesEdgesResolvers.Resolvers<TContext>; NetworkTopCountriesItem?: NetworkTopCountriesItemResolvers.Resolvers<TContext>; diff --git a/x-pack/legacy/plugins/siem/server/index.ts b/x-pack/legacy/plugins/siem/server/index.ts index 882475390ae98..8513f871cb6c1 100644 --- a/x-pack/legacy/plugins/siem/server/index.ts +++ b/x-pack/legacy/plugins/siem/server/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext } from '../../../../../src/core/server'; import { Plugin } from './plugin'; export const plugin = (context: PluginInitializerContext) => { diff --git a/x-pack/legacy/plugins/siem/server/init_server.ts b/x-pack/legacy/plugins/siem/server/init_server.ts index 1f4f1b176497f..6158a33c25cfa 100644 --- a/x-pack/legacy/plugins/siem/server/init_server.ts +++ b/x-pack/legacy/plugins/siem/server/init_server.ts @@ -6,7 +6,6 @@ import { IResolvers, makeExecutableSchema } from 'graphql-tools'; import { schemas } from './graphql'; -import { createAnomaliesResolvers } from './graphql/anomalies'; import { createAuthenticationsResolvers } from './graphql/authentications'; import { createScalarToStringArrayValueResolvers } from './graphql/ecs'; import { createEsValueResolvers, createEventsResolvers } from './graphql/events'; @@ -30,19 +29,18 @@ import { createUncommonProcessesResolvers } from './graphql/uncommon_processes'; import { createWhoAmIResolvers } from './graphql/who_am_i'; import { AppBackendLibs } from './lib/types'; import { createTlsResolvers } from './graphql/tls'; -import { createAlertsResolvers } from './graphql/alerts'; +import { createMatrixHistogramResolvers } from './graphql/matrix_histogram'; export const initServer = (libs: AppBackendLibs) => { const schema = makeExecutableSchema({ resolvers: [ - createAlertsResolvers(libs) as IResolvers, - createAnomaliesResolvers(libs) as IResolvers, createAuthenticationsResolvers(libs) as IResolvers, createEsValueResolvers() as IResolvers, createEventsResolvers(libs) as IResolvers, createHostsResolvers(libs) as IResolvers, createIpDetailsResolvers(libs) as IResolvers, createKpiNetworkResolvers(libs) as IResolvers, + createMatrixHistogramResolvers(libs) as IResolvers, createNoteResolvers(libs) as IResolvers, createPinnedEventResolvers(libs) as IResolvers, createSourcesResolvers(libs) as IResolvers, diff --git a/x-pack/legacy/plugins/siem/server/kibana.index.ts b/x-pack/legacy/plugins/siem/server/kibana.index.ts deleted file mode 100644 index bab7936005c04..0000000000000 --- a/x-pack/legacy/plugins/siem/server/kibana.index.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { PluginInitializerContext } from 'src/core/server'; - -import { signalRulesAlertType } from './lib/detection_engine/signals/signal_rule_alert_type'; -import { createRulesRoute } from './lib/detection_engine/routes/rules/create_rules_route'; -import { createIndexRoute } from './lib/detection_engine/routes/index/create_index_route'; -import { readIndexRoute } from './lib/detection_engine/routes/index/read_index_route'; -import { readRulesRoute } from './lib/detection_engine/routes/rules/read_rules_route'; -import { findRulesRoute } from './lib/detection_engine/routes/rules/find_rules_route'; -import { deleteRulesRoute } from './lib/detection_engine/routes/rules/delete_rules_route'; -import { patchRulesRoute } from './lib/detection_engine/routes/rules/patch_rules_route'; -import { setSignalsStatusRoute } from './lib/detection_engine/routes/signals/open_close_signals_route'; -import { querySignalsRoute } from './lib/detection_engine/routes/signals/query_signals_route'; -import { ServerFacade } from './types'; -import { deleteIndexRoute } from './lib/detection_engine/routes/index/delete_index_route'; -import { isAlertExecutor } from './lib/detection_engine/signals/types'; -import { readTagsRoute } from './lib/detection_engine/routes/tags/read_tags_route'; -import { readPrivilegesRoute } from './lib/detection_engine/routes/privileges/read_privileges_route'; -import { addPrepackedRulesRoute } from './lib/detection_engine/routes/rules/add_prepackaged_rules_route'; -import { createRulesBulkRoute } from './lib/detection_engine/routes/rules/create_rules_bulk_route'; -import { patchRulesBulkRoute } from './lib/detection_engine/routes/rules/patch_rules_bulk_route'; -import { deleteRulesBulkRoute } from './lib/detection_engine/routes/rules/delete_rules_bulk_route'; -import { importRulesRoute } from './lib/detection_engine/routes/rules/import_rules_route'; -import { exportRulesRoute } from './lib/detection_engine/routes/rules/export_rules_route'; -import { findRulesStatusesRoute } from './lib/detection_engine/routes/rules/find_rules_status_route'; -import { getPrepackagedRulesStatusRoute } from './lib/detection_engine/routes/rules/get_prepackaged_rules_status_route'; -import { updateRulesRoute } from './lib/detection_engine/routes/rules/update_rules_route'; -import { updateRulesBulkRoute } from './lib/detection_engine/routes/rules/update_rules_bulk_route'; - -const APP_ID = 'siem'; - -export const initServerWithKibana = (context: PluginInitializerContext, __legacy: ServerFacade) => { - const logger = context.logger.get('plugins', APP_ID); - const version = context.env.packageInfo.version; - - if (__legacy.plugins.alerting != null) { - const type = signalRulesAlertType({ logger, version }); - if (isAlertExecutor(type)) { - __legacy.plugins.alerting.setup.registerType(type); - } - } - - // Detection Engine Rule routes that have the REST endpoints of /api/detection_engine/rules - // All REST rule creation, deletion, updating, etc... - createRulesRoute(__legacy); - readRulesRoute(__legacy); - updateRulesRoute(__legacy); - deleteRulesRoute(__legacy); - findRulesRoute(__legacy); - patchRulesRoute(__legacy); - - addPrepackedRulesRoute(__legacy); - getPrepackagedRulesStatusRoute(__legacy); - createRulesBulkRoute(__legacy); - updateRulesBulkRoute(__legacy); - deleteRulesBulkRoute(__legacy); - patchRulesBulkRoute(__legacy); - - importRulesRoute(__legacy); - exportRulesRoute(__legacy); - - findRulesStatusesRoute(__legacy); - - // Detection Engine Signals routes that have the REST endpoints of /api/detection_engine/signals - // POST /api/detection_engine/signals/status - // Example usage can be found in siem/server/lib/detection_engine/scripts/signals - setSignalsStatusRoute(__legacy); - querySignalsRoute(__legacy); - - // Detection Engine index routes that have the REST endpoints of /api/detection_engine/index - // All REST index creation, policy management for spaces - createIndexRoute(__legacy); - readIndexRoute(__legacy); - deleteIndexRoute(__legacy); - - // Detection Engine tags routes that have the REST endpoints of /api/detection_engine/tags - readTagsRoute(__legacy); - - // Privileges API to get the generic user privileges - readPrivilegesRoute(__legacy); -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/alerts/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/alerts/elasticsearch_adapter.ts deleted file mode 100644 index cedd781596812..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/alerts/elasticsearch_adapter.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get, getOr } from 'lodash/fp'; - -import { AlertsOverTimeData, MatrixOverTimeHistogramData } from '../../graphql/types'; - -import { inspectStringifyObject } from '../../utils/build_query'; - -import { FrameworkAdapter, FrameworkRequest, MatrixHistogramRequestOptions } from '../framework'; -import { buildAlertsHistogramQuery } from './query.dsl'; - -import { AlertsAdapter, AlertsGroupData, AlertsBucket } from './types'; -import { TermAggregation } from '../types'; -import { EventHit } from '../events/types'; - -export class ElasticsearchAlertsAdapter implements AlertsAdapter { - constructor(private readonly framework: FrameworkAdapter) {} - - public async getAlertsHistogramData( - request: FrameworkRequest, - options: MatrixHistogramRequestOptions - ): Promise<AlertsOverTimeData> { - const dsl = buildAlertsHistogramQuery(options); - const response = await this.framework.callWithRequest<EventHit, TermAggregation>( - request, - 'search', - dsl - ); - const totalCount = getOr(0, 'hits.total.value', response); - const matrixHistogramData = getOr([], 'aggregations.alertsByModuleGroup.buckets', response); - const inspect = { - dsl: [inspectStringifyObject(dsl)], - response: [inspectStringifyObject(response)], - }; - return { - inspect, - matrixHistogramData: getAlertsOverTimeByModule(matrixHistogramData), - totalCount, - }; - } -} - -const getAlertsOverTimeByModule = (data: AlertsGroupData[]): MatrixOverTimeHistogramData[] => { - let result: MatrixOverTimeHistogramData[] = []; - data.forEach(({ key: group, alerts }) => { - const alertsData: AlertsBucket[] = get('buckets', alerts); - - result = [ - ...result, - ...alertsData.map(({ key, doc_count }: AlertsBucket) => ({ - x: key, - y: doc_count, - g: group, - })), - ]; - }); - - return result; -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/alerts/index.ts b/x-pack/legacy/plugins/siem/server/lib/alerts/index.ts deleted file mode 100644 index 9cfb1841edfef..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/alerts/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { FrameworkRequest, MatrixHistogramRequestOptions } from '../framework'; -export * from './elasticsearch_adapter'; -import { AlertsAdapter } from './types'; -import { AlertsOverTimeData } from '../../graphql/types'; - -export class Alerts { - constructor(private readonly adapter: AlertsAdapter) {} - - public async getAlertsHistogramData( - req: FrameworkRequest, - options: MatrixHistogramRequestOptions - ): Promise<AlertsOverTimeData> { - return this.adapter.getAlertsHistogramData(req, options); - } -} diff --git a/x-pack/legacy/plugins/siem/server/lib/alerts/types.ts b/x-pack/legacy/plugins/siem/server/lib/alerts/types.ts deleted file mode 100644 index 67da38e8052d2..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/alerts/types.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { AlertsOverTimeData } from '../../graphql/types'; -import { FrameworkRequest, MatrixHistogramRequestOptions } from '../framework'; - -export interface AlertsBucket { - key: number; - doc_count: number; -} - -export interface AlertsGroupData { - key: string; - doc_count: number; - alerts: { - buckets: AlertsBucket[]; - }; -} -export interface AlertsAdapter { - getAlertsHistogramData( - request: FrameworkRequest, - options: MatrixHistogramRequestOptions - ): Promise<AlertsOverTimeData>; -} diff --git a/x-pack/legacy/plugins/siem/server/lib/anomalies/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/anomalies/elasticsearch_adapter.ts deleted file mode 100644 index 0955bc69c7c93..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/anomalies/elasticsearch_adapter.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr } from 'lodash/fp'; - -import { AnomaliesOverTimeData } from '../../graphql/types'; -import { inspectStringifyObject } from '../../utils/build_query'; -import { FrameworkAdapter, FrameworkRequest, MatrixHistogramRequestOptions } from '../framework'; -import { TermAggregation } from '../types'; - -import { AnomalyHit, AnomaliesAdapter, AnomaliesActionGroupData } from './types'; -import { buildAnomaliesOverTimeQuery } from './query.anomalies_over_time.dsl'; -import { MatrixOverTimeHistogramData } from '../../../public/graphql/types'; - -export class ElasticsearchAnomaliesAdapter implements AnomaliesAdapter { - constructor(private readonly framework: FrameworkAdapter) {} - - public async getAnomaliesOverTime( - request: FrameworkRequest, - options: MatrixHistogramRequestOptions - ): Promise<AnomaliesOverTimeData> { - const dsl = buildAnomaliesOverTimeQuery(options); - - const response = await this.framework.callWithRequest<AnomalyHit, TermAggregation>( - request, - 'search', - dsl - ); - - const totalCount = getOr(0, 'hits.total.value', response); - const anomaliesOverTimeBucket = getOr([], 'aggregations.anomalyActionGroup.buckets', response); - - const inspect = { - dsl: [inspectStringifyObject(dsl)], - response: [inspectStringifyObject(response)], - }; - return { - inspect, - matrixHistogramData: getAnomaliesOverTimeByJobId(anomaliesOverTimeBucket), - totalCount, - }; - } -} - -const getAnomaliesOverTimeByJobId = ( - data: AnomaliesActionGroupData[] -): MatrixOverTimeHistogramData[] => { - let result: MatrixOverTimeHistogramData[] = []; - data.forEach(({ key: group, anomalies }) => { - const anomaliesData = getOr([], 'buckets', anomalies).map( - ({ key, doc_count }: { key: number; doc_count: number }) => ({ - x: key, - y: doc_count, - g: group, - }) - ); - result = [...result, ...anomaliesData]; - }); - - return result; -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/anomalies/types.ts b/x-pack/legacy/plugins/siem/server/lib/anomalies/types.ts deleted file mode 100644 index 9fde81da63ec7..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/anomalies/types.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { AnomaliesOverTimeData } from '../../graphql/types'; -import { FrameworkRequest, MatrixHistogramRequestOptions } from '../framework'; -import { SearchHit } from '../types'; - -export interface AnomaliesAdapter { - getAnomaliesOverTime( - req: FrameworkRequest, - options: MatrixHistogramRequestOptions - ): Promise<AnomaliesOverTimeData>; -} - -export interface AnomalySource { - [field: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any -} - -export interface AnomalyHit extends SearchHit { - sort: string[]; - _source: AnomalySource; - aggregations: { - [agg: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any - }; -} - -interface AnomaliesOverTimeHistogramData { - key_as_string: string; - key: number; - doc_count: number; -} - -export interface AnomaliesActionGroupData { - key: number; - anomalies: { - bucket: AnomaliesOverTimeHistogramData[]; - }; - doc_count: number; -} diff --git a/x-pack/legacy/plugins/siem/server/lib/authentications/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/authentications/elasticsearch_adapter.ts index 85008adcd985f..79f13ce4461e5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/authentications/elasticsearch_adapter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/authentications/elasticsearch_adapter.ts @@ -6,50 +6,20 @@ import { getOr } from 'lodash/fp'; -import { - AuthenticationsData, - AuthenticationsEdges, - AuthenticationsOverTimeData, - MatrixOverTimeHistogramData, -} from '../../graphql/types'; +import { AuthenticationsData, AuthenticationsEdges } from '../../graphql/types'; import { mergeFieldsWithHit, inspectStringifyObject } from '../../utils/build_query'; -import { - FrameworkAdapter, - FrameworkRequest, - RequestOptionsPaginated, - MatrixHistogramRequestOptions, -} from '../framework'; +import { FrameworkAdapter, FrameworkRequest, RequestOptionsPaginated } from '../framework'; import { TermAggregation } from '../types'; import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants'; import { auditdFieldsMap, buildQuery } from './query.dsl'; -import { buildAuthenticationsOverTimeQuery } from './query.authentications_over_time.dsl'; import { AuthenticationBucket, AuthenticationData, AuthenticationHit, AuthenticationsAdapter, - AuthenticationsActionGroupData, } from './types'; -const getAuthenticationsOverTimeByAuthenticationResult = ( - data: AuthenticationsActionGroupData[] -): MatrixOverTimeHistogramData[] => { - let result: MatrixOverTimeHistogramData[] = []; - data.forEach(({ key: group, events }) => { - const eventsData = getOr([], 'buckets', events).map( - ({ key, doc_count }: { key: number; doc_count: number }) => ({ - x: key, - y: doc_count, - g: group, - }) - ); - result = [...result, ...eventsData]; - }); - - return result; -}; - export class ElasticsearchAuthenticationAdapter implements AuthenticationsAdapter { constructor(private readonly framework: FrameworkAdapter) {} @@ -109,35 +79,6 @@ export class ElasticsearchAuthenticationAdapter implements AuthenticationsAdapte }, }; } - - public async getAuthenticationsOverTime( - request: FrameworkRequest, - options: MatrixHistogramRequestOptions - ): Promise<AuthenticationsOverTimeData> { - const dsl = buildAuthenticationsOverTimeQuery(options); - const response = await this.framework.callWithRequest<AuthenticationHit, TermAggregation>( - request, - 'search', - dsl - ); - const totalCount = getOr(0, 'hits.total.value', response); - const authenticationsOverTimeBucket = getOr( - [], - 'aggregations.eventActionGroup.buckets', - response - ); - const inspect = { - dsl: [inspectStringifyObject(dsl)], - response: [inspectStringifyObject(response)], - }; - return { - inspect, - matrixHistogramData: getAuthenticationsOverTimeByAuthenticationResult( - authenticationsOverTimeBucket - ), - totalCount, - }; - } } export const formatAuthenticationData = ( diff --git a/x-pack/legacy/plugins/siem/server/lib/authentications/index.ts b/x-pack/legacy/plugins/siem/server/lib/authentications/index.ts index bd5712c105f31..c1b93818943db 100644 --- a/x-pack/legacy/plugins/siem/server/lib/authentications/index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/authentications/index.ts @@ -5,14 +5,9 @@ */ import { AuthenticationsData } from '../../graphql/types'; -import { - FrameworkRequest, - RequestOptionsPaginated, - MatrixHistogramRequestOptions, -} from '../framework'; +import { FrameworkRequest, RequestOptionsPaginated } from '../framework'; import { AuthenticationsAdapter } from './types'; -import { AuthenticationsOverTimeData } from '../../../public/graphql/types'; export class Authentications { constructor(private readonly adapter: AuthenticationsAdapter) {} @@ -23,11 +18,4 @@ export class Authentications { ): Promise<AuthenticationsData> { return this.adapter.getAuthentications(req, options); } - - public async getAuthenticationsOverTime( - req: FrameworkRequest, - options: MatrixHistogramRequestOptions - ): Promise<AuthenticationsOverTimeData> { - return this.adapter.getAuthenticationsOverTime(req, options); - } } diff --git a/x-pack/legacy/plugins/siem/server/lib/authentications/types.ts b/x-pack/legacy/plugins/siem/server/lib/authentications/types.ts index e1ec871ff4b58..2d2c7ba547c09 100644 --- a/x-pack/legacy/plugins/siem/server/lib/authentications/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/authentications/types.ts @@ -4,16 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - AuthenticationsData, - AuthenticationsOverTimeData, - LastSourceHost, -} from '../../graphql/types'; -import { - FrameworkRequest, - RequestOptionsPaginated, - MatrixHistogramRequestOptions, -} from '../framework'; +import { AuthenticationsData, LastSourceHost } from '../../graphql/types'; +import { FrameworkRequest, RequestOptionsPaginated } from '../framework'; import { Hit, SearchHit, TotalHit } from '../types'; export interface AuthenticationsAdapter { @@ -21,10 +13,6 @@ export interface AuthenticationsAdapter { req: FrameworkRequest, options: RequestOptionsPaginated ): Promise<AuthenticationsData>; - getAuthenticationsOverTime( - req: FrameworkRequest, - options: MatrixHistogramRequestOptions - ): Promise<AuthenticationsOverTimeData>; } type StringOrNumber = string | number; @@ -72,17 +60,3 @@ export interface AuthenticationData extends SearchHit { }; }; } - -interface AuthenticationsOverTimeHistogramData { - key_as_string: string; - key: number; - doc_count: number; -} - -export interface AuthenticationsActionGroupData { - key: number; - events: { - bucket: AuthenticationsOverTimeHistogramData[]; - }; - doc_count: number; -} diff --git a/x-pack/legacy/plugins/siem/server/lib/case/saved_object_mappings_temp.ts b/x-pack/legacy/plugins/siem/server/lib/case/saved_object_mappings.ts similarity index 77% rename from x-pack/legacy/plugins/siem/server/lib/case/saved_object_mappings_temp.ts rename to x-pack/legacy/plugins/siem/server/lib/case/saved_object_mappings.ts index bd73805600a33..80cdb9e979a68 100644 --- a/x-pack/legacy/plugins/siem/server/lib/case/saved_object_mappings_temp.ts +++ b/x-pack/legacy/plugins/siem/server/lib/case/saved_object_mappings.ts @@ -5,10 +5,7 @@ */ /* eslint-disable @typescript-eslint/no-empty-interface */ /* eslint-disable @typescript-eslint/camelcase */ -import { - NewCaseFormatted, - NewCommentFormatted, -} from '../../../../../../../x-pack/plugins/case/server'; +import { CaseAttributes, CommentAttributes } from '../../../../../../../x-pack/plugins/case/server'; import { ElasticsearchMappingOf } from '../../utils/typed_elasticsearch_mappings'; // Temporary file to write mappings for case @@ -19,20 +16,10 @@ export const caseSavedObjectType = 'case-workflow'; export const caseCommentSavedObjectType = 'case-workflow-comment'; export const caseSavedObjectMappings: { - [caseSavedObjectType]: ElasticsearchMappingOf<NewCaseFormatted>; + [caseSavedObjectType]: ElasticsearchMappingOf<CaseAttributes>; } = { [caseSavedObjectType]: { properties: { - assignees: { - properties: { - username: { - type: 'keyword', - }, - full_name: { - type: 'keyword', - }, - }, - }, created_at: { type: 'date', }, @@ -58,15 +45,15 @@ export const caseSavedObjectMappings: { tags: { type: 'keyword', }, - case_type: { - type: 'keyword', + updated_at: { + type: 'date', }, }, }, }; export const caseCommentSavedObjectMappings: { - [caseCommentSavedObjectType]: ElasticsearchMappingOf<NewCommentFormatted>; + [caseCommentSavedObjectType]: ElasticsearchMappingOf<CommentAttributes>; } = { [caseCommentSavedObjectType]: { properties: { @@ -86,6 +73,9 @@ export const caseCommentSavedObjectMappings: { }, }, }, + updated_at: { + type: 'date', + }, }, }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts b/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts index 30fdf7520a3ed..9c46f3320e37e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts +++ b/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts @@ -4,11 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup, PluginInitializerContext } from '../../../../../../../src/core/server'; -import { PluginsSetup } from '../../plugin'; +import { CoreSetup, SetupPlugins } from '../../plugin'; -import { Anomalies } from '../anomalies'; -import { ElasticsearchAnomaliesAdapter } from '../anomalies/elasticsearch_adapter'; import { Authentications } from '../authentications'; import { ElasticsearchAuthenticationAdapter } from '../authentications/elasticsearch_adapter'; import { ElasticsearchEventsAdapter, Events } from '../events'; @@ -33,14 +30,14 @@ import { ElasticsearchUncommonProcessesAdapter, UncommonProcesses } from '../unc import { Note } from '../note/saved_object'; import { PinnedEvent } from '../pinned_event/saved_object'; import { Timeline } from '../timeline/saved_object'; -import { Alerts, ElasticsearchAlertsAdapter } from '../alerts'; +import { ElasticsearchMatrixHistogramAdapter, MatrixHistogram } from '../matrix_histogram'; export function compose( core: CoreSetup, - plugins: PluginsSetup, - env: PluginInitializerContext['env'] + plugins: SetupPlugins, + isProductionMode: boolean ): AppBackendLibs { - const framework = new KibanaBackendFrameworkAdapter(core, plugins, env); + const framework = new KibanaBackendFrameworkAdapter(core, plugins, isProductionMode); const sources = new Sources(new ConfigurationSourcesAdapter()); const sourceStatus = new SourceStatus(new ElasticsearchSourceStatusAdapter(framework)); @@ -49,8 +46,6 @@ export function compose( const pinnedEvent = new PinnedEvent(); const domainLibs: AppDomainLibs = { - alerts: new Alerts(new ElasticsearchAlertsAdapter(framework)), - anomalies: new Anomalies(new ElasticsearchAnomaliesAdapter(framework)), authentications: new Authentications(new ElasticsearchAuthenticationAdapter(framework)), events: new Events(new ElasticsearchEventsAdapter(framework)), fields: new IndexFields(new ElasticsearchIndexFieldAdapter(framework)), @@ -59,6 +54,7 @@ export function compose( tls: new TLS(new ElasticsearchTlsAdapter(framework)), kpiHosts: new KpiHosts(new ElasticsearchKpiHostsAdapter(framework)), kpiNetwork: new KpiNetwork(new ElasticsearchKpiNetworkAdapter(framework)), + matrixHistogram: new MatrixHistogram(new ElasticsearchMatrixHistogramAdapter(framework)), network: new Network(new ElasticsearchNetworkAdapter(framework)), overview: new Overview(new ElasticsearchOverviewAdapter(framework)), uncommonProcesses: new UncommonProcesses(new ElasticsearchUncommonProcessesAdapter(framework)), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts index dff6e7136bff2..253bccad2e9f8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts @@ -4,18 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; import { CallWithRequest } from '../types'; // See the reference(s) below on explanations about why -000001 was chosen and // why the is_write_index is true as well as the bootstrapping step which is needed. // Ref: https://www.elastic.co/guide/en/elasticsearch/reference/current/applying-policy-to-template.html export const createBootstrapIndex = async ( - callWithRequest: CallWithRequest< - { path: string; method: 'PUT'; body: unknown }, - CallClusterOptions, - boolean - >, + callWithRequest: CallWithRequest<{ path: string; method: 'PUT'; body: unknown }, boolean>, index: string ): Promise<unknown> => { return callWithRequest('transport.request', { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts index b1d8f994615ae..d165bf69f1da1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts @@ -5,11 +5,10 @@ */ import { IndicesDeleteParams } from 'elasticsearch'; -import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; import { CallWithRequest } from '../types'; export const deleteAllIndex = async ( - callWithRequest: CallWithRequest<IndicesDeleteParams, CallClusterOptions, boolean>, + callWithRequest: CallWithRequest<IndicesDeleteParams, boolean>, index: string ): Promise<boolean> => { return callWithRequest('indices.delete', { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_policy.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_policy.ts index aa31c427ec84f..00213e271c7e8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_policy.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_policy.ts @@ -7,7 +7,7 @@ import { CallWithRequest } from '../types'; export const deletePolicy = async ( - callWithRequest: CallWithRequest<{ path: string; method: 'DELETE' }, {}, unknown>, + callWithRequest: CallWithRequest<{ path: string; method: 'DELETE' }, unknown>, policy: string ): Promise<unknown> => { return callWithRequest('transport.request', { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_template.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_template.ts index 63c32d13ccb8d..3402c25fb1ab1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_template.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_template.ts @@ -5,11 +5,10 @@ */ import { IndicesDeleteTemplateParams } from 'elasticsearch'; -import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; import { CallWithRequest } from '../types'; export const deleteTemplate = async ( - callWithRequest: CallWithRequest<IndicesDeleteTemplateParams, CallClusterOptions, unknown>, + callWithRequest: CallWithRequest<IndicesDeleteTemplateParams, unknown>, name: string ): Promise<unknown> => { return callWithRequest('indices.deleteTemplate', { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts index 705f542b50124..d81f23a283451 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts @@ -9,7 +9,6 @@ import { CallWithRequest } from '../types'; export const getIndexExists = async ( callWithRequest: CallWithRequest< { index: string; size: number; terminate_after: number; allow_no_indices: boolean }, - {}, { _shards: { total: number } } >, index: string diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts index d5ab1a10180c0..8a54ceac8ab78 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts @@ -7,7 +7,7 @@ import { CallWithRequest } from '../types'; export const getPolicyExists = async ( - callWithRequest: CallWithRequest<{ path: string; method: 'GET' }, {}, unknown>, + callWithRequest: CallWithRequest<{ path: string; method: 'GET' }, unknown>, policy: string ): Promise<boolean> => { try { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts index fac402155619e..fd5eec8db4140 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts @@ -5,11 +5,10 @@ */ import { IndicesExistsTemplateParams } from 'elasticsearch'; -import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; import { CallWithRequest } from '../types'; export const getTemplateExists = async ( - callWithRequest: CallWithRequest<IndicesExistsTemplateParams, CallClusterOptions, boolean>, + callWithRequest: CallWithRequest<IndicesExistsTemplateParams, boolean>, template: string ): Promise<boolean> => { return callWithRequest('indices.existsTemplate', { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/read_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/read_index.ts index 0abe2b992b780..ca987f85c446c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/read_index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/read_index.ts @@ -5,11 +5,10 @@ */ import { IndicesGetSettingsParams } from 'elasticsearch'; -import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; import { CallWithRequest } from '../types'; export const readIndex = async ( - callWithRequest: CallWithRequest<IndicesGetSettingsParams, CallClusterOptions, unknown>, + callWithRequest: CallWithRequest<IndicesGetSettingsParams, unknown>, index: string ): Promise<unknown> => { return callWithRequest('indices.get', { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_policy.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_policy.ts index fae28bab749ca..90d5bf9a9871b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_policy.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_policy.ts @@ -7,7 +7,7 @@ import { CallWithRequest } from '../types'; export const setPolicy = async ( - callWithRequest: CallWithRequest<{ path: string; method: 'PUT'; body: unknown }, {}, unknown>, + callWithRequest: CallWithRequest<{ path: string; method: 'PUT'; body: unknown }, unknown>, policy: string, body: unknown ): Promise<unknown> => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_template.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_template.ts index dc9ad5dda9f7d..0894f930feffb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_template.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_template.ts @@ -5,11 +5,10 @@ */ import { IndicesPutTemplateParams } from 'elasticsearch'; -import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; import { CallWithRequest } from '../types'; export const setTemplate = async ( - callWithRequest: CallWithRequest<IndicesPutTemplateParams, CallClusterOptions, unknown>, + callWithRequest: CallWithRequest<IndicesPutTemplateParams, unknown>, name: string, body: unknown ): Promise<unknown> => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/privileges/read_privileges.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/privileges/read_privileges.ts index a93be40738e57..01819eb4703fb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/privileges/read_privileges.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/privileges/read_privileges.ts @@ -7,7 +7,7 @@ import { CallWithRequest } from '../types'; export const readPrivileges = async ( - callWithRequest: CallWithRequest<unknown, unknown, unknown>, + callWithRequest: CallWithRequest<{}, unknown>, index: string ): Promise<unknown> => { return callWithRequest('transport.request', { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/_mock_server.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/_mock_server.ts deleted file mode 100644 index 5b85012fd9f08..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/_mock_server.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Hapi from 'hapi'; -import { KibanaConfig } from 'src/legacy/server/kbn_server'; -import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; -import { savedObjectsClientMock } from '../../../../../../../../../src/core/server/mocks'; -import { alertsClientMock } from '../../../../../../alerting/server/alerts_client.mock'; -import { actionsClientMock } from '../../../../../../../../plugins/actions/server/mocks'; -import { APP_ID, SIGNALS_INDEX_KEY } from '../../../../../common/constants'; -import { ServerFacade } from '../../../../types'; - -const defaultConfig = { - 'kibana.index': '.kibana', - [`xpack.${APP_ID}.${SIGNALS_INDEX_KEY}`]: '.siem-signals', -}; - -const isKibanaConfig = (config: unknown): config is KibanaConfig => - Object.getOwnPropertyDescriptor(config, 'get') != null && - Object.getOwnPropertyDescriptor(config, 'has') != null; - -const assertNever = (): never => { - throw new Error('Unexpected object'); -}; - -const createMockKibanaConfig = (config: Record<string, string>): KibanaConfig => { - const returnConfig = { - get(key: string) { - return config[key]; - }, - has(key: string) { - return config[key] != null; - }, - }; - if (isKibanaConfig(returnConfig)) { - return returnConfig; - } else { - return assertNever(); - } -}; - -export const createMockServer = (config: Record<string, string> = defaultConfig) => { - const server = new Hapi.Server({ - port: 0, - }); - - server.config = () => createMockKibanaConfig(config); - - const actionsClient = actionsClientMock.create(); - const alertsClient = alertsClientMock.create(); - const savedObjectsClient = savedObjectsClientMock.create(); - const elasticsearch = { - getCluster: jest.fn().mockImplementation(() => ({ - callWithRequest: jest.fn(), - })), - }; - server.decorate('request', 'getAlertsClient', () => alertsClient); - server.plugins.elasticsearch = (elasticsearch as unknown) as ElasticsearchPlugin; - server.plugins.spaces = { getSpaceId: () => 'default' }; - server.plugins.actions = { - getActionsClientWithRequest: () => actionsClient, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; // The types have really bad conflicts at the moment so I have to use any - server.decorate('request', 'getSavedObjectsClient', () => savedObjectsClient); - return { - server: server as ServerFacade & Hapi.Server, - alertsClient, - actionsClient, - elasticsearch, - savedObjectsClient, - }; -}; - -export const createMockServerWithoutAlertClientDecoration = ( - config: Record<string, string> = defaultConfig -) => { - const serverWithoutAlertClient = new Hapi.Server({ - port: 0, - }); - - const savedObjectsClient = savedObjectsClientMock.create(); - serverWithoutAlertClient.config = () => createMockKibanaConfig(config); - serverWithoutAlertClient.decorate('request', 'getSavedObjectsClient', () => savedObjectsClient); - serverWithoutAlertClient.plugins.actions = { - getActionsClientWithRequest: () => actionsClient, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; // The types have really bad conflicts at the moment so I have to use any - - const actionsClient = actionsClientMock.create(); - - return { - serverWithoutAlertClient: serverWithoutAlertClient as ServerFacade & Hapi.Server, - actionsClient, - }; -}; - -export const getMockIndexName = () => - jest.fn().mockImplementation(() => ({ - callWithRequest: jest.fn().mockImplementationOnce(() => 'index-name'), - })); - -export const getMockEmptyIndex = () => - jest.fn().mockImplementation(() => ({ - callWithRequest: jest.fn().mockImplementation(() => ({ _shards: { total: 0 } })), - })); - -export const getMockNonEmptyIndex = () => - jest.fn().mockImplementation(() => ({ - callWithRequest: jest.fn().mockImplementation(() => ({ _shards: { total: 1 } })), - })); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/clients_service_mock.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/clients_service_mock.ts new file mode 100644 index 0000000000000..f89e938b8a636 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/clients_service_mock.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + elasticsearchServiceMock, + savedObjectsClientMock, +} from '../../../../../../../../../src/core/server/mocks'; +import { alertsClientMock } from '../../../../../../alerting/server/alerts_client.mock'; +import { ActionsClient } from '../../../../../../../../plugins/actions/server'; +import { actionsClientMock } from '../../../../../../../../plugins/actions/server/mocks'; +import { GetScopedClients } from '../../../../services'; + +const createClients = () => ({ + actionsClient: actionsClientMock.create() as jest.Mocked<ActionsClient>, + alertsClient: alertsClientMock.create(), + clusterClient: elasticsearchServiceMock.createScopedClusterClient(), + savedObjectsClient: savedObjectsClientMock.create(), + spacesClient: { getSpaceId: jest.fn() }, +}); + +const createGetScoped = () => + jest.fn(() => Promise.resolve(createClients()) as ReturnType<GetScopedClients>); + +const createClientsServiceMock = () => { + return { + setup: jest.fn(), + start: jest.fn(), + createGetScoped, + }; +}; + +export const clientsServiceMock = { + create: createClientsServiceMock, + createGetScoped, + createClients, +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/index.ts new file mode 100644 index 0000000000000..250b006814294 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; + +export { clientsServiceMock } from './clients_service_mock'; + +export const createMockServer = () => { + const server = new Hapi.Server({ port: 0 }); + + return { + route: server.route.bind(server), + inject: server.inject.bind(server), + }; +}; + +export const createMockConfig = () => () => ({ + get: jest.fn(), + has: jest.fn(), +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index b008ead8df948..f380b82c1e05f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -17,6 +17,7 @@ import { INTERNAL_IMMUTABLE_KEY, DETECTION_ENGINE_PREPACKAGED_URL, } from '../../../../../common/constants'; +import { ShardsResponse } from '../../../types'; import { RuleAlertType, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { RuleAlertParamsRest, PrepackagedRules } from '../../types'; @@ -413,3 +414,11 @@ export const getFindResultStatus = (): SavedObjectsFindResponse<IRuleSavedAttrib total: 0, saved_objects: [], }); + +export const getIndexName = () => 'index-name'; +export const getEmptyIndex = (): { _shards: Partial<ShardsResponse> } => ({ + _shards: { total: 0 }, +}); +export const getNonEmptyIndex = (): { _shards: Partial<ShardsResponse> } => ({ + _shards: { total: 1 }, +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts index e0d48836013ec..2502009a2e6a2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts @@ -7,9 +7,9 @@ import Hapi from 'hapi'; import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; -import signalsPolicy from './signals_policy.json'; -import { ServerFacade, RequestFacade } from '../../../../types'; -import { transformError, getIndex, callWithRequestFactory } from '../utils'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; +import { transformError, getIndex } from '../utils'; import { getIndexExists } from '../../index/get_index_exists'; import { getPolicyExists } from '../../index/get_policy_exists'; import { setPolicy } from '../../index/set_policy'; @@ -17,8 +17,12 @@ import { setTemplate } from '../../index/set_template'; import { getSignalsTemplate } from './get_signals_template'; import { getTemplateExists } from '../../index/get_template_exists'; import { createBootstrapIndex } from '../../index/create_bootstrap_index'; +import signalsPolicy from './signals_policy.json'; -export const createCreateIndexRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createCreateIndexRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'POST', path: DETECTION_ENGINE_INDEX_URL, @@ -30,11 +34,13 @@ export const createCreateIndexRoute = (server: ServerFacade): Hapi.ServerRoute = }, }, }, - async handler(request: RequestFacade, headers) { + async handler(request: LegacyRequest, headers) { try { - const index = getIndex(request, server); - const callWithRequest = callWithRequestFactory(request, server); - const indexExists = await getIndexExists(callWithRequest, index); + const { clusterClient, spacesClient } = await getClients(request); + const callCluster = clusterClient.callAsCurrentUser; + + const index = getIndex(spacesClient.getSpaceId, config); + const indexExists = await getIndexExists(callCluster, index); if (indexExists) { return headers .response({ @@ -43,16 +49,16 @@ export const createCreateIndexRoute = (server: ServerFacade): Hapi.ServerRoute = }) .code(409); } else { - const policyExists = await getPolicyExists(callWithRequest, index); + const policyExists = await getPolicyExists(callCluster, index); if (!policyExists) { - await setPolicy(callWithRequest, index, signalsPolicy); + await setPolicy(callCluster, index, signalsPolicy); } - const templateExists = await getTemplateExists(callWithRequest, index); + const templateExists = await getTemplateExists(callCluster, index); if (!templateExists) { const template = getSignalsTemplate(index); - await setTemplate(callWithRequest, index, template); + await setTemplate(callCluster, index, template); } - await createBootstrapIndex(callWithRequest, index); + await createBootstrapIndex(callCluster, index); return { acknowledged: true }; } } catch (err) { @@ -68,6 +74,10 @@ export const createCreateIndexRoute = (server: ServerFacade): Hapi.ServerRoute = }; }; -export const createIndexRoute = (server: ServerFacade) => { - server.route(createCreateIndexRoute(server)); +export const createIndexRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +) => { + route(createCreateIndexRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts index c1edc824b81eb..ae61afb6f8d06 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts @@ -7,8 +7,9 @@ import Hapi from 'hapi'; import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; -import { ServerFacade, RequestFacade } from '../../../../types'; -import { transformError, getIndex, callWithRequestFactory } from '../utils'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; +import { transformError, getIndex } from '../utils'; import { getIndexExists } from '../../index/get_index_exists'; import { getPolicyExists } from '../../index/get_policy_exists'; import { deletePolicy } from '../../index/delete_policy'; @@ -26,7 +27,10 @@ import { deleteTemplate } from '../../index/delete_template'; * * And ensuring they're all gone */ -export const createDeleteIndexRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createDeleteIndexRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'DELETE', path: DETECTION_ENGINE_INDEX_URL, @@ -38,11 +42,13 @@ export const createDeleteIndexRoute = (server: ServerFacade): Hapi.ServerRoute = }, }, }, - async handler(request: RequestFacade, headers) { + async handler(request: LegacyRequest, headers) { try { - const index = getIndex(request, server); - const callWithRequest = callWithRequestFactory(request, server); - const indexExists = await getIndexExists(callWithRequest, index); + const { clusterClient, spacesClient } = await getClients(request); + const callCluster = clusterClient.callAsCurrentUser; + + const index = getIndex(spacesClient.getSpaceId, config); + const indexExists = await getIndexExists(callCluster, index); if (!indexExists) { return headers .response({ @@ -51,14 +57,14 @@ export const createDeleteIndexRoute = (server: ServerFacade): Hapi.ServerRoute = }) .code(404); } else { - await deleteAllIndex(callWithRequest, `${index}-*`); - const policyExists = await getPolicyExists(callWithRequest, index); + await deleteAllIndex(callCluster, `${index}-*`); + const policyExists = await getPolicyExists(callCluster, index); if (policyExists) { - await deletePolicy(callWithRequest, index); + await deletePolicy(callCluster, index); } - const templateExists = await getTemplateExists(callWithRequest, index); + const templateExists = await getTemplateExists(callCluster, index); if (templateExists) { - await deleteTemplate(callWithRequest, index); + await deleteTemplate(callCluster, index); } return { acknowledged: true }; } @@ -75,6 +81,10 @@ export const createDeleteIndexRoute = (server: ServerFacade): Hapi.ServerRoute = }; }; -export const deleteIndexRoute = (server: ServerFacade) => { - server.route(createDeleteIndexRoute(server)); +export const deleteIndexRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +) => { + route(createDeleteIndexRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts index 1a5018d446747..41be42f7c0fe1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts @@ -7,11 +7,15 @@ import Hapi from 'hapi'; import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; -import { ServerFacade, RequestFacade } from '../../../../types'; -import { transformError, getIndex, callWithRequestFactory } from '../utils'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; +import { transformError, getIndex } from '../utils'; import { getIndexExists } from '../../index/get_index_exists'; -export const createReadIndexRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createReadIndexRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'GET', path: DETECTION_ENGINE_INDEX_URL, @@ -23,11 +27,14 @@ export const createReadIndexRoute = (server: ServerFacade): Hapi.ServerRoute => }, }, }, - async handler(request: RequestFacade, headers) { + async handler(request: LegacyRequest, headers) { try { - const index = getIndex(request, server); - const callWithRequest = callWithRequestFactory(request, server); - const indexExists = await getIndexExists(callWithRequest, index); + const { clusterClient, spacesClient } = await getClients(request); + const callCluster = clusterClient.callAsCurrentUser; + + const index = getIndex(spacesClient.getSpaceId, config); + const indexExists = await getIndexExists(callCluster, index); + if (indexExists) { // head request is used for if you want to get if the index exists // or not and it will return a content-length: 0 along with either a 200 or 404 @@ -62,6 +69,10 @@ export const createReadIndexRoute = (server: ServerFacade): Hapi.ServerRoute => }; }; -export const readIndexRoute = (server: ServerFacade) => { - server.route(createReadIndexRoute(server)); +export const readIndexRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +) => { + route(createReadIndexRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts index 1ea681afb7949..308ee95a77e20 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts @@ -4,35 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createMockServer } from '../__mocks__/_mock_server'; -import { getPrivilegeRequest, getMockPrivileges } from '../__mocks__/request_responses'; import { readPrivilegesRoute } from './read_privileges_route'; -import * as myUtils from '../utils'; +import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; +import { getPrivilegeRequest, getMockPrivileges } from '../__mocks__/request_responses'; describe('read_privileges', () => { - let { server, elasticsearch } = createMockServer(); + let { route, inject } = createMockServer(); + let config = createMockConfig(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { - jest.spyOn(myUtils, 'getIndex').mockReturnValue('fakeindex'); - ({ server, elasticsearch } = createMockServer()); - elasticsearch.getCluster = jest.fn(() => ({ - callWithRequest: jest.fn(() => getMockPrivileges()), - })); - readPrivilegesRoute(server); - }); - - afterEach(() => { jest.resetAllMocks(); + ({ route, inject } = createMockServer()); + + config = createMockConfig(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(getMockPrivileges()); + + readPrivilegesRoute(route, config, false, getClients); }); describe('normal status codes', () => { test('returns 200 when doing a normal request', async () => { - const { statusCode } = await server.inject(getPrivilegeRequest()); + const { statusCode } = await inject(getPrivilegeRequest()); expect(statusCode).toBe(200); }); test('returns the payload when doing a normal request', async () => { - const { payload } = await server.inject(getPrivilegeRequest()); + const { payload } = await inject(getPrivilegeRequest()); expect(JSON.parse(payload)).toEqual(getMockPrivileges()); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts index 45ecb7dc97288..e9b9bffbaf054 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts @@ -6,13 +6,19 @@ import Hapi from 'hapi'; import { merge } from 'lodash/fp'; + import { DETECTION_ENGINE_PRIVILEGES_URL } from '../../../../../common/constants'; +import { LegacyServices } from '../../../../types'; import { RulesRequest } from '../../rules/types'; -import { ServerFacade } from '../../../../types'; -import { callWithRequestFactory, transformError, getIndex } from '../utils'; +import { GetScopedClients } from '../../../../services'; +import { transformError, getIndex } from '../utils'; import { readPrivileges } from '../../privileges/read_privileges'; -export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createReadPrivilegesRulesRoute = ( + config: LegacyServices['config'], + usingEphemeralEncryptionKey: boolean, + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'GET', path: DETECTION_ENGINE_PRIVILEGES_URL, @@ -26,10 +32,10 @@ export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.Serve }, async handler(request: RulesRequest, headers) { try { - const callWithRequest = callWithRequestFactory(request, server); - const index = getIndex(request, server); - const permissions = await readPrivileges(callWithRequest, index); - const usingEphemeralEncryptionKey = server.usingEphemeralEncryptionKey; + const { clusterClient, spacesClient } = await getClients(request); + + const index = getIndex(spacesClient.getSpaceId, config); + const permissions = await readPrivileges(clusterClient.callAsCurrentUser, index); return merge(permissions, { is_authenticated: request?.auth?.isAuthenticated ?? false, has_encryption_key: !usingEphemeralEncryptionKey, @@ -47,6 +53,11 @@ export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.Serve }; }; -export const readPrivilegesRoute = (server: ServerFacade): void => { - server.route(createReadPrivilegesRulesRoute(server)); +export const readPrivilegesRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + usingEphemeralEncryptionKey: boolean, + getClients: GetScopedClients +) => { + route(createReadPrivilegesRulesRoute(config, usingEphemeralEncryptionKey, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts index ec86de84ff3c7..e018ed4cc22ff 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts @@ -4,12 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, - getMockEmptyIndex, - getMockNonEmptyIndex, -} from '../__mocks__/_mock_server'; +import { omit } from 'lodash/fp'; + import { createRulesRoute } from './create_rules_route'; import { getFindResult, @@ -17,7 +13,10 @@ import { createActionResult, addPrepackagedRulesRequest, getFindResultWithSingleHit, + getEmptyIndex, + getNonEmptyIndex, } from '../__mocks__/request_responses'; +import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; jest.mock('../../rules/get_prepackaged_rules', () => { return { @@ -48,45 +47,56 @@ import { addPrepackedRulesRoute } from './add_prepackaged_rules_route'; import { PrepackagedRules } from '../../types'; describe('add_prepackaged_rules_route', () => { - let { server, alertsClient, actionsClient, elasticsearch } = createMockServer(); + let server = createMockServer(); + let config = createMockConfig(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); - ({ server, alertsClient, actionsClient, elasticsearch } = createMockServer()); - elasticsearch.getCluster = getMockNonEmptyIndex(); - addPrepackedRulesRoute(server); + server = createMockServer(); + config = createMockConfig(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); + + addPrepackedRulesRoute(server.route, config, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when creating a with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { statusCode } = await server.inject(addPrepackagedRulesRequest()); expect(statusCode).toBe(200); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - createRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(addPrepackagedRulesRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { inject, route } = createMockServer(); + createRulesRoute(route, config, getClients); + const { statusCode } = await inject(addPrepackagedRulesRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('it returns a 400 if the index does not exist', async () => { - elasticsearch.getCluster = getMockEmptyIndex(); - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptyIndex()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { payload } = await server.inject(addPrepackagedRulesRequest()); expect(JSON.parse(payload)).toEqual({ - message: - 'Pre-packaged rules cannot be installed until the space index is created: .siem-signals-default', + message: expect.stringContaining( + 'Pre-packaged rules cannot be installed until the space index is created' + ), status_code: 400, }); }); @@ -94,10 +104,10 @@ describe('add_prepackaged_rules_route', () => { describe('payload', () => { test('1 rule is installed and 0 are updated when find results are empty', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { payload } = await server.inject(addPrepackagedRulesRequest()); expect(JSON.parse(payload)).toEqual({ rules_installed: 1, @@ -106,10 +116,10 @@ describe('add_prepackaged_rules_route', () => { }); test('1 rule is updated and 0 are installed when we return a single find and the versions are different', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { payload } = await server.inject(addPrepackagedRulesRequest()); expect(JSON.parse(payload)).toEqual({ rules_installed: 0, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index e796f21d9c03a..c4d0489486ef8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -5,21 +5,23 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../../common/constants'; -import { ServerFacade, RequestFacade } from '../../../../types'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { getIndexExists } from '../../index/get_index_exists'; -import { callWithRequestFactory, getIndex, transformError } from '../utils'; +import { getIndex, transformError } from '../utils'; import { getPrepackagedRules } from '../../rules/get_prepackaged_rules'; import { installPrepackagedRules } from '../../rules/install_prepacked_rules'; import { updatePrepackagedRules } from '../../rules/update_prepacked_rules'; import { getRulesToInstall } from '../../rules/get_rules_to_install'; import { getRulesToUpdate } from '../../rules/get_rules_to_update'; import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; -export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createAddPrepackedRulesRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'PUT', path: DETECTION_ENGINE_PREPACKAGED_URL, @@ -31,29 +33,32 @@ export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerR }, }, }, - async handler(request: RequestFacade, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) - ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { - return headers.response().code(404); - } - + async handler(request: LegacyRequest, headers) { try { - const callWithRequest = callWithRequestFactory(request, server); + const { + actionsClient, + alertsClient, + clusterClient, + savedObjectsClient, + spacesClient, + } = await getClients(request); + + if (!actionsClient || !alertsClient) { + return headers.response().code(404); + } + const rulesFromFileSystem = getPrepackagedRules(); const prepackagedRules = await getExistingPrepackagedRules({ alertsClient }); const rulesToInstall = getRulesToInstall(rulesFromFileSystem, prepackagedRules); const rulesToUpdate = getRulesToUpdate(rulesFromFileSystem, prepackagedRules); - const spaceIndex = getIndex(request, server); + const spaceIndex = getIndex(spacesClient.getSpaceId, config); if (rulesToInstall.length !== 0 || rulesToUpdate.length !== 0) { - const spaceIndexExists = await getIndexExists(callWithRequest, spaceIndex); + const spaceIndexExists = await getIndexExists( + clusterClient.callAsCurrentUser, + spaceIndex + ); if (!spaceIndexExists) { return headers .response({ @@ -90,6 +95,10 @@ export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerR }; }; -export const addPrepackedRulesRoute = (server: ServerFacade): void => { - server.route(createAddPrepackedRulesRoute(server)); +export const addPrepackedRulesRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +): void => { + route(createAddPrepackedRulesRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts index 5cf6d8955d8b2..664d27a7572ad 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -4,57 +4,66 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, - getMockEmptyIndex, -} from '../__mocks__/_mock_server'; -import { createRulesRoute } from './create_rules_route'; import { ServerInjectOptions } from 'hapi'; +import { omit } from 'lodash/fp'; + import { getFindResult, getResult, createActionResult, typicalPayload, getReadBulkRequest, + getEmptyIndex, } from '../__mocks__/request_responses'; +import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { createRulesBulkRoute } from './create_rules_bulk_route'; +import { BulkError } from '../utils'; +import { OutputRuleAlertRest } from '../../types'; describe('create_rules_bulk', () => { - let { server, alertsClient, actionsClient, elasticsearch } = createMockServer(); + let server = createMockServer(); + let config = createMockConfig(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); - ({ server, alertsClient, actionsClient, elasticsearch } = createMockServer()); - createRulesBulkRoute(server); + server = createMockServer(); + config = createMockConfig(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + getClients.mockResolvedValue(clients); + + createRulesBulkRoute(server.route, config, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when creating a single rule with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getReadBulkRequest()); expect(statusCode).toBe(200); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - createRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getReadBulkRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { inject, route } = createMockServer(); + createRulesBulkRoute(route, config, getClients); + const { statusCode } = await inject(getReadBulkRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('it gets a 409 if the index does not exist', async () => { - elasticsearch.getCluster = getMockEmptyIndex(); - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptyIndex()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { payload } = await server.inject(getReadBulkRequest()); expect(JSON.parse(payload)).toEqual([ { @@ -69,10 +78,10 @@ describe('create_rules_bulk', () => { }); test('returns 200 if rule_id is not given as the id is auto generated from the alert framework', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); // missing rule_id should return 200 as it will be auto generated if not given const { rule_id, ...noRuleId } = typicalPayload(); const request: ServerInjectOptions = { @@ -85,10 +94,10 @@ describe('create_rules_bulk', () => { }); test('returns 200 if type is query', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'POST', @@ -105,10 +114,10 @@ describe('create_rules_bulk', () => { }); test('returns 400 if type is not filter or kql', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'POST', @@ -124,4 +133,34 @@ describe('create_rules_bulk', () => { expect(statusCode).toBe(400); }); }); + + test('returns 409 if duplicate rule_ids found in request payload', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + const request: ServerInjectOptions = { + method: 'POST', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, + payload: [typicalPayload(), typicalPayload()], + }; + const { payload } = await server.inject(request); + const output: Array<BulkError | Partial<OutputRuleAlertRest>> = JSON.parse(payload); + expect(output.some(item => item.error?.status_code === 409)).toBeTruthy(); + }); + + test('returns one error object in response when duplicate rule_ids found in request payload', async () => { + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + const request: ServerInjectOptions = { + method: 'POST', + url: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, + payload: [typicalPayload(), typicalPayload()], + }; + const { payload } = await server.inject(request); + const output: Array<BulkError | Partial<OutputRuleAlertRest>> = JSON.parse(payload); + expect(output.length).toBe(1); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 0ffa61e2e2bed..51b7b132fc794 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -5,25 +5,24 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; +import { countBy } from 'lodash/fp'; import uuid from 'uuid'; + import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { GetScopedClients } from '../../../../services'; +import { LegacyServices } from '../../../../types'; import { createRules } from '../../rules/create_rules'; import { BulkRulesRequest } from '../../rules/types'; -import { ServerFacade } from '../../../../types'; import { readRules } from '../../rules/read_rules'; -import { transformOrBulkError } from './utils'; +import { transformOrBulkError, getDuplicates } from './utils'; import { getIndexExists } from '../../index/get_index_exists'; -import { - callWithRequestFactory, - getIndex, - transformBulkError, - createBulkErrorObject, -} from '../utils'; +import { getIndex, transformBulkError, createBulkErrorObject } from '../utils'; import { createRulesBulkSchema } from '../schemas/create_rules_bulk_schema'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; -export const createCreateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createCreateRulesBulkRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'POST', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, @@ -37,109 +36,124 @@ export const createCreateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou }, }, async handler(request: BulkRulesRequest, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) + const { actionsClient, alertsClient, clusterClient, spacesClient } = await getClients( + request ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { + + if (!actionsClient || !alertsClient) { return headers.response().code(404); } + const ruleDefinitions = request.payload; + const mappedDuplicates = countBy('rule_id', ruleDefinitions); + const dupes = getDuplicates(mappedDuplicates); + const rules = await Promise.all( - request.payload.map(async payloadRule => { - const { - description, - enabled, - false_positives: falsePositives, - from, - query, - language, - output_index: outputIndex, - saved_id: savedId, - meta, - filters, - rule_id: ruleId, - index, - interval, - max_signals: maxSignals, - risk_score: riskScore, - name, - severity, - tags, - threat, - to, - type, - references, - timeline_id: timelineId, - timeline_title: timelineTitle, - version, - } = payloadRule; - const ruleIdOrUuid = ruleId ?? uuid.v4(); - try { - const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server); - const callWithRequest = callWithRequestFactory(request, server); - const indexExists = await getIndexExists(callWithRequest, finalIndex); - if (!indexExists) { - return createBulkErrorObject({ - ruleId: ruleIdOrUuid, - statusCode: 400, - message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, - }); - } - if (ruleId != null) { - const rule = await readRules({ alertsClient, ruleId }); - if (rule != null) { - return createBulkErrorObject({ - ruleId, - statusCode: 409, - message: `rule_id: "${ruleId}" already exists`, - }); - } - } - const createdRule = await createRules({ - alertsClient, - actionsClient, + ruleDefinitions + .filter(rule => rule.rule_id == null || !dupes.includes(rule.rule_id)) + .map(async payloadRule => { + const { description, enabled, - falsePositives, + false_positives: falsePositives, from, - immutable: false, query, language, - outputIndex: finalIndex, - savedId, - timelineId, - timelineTitle, + output_index: outputIndex, + saved_id: savedId, meta, filters, - ruleId: ruleIdOrUuid, + rule_id: ruleId, index, interval, - maxSignals, - riskScore, + max_signals: maxSignals, + risk_score: riskScore, name, severity, tags, + threat, to, type, - threat, references, + timeline_id: timelineId, + timeline_title: timelineTitle, version, - }); - return transformOrBulkError(ruleIdOrUuid, createdRule); - } catch (err) { - return transformBulkError(ruleIdOrUuid, err); - } - }) + } = payloadRule; + const ruleIdOrUuid = ruleId ?? uuid.v4(); + try { + const finalIndex = outputIndex ?? getIndex(spacesClient.getSpaceId, config); + const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex); + if (!indexExists) { + return createBulkErrorObject({ + ruleId: ruleIdOrUuid, + statusCode: 400, + message: `To create a rule, the index must exist first. Index ${finalIndex} does not exist`, + }); + } + if (ruleId != null) { + const rule = await readRules({ alertsClient, ruleId }); + if (rule != null) { + return createBulkErrorObject({ + ruleId, + statusCode: 409, + message: `rule_id: "${ruleId}" already exists`, + }); + } + } + const createdRule = await createRules({ + alertsClient, + actionsClient, + description, + enabled, + falsePositives, + from, + immutable: false, + query, + language, + outputIndex: finalIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + ruleId: ruleIdOrUuid, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + version, + }); + return transformOrBulkError(ruleIdOrUuid, createdRule); + } catch (err) { + return transformBulkError(ruleIdOrUuid, err); + } + }) ); - return rules; + return [ + ...rules, + ...dupes.map(ruleId => + createBulkErrorObject({ + ruleId, + statusCode: 409, + message: `rule_id: "${ruleId}" already exists`, + }) + ), + ]; }, }; }; -export const createRulesBulkRoute = (server: ServerFacade): void => { - server.route(createCreateRulesBulkRoute(server)); +export const createRulesBulkRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +): void => { + route(createCreateRulesBulkRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index e51634c0d2c07..4f28771db8ed7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -4,14 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, - getMockNonEmptyIndex, - getMockEmptyIndex, -} from '../__mocks__/_mock_server'; -import { createRulesRoute } from './create_rules_route'; import { ServerInjectOptions } from 'hapi'; +import { omit } from 'lodash/fp'; + +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { createRulesRoute } from './create_rules_route'; import { getFindResult, @@ -20,57 +17,58 @@ import { getCreateRequest, typicalPayload, getFindResultStatus, + getNonEmptyIndex, + getEmptyIndex, } from '../__mocks__/request_responses'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; describe('create_rules', () => { - let { - server, - alertsClient, - actionsClient, - elasticsearch, - savedObjectsClient, - } = createMockServer(); + let server = createMockServer(); + let config = createMockConfig(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); - ({ - server, - alertsClient, - actionsClient, - elasticsearch, - savedObjectsClient, - } = createMockServer()); - elasticsearch.getCluster = getMockNonEmptyIndex(); - createRulesRoute(server); + + server = createMockServer(); + config = createMockConfig(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); + + createRulesRoute(server.route, config, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when creating a single rule with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { statusCode } = await server.inject(getCreateRequest()); expect(statusCode).toBe(200); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - createRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getCreateRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + createRulesRoute(route, config, getClients); + const { statusCode } = await inject(getCreateRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('it returns a 400 if the index does not exist', async () => { - elasticsearch.getCluster = getMockEmptyIndex(); - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptyIndex()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { payload } = await server.inject(getCreateRequest()); expect(JSON.parse(payload)).toEqual({ message: 'To create a rule, the index must exist first. Index .siem-signals does not exist', @@ -79,11 +77,11 @@ describe('create_rules', () => { }); test('returns 200 if rule_id is not given as the id is auto generated from the alert framework', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); // missing rule_id should return 200 as it will be auto generated if not given const { rule_id, ...noRuleId } = typicalPayload(); const request: ServerInjectOptions = { @@ -96,11 +94,11 @@ describe('create_rules', () => { }); test('returns 200 if type is query', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'POST', @@ -115,11 +113,11 @@ describe('create_rules', () => { }); test('returns 400 if type is not filter or kql', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'POST', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts index de874f66d0444..19e772165628d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -5,21 +5,24 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; import uuid from 'uuid'; + import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { GetScopedClients } from '../../../../services'; +import { LegacyServices } from '../../../../types'; import { createRules } from '../../rules/create_rules'; import { RulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { createRulesSchema } from '../schemas/create_rules_schema'; -import { ServerFacade } from '../../../../types'; import { readRules } from '../../rules/read_rules'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { transform } from './utils'; import { getIndexExists } from '../../index/get_index_exists'; -import { callWithRequestFactory, getIndex, transformError } from '../utils'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; +import { getIndex, transformError } from '../utils'; -export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createCreateRulesRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'POST', path: DETECTION_ENGINE_RULES_URL, @@ -59,21 +62,21 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = type, references, } = request.payload; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) - ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { - return headers.response().code(404); - } - try { - const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server); - const callWithRequest = callWithRequestFactory(request, server); - const indexExists = await getIndexExists(callWithRequest, finalIndex); + const { + alertsClient, + actionsClient, + clusterClient, + savedObjectsClient, + spacesClient, + } = await getClients(request); + + if (!actionsClient || !alertsClient) { + return headers.response().code(404); + } + + const finalIndex = outputIndex ?? getIndex(spacesClient.getSpaceId, config); + const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex); if (!indexExists) { return headers .response({ @@ -157,6 +160,10 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = }; }; -export const createRulesRoute = (server: ServerFacade): void => { - server.route(createCreateRulesRoute(server)); +export const createRulesRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +): void => { + route(createCreateRulesRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts index e66fc765c08bf..855bf7f634c26 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts @@ -4,10 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, -} from '../__mocks__/_mock_server'; +import { omit } from 'lodash/fp'; import { ServerInjectOptions } from 'hapi'; import { @@ -20,70 +17,75 @@ import { getDeleteAsPostBulkRequestById, getFindResultStatus, } from '../__mocks__/request_responses'; +import { createMockServer, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { deleteRulesBulkRoute } from './delete_rules_bulk_route'; import { BulkError } from '../utils'; describe('delete_rules', () => { - let { server, alertsClient, savedObjectsClient } = createMockServer(); + let server = createMockServer(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { - ({ server, alertsClient, savedObjectsClient } = createMockServer()); - deleteRulesBulkRoute(server); - }); - - afterEach(() => { jest.resetAllMocks(); + + server = createMockServer(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + deleteRulesBulkRoute(server.route, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when deleting a single rule with a valid actionClient and alertClient by alertId', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); const { statusCode } = await server.inject(getDeleteBulkRequest()); expect(statusCode).toBe(200); }); test('returns 200 when deleting a single rule with a valid actionClient and alertClient by alertId using POST', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); const { statusCode } = await server.inject(getDeleteAsPostBulkRequest()); expect(statusCode).toBe(200); }); test('returns 200 when deleting a single rule with a valid actionClient and alertClient by id', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); const { statusCode } = await server.inject(getDeleteBulkRequestById()); expect(statusCode).toBe(200); }); test('returns 200 when deleting a single rule with a valid actionClient and alertClient by id using POST', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); const { statusCode } = await server.inject(getDeleteAsPostBulkRequestById()); expect(statusCode).toBe(200); }); test('returns 200 because the error is in the payload when deleting a single rule that does not exist with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); const { statusCode } = await server.inject(getDeleteBulkRequest()); expect(statusCode).toBe(200); }); test('returns 404 in the payload when deleting a single rule that does not exist with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); - savedObjectsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.savedObjectsClient.delete.mockResolvedValue({}); const { payload } = await server.inject(getDeleteBulkRequest()); const parsed: BulkError[] = JSON.parse(payload); const expected: BulkError[] = [ @@ -96,18 +98,19 @@ describe('delete_rules', () => { }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - deleteRulesBulkRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getDeleteBulkRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + deleteRulesBulkRoute(route, getClients); + const { statusCode } = await inject(getDeleteBulkRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('returns 400 if given a non-existent id in the payload', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); const request: ServerInjectOptions = { method: 'DELETE', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts index b3f8eafa24115..6438318cb43db 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts @@ -5,19 +5,18 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { deleteRules } from '../../rules/delete_rules'; -import { ServerFacade } from '../../../../types'; +import { LegacyServices } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { queryRulesBulkSchema } from '../schemas/query_rules_bulk_schema'; import { transformOrBulkError, getIdBulkError } from './utils'; import { transformBulkError } from '../utils'; import { QueryBulkRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; +import { deleteRules } from '../../rules/delete_rules'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; -export const createDeleteRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createDeleteRulesBulkRoute = (getClients: GetScopedClients): Hapi.ServerRoute => { return { method: ['POST', 'DELETE'], // allow both POST and DELETE in case their client does not support bodies in DELETE path: `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, @@ -31,14 +30,9 @@ export const createDeleteRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou }, }, async handler(request: QueryBulkRequest, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) - ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { + const { actionsClient, alertsClient, savedObjectsClient } = await getClients(request); + + if (!actionsClient || !alertsClient) { return headers.response().code(404); } const rules = await Promise.all( @@ -78,6 +72,9 @@ export const createDeleteRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou }; }; -export const deleteRulesBulkRoute = (server: ServerFacade): void => { - server.route(createDeleteRulesBulkRoute(server)); +export const deleteRulesBulkRoute = ( + route: LegacyServices['route'], + getClients: GetScopedClients +): void => { + route(createDeleteRulesBulkRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts index 0aa60d3bbd922..a0a6f61223279 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts @@ -4,13 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, -} from '../__mocks__/_mock_server'; - -import { deleteRulesRoute } from './delete_rules_route'; import { ServerInjectOptions } from 'hapi'; +import { omit } from 'lodash/fp'; +import { deleteRulesRoute } from './delete_rules_route'; import { getFindResult, @@ -20,64 +16,70 @@ import { getDeleteRequestById, getFindResultStatus, } from '../__mocks__/request_responses'; +import { createMockServer, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; describe('delete_rules', () => { - let { server, alertsClient, savedObjectsClient } = createMockServer(); + let server = createMockServer(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { - ({ server, alertsClient, savedObjectsClient } = createMockServer()); - deleteRulesRoute(server); - }); - - afterEach(() => { jest.resetAllMocks(); + server = createMockServer(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + + deleteRulesRoute(server.route, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when deleting a single rule with a valid actionClient and alertClient by alertId', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); - savedObjectsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.savedObjectsClient.delete.mockResolvedValue({}); const { statusCode } = await server.inject(getDeleteRequest()); expect(statusCode).toBe(200); }); test('returns 200 when deleting a single rule with a valid actionClient and alertClient by id', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); - savedObjectsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.savedObjectsClient.delete.mockResolvedValue({}); const { statusCode } = await server.inject(getDeleteRequestById()); expect(statusCode).toBe(200); }); test('returns 404 when deleting a single rule that does not exist with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); - savedObjectsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.savedObjectsClient.delete.mockResolvedValue({}); const { statusCode } = await server.inject(getDeleteRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - deleteRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getDeleteRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + deleteRulesRoute(route, getClients); + const { statusCode } = await inject(getDeleteRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('returns 400 if given a non-existent id', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); const request: ServerInjectOptions = { method: 'DELETE', url: DETECTION_ENGINE_RULES_URL, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts index e4d3787c90072..340782523b724 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts @@ -5,19 +5,18 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { deleteRules } from '../../rules/delete_rules'; -import { ServerFacade } from '../../../../types'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { queryRulesSchema } from '../schemas/query_rules_schema'; import { getIdError, transform } from './utils'; import { transformError } from '../utils'; import { QueryRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; -export const createDeleteRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createDeleteRulesRoute = (getClients: GetScopedClients): Hapi.ServerRoute => { return { method: 'DELETE', path: DETECTION_ENGINE_RULES_URL, @@ -30,20 +29,16 @@ export const createDeleteRulesRoute = (server: ServerFacade): Hapi.ServerRoute = query: queryRulesSchema, }, }, - async handler(request: QueryRequest, headers) { + async handler(request: QueryRequest & LegacyRequest, headers) { const { id, rule_id: ruleId } = request.query; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) - ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { - return headers.response().code(404); - } try { + const { actionsClient, alertsClient, savedObjectsClient } = await getClients(request); + + if (!actionsClient || !alertsClient) { + return headers.response().code(404); + } + const rule = await deleteRules({ actionsClient, alertsClient, @@ -95,6 +90,9 @@ export const createDeleteRulesRoute = (server: ServerFacade): Hapi.ServerRoute = }; }; -export const deleteRulesRoute = (server: ServerFacade): void => { - server.route(createDeleteRulesRoute(server)); +export const deleteRulesRoute = ( + route: LegacyServices['route'], + getClients: GetScopedClients +): void => { + route(createDeleteRulesRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts index 5da5ffcd58bf1..1966b06701803 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts @@ -5,17 +5,21 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; + import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { ExportRulesRequest } from '../../rules/types'; -import { ServerFacade } from '../../../../types'; import { getNonPackagedRulesCount } from '../../rules/get_existing_prepackaged_rules'; import { exportRulesSchema, exportRulesQuerySchema } from '../schemas/export_rules_schema'; import { getExportByObjectIds } from '../../rules/get_export_by_object_ids'; import { getExportAll } from '../../rules/get_export_all'; import { transformError } from '../utils'; -export const createExportRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createExportRulesRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'POST', path: `${DETECTION_ENGINE_RULES_URL}/_export`, @@ -29,15 +33,15 @@ export const createExportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = query: exportRulesQuerySchema, }, }, - async handler(request: ExportRulesRequest, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + async handler(request: ExportRulesRequest & LegacyRequest, headers) { + const { alertsClient } = await getClients(request); if (!alertsClient) { return headers.response().code(404); } try { - const exportSizeLimit = server.config().get<number>('savedObjects.maxImportExportSize'); + const exportSizeLimit = config().get<number>('savedObjects.maxImportExportSize'); if (request.payload?.objects != null && request.payload.objects.length > exportSizeLimit) { return headers .response({ @@ -82,6 +86,10 @@ export const createExportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = }; }; -export const exportRulesRoute = (server: ServerFacade): void => { - server.route(createExportRulesRoute(server)); +export const exportRulesRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +): void => { + route(createExportRulesRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts index 62c9f44da1e33..5b75f17164acf 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, -} from '../__mocks__/_mock_server'; +import { omit } from 'lodash/fp'; + +import { createMockServer } from '../__mocks__'; +import { clientsServiceMock } from '../__mocks__/clients_service_mock'; import { findRulesRoute } from './find_rules_route'; import { ServerInjectOptions } from 'hapi'; @@ -16,43 +16,49 @@ import { getFindResult, getResult, getFindRequest } from '../__mocks__/request_r import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; describe('find_rules', () => { - let { server, alertsClient, actionsClient } = createMockServer(); + let server = createMockServer(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { - ({ server, alertsClient, actionsClient } = createMockServer()); - findRulesRoute(server); - }); - - afterEach(() => { jest.resetAllMocks(); + + server = createMockServer(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + + findRulesRoute(server.route, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when finding a single rule with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - actionsClient.find.mockResolvedValue({ + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.actionsClient.find.mockResolvedValue({ page: 1, perPage: 1, total: 0, data: [], }); - alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getFindRequest()); expect(statusCode).toBe(200); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - findRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getFindRequest()); + const { route, inject } = createMockServer(); + getClients.mockResolvedValue(omit('alertsClient', clients)); + findRulesRoute(route, getClients); + const { statusCode } = await inject(getFindRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('returns 400 if a bad query parameter is given', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'GET', url: `${DETECTION_ENGINE_RULES_URL}/_find?invalid_value=500`, @@ -62,8 +68,8 @@ describe('find_rules', () => { }); test('returns 200 if the set of optional query parameters are given', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'GET', url: `${DETECTION_ENGINE_RULES_URL}/_find?page=2&per_page=20&sort_field=timestamp&fields=["field-1","field-2","field-3]`, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts index b15c1db7222cf..4297e4aebfd58 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts @@ -5,17 +5,17 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { findRules } from '../../rules/find_rules'; import { FindRulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { findRulesSchema } from '../schemas/find_rules_schema'; -import { ServerFacade } from '../../../../types'; import { transformFindAlerts } from './utils'; import { transformError } from '../utils'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -export const createFindRulesRoute = (): Hapi.ServerRoute => { +export const createFindRulesRoute = (getClients: GetScopedClients): Hapi.ServerRoute => { return { method: 'GET', path: `${DETECTION_ENGINE_RULES_URL}/_find`, @@ -28,17 +28,14 @@ export const createFindRulesRoute = (): Hapi.ServerRoute => { query: findRulesSchema, }, }, - async handler(request: FindRulesRequest, headers) { + async handler(request: FindRulesRequest & LegacyRequest, headers) { const { query } = request; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { - return headers.response().code(404); - } - try { + const { alertsClient, savedObjectsClient } = await getClients(request); + if (!alertsClient) { + return headers.response().code(404); + } + const rules = await findRules({ alertsClient, perPage: query.per_page, @@ -86,6 +83,6 @@ export const createFindRulesRoute = (): Hapi.ServerRoute => { }; }; -export const findRulesRoute = (server: ServerFacade) => { - server.route(createFindRulesRoute()); +export const findRulesRoute = (route: LegacyServices['route'], getClients: GetScopedClients) => { + route(createFindRulesRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts index 8b3113a044b5a..fe8742ff0b60c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts @@ -5,10 +5,11 @@ */ import Hapi from 'hapi'; -import { isFunction, snakeCase } from 'lodash/fp'; +import { snakeCase } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { ServerFacade } from '../../../../types'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { findRulesStatusesSchema } from '../schemas/find_rules_statuses_schema'; import { FindRulesStatusesRequest, @@ -29,7 +30,7 @@ const convertToSnakeCase = <T extends Record<string, any>>(obj: T): Partial<T> | }, {}); }; -export const createFindRulesStatusRoute: Hapi.ServerRoute = { +export const createFindRulesStatusRoute = (getClients: GetScopedClients): Hapi.ServerRoute => ({ method: 'GET', path: `${DETECTION_ENGINE_RULES_URL}/_find_statuses`, options: { @@ -41,19 +42,17 @@ export const createFindRulesStatusRoute: Hapi.ServerRoute = { query: findRulesStatusesSchema, }, }, - async handler(request: FindRulesStatusesRequest, headers) { + async handler(request: FindRulesStatusesRequest & LegacyRequest, headers) { const { query } = request; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { + const { alertsClient, savedObjectsClient } = await getClients(request); + + if (!alertsClient) { return headers.response().code(404); } // build return object with ids as keys and errors as values. /* looks like this - { + { "someAlertId": [{"myerrorobject": "some error value"}, etc..], "anotherAlertId": ... } @@ -86,8 +85,11 @@ export const createFindRulesStatusRoute: Hapi.ServerRoute = { }, Promise.resolve<RuleStatusResponse>({})); return statuses; }, -}; +}); -export const findRulesStatusesRoute = (server: ServerFacade): void => { - server.route(createFindRulesStatusRoute); +export const findRulesStatusesRoute = ( + route: LegacyServices['route'], + getClients: GetScopedClients +): void => { + route(createFindRulesStatusRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts index de7f0fe26cc74..8f27910a7e5e2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts @@ -4,19 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, - getMockNonEmptyIndex, -} from '../__mocks__/_mock_server'; -import { createRulesRoute } from './create_rules_route'; +import { omit } from 'lodash/fp'; + +import { getPrepackagedRulesStatusRoute } from './get_prepackaged_rules_status_route'; + import { getFindResult, getResult, createActionResult, getFindResultWithSingleHit, getPrepackagedRulesStatusRequest, + getNonEmptyIndex, } from '../__mocks__/request_responses'; +import { createMockServer, clientsServiceMock } from '../__mocks__'; jest.mock('../../rules/get_prepackaged_rules', () => { return { @@ -41,44 +41,49 @@ jest.mock('../../rules/get_prepackaged_rules', () => { }; }); -import { getPrepackagedRulesStatusRoute } from './get_prepackaged_rules_status_route'; - describe('get_prepackaged_rule_status_route', () => { - let { server, alertsClient, actionsClient, elasticsearch } = createMockServer(); + let server = createMockServer(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); - ({ server, alertsClient, actionsClient, elasticsearch } = createMockServer()); - elasticsearch.getCluster = getMockNonEmptyIndex(); - getPrepackagedRulesStatusRoute(server); + + server = createMockServer(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); + + getPrepackagedRulesStatusRoute(server.route, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when creating a with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getPrepackagedRulesStatusRequest()); expect(statusCode).toBe(200); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - createRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject( - getPrepackagedRulesStatusRequest() - ); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + getPrepackagedRulesStatusRoute(route, getClients); + const { statusCode } = await inject(getPrepackagedRulesStatusRequest()); expect(statusCode).toBe(404); }); }); describe('payload', () => { test('0 rules installed, 0 custom rules, 1 rules not installed, and 1 rule not updated', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { payload } = await server.inject(getPrepackagedRulesStatusRequest()); expect(JSON.parse(payload)).toEqual({ rules_custom_installed: 0, @@ -89,10 +94,10 @@ describe('get_prepackaged_rule_status_route', () => { }); test('1 rule installed, 1 custom rules, 0 rules not installed, and 1 rule to not updated', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.create.mockResolvedValue(createActionResult()); - alertsClient.create.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.create.mockResolvedValue(createActionResult()); + clients.alertsClient.create.mockResolvedValue(getResult()); const { payload } = await server.inject(getPrepackagedRulesStatusRequest()); expect(JSON.parse(payload)).toEqual({ rules_custom_installed: 1, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts index c999292ba7674..bee57d6b38127 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts @@ -5,10 +5,10 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_PREPACKAGED_URL } from '../../../../../common/constants'; -import { ServerFacade, RequestFacade } from '../../../../types'; +import { LegacyServices, LegacyRequest } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { transformError } from '../utils'; import { getPrepackagedRules } from '../../rules/get_prepackaged_rules'; import { getRulesToInstall } from '../../rules/get_rules_to_install'; @@ -16,7 +16,9 @@ import { getRulesToUpdate } from '../../rules/get_rules_to_update'; import { findRules } from '../../rules/find_rules'; import { getExistingPrepackagedRules } from '../../rules/get_existing_prepackaged_rules'; -export const createGetPrepackagedRulesStatusRoute = (): Hapi.ServerRoute => { +export const createGetPrepackagedRulesStatusRoute = ( + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'GET', path: `${DETECTION_ENGINE_PREPACKAGED_URL}/_status`, @@ -28,8 +30,8 @@ export const createGetPrepackagedRulesStatusRoute = (): Hapi.ServerRoute => { }, }, }, - async handler(request: RequestFacade, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + async handler(request: LegacyRequest, headers) { + const { alertsClient } = await getClients(request); if (!alertsClient) { return headers.response().code(404); @@ -67,6 +69,9 @@ export const createGetPrepackagedRulesStatusRoute = (): Hapi.ServerRoute => { }; }; -export const getPrepackagedRulesStatusRoute = (server: ServerFacade): void => { - server.route(createGetPrepackagedRulesStatusRoute()); +export const getPrepackagedRulesStatusRoute = ( + route: LegacyServices['route'], + getClients: GetScopedClients +): void => { + route(createGetPrepackagedRulesStatusRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index 5e87c99d815ef..a9188cc2adc12 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -5,40 +5,39 @@ */ import Hapi from 'hapi'; -import { chunk, isEmpty, isFunction } from 'lodash/fp'; +import { chunk, isEmpty } from 'lodash/fp'; import { extname } from 'path'; import uuid from 'uuid'; + import { createPromiseFromStreams } from '../../../../../../../../../src/legacy/utils/streams'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { LegacyServices, LegacyRequest } from '../../../../types'; import { createRules } from '../../rules/create_rules'; import { ImportRulesRequest } from '../../rules/types'; -import { ServerFacade } from '../../../../types'; import { readRules } from '../../rules/read_rules'; import { getIndexExists } from '../../index/get_index_exists'; -import { - callWithRequestFactory, - getIndex, - createBulkErrorObject, - ImportRuleResponse, -} from '../utils'; +import { getIndex, createBulkErrorObject, ImportRuleResponse } from '../utils'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { ImportRuleAlertRest } from '../../types'; import { patchRules } from '../../rules/patch_rules'; import { importRulesQuerySchema, importRulesPayloadSchema } from '../schemas/import_rules_schema'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; +import { GetScopedClients } from '../../../../services'; type PromiseFromStreams = ImportRuleAlertRest | Error; const CHUNK_PARSED_OBJECT_SIZE = 10; -export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createImportRulesRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'POST', path: `${DETECTION_ENGINE_RULES_URL}/_import`, options: { tags: ['access:siem'], payload: { - maxBytes: server.config().get('savedObjects.maxImportPayloadBytes'), + maxBytes: config().get('savedObjects.maxImportPayloadBytes'), output: 'stream', allow: 'multipart/form-data', }, @@ -50,17 +49,19 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = payload: importRulesPayloadSchema, }, }, - async handler(request: ImportRulesRequest, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) - ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { + async handler(request: ImportRulesRequest & LegacyRequest, headers) { + const { + actionsClient, + alertsClient, + clusterClient, + spacesClient, + savedObjectsClient, + } = await getClients(request); + + if (!actionsClient || !alertsClient) { return headers.response().code(404); } + const { filename } = request.payload.file.hapi; const fileExtension = extname(filename).toLowerCase(); if (fileExtension !== '.ndjson') { @@ -72,7 +73,7 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = .code(400); } - const objectLimit = server.config().get<number>('savedObjects.maxImportExportSize'); + const objectLimit = config().get<number>('savedObjects.maxImportExportSize'); const readStream = createRulesStreamFromNdJson(request.payload.file, objectLimit); const parsedObjects = await createPromiseFromStreams<PromiseFromStreams[]>([readStream]); const uniqueParsedObjects = Array.from( @@ -146,9 +147,11 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = version, } = parsedRule; try { - const finalIndex = getIndex(request, server); - const callWithRequest = callWithRequestFactory(request, server); - const indexExists = await getIndexExists(callWithRequest, finalIndex); + const finalIndex = getIndex(spacesClient.getSpaceId, config); + const indexExists = await getIndexExists( + clusterClient.callAsCurrentUser, + finalIndex + ); if (!indexExists) { resolve( createBulkErrorObject({ @@ -261,6 +264,10 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = }; }; -export const importRulesRoute = (server: ServerFacade): void => { - server.route(createImportRulesRoute(server)); +export const importRulesRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +): void => { + route(createImportRulesRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts index aa0dd04786a2e..02af4135b534f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk.test.ts @@ -4,13 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, -} from '../__mocks__/_mock_server'; - -import { patchRulesRoute } from './patch_rules_route'; import { ServerInjectOptions } from 'hapi'; +import { patchRulesRoute } from './patch_rules_route'; +import { omit } from 'lodash/fp'; import { getFindResult, @@ -20,43 +16,51 @@ import { getFindResultWithSingleHit, getPatchBulkRequest, } from '../__mocks__/request_responses'; +import { createMockServer, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { patchRulesBulkRoute } from './patch_rules_bulk_route'; import { BulkError } from '../utils'; describe('patch_rules_bulk', () => { - let { server, alertsClient, actionsClient } = createMockServer(); + let server = createMockServer(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); - ({ server, alertsClient, actionsClient } = createMockServer()); - patchRulesBulkRoute(server); + + server = createMockServer(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + patchRulesBulkRoute(server.route, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getPatchBulkRequest()); expect(statusCode).toBe(200); }); test('returns 200 as a response when updating a single rule that does not exist', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getPatchBulkRequest()); expect(statusCode).toBe(200); }); test('returns 404 within the payload when updating a single rule that does not exist', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const { payload } = await server.inject(getPatchBulkRequest()); const parsed: BulkError[] = JSON.parse(payload); const expected: BulkError[] = [ @@ -69,17 +73,18 @@ describe('patch_rules_bulk', () => { }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - patchRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getPatchBulkRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + patchRulesRoute(route, getClients); + const { statusCode } = await inject(getPatchBulkRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('returns 400 if id is not given in either the body or the url', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); const { rule_id, ...noId } = typicalPayload(); const request: ServerInjectOptions = { method: 'PATCH', @@ -91,9 +96,9 @@ describe('patch_rules_bulk', () => { }); test('returns errors as 200 to just indicate ok something happened', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'PATCH', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -104,9 +109,9 @@ describe('patch_rules_bulk', () => { }); test('returns 404 in the payload if the record does not exist yet', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'PATCH', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -124,10 +129,10 @@ describe('patch_rules_bulk', () => { }); test('returns 200 if type is query', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'PATCH', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -138,10 +143,10 @@ describe('patch_rules_bulk', () => { }); test('returns 400 if type is not filter or kql', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'PATCH', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index 00184b6c16b7e..d3f92e9e05bcc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -5,21 +5,21 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; + import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { BulkPatchRulesRequest, IRuleSavedAttributesSavedObjectAttributes, } from '../../rules/types'; -import { ServerFacade } from '../../../../types'; +import { LegacyServices } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { transformOrBulkError, getIdBulkError } from './utils'; import { transformBulkError } from '../utils'; import { patchRulesBulkSchema } from '../schemas/patch_rules_bulk_schema'; import { patchRules } from '../../rules/patch_rules'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; -export const createPatchRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createPatchRulesBulkRoute = (getClients: GetScopedClients): Hapi.ServerRoute => { return { method: 'PATCH', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -33,14 +33,9 @@ export const createPatchRulesBulkRoute = (server: ServerFacade): Hapi.ServerRout }, }, async handler(request: BulkPatchRulesRequest, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) - ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { + const { actionsClient, alertsClient, savedObjectsClient } = await getClients(request); + + if (!actionsClient || !alertsClient) { return headers.response().code(404); } @@ -132,6 +127,9 @@ export const createPatchRulesBulkRoute = (server: ServerFacade): Hapi.ServerRout }; }; -export const patchRulesBulkRoute = (server: ServerFacade): void => { - server.route(createPatchRulesBulkRoute(server)); +export const patchRulesBulkRoute = ( + route: LegacyServices['route'], + getClients: GetScopedClients +): void => { + route(createPatchRulesBulkRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts index d315d45046e2d..cc84b08fdef11 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -4,13 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, -} from '../__mocks__/_mock_server'; - -import { patchRulesRoute } from './patch_rules_route'; import { ServerInjectOptions } from 'hapi'; +import { omit } from 'lodash/fp'; +import { patchRulesRoute } from './patch_rules_route'; import { getFindResult, @@ -21,51 +17,59 @@ import { typicalPayload, getFindResultWithSingleHit, } from '../__mocks__/request_responses'; +import { createMockServer, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; describe('patch_rules', () => { - let { server, alertsClient, actionsClient, savedObjectsClient } = createMockServer(); + let server = createMockServer(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); - ({ server, alertsClient, actionsClient, savedObjectsClient } = createMockServer()); - patchRulesRoute(server); + server = createMockServer(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + patchRulesRoute(server.route, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { statusCode } = await server.inject(getPatchRequest()); expect(statusCode).toBe(200); }); test('returns 404 when updating a single rule that does not exist', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { statusCode } = await server.inject(getPatchRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - patchRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getPatchRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + patchRulesRoute(route, getClients); + const { statusCode } = await inject(getPatchRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('returns 400 if id is not given in either the body or the url', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { rule_id, ...noId } = typicalPayload(); const request: ServerInjectOptions = { method: 'PATCH', @@ -79,10 +83,10 @@ describe('patch_rules', () => { }); test('returns 404 if the record does not exist yet', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const request: ServerInjectOptions = { method: 'PATCH', url: DETECTION_ENGINE_RULES_URL, @@ -93,11 +97,11 @@ describe('patch_rules', () => { }); test('returns 200 if type is query', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const request: ServerInjectOptions = { method: 'PATCH', url: DETECTION_ENGINE_RULES_URL, @@ -108,11 +112,11 @@ describe('patch_rules', () => { }); test('returns 400 if type is not filter or kql', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'PATCH', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts index e27ae81362f27..761d22b084237 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -5,18 +5,18 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; + import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { patchRules } from '../../rules/patch_rules'; import { PatchRulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { patchRulesSchema } from '../schemas/patch_rules_schema'; -import { ServerFacade } from '../../../../types'; +import { LegacyServices } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { getIdError, transform } from './utils'; import { transformError } from '../utils'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; -export const createPatchRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createPatchRulesRoute = (getClients: GetScopedClients): Hapi.ServerRoute => { return { method: 'PATCH', path: DETECTION_ENGINE_RULES_URL, @@ -59,21 +59,16 @@ export const createPatchRulesRoute = (server: ServerFacade): Hapi.ServerRoute => version, } = request.payload; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) - ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { - return headers.response().code(404); - } - try { + const { alertsClient, actionsClient, savedObjectsClient } = await getClients(request); + + if (!actionsClient || !alertsClient) { + return headers.response().code(404); + } + const rule = await patchRules({ - alertsClient, actionsClient, + alertsClient, description, enabled, falsePositives, @@ -146,6 +141,6 @@ export const createPatchRulesRoute = (server: ServerFacade): Hapi.ServerRoute => }; }; -export const patchRulesRoute = (server: ServerFacade) => { - server.route(createPatchRulesRoute(server)); +export const patchRulesRoute = (route: LegacyServices['route'], getClients: GetScopedClients) => { + route(createPatchRulesRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts index 000cd29af8ba9..7c4653af97f21 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts @@ -4,14 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, -} from '../__mocks__/_mock_server'; - -import { readRulesRoute } from './read_rules_route'; import { ServerInjectOptions } from 'hapi'; +import { omit } from 'lodash/fp'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { readRulesRoute } from './read_rules_route'; import { getFindResult, getResult, @@ -19,43 +16,48 @@ import { getFindResultWithSingleHit, getFindResultStatus, } from '../__mocks__/request_responses'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { createMockServer, clientsServiceMock } from '../__mocks__'; describe('read_signals', () => { - let { server, alertsClient, savedObjectsClient } = createMockServer(); + let server = createMockServer(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { - ({ server, alertsClient, savedObjectsClient } = createMockServer()); - readRulesRoute(server); - }); - - afterEach(() => { jest.resetAllMocks(); + + server = createMockServer(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + readRulesRoute(server.route, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when reading a single rule with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { statusCode } = await server.inject(getReadRequest()); expect(statusCode).toBe(200); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - readRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getReadRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + readRulesRoute(route, getClients); + const { statusCode } = await inject(getReadRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('returns 400 if given a non-existent id', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.delete.mockResolvedValue({}); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.delete.mockResolvedValue({}); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const request: ServerInjectOptions = { method: 'GET', url: DETECTION_ENGINE_RULES_URL, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts index e82ad92704695..0180b208d1bb7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts @@ -5,18 +5,18 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { getIdError, transform } from './utils'; import { transformError } from '../utils'; import { readRules } from '../../rules/read_rules'; -import { ServerFacade } from '../../../../types'; +import { LegacyServices, LegacyRequest } from '../../../../types'; import { queryRulesSchema } from '../schemas/query_rules_schema'; import { QueryRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; +import { GetScopedClients } from '../../../../services'; -export const createReadRulesRoute: Hapi.ServerRoute = { +export const createReadRulesRoute = (getClients: GetScopedClients): Hapi.ServerRoute => ({ method: 'GET', path: DETECTION_ENGINE_RULES_URL, options: { @@ -28,16 +28,15 @@ export const createReadRulesRoute: Hapi.ServerRoute = { query: queryRulesSchema, }, }, - async handler(request: QueryRequest, headers) { + async handler(request: QueryRequest & LegacyRequest, headers) { const { id, rule_id: ruleId } = request.query; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { - return headers.response().code(404); - } + try { + const { alertsClient, savedObjectsClient } = await getClients(request); + if (!alertsClient) { + return headers.response().code(404); + } + const rule = await readRules({ alertsClient, id, @@ -84,8 +83,8 @@ export const createReadRulesRoute: Hapi.ServerRoute = { .code(error.statusCode); } }, -}; +}); -export const readRulesRoute = (server: ServerFacade) => { - server.route(createReadRulesRoute); +export const readRulesRoute = (route: LegacyServices['route'], getClients: GetScopedClients) => { + route(createReadRulesRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts index 81b6444f38603..9ff7ebc37aab1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk.test.ts @@ -4,14 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, -} from '../__mocks__/_mock_server'; - -import { updateRulesRoute } from './update_rules_route'; import { ServerInjectOptions } from 'hapi'; +import { omit } from 'lodash/fp'; +import { updateRulesRoute } from './update_rules_route'; import { getFindResult, getResult, @@ -20,43 +16,52 @@ import { getFindResultWithSingleHit, getUpdateBulkRequest, } from '../__mocks__/request_responses'; +import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { updateRulesBulkRoute } from './update_rules_bulk_route'; import { BulkError } from '../utils'; describe('update_rules_bulk', () => { - let { server, alertsClient, actionsClient } = createMockServer(); + let server = createMockServer(); + let config = createMockConfig(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); - ({ server, alertsClient, actionsClient } = createMockServer()); - updateRulesBulkRoute(server); + server = createMockServer(); + config = createMockConfig(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + updateRulesBulkRoute(server.route, config, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getUpdateBulkRequest()); expect(statusCode).toBe(200); }); test('returns 200 as a response when updating a single rule that does not exist', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getUpdateBulkRequest()); expect(statusCode).toBe(200); }); test('returns 404 within the payload when updating a single rule that does not exist', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const { payload } = await server.inject(getUpdateBulkRequest()); const parsed: BulkError[] = JSON.parse(payload); const expected: BulkError[] = [ @@ -69,17 +74,18 @@ describe('update_rules_bulk', () => { }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - updateRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getUpdateBulkRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + updateRulesRoute(route, config, getClients); + const { statusCode } = await inject(getUpdateBulkRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('returns 400 if id is not given in either the body or the url', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); const { rule_id, ...noId } = typicalPayload(); const request: ServerInjectOptions = { method: 'PUT', @@ -91,9 +97,9 @@ describe('update_rules_bulk', () => { }); test('returns errors as 200 to just indicate ok something happened', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'PUT', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -104,9 +110,9 @@ describe('update_rules_bulk', () => { }); test('returns 404 in the payload if the record does not exist yet', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'PUT', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -124,10 +130,10 @@ describe('update_rules_bulk', () => { }); test('returns 200 if type is query', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const request: ServerInjectOptions = { method: 'PUT', url: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -138,10 +144,10 @@ describe('update_rules_bulk', () => { }); test('returns 400 if type is not filter or kql', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'PUT', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 671497f9f65db..98ed01474c3dc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -5,21 +5,24 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; + import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { BulkUpdateRulesRequest, IRuleSavedAttributesSavedObjectAttributes, } from '../../rules/types'; -import { ServerFacade } from '../../../../types'; +import { LegacyServices } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { transformOrBulkError, getIdBulkError } from './utils'; import { transformBulkError, getIndex } from '../utils'; import { updateRulesBulkSchema } from '../schemas/update_rules_bulk_schema'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; import { updateRules } from '../../rules/update_rules'; -export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createUpdateRulesBulkRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'PUT', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -33,14 +36,11 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou }, }, async handler(request: BulkUpdateRulesRequest, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) + const { actionsClient, alertsClient, savedObjectsClient, spacesClient } = await getClients( + request ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { + + if (!actionsClient || !alertsClient) { return headers.response().code(404); } @@ -74,7 +74,7 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou references, version, } = payloadRule; - const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server); + const finalIndex = outputIndex ?? getIndex(spacesClient.getSpaceId, config); const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; try { const rule = await updateRules({ @@ -134,6 +134,10 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou }; }; -export const updateRulesBulkRoute = (server: ServerFacade): void => { - server.route(createUpdateRulesBulkRoute(server)); +export const updateRulesBulkRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +): void => { + route(createUpdateRulesBulkRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index c4f10d7a20327..7cadfa94467a7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -4,14 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createMockServer, - createMockServerWithoutAlertClientDecoration, -} from '../__mocks__/_mock_server'; - -import { updateRulesRoute } from './update_rules_route'; import { ServerInjectOptions } from 'hapi'; +import { omit } from 'lodash/fp'; +import { updateRulesRoute } from './update_rules_route'; import { getFindResult, getFindResultStatus, @@ -21,51 +17,62 @@ import { typicalPayload, getFindResultWithSingleHit, } from '../__mocks__/request_responses'; +import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; describe('update_rules', () => { - let { server, alertsClient, actionsClient, savedObjectsClient } = createMockServer(); + let server = createMockServer(); + let config = createMockConfig(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); - ({ server, alertsClient, actionsClient, savedObjectsClient } = createMockServer()); - updateRulesRoute(server); + + server = createMockServer(); + config = createMockConfig(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + updateRulesRoute(server.route, config, getClients); }); describe('status codes with actionClient and alertClient', () => { test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { statusCode } = await server.inject(getUpdateRequest()); expect(statusCode).toBe(200); }); test('returns 404 when updating a single rule that does not exist', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { statusCode } = await server.inject(getUpdateRequest()); expect(statusCode).toBe(404); }); test('returns 404 if alertClient is not available on the route', async () => { - const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); - updateRulesRoute(serverWithoutAlertClient); - const { statusCode } = await serverWithoutAlertClient.inject(getUpdateRequest()); + getClients.mockResolvedValue(omit('alertsClient', clients)); + const { route, inject } = createMockServer(); + updateRulesRoute(route, config, getClients); + const { statusCode } = await inject(getUpdateRequest()); expect(statusCode).toBe(404); }); }); describe('validation', () => { test('returns 400 if id is not given in either the body or the url', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { rule_id, ...noId } = typicalPayload(); const request: ServerInjectOptions = { method: 'PUT', @@ -79,10 +86,10 @@ describe('update_rules', () => { }); test('returns 404 if the record does not exist yet', async () => { - alertsClient.find.mockResolvedValue(getFindResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const request: ServerInjectOptions = { method: 'PUT', url: DETECTION_ENGINE_RULES_URL, @@ -93,11 +100,11 @@ describe('update_rules', () => { }); test('returns 200 if type is query', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const request: ServerInjectOptions = { method: 'PUT', url: DETECTION_ENGINE_RULES_URL, @@ -108,11 +115,11 @@ describe('update_rules', () => { }); test('returns 400 if type is not filter or kql', async () => { - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - alertsClient.get.mockResolvedValue(getResult()); - actionsClient.update.mockResolvedValue(updateActionResult()); - alertsClient.update.mockResolvedValue(getResult()); - savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); + clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); + clients.alertsClient.get.mockResolvedValue(getResult()); + clients.actionsClient.update.mockResolvedValue(updateActionResult()); + clients.alertsClient.update.mockResolvedValue(getResult()); + clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'PUT', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index a01627d2094b7..80fdfc1df8e0e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -5,18 +5,20 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { UpdateRulesRequest, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { updateRulesSchema } from '../schemas/update_rules_schema'; -import { ServerFacade } from '../../../../types'; +import { LegacyServices } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { getIdError, transform } from './utils'; import { transformError, getIndex } from '../utils'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; import { updateRules } from '../../rules/update_rules'; -export const createUpdateRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { +export const createUpdateRulesRoute = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'PUT', path: DETECTION_ENGINE_RULES_URL, @@ -59,19 +61,16 @@ export const createUpdateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = version, } = request.payload; - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = await server.plugins.actions.getActionsClientWithRequest( - KibanaRequest.from((request as unknown) as Hapi.Request) - ); - const savedObjectsClient = isFunction(request.getSavedObjectsClient) - ? request.getSavedObjectsClient() - : null; - if (!alertsClient || !savedObjectsClient) { - return headers.response().code(404); - } - try { - const finalIndex = outputIndex != null ? outputIndex : getIndex(request, server); + const { alertsClient, actionsClient, savedObjectsClient, spacesClient } = await getClients( + request + ); + + if (!actionsClient || !alertsClient) { + return headers.response().code(404); + } + + const finalIndex = outputIndex ?? getIndex(spacesClient.getSpaceId, config); const rule = await updateRules({ alertsClient, actionsClient, @@ -148,6 +147,10 @@ export const createUpdateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = }; }; -export const updateRulesRoute = (server: ServerFacade) => { - server.route(createUpdateRulesRoute(server)); +export const updateRulesRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +) => { + route(createUpdateRulesRoute(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts index 7e7d67333e78d..fb3262c476b40 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts @@ -15,6 +15,7 @@ import { transformRulesToNdjson, transformAlertsToRules, transformOrImportError, + getDuplicates, } from './utils'; import { getResult } from '../__mocks__/request_responses'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; @@ -1202,4 +1203,25 @@ describe('utils', () => { expect(output).toEqual(expected); }); }); + + describe('getDuplicates', () => { + test("returns array of ruleIds showing the duplicate keys of 'value2' and 'value3'", () => { + const output = getDuplicates({ + value1: 1, + value2: 2, + value3: 2, + }); + const expected = ['value2', 'value3']; + expect(output).toEqual(expected); + }); + test('returns null when given a map of no duplicates', () => { + const output = getDuplicates({ + value1: 1, + value2: 1, + value3: 1, + }); + const expected: string[] = []; + expect(output).toEqual(expected); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts index abb94c10209dc..df9e3021e400f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts @@ -5,6 +5,7 @@ */ import { pickBy } from 'lodash/fp'; +import { Dictionary } from 'lodash'; import { SavedObject } from 'kibana/server'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; import { @@ -215,3 +216,11 @@ export const transformOrImportError = ( }); } }; + +export const getDuplicates = (lodashDict: Dictionary<number>): string[] => { + const hasDuplicates = Object.values(lodashDict).some(i => i > 1); + if (hasDuplicates) { + return Object.keys(lodashDict).filter(key => lodashDict[key] > 1); + } + return []; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts index 35e1e5933af64..3e7ed4de6d8c6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createMockServer } from '../__mocks__/_mock_server'; +import { ServerInjectOptions } from 'hapi'; +import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../../common/constants'; import { setSignalsStatusRoute } from './open_close_signals_route'; import * as myUtils from '../utils'; -import { ServerInjectOptions } from 'hapi'; import { getSetSignalStatusByIdsRequest, @@ -16,19 +16,27 @@ import { typicalSetStatusSignalByQueryPayload, setStatusSignalMissingIdsAndQueryPayload, } from '../__mocks__/request_responses'; -import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../../common/constants'; +import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; describe('set signal status', () => { - let { server, elasticsearch } = createMockServer(); + let server = createMockServer(); + let config = createMockConfig(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); jest.spyOn(myUtils, 'getIndex').mockReturnValue('fakeindex'); - ({ server, elasticsearch } = createMockServer()); - elasticsearch.getCluster = jest.fn(() => ({ - callWithRequest: jest.fn(() => true), - })); - setSignalsStatusRoute(server); + + server = createMockServer(); + config = createMockConfig(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(true); + + setSignalsStatusRoute(server.route, config, getClients); }); describe('status on signal', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts index 4755869c3d908..b2b938625180e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts @@ -6,12 +6,16 @@ import Hapi from 'hapi'; import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../../common/constants'; +import { LegacyServices } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { SignalsStatusRequest } from '../../signals/types'; import { setSignalsStatusSchema } from '../schemas/set_signal_status_schema'; -import { ServerFacade } from '../../../../types'; import { transformError, getIndex } from '../utils'; -export const setSignalsStatusRouteDef = (server: ServerFacade): Hapi.ServerRoute => { +export const setSignalsStatusRouteDef = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'POST', path: DETECTION_ENGINE_SIGNALS_STATUS_URL, @@ -26,8 +30,9 @@ export const setSignalsStatusRouteDef = (server: ServerFacade): Hapi.ServerRoute }, async handler(request: SignalsStatusRequest) { const { signal_ids: signalIds, query, status } = request.payload; - const index = getIndex(request, server); - const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); + const { clusterClient, spacesClient } = await getClients(request); + const index = getIndex(spacesClient.getSpaceId, config); + let queryObject; if (signalIds) { queryObject = { ids: { values: signalIds } }; @@ -40,7 +45,7 @@ export const setSignalsStatusRouteDef = (server: ServerFacade): Hapi.ServerRoute }; } try { - return callWithRequest(request, 'updateByQuery', { + return clusterClient.callAsCurrentUser('updateByQuery', { index, body: { script: { @@ -58,6 +63,10 @@ export const setSignalsStatusRouteDef = (server: ServerFacade): Hapi.ServerRoute }; }; -export const setSignalsStatusRoute = (server: ServerFacade) => { - server.route(setSignalsStatusRouteDef(server)); +export const setSignalsStatusRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +) => { + route(setSignalsStatusRouteDef(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts index 5b86d0a4b36c0..9439adfcec3cb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts @@ -4,77 +4,78 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createMockServer } from '../__mocks__/_mock_server'; -import { querySignalsRoute } from './query_signals_route'; -import * as myUtils from '../utils'; import { ServerInjectOptions } from 'hapi'; +import { querySignalsRoute } from './query_signals_route'; +import * as myUtils from '../utils'; import { getSignalsQueryRequest, getSignalsAggsQueryRequest, typicalSignalsQuery, typicalSignalsQueryAggs, } from '../__mocks__/request_responses'; +import { createMockServer, createMockConfig, clientsServiceMock } from '../__mocks__'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../common/constants'; describe('query for signal', () => { - let { server, elasticsearch } = createMockServer(); + let server = createMockServer(); + let config = createMockConfig(); + let getClients = clientsServiceMock.createGetScoped(); + let clients = clientsServiceMock.createClients(); beforeEach(() => { jest.resetAllMocks(); jest.spyOn(myUtils, 'getIndex').mockReturnValue('fakeindex'); - ({ server, elasticsearch } = createMockServer()); - elasticsearch.getCluster = jest.fn(() => ({ - callWithRequest: jest.fn(() => true), - })); - querySignalsRoute(server); + + server = createMockServer(); + config = createMockConfig(); + getClients = clientsServiceMock.createGetScoped(); + clients = clientsServiceMock.createClients(); + + getClients.mockResolvedValue(clients); + clients.clusterClient.callAsCurrentUser.mockResolvedValue(true); + + querySignalsRoute(server.route, config, getClients); }); describe('query and agg on signals index', () => { test('returns 200 when using single query', async () => { - elasticsearch.getCluster = jest.fn(() => ({ - callWithRequest: jest.fn( - (_req, _type: string, queryBody: { index: string; body: object }) => { - expect(queryBody.body).toMatchObject({ ...typicalSignalsQueryAggs() }); - return true; - } - ), - })); - const { statusCode } = await server.inject(getSignalsAggsQueryRequest()); + const { statusCode } = await server.inject(getSignalsQueryRequest()); + expect(statusCode).toBe(200); + expect(clients.clusterClient.callAsCurrentUser).toHaveBeenCalledWith( + 'search', + expect.objectContaining({ body: typicalSignalsQuery() }) + ); expect(myUtils.getIndex).toHaveReturnedWith('fakeindex'); }); test('returns 200 when using single agg', async () => { - elasticsearch.getCluster = jest.fn(() => ({ - callWithRequest: jest.fn( - (_req, _type: string, queryBody: { index: string; body: object }) => { - expect(queryBody.body).toMatchObject({ ...typicalSignalsQueryAggs() }); - return true; - } - ), - })); const { statusCode } = await server.inject(getSignalsAggsQueryRequest()); + expect(statusCode).toBe(200); + expect(clients.clusterClient.callAsCurrentUser).toHaveBeenCalledWith( + 'search', + expect.objectContaining({ body: typicalSignalsQueryAggs() }) + ); expect(myUtils.getIndex).toHaveReturnedWith('fakeindex'); }); test('returns 200 when using aggs and query together', async () => { - const allTogether = getSignalsQueryRequest(); - allTogether.payload = { ...typicalSignalsQueryAggs(), ...typicalSignalsQuery() }; - elasticsearch.getCluster = jest.fn(() => ({ - callWithRequest: jest.fn( - (_req, _type: string, queryBody: { index: string; body: object }) => { - expect(queryBody.body).toMatchObject({ - ...typicalSignalsQueryAggs(), - ...typicalSignalsQuery(), - }); - return true; - } - ), - })); - const { statusCode } = await server.inject(allTogether); + const request = getSignalsQueryRequest(); + request.payload = { ...typicalSignalsQueryAggs(), ...typicalSignalsQuery() }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + expect(clients.clusterClient.callAsCurrentUser).toHaveBeenCalledWith( + 'search', + expect.objectContaining({ + body: { + ...typicalSignalsQuery(), + ...typicalSignalsQueryAggs(), + }, + }) + ); expect(myUtils.getIndex).toHaveReturnedWith('fakeindex'); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts index 6d1896b1a8171..a3602ffbded41 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts @@ -6,12 +6,16 @@ import Hapi from 'hapi'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../common/constants'; +import { LegacyServices } from '../../../../types'; +import { GetScopedClients } from '../../../../services'; import { SignalsQueryRequest } from '../../signals/types'; import { querySignalsSchema } from '../schemas/query_signals_index_schema'; -import { ServerFacade } from '../../../../types'; import { transformError, getIndex } from '../utils'; -export const querySignalsRouteDef = (server: ServerFacade): Hapi.ServerRoute => { +export const querySignalsRouteDef = ( + config: LegacyServices['config'], + getClients: GetScopedClients +): Hapi.ServerRoute => { return { method: 'POST', path: DETECTION_ENGINE_QUERY_SIGNALS_URL, @@ -26,11 +30,12 @@ export const querySignalsRouteDef = (server: ServerFacade): Hapi.ServerRoute => }, async handler(request: SignalsQueryRequest) { const { query, aggs, _source, track_total_hits, size } = request.payload; - const index = getIndex(request, server); - const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); + const { clusterClient, spacesClient } = await getClients(request); + + const index = getIndex(spacesClient.getSpaceId, config); try { - return callWithRequest(request, 'search', { + return clusterClient.callAsCurrentUser('search', { index, body: { query, aggs, _source, track_total_hits, size }, }); @@ -42,6 +47,10 @@ export const querySignalsRouteDef = (server: ServerFacade): Hapi.ServerRoute => }; }; -export const querySignalsRoute = (server: ServerFacade) => { - server.route(querySignalsRouteDef(server)); +export const querySignalsRoute = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + getClients: GetScopedClients +) => { + route(querySignalsRouteDef(config, getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts index f6d297b0cbf43..b17be21d15a19 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts @@ -5,13 +5,14 @@ */ import Hapi from 'hapi'; -import { isFunction } from 'lodash/fp'; + import { DETECTION_ENGINE_TAGS_URL } from '../../../../../common/constants'; -import { ServerFacade, RequestFacade } from '../../../../types'; +import { LegacyServices, LegacyRequest } from '../../../../types'; import { transformError } from '../utils'; import { readTags } from '../../tags/read_tags'; +import { GetScopedClients } from '../../../../services'; -export const createReadTagsRoute: Hapi.ServerRoute = { +export const createReadTagsRoute = (getClients: GetScopedClients): Hapi.ServerRoute => ({ method: 'GET', path: DETECTION_ENGINE_TAGS_URL, options: { @@ -22,8 +23,9 @@ export const createReadTagsRoute: Hapi.ServerRoute = { }, }, }, - async handler(request: RequestFacade, headers) { - const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + async handler(request: LegacyRequest, headers) { + const { alertsClient } = await getClients(request); + if (!alertsClient) { return headers.response().code(404); } @@ -43,8 +45,8 @@ export const createReadTagsRoute: Hapi.ServerRoute = { .code(error.statusCode); } }, -}; +}); -export const readTagsRoute = (server: ServerFacade) => { - server.route(createReadTagsRoute); +export const readTagsRoute = (route: LegacyServices['route'], getClients: GetScopedClients) => { + route(createReadTagsRoute(getClients)); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index 3e3ccfe5babef..957ddd4ee6caa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -11,10 +11,12 @@ import { transformBulkError, BulkError, createSuccessObject, + getIndex, ImportSuccessError, createImportErrorObject, transformImportError, } from './utils'; +import { createMockConfig } from './__mocks__'; describe('utils', () => { describe('transformError', () => { @@ -292,4 +294,22 @@ describe('utils', () => { expect(transformed).toEqual(expected); }); }); + + describe('getIndex', () => { + let mockConfig = createMockConfig(); + + beforeEach(() => { + mockConfig = () => ({ + get: jest.fn(() => 'mockSignalsIndex'), + has: jest.fn(), + }); + }); + + it('appends the space id to the configured index', () => { + const getSpaceId = jest.fn(() => 'myspace'); + const index = getIndex(getSpaceId, mockConfig); + + expect(index).toEqual('mockSignalsIndex-myspace'); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index af78f60f16ae4..4a586e21c9e7f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; import { APP_ID, SIGNALS_INDEX_KEY } from '../../../../common/constants'; -import { ServerFacade, RequestFacade } from '../../../types'; +import { LegacyServices } from '../../../types'; export interface OutputError { message: string; @@ -174,21 +174,9 @@ export const transformBulkError = ( } }; -export const getIndex = ( - request: RequestFacade | Omit<RequestFacade, 'query'>, - server: ServerFacade -): string => { - const spaceId = server.plugins.spaces.getSpaceId(request); - const signalsIndex = server.config().get(`xpack.${APP_ID}.${SIGNALS_INDEX_KEY}`); - return `${signalsIndex}-${spaceId}`; -}; +export const getIndex = (getSpaceId: () => string, config: LegacyServices['config']): string => { + const signalsIndex = config().get<string>(`xpack.${APP_ID}.${SIGNALS_INDEX_KEY}`); + const spaceId = getSpaceId(); -export const callWithRequestFactory = ( - request: RequestFacade | Omit<RequestFacade, 'query'>, - server: ServerFacade -) => { - const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); - return <T, U>(endpoint: string, params: T, options?: U) => { - return callWithRequest(request, endpoint, params, options); - }; + return `${signalsIndex}-${spaceId}`; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts index 8d00ddb18be6b..25bac383ecf72 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts @@ -5,7 +5,6 @@ */ import { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock'; -import { AlertsClient } from '../../../../../alerting'; import { getResult, getFindResultWithSingleHit, @@ -28,10 +27,7 @@ describe('get_existing_prepackaged_rules', () => { test('should return a single item in a single page', async () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getExistingPrepackagedRules({ - alertsClient: unsafeCast, - }); + const rules = await getExistingPrepackagedRules({ alertsClient }); expect(rules).toEqual([getResult()]); }); @@ -70,10 +66,7 @@ describe('get_existing_prepackaged_rules', () => { }) ); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getExistingPrepackagedRules({ - alertsClient: unsafeCast, - }); + const rules = await getExistingPrepackagedRules({ alertsClient }); expect(rules).toEqual([result1, result2, result3]); }); }); @@ -82,10 +75,7 @@ describe('get_existing_prepackaged_rules', () => { test('should return a single item in a single page', async () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getNonPackagedRules({ - alertsClient: unsafeCast, - }); + const rules = await getNonPackagedRules({ alertsClient }); expect(rules).toEqual([getResult()]); }); @@ -113,10 +103,7 @@ describe('get_existing_prepackaged_rules', () => { getFindResultWithMultiHits({ data: [result1, result2], perPage: 2, page: 1, total: 2 }) ); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getNonPackagedRules({ - alertsClient: unsafeCast, - }); + const rules = await getNonPackagedRules({ alertsClient }); expect(rules).toEqual([result1, result2]); }); @@ -152,10 +139,7 @@ describe('get_existing_prepackaged_rules', () => { }) ); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getNonPackagedRules({ - alertsClient: unsafeCast, - }); + const rules = await getNonPackagedRules({ alertsClient }); expect(rules).toEqual([result1, result2, result3]); }); }); @@ -164,11 +148,7 @@ describe('get_existing_prepackaged_rules', () => { test('should return a single item in a single page', async () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getRules({ - alertsClient: unsafeCast, - filter: '', - }); + const rules = await getRules({ alertsClient, filter: '' }); expect(rules).toEqual([getResult()]); }); @@ -196,11 +176,7 @@ describe('get_existing_prepackaged_rules', () => { getFindResultWithMultiHits({ data: [result1, result2], perPage: 2, page: 1, total: 2 }) ); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getRules({ - alertsClient: unsafeCast, - filter: '', - }); + const rules = await getRules({ alertsClient, filter: '' }); expect(rules).toEqual([result1, result2]); }); }); @@ -209,11 +185,7 @@ describe('get_existing_prepackaged_rules', () => { test('it returns a count', async () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getRulesCount({ - alertsClient: unsafeCast, - filter: '', - }); + const rules = await getRulesCount({ alertsClient, filter: '' }); expect(rules).toEqual(1); }); }); @@ -222,10 +194,7 @@ describe('get_existing_prepackaged_rules', () => { test('it returns a count', async () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getNonPackagedRulesCount({ - alertsClient: unsafeCast, - }); + const rules = await getNonPackagedRulesCount({ alertsClient }); expect(rules).toEqual(1); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts index ff48b9f5f7c33..35d3489dad6fc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts @@ -10,7 +10,6 @@ import { getFindResultWithSingleHit, FindHit, } from '../routes/__mocks__/request_responses'; -import { AlertsClient } from '../../../../../alerting'; import { getExportAll } from './get_export_all'; describe('getExportAll', () => { @@ -19,8 +18,7 @@ describe('getExportAll', () => { alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const exports = await getExportAll(unsafeCast); + const exports = await getExportAll(alertsClient); expect(exports).toEqual({ rulesNdjson: '{"created_at":"2019-12-13T16:40:33.400Z","updated_at":"2019-12-13T16:40:33.400Z","created_by":"elastic","description":"Detecting root and admin users","enabled":true,"false_positives":[],"filters":[{"query":{"match_phrase":{"host.name":"some-host"}}}],"from":"now-6m","id":"04128c15-0d1b-4716-a4c5-46997ac7f3bd","immutable":false,"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"rule-1","language":"kuery","output_index":".siem-signals","max_signals":100,"risk_score":50,"name":"Detect Root/Admin Users","query":"user.name: root or user.name: admin","references":["http://www.example.com","https://ww.example.com"],"saved_id":"some-id","timeline_id":"some-timeline-id","timeline_title":"some-timeline-title","meta":{"someMeta":"someField"},"severity":"high","updated_by":"elastic","tags":[],"to":"now","type":"query","threat":[{"framework":"MITRE ATT&CK","tactic":{"id":"TA0040","name":"impact","reference":"https://attack.mitre.org/tactics/TA0040/"},"technique":[{"id":"T1499","name":"endpoint denial of service","reference":"https://attack.mitre.org/techniques/T1499/"}]}],"version":1}\n', @@ -39,8 +37,7 @@ describe('getExportAll', () => { alertsClient.find.mockResolvedValue(findResult); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const exports = await getExportAll(unsafeCast); + const exports = await getExportAll(alertsClient); expect(exports).toEqual({ rulesNdjson: '', exportDetails: '{"exported_count":0,"missing_rules":[],"missing_rules_count":0}\n', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 236d04acc782b..4b6ea527a2027 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -11,7 +11,6 @@ import { getFindResultWithSingleHit, FindHit, } from '../routes/__mocks__/request_responses'; -import { AlertsClient } from '../../../../../alerting'; describe('get_export_by_object_ids', () => { describe('getExportByObjectIds', () => { @@ -20,9 +19,8 @@ describe('get_export_by_object_ids', () => { alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const objects = [{ rule_id: 'rule-1' }]; - const exports = await getExportByObjectIds(unsafeCast, objects); + const exports = await getExportByObjectIds(alertsClient, objects); expect(exports).toEqual({ rulesNdjson: '{"created_at":"2019-12-13T16:40:33.400Z","updated_at":"2019-12-13T16:40:33.400Z","created_by":"elastic","description":"Detecting root and admin users","enabled":true,"false_positives":[],"filters":[{"query":{"match_phrase":{"host.name":"some-host"}}}],"from":"now-6m","id":"04128c15-0d1b-4716-a4c5-46997ac7f3bd","immutable":false,"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"rule-1","language":"kuery","output_index":".siem-signals","max_signals":100,"risk_score":50,"name":"Detect Root/Admin Users","query":"user.name: root or user.name: admin","references":["http://www.example.com","https://ww.example.com"],"saved_id":"some-id","timeline_id":"some-timeline-id","timeline_title":"some-timeline-title","meta":{"someMeta":"someField"},"severity":"high","updated_by":"elastic","tags":[],"to":"now","type":"query","threat":[{"framework":"MITRE ATT&CK","tactic":{"id":"TA0040","name":"impact","reference":"https://attack.mitre.org/tactics/TA0040/"},"technique":[{"id":"T1499","name":"endpoint denial of service","reference":"https://attack.mitre.org/techniques/T1499/"}]}],"version":1}\n', @@ -45,9 +43,8 @@ describe('get_export_by_object_ids', () => { alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(findResult); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const objects = [{ rule_id: 'rule-1' }]; - const exports = await getExportByObjectIds(unsafeCast, objects); + const exports = await getExportByObjectIds(alertsClient, objects); expect(exports).toEqual({ rulesNdjson: '', exportDetails: @@ -62,9 +59,8 @@ describe('get_export_by_object_ids', () => { alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const objects = [{ rule_id: 'rule-1' }]; - const exports = await getRulesFromObjects(unsafeCast, objects); + const exports = await getRulesFromObjects(alertsClient, objects); const expected: RulesErrors = { exportedCount: 1, missingRules: [], @@ -138,9 +134,8 @@ describe('get_export_by_object_ids', () => { alertsClient.get.mockResolvedValue(result); alertsClient.find.mockResolvedValue(findResult); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const objects = [{ rule_id: 'rule-1' }]; - const exports = await getRulesFromObjects(unsafeCast, objects); + const exports = await getRulesFromObjects(alertsClient, objects); const expected: RulesErrors = { exportedCount: 0, missingRules: [{ rule_id: 'rule-1' }], @@ -162,9 +157,8 @@ describe('get_export_by_object_ids', () => { alertsClient.get.mockRejectedValue({ output: { statusCode: 404 } }); alertsClient.find.mockResolvedValue(findResult); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const objects = [{ rule_id: 'rule-1' }]; - const exports = await getRulesFromObjects(unsafeCast, objects); + const exports = await getRulesFromObjects(alertsClient, objects); const expected: RulesErrors = { exportedCount: 0, missingRules: [{ rule_id: 'rule-1' }], diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts index 6ba0aa95bdd7b..c637860c5646a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts @@ -6,7 +6,6 @@ import { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock'; import { readRules } from './read_rules'; -import { AlertsClient } from '../../../../../alerting'; import { getResult, getFindResultWithSingleHit } from '../routes/__mocks__/request_responses'; describe('read_rules', () => { @@ -15,9 +14,8 @@ describe('read_rules', () => { const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const rule = await readRules({ - alertsClient: unsafeCast, + alertsClient, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ruleId: undefined, }); @@ -28,9 +26,8 @@ describe('read_rules', () => { const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const rule = await readRules({ - alertsClient: unsafeCast, + alertsClient, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ruleId: null, }); @@ -42,9 +39,8 @@ describe('read_rules', () => { alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const rule = await readRules({ - alertsClient: unsafeCast, + alertsClient, id: undefined, ruleId: 'rule-1', }); @@ -56,9 +52,8 @@ describe('read_rules', () => { alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const rule = await readRules({ - alertsClient: unsafeCast, + alertsClient, id: null, ruleId: 'rule-1', }); @@ -70,9 +65,8 @@ describe('read_rules', () => { alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const rule = await readRules({ - alertsClient: unsafeCast, + alertsClient, id: null, ruleId: null, }); @@ -84,9 +78,8 @@ describe('read_rules', () => { alertsClient.get.mockResolvedValue(getResult()); alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const rule = await readRules({ - alertsClient: unsafeCast, + alertsClient, id: undefined, ruleId: undefined, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts index 8c44d82f46b53..8579447a74c69 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts @@ -14,10 +14,10 @@ import { SavedObjectsClientContract, } from 'kibana/server'; import { SIGNALS_ID } from '../../../../common/constants'; +import { LegacyRequest } from '../../../types'; import { AlertsClient } from '../../../../../alerting/server'; import { ActionsClient } from '../../../../../../../plugins/actions/server'; import { RuleAlertParams, RuleTypeParams, RuleAlertParamsRest } from '../types'; -import { RequestFacade } from '../../../types'; import { Alert } from '../../../../../alerting/server/types'; export type PatchRuleAlertParamsRest = Partial<RuleAlertParamsRest> & { @@ -39,19 +39,19 @@ export interface FindParamsRest { filter: string; } -export interface PatchRulesRequest extends RequestFacade { +export interface PatchRulesRequest extends LegacyRequest { payload: PatchRuleAlertParamsRest; } -export interface BulkPatchRulesRequest extends RequestFacade { +export interface BulkPatchRulesRequest extends LegacyRequest { payload: PatchRuleAlertParamsRest[]; } -export interface UpdateRulesRequest extends RequestFacade { +export interface UpdateRulesRequest extends LegacyRequest { payload: UpdateRuleAlertParamsRest; } -export interface BulkUpdateRulesRequest extends RequestFacade { +export interface BulkUpdateRulesRequest extends LegacyRequest { payload: UpdateRuleAlertParamsRest[]; } @@ -99,11 +99,11 @@ export interface IRuleStatusFindType { export type RuleStatusString = 'succeeded' | 'failed' | 'going to run' | 'executing'; -export interface RulesRequest extends RequestFacade { +export interface RulesRequest extends LegacyRequest { payload: RuleAlertParamsRest; } -export interface BulkRulesRequest extends RequestFacade { +export interface BulkRulesRequest extends LegacyRequest { payload: RuleAlertParamsRest[]; } @@ -112,12 +112,12 @@ export interface HapiReadableStream extends Readable { filename: string; }; } -export interface ImportRulesRequest extends Omit<RequestFacade, 'query'> { +export interface ImportRulesRequest extends Omit<LegacyRequest, 'query'> { query: { overwrite: boolean }; payload: { file: HapiReadableStream }; } -export interface ExportRulesRequest extends Omit<RequestFacade, 'query'> { +export interface ExportRulesRequest extends Omit<LegacyRequest, 'query'> { payload: { objects: Array<{ rule_id: string }> | null | undefined }; query: { file_name: string; @@ -125,11 +125,11 @@ export interface ExportRulesRequest extends Omit<RequestFacade, 'query'> { }; } -export type QueryRequest = Omit<RequestFacade, 'query'> & { +export type QueryRequest = Omit<LegacyRequest, 'query'> & { query: { id: string | undefined; rule_id: string | undefined }; }; -export interface QueryBulkRequest extends RequestFacade { +export interface QueryBulkRequest extends LegacyRequest { payload: Array<QueryRequest['query']>; } @@ -143,7 +143,7 @@ export interface FindRuleParams { sortOrder?: 'asc' | 'desc'; } -export interface FindRulesRequest extends Omit<RequestFacade, 'query'> { +export interface FindRulesRequest extends Omit<LegacyRequest, 'query'> { query: { per_page: number; page: number; @@ -155,7 +155,7 @@ export interface FindRulesRequest extends Omit<RequestFacade, 'query'> { }; } -export interface FindRulesStatusesRequest extends Omit<RequestFacade, 'query'> { +export interface FindRulesStatusesRequest extends Omit<LegacyRequest, 'query'> { query: { ids: string[]; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index cd28f348a27c3..79337aa91b1fe 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -7,6 +7,7 @@ import { schema } from '@kbn/config-schema'; import { Logger } from 'src/core/server'; import moment from 'moment'; +import { i18n } from '@kbn/i18n'; import { SIGNALS_ID, DEFAULT_MAX_SIGNALS, @@ -32,7 +33,14 @@ export const signalRulesAlertType = ({ return { id: SIGNALS_ID, name: 'SIEM Signals', - actionGroups: ['default'], + actionGroups: [ + { + id: 'default', + name: i18n.translate('xpack.siem.detectionEngine.signalRuleAlert.actionGroups.default', { + defaultMessage: 'Default', + }), + }, + ], validate: { params: schema.object({ description: schema.string(), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts index 9b7b2b8f1fff9..e9159ab87a0c0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts @@ -6,7 +6,7 @@ import { RuleAlertParams, OutputRuleAlertRest } from '../types'; import { SearchResponse } from '../../types'; -import { RequestFacade } from '../../../types'; +import { LegacyRequest } from '../../../types'; import { AlertType, State, AlertExecutorOptions } from '../../../../../alerting/server/types'; export interface SignalsParams { @@ -35,11 +35,11 @@ export type SignalsStatusRestParams = Omit<SignalsStatusParams, 'signalIds'> & { export type SignalsQueryRestParams = SignalQueryParams; -export interface SignalsStatusRequest extends RequestFacade { +export interface SignalsStatusRequest extends LegacyRequest { payload: SignalsStatusRestParams; } -export interface SignalsQueryRequest extends RequestFacade { +export interface SignalsQueryRequest extends LegacyRequest { payload: SignalsQueryRestParams; } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts index 87739bf785012..940011924de79 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts @@ -5,7 +5,6 @@ */ import { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock'; -import { AlertsClient } from '../../../../../alerting'; import { getResult, getFindResultWithMultiHits } from '../routes/__mocks__/request_responses'; import { INTERNAL_RULE_ID_KEY, INTERNAL_IDENTIFIER } from '../../../../common/constants'; import { readRawTags, readTags, convertTagsToSet, convertToTags, isTags } from './read_tags'; @@ -30,10 +29,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readRawTags({ - alertsClient: unsafeCast, - }); + const tags = await readRawTags({ alertsClient }); expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); }); @@ -51,10 +47,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readRawTags({ - alertsClient: unsafeCast, - }); + const tags = await readRawTags({ alertsClient }); expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); }); @@ -72,10 +65,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readRawTags({ - alertsClient: unsafeCast, - }); + const tags = await readRawTags({ alertsClient }); expect(tags).toEqual([]); }); @@ -88,10 +78,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readRawTags({ - alertsClient: unsafeCast, - }); + const tags = await readRawTags({ alertsClient }); expect(tags).toEqual(['tag 1', 'tag 2']); }); @@ -104,10 +91,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readRawTags({ - alertsClient: unsafeCast, - }); + const tags = await readRawTags({ alertsClient }); expect(tags).toEqual([]); }); }); @@ -127,10 +111,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); + const tags = await readTags({ alertsClient }); expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); }); @@ -148,10 +129,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); + const tags = await readTags({ alertsClient }); expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); }); @@ -169,10 +147,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1, result2] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); + const tags = await readTags({ alertsClient }); expect(tags).toEqual([]); }); @@ -185,10 +160,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); + const tags = await readTags({ alertsClient }); expect(tags).toEqual(['tag 1', 'tag 2']); }); @@ -201,10 +173,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); + const tags = await readTags({ alertsClient }); expect(tags).toEqual([]); }); @@ -221,10 +190,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); + const tags = await readTags({ alertsClient }); expect(tags).toEqual(['tag 1']); }); @@ -257,10 +223,7 @@ describe('read_tags', () => { const alertsClient = alertsClientMock.create(); alertsClient.find.mockResolvedValue(getFindResultWithMultiHits({ data: [result1] })); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const tags = await readTags({ - alertsClient: unsafeCast, - }); + const tags = await readTags({ alertsClient }); expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4', 'tag 5']); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts index e15053db75777..08cb2e7bc19ee 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { CallAPIOptions } from '../../../../../../../src/core/server'; import { Filter } from '../../../../../../../src/plugins/data/server'; import { IRuleStatusAttributes } from './rules/types'; @@ -117,4 +118,9 @@ export type PrepackagedRules = Omit< | 'created_at' > & { rule_id: string; immutable: boolean }; -export type CallWithRequest<T, U, V> = (endpoint: string, params: T, options?: U) => Promise<V>; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type CallWithRequest<T extends Record<string, any>, V> = ( + endpoint: string, + params: T, + options?: CallAPIOptions +) => Promise<V>; diff --git a/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.test.ts index b1f0c3c4a3a18..42dc13d84fd98 100644 --- a/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.test.ts @@ -519,7 +519,6 @@ describe('events elasticsearch_adapter', () => { return mockResponseMap; }); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts index 38b95cc5772f2..af6f8314b362a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts @@ -25,13 +25,12 @@ import { TimelineData, TimelineDetailsData, TimelineEdges, - EventsOverTimeData, } from '../../graphql/types'; import { baseCategoryFields } from '../../utils/beat_schema/8.0.0'; import { reduceFields } from '../../utils/build_query/reduce_fields'; import { mergeFieldsWithHit, inspectStringifyObject } from '../../utils/build_query'; import { eventFieldsMap } from '../ecs_fields'; -import { FrameworkAdapter, FrameworkRequest, MatrixHistogramRequestOptions } from '../framework'; +import { FrameworkAdapter, FrameworkRequest } from '../framework'; import { TermAggregation } from '../types'; import { buildDetailsQuery, buildTimelineQuery } from './query.dsl'; @@ -43,10 +42,7 @@ import { LastEventTimeRequestOptions, RequestDetailsOptions, TimelineRequestOptions, - EventsActionGroupData, } from './types'; -import { buildEventsOverTimeQuery } from './query.events_over_time.dsl'; -import { MatrixOverTimeHistogramData } from '../../../public/graphql/types'; export class ElasticsearchEventsAdapter implements EventsAdapter { constructor(private readonly framework: FrameworkAdapter) {} @@ -129,65 +125,8 @@ export class ElasticsearchEventsAdapter implements EventsAdapter { lastSeen: getOr(null, 'aggregations.last_seen_event.value_as_string', response), }; } - - public async getEventsOverTime( - request: FrameworkRequest, - options: MatrixHistogramRequestOptions - ): Promise<EventsOverTimeData> { - const dsl = buildEventsOverTimeQuery(options); - const response = await this.framework.callWithRequest<EventHit, TermAggregation>( - request, - 'search', - dsl - ); - const totalCount = getOr(0, 'hits.total.value', response); - const eventsOverTimeBucket = getOr([], 'aggregations.eventActionGroup.buckets', response); - const inspect = { - dsl: [inspectStringifyObject(dsl)], - response: [inspectStringifyObject(response)], - }; - return { - inspect, - matrixHistogramData: getEventsOverTimeByActionName(eventsOverTimeBucket), - totalCount, - }; - } } -/** - * Not in use at the moment, - * reserved this parser for next feature of switchign between total events and grouped events - */ -export const getTotalEventsOverTime = ( - data: EventsActionGroupData[] -): MatrixOverTimeHistogramData[] => { - return data && data.length > 0 - ? data.map<MatrixOverTimeHistogramData>(({ key, doc_count }) => ({ - x: key, - y: doc_count, - g: 'total events', - })) - : []; -}; - -const getEventsOverTimeByActionName = ( - data: EventsActionGroupData[] -): MatrixOverTimeHistogramData[] => { - let result: MatrixOverTimeHistogramData[] = []; - data.forEach(({ key: group, events }) => { - const eventsData = getOr([], 'buckets', events).map( - ({ key, doc_count }: { key: number; doc_count: number }) => ({ - x: key, - y: doc_count, - g: group, - }) - ); - result = [...result, ...eventsData]; - }); - - return result; -}; - export const formatEventsData = ( fields: readonly string[], hit: EventHit, diff --git a/x-pack/legacy/plugins/siem/server/lib/events/index.ts b/x-pack/legacy/plugins/siem/server/lib/events/index.ts index 9e2457904f8c0..9c1f87aa3d8bf 100644 --- a/x-pack/legacy/plugins/siem/server/lib/events/index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/events/index.ts @@ -5,7 +5,7 @@ */ import { LastEventTimeData, TimelineData, TimelineDetailsData } from '../../graphql/types'; -import { FrameworkRequest, RequestBasicOptions } from '../framework'; +import { FrameworkRequest } from '../framework'; export * from './elasticsearch_adapter'; import { EventsAdapter, @@ -13,7 +13,6 @@ import { LastEventTimeRequestOptions, RequestDetailsOptions, } from './types'; -import { EventsOverTimeData } from '../../../public/graphql/types'; export class Events { constructor(private readonly adapter: EventsAdapter) {} @@ -38,11 +37,4 @@ export class Events { ): Promise<LastEventTimeData> { return this.adapter.getLastEventTimeData(req, options); } - - public async getEventsOverTime( - req: FrameworkRequest, - options: RequestBasicOptions - ): Promise<EventsOverTimeData> { - return this.adapter.getEventsOverTime(req, options); - } } diff --git a/x-pack/legacy/plugins/siem/server/lib/events/mock.ts b/x-pack/legacy/plugins/siem/server/lib/events/mock.ts index 195c0cd674af5..3eb841cbad411 100644 --- a/x-pack/legacy/plugins/siem/server/lib/events/mock.ts +++ b/x-pack/legacy/plugins/siem/server/lib/events/mock.ts @@ -189,8 +189,7 @@ export const mockOptions: RequestDetailsOptions = { }; export const mockRequest = { - params: {}, - payload: { + body: { operationName: 'GetNetworkTopNFlowQuery', variables: { indexName: 'auditbeat-8.0.0-2019.03.29-000003', diff --git a/x-pack/legacy/plugins/siem/server/lib/events/types.ts b/x-pack/legacy/plugins/siem/server/lib/events/types.ts index 2da0ff13638e1..3a4a8705f7387 100644 --- a/x-pack/legacy/plugins/siem/server/lib/events/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/events/types.ts @@ -11,14 +11,8 @@ import { SourceConfiguration, TimelineData, TimelineDetailsData, - EventsOverTimeData, } from '../../graphql/types'; -import { - FrameworkRequest, - RequestOptions, - RequestOptionsPaginated, - RequestBasicOptions, -} from '../framework'; +import { FrameworkRequest, RequestOptions, RequestOptionsPaginated } from '../framework'; import { SearchHit } from '../types'; export interface EventsAdapter { @@ -31,10 +25,6 @@ export interface EventsAdapter { req: FrameworkRequest, options: LastEventTimeRequestOptions ): Promise<LastEventTimeData>; - getEventsOverTime( - req: FrameworkRequest, - options: RequestBasicOptions - ): Promise<EventsOverTimeData>; } export interface TimelineRequestOptions extends RequestOptions { diff --git a/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts index 39f75e6ea36c3..4cce0b0999257 100644 --- a/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts @@ -9,35 +9,27 @@ import { GraphQLSchema } from 'graphql'; import { runHttpQuery } from 'apollo-server-core'; import { schema as configSchema } from '@kbn/config-schema'; import { - CoreSetup, IRouter, KibanaResponseFactory, RequestHandlerContext, - PluginInitializerContext, KibanaRequest, } from '../../../../../../../src/core/server'; import { IndexPatternsFetcher } from '../../../../../../../src/plugins/data/server'; import { AuthenticatedUser } from '../../../../../../plugins/security/common/model'; -import { RequestFacade } from '../../types'; +import { CoreSetup, SetupPlugins } from '../../plugin'; import { FrameworkAdapter, FrameworkIndexPatternsService, FrameworkRequest, internalFrameworkRequest, - WrappableRequest, } from './types'; -import { SiemPluginSecurity, PluginsSetup } from '../../plugin'; export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { - public version: string; - private isProductionMode: boolean; private router: IRouter; - private security: SiemPluginSecurity; + private security: SetupPlugins['security']; - constructor(core: CoreSetup, plugins: PluginsSetup, env: PluginInitializerContext['env']) { - this.version = env.packageInfo.version; - this.isProductionMode = env.mode.prod; + constructor(core: CoreSetup, plugins: SetupPlugins, private isProductionMode: boolean) { this.router = core.http.createRouter(); this.security = plugins.security; } @@ -68,13 +60,7 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { this.router.post( { path: routePath, - validate: { - body: configSchema.object({ - operationName: configSchema.string(), - query: configSchema.string(), - variables: configSchema.object({}, { allowUnknowns: true }), - }), - }, + validate: { body: configSchema.object({}, { allowUnknowns: true }) }, options: { tags: ['access:siem'], }, @@ -84,7 +70,7 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { const user = await this.getCurrentUserInfo(request); const gqlResponse = await runHttpQuery([request], { method: 'POST', - options: (req: RequestFacade) => ({ + options: (req: KibanaRequest) => ({ context: { req: wrapRequest(req, context, user) }, schema, }), @@ -104,39 +90,6 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { ); if (!this.isProductionMode) { - this.router.get( - { - path: routePath, - validate: { query: configSchema.object({}, { allowUnknowns: true }) }, - options: { - tags: ['access:siem'], - }, - }, - async (context, request, response) => { - try { - const user = await this.getCurrentUserInfo(request); - const { query } = request; - const gqlResponse = await runHttpQuery([request], { - method: 'GET', - options: (req: RequestFacade) => ({ - context: { req: wrapRequest(req, context, user) }, - schema, - }), - query, - }); - - return response.ok({ - body: gqlResponse, - headers: { - 'content-type': 'application/json', - }, - }); - } catch (error) { - return this.handleError(error, response); - } - } - ); - this.router.get( { path: `${routePath}/graphiql`, @@ -150,7 +103,7 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { request.query, { endpointURL: routePath, - passHeader: `'kbn-version': '${this.version}'`, + passHeader: "'kbn-xsrf': 'graphiql'", }, request ); @@ -208,20 +161,15 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { } } -export function wrapRequest<InternalRequest extends WrappableRequest>( - req: InternalRequest, +export function wrapRequest( + request: KibanaRequest, context: RequestHandlerContext, user: AuthenticatedUser | null -): FrameworkRequest<InternalRequest> { - const { auth, params, payload, query } = req; - +): FrameworkRequest { return { - [internalFrameworkRequest]: req, - auth, + [internalFrameworkRequest]: request, + body: request.body, context, - params, - payload, - query, user, }; } diff --git a/x-pack/legacy/plugins/siem/server/lib/framework/types.ts b/x-pack/legacy/plugins/siem/server/lib/framework/types.ts index 67861ce0dcf28..7d049d1dcd195 100644 --- a/x-pack/legacy/plugins/siem/server/lib/framework/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/framework/types.ts @@ -6,9 +6,8 @@ import { IndicesGetMappingParams } from 'elasticsearch'; import { GraphQLSchema } from 'graphql'; -import { RequestAuth } from 'hapi'; -import { RequestHandlerContext } from '../../../../../../../src/core/server'; +import { RequestHandlerContext, KibanaRequest } from '../../../../../../../src/core/server'; import { AuthenticatedUser } from '../../../../../../plugins/security/common/model'; import { ESQuery } from '../../../common/typed_json'; import { @@ -18,15 +17,14 @@ import { SourceConfiguration, TimerangeInput, Maybe, + HistogramType, } from '../../graphql/types'; -import { RequestFacade } from '../../types'; export * from '../../utils/typed_resolvers'; export const internalFrameworkRequest = Symbol('internalFrameworkRequest'); export interface FrameworkAdapter { - version: string; registerGraphQLEndpoint(routePath: string, schema: GraphQLSchema): void; callWithRequest<Hit = {}, Aggregation = undefined>( req: FrameworkRequest, @@ -46,24 +44,12 @@ export interface FrameworkAdapter { getIndexPatternsService(req: FrameworkRequest): FrameworkIndexPatternsService; } -export interface FrameworkRequest<InternalRequest extends WrappableRequest = RequestFacade> { - [internalFrameworkRequest]: InternalRequest; +export interface FrameworkRequest extends Pick<KibanaRequest, 'body'> { + [internalFrameworkRequest]: KibanaRequest; context: RequestHandlerContext; - payload: InternalRequest['payload']; - params: InternalRequest['params']; - query: InternalRequest['query']; - auth: InternalRequest['auth']; user: AuthenticatedUser | null; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export interface WrappableRequest<Payload = any, Params = any, Query = any> { - payload: Payload; - params: Params; - query: Query; - auth: RequestAuth; -} - export interface DatabaseResponse { took: number; timeout: boolean; @@ -132,7 +118,8 @@ export interface RequestBasicOptions { } export interface MatrixHistogramRequestOptions extends RequestBasicOptions { - stackByField?: Maybe<string>; + stackByField: Maybe<string>; + histogramType: HistogramType; } export interface RequestOptions extends RequestBasicOptions { diff --git a/x-pack/legacy/plugins/siem/server/lib/hosts/elasticsearch_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/hosts/elasticsearch_adapter.test.ts index 0d698f1e19213..20510e1089f96 100644 --- a/x-pack/legacy/plugins/siem/server/lib/hosts/elasticsearch_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/hosts/elasticsearch_adapter.test.ts @@ -159,7 +159,6 @@ describe('hosts elasticsearch_adapter', () => { const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockGetHostsResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), @@ -180,7 +179,6 @@ describe('hosts elasticsearch_adapter', () => { const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockGetHostOverviewResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), @@ -201,7 +199,6 @@ describe('hosts elasticsearch_adapter', () => { const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockGetHostLastFirstSeenResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/server/lib/hosts/mock.ts b/x-pack/legacy/plugins/siem/server/lib/hosts/mock.ts index 66b73742cc45e..6b72c4a5a2843 100644 --- a/x-pack/legacy/plugins/siem/server/lib/hosts/mock.ts +++ b/x-pack/legacy/plugins/siem/server/lib/hosts/mock.ts @@ -49,8 +49,7 @@ export const mockGetHostsOptions: HostsRequestOptions = { }; export const mockGetHostsRequest = { - params: {}, - payload: { + body: { operationName: 'GetHostsTableQuery', variables: { sourceId: 'default', @@ -67,7 +66,6 @@ export const mockGetHostsRequest = { query: 'query GetHostsTableQuery($sourceId: ID!, $timerange: TimerangeInput!, $pagination: PaginationInput!, $sort: HostsSortField!, $filterQuery: String) {\n source(id: $sourceId) {\n id\n Hosts(timerange: $timerange, pagination: $pagination, sort: $sort, filterQuery: $filterQuery) {\n totalCount\n edges {\n node {\n _id\n host {\n id\n name\n os {\n name\n version\n __typename\n }\n __typename\n }\n __typename\n }\n cursor {\n value\n __typename\n }\n __typename\n }\n pageInfo {\n endCursor {\n value\n __typename\n }\n hasNextPage\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', }, - query: {}, }; export const mockGetHostsResponse = { @@ -327,14 +325,12 @@ export const mockGetHostOverviewOptions: HostOverviewRequestOptions = { }; export const mockGetHostOverviewRequest = { - params: {}, - payload: { + body: { operationName: 'GetHostOverviewQuery', variables: { sourceId: 'default', hostName: 'siem-es' }, query: 'query GetHostOverviewQuery($sourceId: ID!, $hostName: String!, $timerange: TimerangeInput!) {\n source(id: $sourceId) {\n id\n HostOverview(hostName: $hostName, timerange: $timerange) {\n _id\n host {\n architecture\n id\n ip\n mac\n name\n os {\n family\n name\n platform\n version\n __typename\n }\n type\n __typename\n }\n cloud {\n instance {\n id\n __typename\n }\n machine {\n type\n __typename\n }\n provider\n region\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', }, - query: {}, }; export const mockGetHostOverviewResponse = { @@ -520,14 +516,12 @@ export const mockGetHostLastFirstSeenOptions: HostLastFirstSeenRequestOptions = }; export const mockGetHostLastFirstSeenRequest = { - params: {}, - payload: { + body: { operationName: 'GetHostLastFirstSeenQuery', variables: { sourceId: 'default', hostName: 'siem-es' }, query: 'query GetHostLastFirstSeenQuery($sourceId: ID!, $hostName: String!) {\n source(id: $sourceId) {\n id\n HostLastFirstSeen(hostName: $hostName) {\n firstSeen\n lastSeen\n __typename\n }\n __typename\n }\n}\n', }, - query: {}, }; export const mockGetHostLastFirstSeenResponse = { diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.test.ts index 4a179073852b0..059d15220b619 100644 --- a/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/elasticsearch_adapter.test.ts @@ -53,7 +53,6 @@ describe('getKpiHosts', () => { let data: KpiHostsData; const mockCallWithRequest = jest.fn(); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), @@ -167,7 +166,6 @@ describe('getKpiHostDetails', () => { let data: KpiHostDetailsData; const mockCallWithRequest = jest.fn(); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/mock.ts b/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/mock.ts index a1962067f9bec..b82a540900bd0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/mock.ts +++ b/x-pack/legacy/plugins/siem/server/lib/kpi_hosts/mock.ts @@ -43,8 +43,7 @@ export const mockKpiHostDetailsOptions: RequestBasicOptions = { }; export const mockKpiHostsRequest = { - params: {}, - payload: { + body: { operationName: 'GetKpiHostsQuery', variables: { sourceId: 'default', @@ -54,12 +53,10 @@ export const mockKpiHostsRequest = { query: 'fragment KpiHostChartFields on KpiHostHistogramData {\n x\n y\n __typename\n}\n\nquery GetKpiHostsQuery($sourceId: ID!, $timerange: TimerangeInput!, $filterQuery: String, $defaultIndex: [String!]!) {\n source(id: $sourceId) {\n id\n KpiHosts(timerange: $timerange, filterQuery: $filterQuery, defaultIndex: $defaultIndex) {\n hosts\n hostsHistogram {\n ...KpiHostChartFields\n __typename\n }\n authSuccess\n authSuccessHistogram {\n ...KpiHostChartFields\n __typename\n }\n authFailure\n authFailureHistogram {\n ...KpiHostChartFields\n __typename\n }\n uniqueSourceIps\n uniqueSourceIpsHistogram {\n ...KpiHostChartFields\n __typename\n }\n uniqueDestinationIps\n uniqueDestinationIpsHistogram {\n ...KpiHostChartFields\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', }, - query: {}, }; export const mockKpiHostDetailsRequest = { - params: {}, - payload: { + body: { operationName: 'GetKpiHostDetailsQuery', variables: { sourceId: 'default', @@ -69,7 +66,6 @@ export const mockKpiHostDetailsRequest = { query: 'fragment KpiHostDetailsChartFields on KpiHostHistogramData {\n x\n y\n __typename\n}\n\nquery GetKpiHostDetailsQuery($sourceId: ID!, $timerange: TimerangeInput!, $filterQuery: String, $defaultIndex: [String!]!, $hostName: String!) {\n source(id: $sourceId) {\n id\n KpiHostDetails(timerange: $timerange, filterQuery: $filterQuery, defaultIndex: $defaultIndex, hostName: $hostName) {\n authSuccess\n authSuccessHistogram {\n ...KpiHostDetailsChartFields\n __typename\n }\n authFailure\n authFailureHistogram {\n ...KpiHostDetailsChartFields\n __typename\n }\n uniqueSourceIps\n uniqueSourceIpsHistogram {\n ...KpiHostDetailsChartFields\n __typename\n }\n uniqueDestinationIps\n uniqueDestinationIpsHistogram {\n ...KpiHostDetailsChartFields\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', }, - query: {}, }; const mockUniqueIpsResponse = { diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_network/elastic_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/kpi_network/elastic_adapter.test.ts index 11d007f591fac..58ee7c9aa1cf8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/kpi_network/elastic_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/kpi_network/elastic_adapter.test.ts @@ -48,7 +48,6 @@ describe('Network Kpi elasticsearch_adapter', () => { const mockCallWithRequest = jest.fn(); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/server/lib/kpi_network/mock.ts b/x-pack/legacy/plugins/siem/server/lib/kpi_network/mock.ts index 5b0601b88c779..7d86769de09f1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/kpi_network/mock.ts +++ b/x-pack/legacy/plugins/siem/server/lib/kpi_network/mock.ts @@ -24,8 +24,7 @@ export const mockOptions: RequestBasicOptions = { }; export const mockRequest = { - params: {}, - payload: { + body: { operationName: 'GetKpiNetworkQuery', variables: { sourceId: 'default', @@ -35,7 +34,6 @@ export const mockRequest = { query: 'fragment KpiNetworkChartFields on KpiNetworkHistogramData {\n x\n y\n __typename\n}\n\nquery GetKpiNetworkQuery($sourceId: ID!, $timerange: TimerangeInput!, $filterQuery: String, $defaultIndex: [String!]!) {\n source(id: $sourceId) {\n id\n KpiNetwork(timerange: $timerange, filterQuery: $filterQuery, defaultIndex: $defaultIndex) {\n networkEvents\n uniqueFlowId\n uniqueSourcePrivateIps\n uniqueSourcePrivateIpsHistogram {\n ...KpiNetworkChartFields\n __typename\n }\n uniqueDestinationPrivateIps\n uniqueDestinationPrivateIpsHistogram {\n ...KpiNetworkChartFields\n __typename\n }\n dnsQueries\n tlsHandshakes\n __typename\n }\n __typename\n }\n}\n', }, - query: {}, }; export const mockResponse = { diff --git a/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/elasticsearch_adapter.ts new file mode 100644 index 0000000000000..f661fe165130e --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/elasticsearch_adapter.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; + +import { MatrixHistogramOverTimeData, HistogramType } from '../../graphql/types'; +import { inspectStringifyObject } from '../../utils/build_query'; +import { FrameworkAdapter, FrameworkRequest, MatrixHistogramRequestOptions } from '../framework'; +import { MatrixHistogramAdapter, MatrixHistogramDataConfig, MatrixHistogramHit } from './types'; +import { TermAggregation } from '../types'; +import { buildAnomaliesOverTimeQuery } from './query.anomalies_over_time.dsl'; +import { buildDnsHistogramQuery } from './query_dns_histogram.dsl'; +import { buildEventsOverTimeQuery } from './query.events_over_time.dsl'; +import { getDnsParsedData, getGenericData } from './utils'; +import { buildAuthenticationsOverTimeQuery } from './query.authentications_over_time.dsl'; +import { buildAlertsHistogramQuery } from './query_alerts.dsl'; + +const matrixHistogramConfig: MatrixHistogramDataConfig = { + [HistogramType.alerts]: { + buildDsl: buildAlertsHistogramQuery, + aggName: 'aggregations.alertsGroup.buckets', + parseKey: 'alerts.buckets', + }, + [HistogramType.anomalies]: { + buildDsl: buildAnomaliesOverTimeQuery, + aggName: 'aggregations.anomalyActionGroup.buckets', + parseKey: 'anomalies.buckets', + }, + [HistogramType.authentications]: { + buildDsl: buildAuthenticationsOverTimeQuery, + aggName: 'aggregations.eventActionGroup.buckets', + parseKey: 'events.buckets', + }, + [HistogramType.dns]: { + buildDsl: buildDnsHistogramQuery, + aggName: 'aggregations.NetworkDns.buckets', + parseKey: 'dns.buckets', + parser: getDnsParsedData, + }, + [HistogramType.events]: { + buildDsl: buildEventsOverTimeQuery, + aggName: 'aggregations.eventActionGroup.buckets', + parseKey: 'events.buckets', + }, +}; + +export class ElasticsearchMatrixHistogramAdapter implements MatrixHistogramAdapter { + constructor(private readonly framework: FrameworkAdapter) {} + + public async getHistogramData( + request: FrameworkRequest, + options: MatrixHistogramRequestOptions + ): Promise<MatrixHistogramOverTimeData> { + const myConfig = getOr(null, options.histogramType, matrixHistogramConfig); + if (myConfig == null) { + throw new Error(`This histogram type ${options.histogramType} is unknown to the server side`); + } + const dsl = myConfig.buildDsl(options); + const response = await this.framework.callWithRequest< + MatrixHistogramHit<HistogramType>, + TermAggregation + >(request, 'search', dsl); + const totalCount = getOr(0, 'hits.total.value', response); + const matrixHistogramData = getOr([], myConfig.aggName, response); + const inspect = { + dsl: [inspectStringifyObject(dsl)], + response: [inspectStringifyObject(response)], + }; + + return { + inspect, + matrixHistogramData: myConfig.parser + ? myConfig.parser<typeof options.histogramType>(matrixHistogramData, myConfig.parseKey) + : getGenericData<typeof options.histogramType>(matrixHistogramData, myConfig.parseKey), + totalCount, + }; + } +} diff --git a/x-pack/legacy/plugins/siem/server/lib/alerts/elasticseatch_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/elasticseatch_adapter.test.ts similarity index 85% rename from x-pack/legacy/plugins/siem/server/lib/alerts/elasticseatch_adapter.test.ts rename to x-pack/legacy/plugins/siem/server/lib/matrix_histogram/elasticseatch_adapter.test.ts index 3aefb6c0e1e5f..0b63785d2203b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/alerts/elasticseatch_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/elasticseatch_adapter.test.ts @@ -6,7 +6,7 @@ import { FrameworkAdapter, FrameworkRequest, MatrixHistogramRequestOptions } from '../framework'; import expect from '@kbn/expect'; -import { ElasticsearchAlertsAdapter } from './elasticsearch_adapter'; +import { ElasticsearchMatrixHistogramAdapter } from './elasticsearch_adapter'; import { mockRequest, mockOptions, @@ -15,7 +15,7 @@ import { mockAlertsHistogramDataFormattedResponse, } from './mock'; -jest.mock('./query.dsl', () => { +jest.mock('./query_alerts.dsl', () => { return { buildAlertsHistogramQuery: jest.fn(() => mockAlertsHistogramQueryDsl), }; @@ -29,7 +29,6 @@ describe('alerts elasticsearch_adapter', () => { return mockAlertsHistogramDataResponse; }); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), @@ -38,8 +37,8 @@ describe('alerts elasticsearch_adapter', () => { callWithRequest: mockCallWithRequest, })); - const EsNetworkTimelineAlerts = new ElasticsearchAlertsAdapter(mockFramework); - const data = await EsNetworkTimelineAlerts.getAlertsHistogramData( + const adapter = new ElasticsearchMatrixHistogramAdapter(mockFramework); + const data = await adapter.getHistogramData( (mockRequest as unknown) as FrameworkRequest, (mockOptions as unknown) as MatrixHistogramRequestOptions ); diff --git a/x-pack/legacy/plugins/siem/server/lib/anomalies/index.ts b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/index.ts similarity index 55% rename from x-pack/legacy/plugins/siem/server/lib/anomalies/index.ts rename to x-pack/legacy/plugins/siem/server/lib/matrix_histogram/index.ts index 727c45a3bac44..900a6ab619ae0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/anomalies/index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/index.ts @@ -6,16 +6,16 @@ import { FrameworkRequest, MatrixHistogramRequestOptions } from '../framework'; export * from './elasticsearch_adapter'; -import { AnomaliesAdapter } from './types'; -import { AnomaliesOverTimeData } from '../../../public/graphql/types'; +import { MatrixHistogramAdapter } from './types'; +import { MatrixHistogramOverTimeData } from '../../graphql/types'; -export class Anomalies { - constructor(private readonly adapter: AnomaliesAdapter) {} +export class MatrixHistogram { + constructor(private readonly adapter: MatrixHistogramAdapter) {} - public async getAnomaliesOverTime( + public async getMatrixHistogramData( req: FrameworkRequest, options: MatrixHistogramRequestOptions - ): Promise<AnomaliesOverTimeData> { - return this.adapter.getAnomaliesOverTime(req, options); + ): Promise<MatrixHistogramOverTimeData> { + return this.adapter.getHistogramData(req, options); } } diff --git a/x-pack/legacy/plugins/siem/server/lib/alerts/mock.ts b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/mock.ts similarity index 95% rename from x-pack/legacy/plugins/siem/server/lib/alerts/mock.ts rename to x-pack/legacy/plugins/siem/server/lib/matrix_histogram/mock.ts index fe0b6673f3191..3e51e926bea87 100644 --- a/x-pack/legacy/plugins/siem/server/lib/alerts/mock.ts +++ b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/mock.ts @@ -5,6 +5,7 @@ */ import { defaultIndexPattern } from '../../../default_index_pattern'; +import { HistogramType } from '../../graphql/types'; export const mockAlertsHistogramDataResponse = { took: 513, @@ -36,7 +37,7 @@ export const mockAlertsHistogramDataResponse = { hits: [], }, aggregations: { - alertsByModuleGroup: { + alertsGroup: { doc_count_error_upper_bound: 0, sum_other_doc_count: 802087, buckets: [ @@ -112,4 +113,6 @@ export const mockOptions = { }, defaultIndex: defaultIndexPattern, filterQuery: '', + stackByField: 'event.module', + histogramType: HistogramType.alerts, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/anomalies/query.anomalies_over_time.dsl.ts b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query.anomalies_over_time.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/anomalies/query.anomalies_over_time.dsl.ts rename to x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query.anomalies_over_time.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/authentications/query.authentications_over_time.dsl.ts b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query.authentications_over_time.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/authentications/query.authentications_over_time.dsl.ts rename to x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query.authentications_over_time.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/events/query.events_over_time.dsl.ts b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query.events_over_time.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/server/lib/events/query.events_over_time.dsl.ts rename to x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query.events_over_time.dsl.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/alerts/query.dsl.ts b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query_alerts.dsl.ts similarity index 98% rename from x-pack/legacy/plugins/siem/server/lib/alerts/query.dsl.ts rename to x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query_alerts.dsl.ts index eb82327197543..4963f01d67a4f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/alerts/query.dsl.ts +++ b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query_alerts.dsl.ts @@ -82,7 +82,7 @@ export const buildAlertsHistogramQuery = ({ }, }; return { - alertsByModuleGroup: { + alertsGroup: { terms: { field: stackByField, missing: 'All others', diff --git a/x-pack/legacy/plugins/siem/server/lib/network/query_dns_histogram.dsl.ts b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query_dns_histogram.dsl.ts similarity index 98% rename from x-pack/legacy/plugins/siem/server/lib/network/query_dns_histogram.dsl.ts rename to x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query_dns_histogram.dsl.ts index 1ce324e0ffff8..a6c75fe01eb15 100644 --- a/x-pack/legacy/plugins/siem/server/lib/network/query_dns_histogram.dsl.ts +++ b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/query_dns_histogram.dsl.ts @@ -42,7 +42,7 @@ export const buildDnsHistogramQuery = ({ NetworkDns: { ...dateHistogram, aggs: { - histogram: { + dns: { terms: { field: stackByField, order: { diff --git a/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/types.ts b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/types.ts new file mode 100644 index 0000000000000..87ea4b81f5fba --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/types.ts @@ -0,0 +1,144 @@ +/* + * 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 { + MatrixHistogramOverTimeData, + HistogramType, + MatrixOverTimeHistogramData, +} from '../../graphql/types'; +import { FrameworkRequest, MatrixHistogramRequestOptions } from '../framework'; +import { SearchHit } from '../types'; +import { EventHit } from '../events/types'; +import { AuthenticationHit } from '../authentications/types'; + +export interface HistogramBucket { + key: number; + doc_count: number; +} + +interface AlertsGroupData { + key: string; + doc_count: number; + alerts: { + buckets: HistogramBucket[]; + }; +} + +interface AnomaliesOverTimeHistogramData { + key_as_string: string; + key: number; + doc_count: number; +} + +export interface AnomaliesActionGroupData { + key: number; + anomalies: { + bucket: AnomaliesOverTimeHistogramData[]; + }; + doc_count: number; +} + +export interface AnomalySource { + [field: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any +} + +export interface AnomalyHit extends SearchHit { + sort: string[]; + _source: AnomalySource; + aggregations: { + [agg: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any + }; +} + +interface EventsOverTimeHistogramData { + key_as_string: string; + key: number; + doc_count: number; +} + +export interface EventsActionGroupData { + key: number; + events: { + bucket: EventsOverTimeHistogramData[]; + }; + doc_count: number; +} + +export interface DnsHistogramSubBucket { + key: string; + doc_count: number; + orderAgg: { + value: number; + }; +} +interface DnsHistogramBucket { + doc_count_error_upper_bound: number; + sum_other_doc_count: number; + buckets: DnsHistogramSubBucket[]; +} + +export interface DnsHistogramGroupData { + key: number; + doc_count: number; + key_as_string: string; + histogram: DnsHistogramBucket; +} + +export interface MatrixHistogramSchema<T> { + buildDsl: (options: MatrixHistogramRequestOptions) => {}; + aggName: string; + parseKey: string; + parser?: <T>( + data: MatrixHistogramParseData<T>, + keyBucket: string + ) => MatrixOverTimeHistogramData[]; +} + +export type MatrixHistogramParseData<T> = T extends HistogramType.alerts + ? AlertsGroupData[] + : T extends HistogramType.anomalies + ? AnomaliesActionGroupData[] + : T extends HistogramType.dns + ? DnsHistogramGroupData[] + : T extends HistogramType.authentications + ? AuthenticationsActionGroupData[] + : T extends HistogramType.events + ? EventsActionGroupData[] + : never; + +export type MatrixHistogramHit<T> = T extends HistogramType.alerts + ? EventHit + : T extends HistogramType.anomalies + ? AnomalyHit + : T extends HistogramType.dns + ? EventHit + : T extends HistogramType.authentications + ? AuthenticationHit + : T extends HistogramType.events + ? EventHit + : never; + +export type MatrixHistogramDataConfig = Record<HistogramType, MatrixHistogramSchema<HistogramType>>; +interface AuthenticationsOverTimeHistogramData { + key_as_string: string; + key: number; + doc_count: number; +} + +export interface AuthenticationsActionGroupData { + key: number; + events: { + bucket: AuthenticationsOverTimeHistogramData[]; + }; + doc_count: number; +} + +export interface MatrixHistogramAdapter { + getHistogramData( + request: FrameworkRequest, + options: MatrixHistogramRequestOptions + ): Promise<MatrixHistogramOverTimeData>; +} diff --git a/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/utils.ts b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/utils.ts new file mode 100644 index 0000000000000..67568b96fee90 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/matrix_histogram/utils.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get, getOr } from 'lodash/fp'; +import { MatrixHistogramParseData, DnsHistogramSubBucket, HistogramBucket } from './types'; +import { MatrixOverTimeHistogramData } from '../../graphql/types'; + +export const getDnsParsedData = <T>( + data: MatrixHistogramParseData<T>, + keyBucket: string +): MatrixOverTimeHistogramData[] => { + let result: MatrixOverTimeHistogramData[] = []; + data.forEach((bucketData: unknown) => { + const time = get('key', bucketData); + const histData = getOr([], keyBucket, bucketData).map( + ({ key, doc_count }: DnsHistogramSubBucket) => ({ + x: time, + y: doc_count, + g: key, + }) + ); + result = [...result, ...histData]; + }); + return result; +}; + +export const getGenericData = <T>( + data: MatrixHistogramParseData<T>, + keyBucket: string +): MatrixOverTimeHistogramData[] => { + let result: MatrixOverTimeHistogramData[] = []; + data.forEach((bucketData: unknown) => { + const group = get('key', bucketData); + const histData = getOr([], keyBucket, bucketData).map( + ({ key, doc_count }: HistogramBucket) => ({ + x: key, + y: doc_count, + g: group, + }) + ); + result = [...result, ...histData]; + }); + + return result; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/network/elastic_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/network/elastic_adapter.test.ts index eeea4bec2fb25..eab461ee07ca7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/network/elastic_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/network/elastic_adapter.test.ts @@ -35,7 +35,6 @@ describe('Network Top N flow elasticsearch_adapter with FlowTarget=source', () = const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, getIndexPatternsService: jest.fn(), registerGraphQLEndpoint: jest.fn(), @@ -61,7 +60,6 @@ describe('Network Top N flow elasticsearch_adapter with FlowTarget=source', () = const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockNoDataResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), @@ -101,7 +99,6 @@ describe('Network Top N flow elasticsearch_adapter with FlowTarget=source', () = ].buckets[0].location.top_geo.hits.hits = []; mockCallWithRequest.mockResolvedValue(mockNoGeoDataResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, getIndexPatternsService: jest.fn(), registerGraphQLEndpoint: jest.fn(), @@ -132,7 +129,6 @@ describe('Network Top N flow elasticsearch_adapter with FlowTarget=source', () = const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockNoPaginationResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), @@ -155,7 +151,6 @@ describe('Network Top N flow elasticsearch_adapter with FlowTarget=source', () = const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockResponseIp); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, getIndexPatternsService: jest.fn(), registerGraphQLEndpoint: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts index 4bd980fd2ff80..39babc58ee138 100644 --- a/x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts @@ -18,16 +18,9 @@ import { NetworkHttpData, NetworkHttpEdges, NetworkTopNFlowEdges, - NetworkDsOverTimeData, - MatrixOverTimeHistogramData, } from '../../graphql/types'; import { inspectStringifyObject } from '../../utils/build_query'; -import { - DatabaseSearchResponse, - FrameworkAdapter, - FrameworkRequest, - MatrixHistogramRequestOptions, -} from '../framework'; +import { DatabaseSearchResponse, FrameworkAdapter, FrameworkRequest } from '../framework'; import { TermAggregation } from '../types'; import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants'; @@ -38,7 +31,6 @@ import { NetworkTopNFlowRequestOptions, } from './index'; import { buildDnsQuery } from './query_dns.dsl'; -import { buildDnsHistogramQuery } from './query_dns_histogram.dsl'; import { buildTopNFlowQuery, getOppositeField } from './query_top_n_flow.dsl'; import { buildHttpQuery } from './query_http.dsl'; import { buildTopCountriesQuery } from './query_top_countries.dsl'; @@ -48,9 +40,7 @@ import { NetworkTopCountriesBuckets, NetworkHttpBuckets, NetworkTopNFlowBuckets, - DnsHistogramGroupData, } from './types'; -import { EventHit } from '../events/types'; export class ElasticsearchNetworkAdapter implements NetworkAdapter { constructor(private readonly framework: FrameworkAdapter) {} @@ -202,41 +192,8 @@ export class ElasticsearchNetworkAdapter implements NetworkAdapter { totalCount, }; } - - public async getNetworkDnsHistogramData( - request: FrameworkRequest, - options: MatrixHistogramRequestOptions - ): Promise<NetworkDsOverTimeData> { - const dsl = buildDnsHistogramQuery(options); - const response = await this.framework.callWithRequest<EventHit, TermAggregation>( - request, - 'search', - dsl - ); - const totalCount = getOr(0, 'hits.total.value', response); - const matrixHistogramData = getOr([], 'aggregations.NetworkDns.buckets', response); - const inspect = { - dsl: [inspectStringifyObject(dsl)], - response: [inspectStringifyObject(response)], - }; - return { - inspect, - matrixHistogramData: getHistogramData(matrixHistogramData), - totalCount, - }; - } } -const getHistogramData = (data: DnsHistogramGroupData[]): MatrixOverTimeHistogramData[] => { - return data.reduce( - (acc: MatrixOverTimeHistogramData[], { key: time, histogram: { buckets } }) => { - const temp = buckets.map(({ key, doc_count }) => ({ x: time, y: doc_count, g: key })); - return [...acc, ...temp]; - }, - [] - ); -}; - const getTopNFlowEdges = ( response: DatabaseSearchResponse<NetworkTopNFlowData, TermAggregation>, options: NetworkTopNFlowRequestOptions diff --git a/x-pack/legacy/plugins/siem/server/lib/network/index.ts b/x-pack/legacy/plugins/siem/server/lib/network/index.ts index cbcd33b753d8a..42ce9f0726ddb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/network/index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/network/index.ts @@ -14,13 +14,8 @@ import { NetworkTopCountriesData, NetworkTopNFlowData, NetworkTopTablesSortField, - NetworkDsOverTimeData, } from '../../graphql/types'; -import { - FrameworkRequest, - RequestOptionsPaginated, - MatrixHistogramRequestOptions, -} from '../framework'; +import { FrameworkRequest, RequestOptionsPaginated } from '../framework'; export * from './elasticsearch_adapter'; import { NetworkAdapter } from './types'; @@ -73,13 +68,6 @@ export class Network { return this.adapter.getNetworkDns(req, options); } - public async getNetworkDnsHistogramData( - req: FrameworkRequest, - options: MatrixHistogramRequestOptions - ): Promise<NetworkDsOverTimeData> { - return this.adapter.getNetworkDnsHistogramData(req, options); - } - public async getNetworkHttp( req: FrameworkRequest, options: NetworkHttpRequestOptions diff --git a/x-pack/legacy/plugins/siem/server/lib/network/mock.ts b/x-pack/legacy/plugins/siem/server/lib/network/mock.ts index 21b00bf188d20..7ea692f27ef04 100644 --- a/x-pack/legacy/plugins/siem/server/lib/network/mock.ts +++ b/x-pack/legacy/plugins/siem/server/lib/network/mock.ts @@ -59,8 +59,7 @@ export const mockOptions: NetworkTopNFlowRequestOptions = { }; export const mockRequest = { - params: {}, - payload: { + body: { operationName: 'GetNetworkTopNFlowQuery', variables: { filterQuery: '', @@ -1507,10 +1506,10 @@ export const mockOptionsIp: NetworkTopNFlowRequestOptions = { export const mockRequestIp = { ...mockRequest, - payload: { - ...mockRequest.payload, + body: { + ...mockRequest.body, variables: { - ...mockRequest.payload.variables, + ...mockRequest.body.variables, ip: '1.1.1.1', }, }, diff --git a/x-pack/legacy/plugins/siem/server/lib/network/types.ts b/x-pack/legacy/plugins/siem/server/lib/network/types.ts index b5563f9a2fef1..b7848be097151 100644 --- a/x-pack/legacy/plugins/siem/server/lib/network/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/network/types.ts @@ -9,13 +9,8 @@ import { NetworkHttpData, NetworkTopCountriesData, NetworkTopNFlowData, - NetworkDsOverTimeData, } from '../../graphql/types'; -import { - FrameworkRequest, - RequestOptionsPaginated, - MatrixHistogramRequestOptions, -} from '../framework'; +import { FrameworkRequest, RequestOptionsPaginated } from '../framework'; import { TotalValue } from '../types'; import { NetworkDnsRequestOptions } from '.'; @@ -29,10 +24,6 @@ export interface NetworkAdapter { options: RequestOptionsPaginated ): Promise<NetworkTopNFlowData>; getNetworkDns(req: FrameworkRequest, options: NetworkDnsRequestOptions): Promise<NetworkDnsData>; - getNetworkDnsHistogramData( - request: FrameworkRequest, - options: MatrixHistogramRequestOptions - ): Promise<NetworkDsOverTimeData>; getNetworkHttp(req: FrameworkRequest, options: RequestOptionsPaginated): Promise<NetworkHttpData>; } diff --git a/x-pack/legacy/plugins/siem/server/lib/overview/elastic_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/overview/elastic_adapter.test.ts index 29035f4539be8..f421704dffe12 100644 --- a/x-pack/legacy/plugins/siem/server/lib/overview/elastic_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/overview/elastic_adapter.test.ts @@ -36,7 +36,6 @@ describe('Siem Overview elasticsearch_adapter', () => { const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockResponseNetwork); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), @@ -70,7 +69,6 @@ describe('Siem Overview elasticsearch_adapter', () => { const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockNoDataResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), @@ -108,7 +106,6 @@ describe('Siem Overview elasticsearch_adapter', () => { const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockResponseHost); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), @@ -148,7 +145,6 @@ describe('Siem Overview elasticsearch_adapter', () => { const mockCallWithRequest = jest.fn(); mockCallWithRequest.mockResolvedValue(mockNoDataResponse); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/server/lib/overview/mock.ts b/x-pack/legacy/plugins/siem/server/lib/overview/mock.ts index 6196f45029313..410b4d90b1e78 100644 --- a/x-pack/legacy/plugins/siem/server/lib/overview/mock.ts +++ b/x-pack/legacy/plugins/siem/server/lib/overview/mock.ts @@ -24,8 +24,7 @@ export const mockOptionsNetwork: RequestBasicOptions = { }; export const mockRequestNetwork = { - params: {}, - payload: { + body: { operationName: 'GetOverviewNetworkQuery', variables: { sourceId: 'default', @@ -35,7 +34,6 @@ export const mockRequestNetwork = { query: 'query GetOverviewNetworkQuery(\n $sourceId: ID!\n $timerange: TimerangeInput!\n $filterQuery: String\n ) {\n source(id: $sourceId) {\n id\n OverviewNetwork(timerange: $timerange, filterQuery: $filterQuery) {\n packetbeatFlow\n packetbeatDNS\n filebeatSuricata\n filebeatZeek\n auditbeatSocket\n }\n }\n }', }, - query: {}, }; export const mockResponseNetwork = { @@ -97,8 +95,7 @@ export const mockOptionsHost: RequestBasicOptions = { }; export const mockRequestHost = { - params: {}, - payload: { + body: { operationName: 'GetOverviewHostQuery', variables: { sourceId: 'default', @@ -108,7 +105,6 @@ export const mockRequestHost = { query: 'query GetOverviewHostQuery(\n $sourceId: ID!\n $timerange: TimerangeInput!\n $filterQuery: String\n ) {\n source(id: $sourceId) {\n id\n OverviewHost(timerange: $timerange, filterQuery: $filterQuery) {\n auditbeatAuditd\n auditbeatFIM\n auditbeatLogin\n auditbeatPackage\n auditbeatProcess\n auditbeatUser\n }\n }\n }', }, - query: {}, }; export const mockResponseHost = { diff --git a/x-pack/legacy/plugins/siem/server/lib/tls/elasticsearch_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/tls/elasticsearch_adapter.test.ts index 32a5c72215dda..428685cbaddb8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/tls/elasticsearch_adapter.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/tls/elasticsearch_adapter.test.ts @@ -22,7 +22,6 @@ describe('elasticsearch_adapter', () => { let data: TlsData; const mockCallWithRequest = jest.fn(); const mockFramework: FrameworkAdapter = { - version: 'mock', callWithRequest: mockCallWithRequest, registerGraphQLEndpoint: jest.fn(), getIndexPatternsService: jest.fn(), diff --git a/x-pack/legacy/plugins/siem/server/lib/tls/mock.ts b/x-pack/legacy/plugins/siem/server/lib/tls/mock.ts index a81862b6e7e90..4b27d541ec992 100644 --- a/x-pack/legacy/plugins/siem/server/lib/tls/mock.ts +++ b/x-pack/legacy/plugins/siem/server/lib/tls/mock.ts @@ -212,8 +212,7 @@ export const expectedTlsEdges = [ ]; export const mockRequest = { - params: {}, - payload: { + body: { operationName: 'GetTlsQuery', variables: { defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], @@ -229,7 +228,6 @@ export const mockRequest = { query: 'query GetTlsQuery($sourceId: ID!, $filterQuery: String, $flowTarget: FlowTarget!, $ip: String!, $pagination: PaginationInputPaginated!, $sort: TlsSortField!, $timerange: TimerangeInput!, $defaultIndex: [String!]!, $inspect: Boolean!) {\n source(id: $sourceId) {\n id\n Tls(filterQuery: $filterQuery, flowTarget: $flowTarget, ip: $ip, pagination: $pagination, sort: $sort, timerange: $timerange, defaultIndex: $defaultIndex) {\n totalCount\n edges {\n node {\n _id\n alternativeNames\n commonNames\n ja3\n issuerNames\n notAfter\n __typename\n }\n cursor {\n value\n __typename\n }\n __typename\n }\n pageInfo {\n activePage\n fakeTotalCount\n showMorePagesIndicator\n __typename\n }\n inspect @include(if: $inspect) {\n dsl\n response\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', }, - query: {}, }; export const mockResponse = { diff --git a/x-pack/legacy/plugins/siem/server/lib/types.ts b/x-pack/legacy/plugins/siem/server/lib/types.ts index 9034ab4e6af83..323ced734d24b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/types.ts @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { AuthenticatedUser } from '../../../../../plugins/security/public'; +import { RequestHandlerContext } from '../../../../../../src/core/server'; export { ConfigType as Configuration } from '../../../../../plugins/siem/server'; -import { Anomalies } from './anomalies'; + import { Authentications } from './authentications'; import { Events } from './events'; import { FrameworkAdapter, FrameworkRequest } from './framework'; @@ -23,18 +25,17 @@ import { Note } from './note/saved_object'; import { PinnedEvent } from './pinned_event/saved_object'; import { Timeline } from './timeline/saved_object'; import { TLS } from './tls'; -import { Alerts } from './alerts'; +import { MatrixHistogram } from './matrix_histogram'; export * from './hosts'; export interface AppDomainLibs { - alerts: Alerts; - anomalies: Anomalies; authentications: Authentications; events: Events; fields: IndexFields; hosts: Hosts; ipDetails: IpDetails; + matrixHistogram: MatrixHistogram; network: Network; kpiNetwork: KpiNetwork; overview: Overview; @@ -54,6 +55,8 @@ export interface AppBackendLibs extends AppDomainLibs { export interface SiemContext { req: FrameworkRequest; + context: RequestHandlerContext; + user: AuthenticatedUser | null; } export interface TotalValue { diff --git a/x-pack/legacy/plugins/siem/server/plugin.ts b/x-pack/legacy/plugins/siem/server/plugin.ts index 94314367be59c..e15248e5200ee 100644 --- a/x-pack/legacy/plugins/siem/server/plugin.ts +++ b/x-pack/legacy/plugins/siem/server/plugin.ts @@ -5,39 +5,71 @@ */ import { i18n } from '@kbn/i18n'; -import { CoreSetup, PluginInitializerContext, Logger } from 'src/core/server'; -import { SecurityPluginSetup } from '../../../../plugins/security/server'; -import { PluginSetupContract as FeaturesSetupContract } from '../../../../plugins/features/server'; + +import { + CoreSetup, + CoreStart, + PluginInitializerContext, + Logger, +} from '../../../../../src/core/server'; +import { SecurityPluginSetup as SecuritySetup } from '../../../../plugins/security/server'; +import { PluginSetupContract as FeaturesSetup } from '../../../../plugins/features/server'; +import { EncryptedSavedObjectsPluginSetup as EncryptedSavedObjectsSetup } from '../../../../plugins/encrypted_saved_objects/server'; +import { SpacesPluginSetup as SpacesSetup } from '../../../../plugins/spaces/server'; +import { PluginStartContract as ActionsStart } from '../../../../plugins/actions/server'; +import { LegacyServices } from './types'; import { initServer } from './init_server'; import { compose } from './lib/compose/kibana'; +import { initRoutes, LegacyInitRoutes } from './routes'; +import { isAlertExecutor } from './lib/detection_engine/signals/types'; +import { signalRulesAlertType } from './lib/detection_engine/signals/signal_rule_alert_type'; import { noteSavedObjectType, pinnedEventSavedObjectType, timelineSavedObjectType, ruleStatusSavedObjectType, } from './saved_objects'; +import { ClientsService } from './services'; + +export { CoreSetup, CoreStart }; -export type SiemPluginSecurity = Pick<SecurityPluginSetup, 'authc'>; +export interface SetupPlugins { + encryptedSavedObjects: EncryptedSavedObjectsSetup; + features: FeaturesSetup; + security: SecuritySetup; + spaces?: SpacesSetup; +} -export interface PluginsSetup { - features: FeaturesSetupContract; - security: SiemPluginSecurity; +export interface StartPlugins { + actions: ActionsStart; } export class Plugin { readonly name = 'siem'; private readonly logger: Logger; private context: PluginInitializerContext; + private clients: ClientsService; + private legacyInitRoutes?: LegacyInitRoutes; constructor(context: PluginInitializerContext) { this.context = context; this.logger = context.logger.get('plugins', this.name); + this.clients = new ClientsService(); this.logger.debug('Shim plugin initialized'); } - public setup(core: CoreSetup, plugins: PluginsSetup) { + public setup(core: CoreSetup, plugins: SetupPlugins, __legacy: LegacyServices) { this.logger.debug('Shim plugin setup'); + + this.clients.setup(core.elasticsearch.dataClient, plugins.spaces?.spacesService); + + this.legacyInitRoutes = initRoutes( + __legacy.route, + __legacy.config, + plugins.encryptedSavedObjects?.usingEphemeralEncryptionKey ?? false + ); + plugins.features.registerFeature({ id: this.name, name: i18n.translate('xpack.siem.featureRegistry.linkSiemTitle', { @@ -98,7 +130,23 @@ export class Plugin { }, }); - const libs = compose(core, plugins, this.context.env); + if (__legacy.alerting != null) { + const type = signalRulesAlertType({ + logger: this.logger, + version: this.context.env.packageInfo.version, + }); + if (isAlertExecutor(type)) { + __legacy.alerting.setup.registerType(type); + } + } + + const libs = compose(core, plugins, this.context.env.mode.prod); initServer(libs); } + + public start(core: CoreStart, plugins: StartPlugins) { + this.clients.start(core.savedObjects, plugins.actions); + + this.legacyInitRoutes!(this.clients.createGetScoped()); + } } diff --git a/x-pack/legacy/plugins/siem/server/routes/index.ts b/x-pack/legacy/plugins/siem/server/routes/index.ts new file mode 100644 index 0000000000000..82fc4d8c11722 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/routes/index.ts @@ -0,0 +1,78 @@ +/* + * 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 { LegacyServices } from '../types'; +import { GetScopedClients } from '../services'; + +import { createRulesRoute } from '../lib/detection_engine/routes/rules/create_rules_route'; +import { createIndexRoute } from '../lib/detection_engine/routes/index/create_index_route'; +import { readIndexRoute } from '../lib/detection_engine/routes/index/read_index_route'; +import { readRulesRoute } from '../lib/detection_engine/routes/rules/read_rules_route'; +import { findRulesRoute } from '../lib/detection_engine/routes/rules/find_rules_route'; +import { deleteRulesRoute } from '../lib/detection_engine/routes/rules/delete_rules_route'; +import { updateRulesRoute } from '../lib/detection_engine/routes/rules/update_rules_route'; +import { patchRulesRoute } from '../lib/detection_engine/routes/rules/patch_rules_route'; +import { setSignalsStatusRoute } from '../lib/detection_engine/routes/signals/open_close_signals_route'; +import { querySignalsRoute } from '../lib/detection_engine/routes/signals/query_signals_route'; +import { deleteIndexRoute } from '../lib/detection_engine/routes/index/delete_index_route'; +import { readTagsRoute } from '../lib/detection_engine/routes/tags/read_tags_route'; +import { readPrivilegesRoute } from '../lib/detection_engine/routes/privileges/read_privileges_route'; +import { addPrepackedRulesRoute } from '../lib/detection_engine/routes/rules/add_prepackaged_rules_route'; +import { createRulesBulkRoute } from '../lib/detection_engine/routes/rules/create_rules_bulk_route'; +import { updateRulesBulkRoute } from '../lib/detection_engine/routes/rules/update_rules_bulk_route'; +import { patchRulesBulkRoute } from '../lib/detection_engine/routes/rules/patch_rules_bulk_route'; +import { deleteRulesBulkRoute } from '../lib/detection_engine/routes/rules/delete_rules_bulk_route'; +import { importRulesRoute } from '../lib/detection_engine/routes/rules/import_rules_route'; +import { exportRulesRoute } from '../lib/detection_engine/routes/rules/export_rules_route'; +import { findRulesStatusesRoute } from '../lib/detection_engine/routes/rules/find_rules_status_route'; +import { getPrepackagedRulesStatusRoute } from '../lib/detection_engine/routes/rules/get_prepackaged_rules_status_route'; + +export type LegacyInitRoutes = (getClients: GetScopedClients) => void; + +export const initRoutes = ( + route: LegacyServices['route'], + config: LegacyServices['config'], + usingEphemeralEncryptionKey: boolean +) => (getClients: GetScopedClients): void => { + // Detection Engine Rule routes that have the REST endpoints of /api/detection_engine/rules + // All REST rule creation, deletion, updating, etc...... + createRulesRoute(route, config, getClients); + readRulesRoute(route, getClients); + updateRulesRoute(route, config, getClients); + patchRulesRoute(route, getClients); + deleteRulesRoute(route, getClients); + findRulesRoute(route, getClients); + + addPrepackedRulesRoute(route, config, getClients); + getPrepackagedRulesStatusRoute(route, getClients); + createRulesBulkRoute(route, config, getClients); + updateRulesBulkRoute(route, config, getClients); + patchRulesBulkRoute(route, getClients); + deleteRulesBulkRoute(route, getClients); + + importRulesRoute(route, config, getClients); + exportRulesRoute(route, config, getClients); + + findRulesStatusesRoute(route, getClients); + + // Detection Engine Signals routes that have the REST endpoints of /api/detection_engine/signals + // POST /api/detection_engine/signals/status + // Example usage can be found in siem/server/lib/detection_engine/scripts/signals + setSignalsStatusRoute(route, config, getClients); + querySignalsRoute(route, config, getClients); + + // Detection Engine index routes that have the REST endpoints of /api/detection_engine/index + // All REST index creation, policy management for spaces + createIndexRoute(route, config, getClients); + readIndexRoute(route, config, getClients); + deleteIndexRoute(route, config, getClients); + + // Detection Engine tags routes that have the REST endpoints of /api/detection_engine/tags + readTagsRoute(route, getClients); + + // Privileges API to get the generic user privileges + readPrivilegesRoute(route, config, usingEphemeralEncryptionKey, getClients); +}; diff --git a/x-pack/legacy/plugins/siem/server/saved_objects.ts b/x-pack/legacy/plugins/siem/server/saved_objects.ts index 8b9a1891c8a50..58da333c7bc9a 100644 --- a/x-pack/legacy/plugins/siem/server/saved_objects.ts +++ b/x-pack/legacy/plugins/siem/server/saved_objects.ts @@ -16,6 +16,10 @@ import { ruleStatusSavedObjectMappings, ruleStatusSavedObjectType, } from './lib/detection_engine/rules/saved_object_mappings'; +import { + caseSavedObjectMappings, + caseCommentSavedObjectMappings, +} from './lib/case/saved_object_mappings'; export { noteSavedObjectType, @@ -27,5 +31,8 @@ export const savedObjectMappings = { ...timelineSavedObjectMappings, ...noteSavedObjectMappings, ...pinnedEventSavedObjectMappings, + // TODO: Remove once while Saved Object Mappings API is programmed for the NP See: https://github.com/elastic/kibana/issues/50309 + ...caseSavedObjectMappings, + ...caseCommentSavedObjectMappings, ...ruleStatusSavedObjectMappings, }; diff --git a/x-pack/legacy/plugins/siem/server/services/clients.test.ts b/x-pack/legacy/plugins/siem/server/services/clients.test.ts new file mode 100644 index 0000000000000..7f63a8f5e949c --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/services/clients.test.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { actionsMock } from '../../../../../plugins/actions/server/mocks'; + +import { ClientsService } from './clients'; + +describe('ClientsService', () => { + describe('spacesClient', () => { + describe('#getSpaceId', () => { + it('returns the default spaceId if spaces are disabled', async () => { + const clients = new ClientsService(); + + const actions = actionsMock.createStart(); + const { elasticsearch } = coreMock.createSetup(); + const { savedObjects } = coreMock.createStart(); + const request = httpServerMock.createRawRequest(); + const spacesService = undefined; + + clients.setup(elasticsearch.dataClient, spacesService); + clients.start(savedObjects, actions); + + const { spacesClient } = await clients.createGetScoped()(request); + expect(spacesClient.getSpaceId()).toEqual('default'); + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/services/clients.ts b/x-pack/legacy/plugins/siem/server/services/clients.ts new file mode 100644 index 0000000000000..ca50eda4e7a6c --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/services/clients.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + IClusterClient, + IScopedClusterClient, + KibanaRequest, + LegacyRequest, + SavedObjectsClientContract, +} from '../../../../../../src/core/server'; +import { ActionsClient } from '../../../../../plugins/actions/server'; +import { AlertsClient } from '../../../../../legacy/plugins/alerting/server'; +import { SpacesServiceSetup } from '../../../../../plugins/spaces/server'; +import { CoreStart, StartPlugins } from '../plugin'; + +export interface Clients { + actionsClient?: ActionsClient; + clusterClient: IScopedClusterClient; + spacesClient: { getSpaceId: () => string }; + savedObjectsClient: SavedObjectsClientContract; +} +interface LegacyClients { + alertsClient?: AlertsClient; +} +export type GetScopedClients = (request: LegacyRequest) => Promise<Clients & LegacyClients>; + +export class ClientsService { + private actions?: StartPlugins['actions']; + private clusterClient?: IClusterClient; + private savedObjects?: CoreStart['savedObjects']; + private spacesService?: SpacesServiceSetup; + + public setup(clusterClient: IClusterClient, spacesService?: SpacesServiceSetup) { + this.clusterClient = clusterClient; + this.spacesService = spacesService; + } + + public start(savedObjects: CoreStart['savedObjects'], actions: StartPlugins['actions']) { + this.savedObjects = savedObjects; + this.actions = actions; + } + + public createGetScoped(): GetScopedClients { + if (!this.clusterClient || !this.savedObjects) { + throw new Error('Services not initialized'); + } + + return async (request: LegacyRequest) => { + const kibanaRequest = KibanaRequest.from(request); + + return { + alertsClient: request.getAlertsClient?.(), + actionsClient: await this.actions?.getActionsClientWithRequest?.(kibanaRequest), + clusterClient: this.clusterClient!.asScoped(kibanaRequest), + savedObjectsClient: this.savedObjects!.getScopedClient(kibanaRequest), + spacesClient: { + getSpaceId: () => this.spacesService?.getSpaceId?.(kibanaRequest) ?? 'default', + }, + }; + }; + } +} diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/index.ts b/x-pack/legacy/plugins/siem/server/services/index.ts similarity index 79% rename from x-pack/legacy/plugins/lens/public/metric_visualization_plugin/index.ts rename to x-pack/legacy/plugins/siem/server/services/index.ts index f75dce9b7507f..f4deea2c2a3fd 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/index.ts +++ b/x-pack/legacy/plugins/siem/server/services/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './plugin'; +export { ClientsService, GetScopedClients } from './clients'; diff --git a/x-pack/legacy/plugins/siem/server/types.ts b/x-pack/legacy/plugins/siem/server/types.ts index 7c07e63404eaa..e7831bb5d0451 100644 --- a/x-pack/legacy/plugins/siem/server/types.ts +++ b/x-pack/legacy/plugins/siem/server/types.ts @@ -6,28 +6,10 @@ import { Legacy } from 'kibana'; -export interface ServerFacade { +export { LegacyRequest } from '../../../../../src/core/server'; + +export interface LegacyServices { + alerting?: Legacy.Server['plugins']['alerting']; config: Legacy.Server['config']; - usingEphemeralEncryptionKey: boolean; - plugins: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - actions: any; // We have to do this at the moment because the types are not compatible - alerting?: Legacy.Server['plugins']['alerting']; - elasticsearch: Legacy.Server['plugins']['elasticsearch']; - spaces: Legacy.Server['plugins']['spaces']; - savedObjects: Legacy.Server['savedObjects']['SavedObjectsClient']; - }; route: Legacy.Server['route']; } - -export interface RequestFacade { - auth: Legacy.Request['auth']; - getAlertsClient?: Legacy.Request['getAlertsClient']; - getActionsClient?: Legacy.Request['getActionsClient']; - getSavedObjectsClient?: Legacy.Request['getSavedObjectsClient']; - headers: Legacy.Request['headers']; - method: Legacy.Request['method']; - params: Legacy.Request['params']; - payload: unknown; - query: Legacy.Request['query']; -} diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_list.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_list.tsx index 642a12411e6f3..8192fe4e026af 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_list.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_list.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { parse } from 'query-string'; import React, { Fragment, useState, useEffect } from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { parse } from 'querystring'; import { EuiButton, EuiCallOut, EuiLink, EuiEmptyPrompt, EuiSpacer, EuiIcon } from '@elastic/eui'; import { APP_SLM_CLUSTER_PRIVILEGES } from '../../../../../common/constants'; @@ -86,7 +86,7 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara const [filteredPolicy, setFilteredPolicy] = useState<string | undefined>(undefined); useEffect(() => { if (search) { - const parsedParams = parse(search.replace(/^\?/, '')); + const parsedParams = parse(search.replace(/^\?/, ''), { sort: false }); const { repository, policy } = parsedParams; if (policy && policy !== filteredPolicy) { diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/repository_add/repository_add.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/repository_add/repository_add.tsx index b4a76ff4329cf..a12ecb4baef5d 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/repository_add/repository_add.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/repository_add/repository_add.tsx @@ -3,9 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import { parse } from 'query-string'; import React, { useEffect, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { parse } from 'querystring'; import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; import { Repository, EmptyRepository } from '../../../../common/types'; @@ -44,7 +45,8 @@ export const RepositoryAdd: React.FunctionComponent<RouteComponentProps> = ({ if (error) { setSaveError(error); } else { - const { redirect } = parse(search.replace(/^\?/, '')); + const { redirect } = parse(search.replace(/^\?/, ''), { sort: false }); + history.push(redirect ? (redirect as string) : `${BASE_PATH}/${section}/${name}`); } }; diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/index.ts b/x-pack/legacy/plugins/uptime/public/components/connected/index.ts index 13309acd03622..585f0bf7f25f5 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/index.ts +++ b/x-pack/legacy/plugins/uptime/public/components/connected/index.ts @@ -10,3 +10,5 @@ export { KueryBar } from './kuerybar/kuery_bar_container'; export { FilterGroup } from './filter_group/filter_group_container'; export { MonitorStatusDetails } from './monitor/status_details_container'; export { MonitorStatusBar } from './monitor/status_bar_container'; +export { MonitorListDrawer } from './monitor/list_drawer_container'; +export { MonitorListActionsPopover } from './monitor/drawer_popover_container'; diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/drawer_popover_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/drawer_popover_container.tsx new file mode 100644 index 0000000000000..be29e12f716a9 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/drawer_popover_container.tsx @@ -0,0 +1,26 @@ +/* + * 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 { connect } from 'react-redux'; +import { AppState } from '../../../state'; +import { isIntegrationsPopupOpen } from '../../../state/selectors'; +import { PopoverState, toggleIntegrationsPopover } from '../../../state/actions'; +import { MonitorListActionsPopoverComponent } from '../../functional/monitor_list/monitor_list_drawer'; + +const mapStateToProps = (state: AppState) => ({ + popoverState: isIntegrationsPopupOpen(state), +}); + +const mapDispatchToProps = (dispatch: any) => ({ + togglePopoverIsVisible: (popoverState: PopoverState) => { + return dispatch(toggleIntegrationsPopover(popoverState)); + }, +}); + +export const MonitorListActionsPopover = connect( + mapStateToProps, + mapDispatchToProps +)(MonitorListActionsPopoverComponent); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/list_drawer_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/list_drawer_container.tsx new file mode 100644 index 0000000000000..8c670b485cc56 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/list_drawer_container.tsx @@ -0,0 +1,49 @@ +/* + * 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, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { AppState } from '../../../state'; +import { getMonitorDetails } from '../../../state/selectors'; +import { MonitorDetailsActionPayload } from '../../../state/actions/types'; +import { fetchMonitorDetails } from '../../../state/actions/monitor'; +import { MonitorListDrawerComponent } from '../../functional/monitor_list/monitor_list_drawer/monitor_list_drawer'; +import { useUrlParams } from '../../../hooks'; +import { MonitorSummary } from '../../../../common/graphql/types'; +import { MonitorDetails } from '../../../../common/runtime_types/monitor'; + +interface ContainerProps { + summary: MonitorSummary; + monitorDetails: MonitorDetails; + loadMonitorDetails: typeof fetchMonitorDetails; +} + +const Container: React.FC<ContainerProps> = ({ summary, loadMonitorDetails, monitorDetails }) => { + const monitorId = summary?.monitor_id; + + const [getUrlParams] = useUrlParams(); + const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = getUrlParams(); + + useEffect(() => { + loadMonitorDetails({ + dateStart, + dateEnd, + monitorId, + }); + }, [dateStart, dateEnd, monitorId, loadMonitorDetails]); + return <MonitorListDrawerComponent monitorDetails={monitorDetails} summary={summary} />; +}; + +const mapStateToProps = (state: AppState, { summary }: any) => ({ + monitorDetails: getMonitorDetails(state, summary), +}); + +const mapDispatchToProps = (dispatch: any) => ({ + loadMonitorDetails: (actionPayload: MonitorDetailsActionPayload) => + dispatch(fetchMonitorDetails(actionPayload)), +}); + +export const MonitorListDrawer = connect(mapStateToProps, mapDispatchToProps)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx index db6337732091a..b2b555d32a3c7 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx @@ -31,7 +31,7 @@ interface OwnProps { type Props = OwnProps & StateProps & DispatchProps; -export const Container: React.FC<Props> = ({ +const Container: React.FC<Props> = ({ loadMonitorStatus, monitorId, monitorStatus, diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/index.ts index 4ec4cf4f52607..e86ba548fb5d9 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/index.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/index.ts @@ -6,7 +6,6 @@ export { DonutChart } from './charts/donut_chart'; export { EmptyState } from './empty_state'; -export { IntegrationLink } from './integration_link'; export { KueryBarComponent } from './kuery_bar/kuery_bar'; export { MonitorCharts } from './monitor_charts'; export { MonitorList } from './monitor_list'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/kuery_bar.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/kuery_bar.tsx index b1eb3f38097b2..496e8d898df3c 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/kuery_bar.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/kuery_bar/kuery_bar.tsx @@ -15,7 +15,7 @@ import { esKuery, IIndexPattern, QuerySuggestion, - DataPublicPluginStart, + DataPublicPluginSetup, } from '../../../../../../../../src/plugins/data/public'; const Container = styled.div` @@ -33,7 +33,7 @@ function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) { } interface Props { - autocomplete: DataPublicPluginStart['autocomplete']; + autocomplete: DataPublicPluginSetup['autocomplete']; loadIndexPattern: any; indexPattern: any; } diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx index de6cc1982f1a8..58250222e1330 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx @@ -28,11 +28,11 @@ import { import { MonitorListStatusColumn } from './monitor_list_status_column'; import { formatUptimeGraphQLErrorList } from '../../../lib/helper/format_error_list'; import { ExpandedRowMap } from './types'; -import { MonitorListDrawer } from './monitor_list_drawer'; import { MonitorBarSeries } from '../charts'; import { MonitorPageLink } from './monitor_page_link'; import { OverviewPageLink } from './overview_page_link'; import * as labels from './translations'; +import { MonitorListDrawer } from '../../connected'; interface MonitorListQueryResult { monitorStates?: MonitorSummaryResult; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/integration_group.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_group.test.tsx.snap similarity index 66% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/integration_group.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_group.test.tsx.snap index bab69a6de9708..bb578d850ff7e 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/integration_group.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_group.test.tsx.snap @@ -4,6 +4,15 @@ exports[`IntegrationGroup will not display APM links when APM is unavailable 1`] <EuiFlexGroup direction="column" > + <EuiFlexItem> + <IntegrationLink + ariaLabel="Search APM for this monitor" + href="/app/apm#/services?kuery=url.domain:%20%22undefined%22&rangeFrom=now-15m&rangeTo=now" + iconType="apmApp" + message="Check APM for domain" + tooltipContent="Click here to check APM for the domain \\"\\"." + /> + </EuiFlexItem> <EuiFlexItem> <IntegrationLink ariaLabel="Check Infrastructure UI for this montor's ip address" @@ -62,12 +71,36 @@ exports[`IntegrationGroup will not display infra links when infra is unavailable <EuiFlexItem> <IntegrationLink ariaLabel="Search APM for this monitor" - href="foo/app/apm#/services?kuery=url.domain:%20%22undefined%22&rangeFrom=now-12m&rangeTo=now-1m" + href="/app/apm#/services?kuery=url.domain:%20%22undefined%22&rangeFrom=now-15m&rangeTo=now" iconType="apmApp" message="Check APM for domain" tooltipContent="Click here to check APM for the domain \\"\\"." /> </EuiFlexItem> + <EuiFlexItem> + <IntegrationLink + ariaLabel="Check Infrastructure UI for this montor's ip address" + iconType="metricsApp" + message="Show host metrics" + tooltipContent="Check Infrastructure UI for the IP \\"\\"" + /> + </EuiFlexItem> + <EuiFlexItem> + <IntegrationLink + ariaLabel="Check Infrastructure UI for this monitor's pod UID" + iconType="metricsApp" + message="Show pod metrics" + tooltipContent="Check Infrastructure UI for pod UID \\"\\"." + /> + </EuiFlexItem> + <EuiFlexItem> + <IntegrationLink + ariaLabel="Check Infrastructure UI for this monitor's container ID" + iconType="metricsApp" + message="Show container metrics" + tooltipContent="Check Infrastructure UI for container ID \\"\\"" + /> + </EuiFlexItem> <EuiFlexItem> <IntegrationLink ariaLabel="Check Logging UI for this monitor's ip address" @@ -102,7 +135,7 @@ exports[`IntegrationGroup will not display logging links when logging is unavail <EuiFlexItem> <IntegrationLink ariaLabel="Search APM for this monitor" - href="foo/app/apm#/services?kuery=url.domain:%20%22undefined%22&rangeFrom=now-12m&rangeTo=now-1m" + href="/app/apm#/services?kuery=url.domain:%20%22undefined%22&rangeFrom=now-15m&rangeTo=now" iconType="apmApp" message="Check APM for domain" tooltipContent="Click here to check APM for the domain \\"\\"." @@ -132,5 +165,29 @@ exports[`IntegrationGroup will not display logging links when logging is unavail tooltipContent="Check Infrastructure UI for container ID \\"\\"" /> </EuiFlexItem> + <EuiFlexItem> + <IntegrationLink + ariaLabel="Check Logging UI for this monitor's ip address" + iconType="logsApp" + message="Show host logs" + tooltipContent="Check Logging UI for the IP \\"\\"" + /> + </EuiFlexItem> + <EuiFlexItem> + <IntegrationLink + ariaLabel="Show pod logs" + iconType="logsApp" + message="Show pod logs" + tooltipContent="Check for logs for pod UID \\"\\"" + /> + </EuiFlexItem> + <EuiFlexItem> + <IntegrationLink + ariaLabel="Show container logs" + iconType="logsApp" + message="Show container logs" + tooltipContent="Check Logging UI for container ID \\"\\"" + /> + </EuiFlexItem> </EuiFlexGroup> `; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/integration_link.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_link.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/integration_link.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_link.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap index 29f2c0b63991e..cf754581b1a33 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap @@ -52,7 +52,6 @@ exports[`MonitorListDrawer component renders a MonitorListDrawer when there are } > <MonitorListDrawerComponent - loadMonitorDetails={[Function]} monitorDetails={ Object { "error": Object { @@ -157,7 +156,6 @@ exports[`MonitorListDrawer component renders a MonitorListDrawer when there is o } > <MonitorListDrawerComponent - loadMonitorDetails={[Function]} monitorDetails={ Object { "error": Object { diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/integration_group.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx similarity index 51% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/integration_group.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx index 98fdc994fc7a8..723f8f9f4430a 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/integration_group.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { MonitorSummary } from '../../../../common/graphql/types'; +import { MonitorSummary } from '../../../../../../common/graphql/types'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { IntegrationGroup } from '../integration_group'; @@ -24,47 +24,17 @@ describe('IntegrationGroup', () => { }); it('will not display APM links when APM is unavailable', () => { - const component = shallowWithIntl( - <IntegrationGroup - basePath="foo" - dateRangeStart="now-12m" - dateRangeEnd="now-1m" - isApmAvailable={false} - isInfraAvailable={true} - isLogsAvailable={true} - summary={summary} - /> - ); + const component = shallowWithIntl(<IntegrationGroup summary={summary} />); expect(component).toMatchSnapshot(); }); it('will not display infra links when infra is unavailable', () => { - const component = shallowWithIntl( - <IntegrationGroup - basePath="foo" - dateRangeStart="now-12m" - dateRangeEnd="now-1m" - isApmAvailable={true} - isInfraAvailable={false} - isLogsAvailable={true} - summary={summary} - /> - ); + const component = shallowWithIntl(<IntegrationGroup summary={summary} />); expect(component).toMatchSnapshot(); }); it('will not display logging links when logging is unavailable', () => { - const component = shallowWithIntl( - <IntegrationGroup - basePath="foo" - dateRangeStart="now-12m" - dateRangeEnd="now-1m" - isApmAvailable={true} - isInfraAvailable={true} - isLogsAvailable={false} - summary={summary} - /> - ); + const component = shallowWithIntl(<IntegrationGroup summary={summary} />); expect(component).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/integration_link.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/integration_link.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/integration_link.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/integration_link.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx index c222728df3bb3..d870acefaaea6 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx @@ -12,7 +12,6 @@ import { shallowWithRouter } from '../../../../../lib'; describe('MonitorListDrawer component', () => { let summary: MonitorSummary; - let loadMonitorDetails: any; let monitorDetails: MonitorDetails; beforeEach(() => { @@ -47,16 +46,11 @@ describe('MonitorListDrawer component', () => { 'Get https://expired.badssl.com: x509: certificate has expired or is not yet valid', }, }; - loadMonitorDetails = () => null; }); it('renders nothing when no summary data is present', () => { const component = shallowWithRouter( - <MonitorListDrawerComponent - loadMonitorDetails={loadMonitorDetails} - summary={summary} - monitorDetails={monitorDetails} - /> + <MonitorListDrawerComponent summary={summary} monitorDetails={monitorDetails} /> ); expect(component).toEqual({}); }); @@ -64,22 +58,14 @@ describe('MonitorListDrawer component', () => { it('renders nothing when no check data is present', () => { delete summary.state.checks; const component = shallowWithRouter( - <MonitorListDrawerComponent - summary={summary} - loadMonitorDetails={loadMonitorDetails} - monitorDetails={monitorDetails} - /> + <MonitorListDrawerComponent summary={summary} monitorDetails={monitorDetails} /> ); expect(component).toEqual({}); }); it('renders a MonitorListDrawer when there is only one check', () => { const component = shallowWithRouter( - <MonitorListDrawerComponent - summary={summary} - loadMonitorDetails={loadMonitorDetails} - monitorDetails={monitorDetails} - /> + <MonitorListDrawerComponent summary={summary} monitorDetails={monitorDetails} /> ); expect(component).toMatchSnapshot(); }); @@ -110,11 +96,7 @@ describe('MonitorListDrawer component', () => { ]; summary.state.checks = checks; const component = shallowWithRouter( - <MonitorListDrawerComponent - summary={summary} - loadMonitorDetails={loadMonitorDetails} - monitorDetails={monitorDetails} - /> + <MonitorListDrawerComponent summary={summary} monitorDetails={monitorDetails} /> ); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/index.ts index 73fb07db60de8..2933a71c2240b 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/index.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/index.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { MonitorListDrawer } from './monitor_list_drawer'; export { LocationLink } from './location_link'; +export { MonitorListActionsPopoverComponent } from './monitor_list_actions_popover'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/integration_group.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/integration_group.tsx similarity index 95% rename from x-pack/legacy/plugins/uptime/public/components/functional/integration_group.tsx rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/integration_group.tsx index da66235e37f1a..34bff58a3e2d9 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/integration_group.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/integration_group.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React from 'react'; +import React, { useContext } from 'react'; import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -18,32 +18,29 @@ import { getLoggingContainerHref, getLoggingIpHref, getLoggingKubernetesHref, -} from '../../lib/helper'; -import { MonitorSummary } from '../../../common/graphql/types'; +} from '../../../../lib/helper'; +import { MonitorSummary } from '../../../../../common/graphql/types'; +import { UptimeSettingsContext } from '../../../../contexts'; interface IntegrationGroupProps { - basePath: string; - dateRangeStart: string; - dateRangeEnd: string; - isApmAvailable: boolean; - isInfraAvailable: boolean; - isLogsAvailable: boolean; summary: MonitorSummary; } -export const IntegrationGroup = ({ - basePath, - dateRangeStart, - dateRangeEnd, - isApmAvailable, - isInfraAvailable, - isLogsAvailable, - summary, -}: IntegrationGroupProps) => { +export const IntegrationGroup = ({ summary }: IntegrationGroupProps) => { + const { + basePath, + dateRangeStart, + dateRangeEnd, + isApmAvailable, + isInfraAvailable, + isLogsAvailable, + } = useContext(UptimeSettingsContext); + const domain = get<string>(summary, 'state.url.domain', ''); const podUid = get<string | undefined>(summary, 'state.checks[0].kubernetes.pod.uid', undefined); const containerId = get<string | undefined>(summary, 'state.checks[0].container.id', undefined); const ip = get<string | undefined>(summary, 'state.checks[0].monitor.ip', undefined); + return isApmAvailable || isInfraAvailable || isLogsAvailable ? ( <EuiFlexGroup direction="column"> {isApmAvailable ? ( diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/integration_link.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/integration_link.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/integration_link.tsx rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/integration_link.tsx index a545cd7c42927..4b4c2003931a3 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/integration_link.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/integration_link.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiText, EuiToolTip } from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiText, EuiToolTip } from '@elastic/eui'; interface IntegrationLinkProps { ariaLabel: string; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_actions_popover.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_actions_popover.tsx similarity index 58% rename from x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_actions_popover.tsx rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_actions_popover.tsx index af06761f50c83..6b946baa8d403 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_actions_popover.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_actions_popover.tsx @@ -4,17 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiPopover, EuiButton } from '@elastic/eui'; -import React, { useContext } from 'react'; -import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; -import { connect } from 'react-redux'; -import { MonitorSummary } from '../../../../common/graphql/types'; -import { IntegrationGroup } from '../integration_group'; -import { UptimeSettingsContext } from '../../../contexts'; -import { isIntegrationsPopupOpen } from '../../../state/selectors'; -import { AppState } from '../../../state'; -import { toggleIntegrationsPopover, PopoverState } from '../../../state/actions'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { EuiPopover, EuiButton } from '@elastic/eui'; +import { IntegrationGroup } from './integration_group'; +import { MonitorSummary } from '../../../../../common/graphql/types'; +import { toggleIntegrationsPopover, PopoverState } from '../../../../state/actions'; interface MonitorListActionsPopoverProps { summary: MonitorSummary; @@ -22,20 +18,12 @@ interface MonitorListActionsPopoverProps { togglePopoverIsVisible: typeof toggleIntegrationsPopover; } -const MonitorListActionsPopoverComponent = ({ +export const MonitorListActionsPopoverComponent = ({ summary, popoverState, togglePopoverIsVisible, }: MonitorListActionsPopoverProps) => { const popoverId = `${summary.monitor_id}_popover`; - const { - basePath, - dateRangeStart, - dateRangeEnd, - isApmAvailable, - isInfraAvailable, - isLogsAvailable, - } = useContext(UptimeSettingsContext); const monitorUrl: string | undefined = get(summary, 'state.url.full', undefined); const isPopoverOpen: boolean = @@ -64,30 +52,7 @@ const MonitorListActionsPopoverComponent = ({ id={popoverId} isOpen={isPopoverOpen} > - <IntegrationGroup - basePath={basePath} - dateRangeStart={dateRangeStart} - dateRangeEnd={dateRangeEnd} - isApmAvailable={isApmAvailable} - isInfraAvailable={isInfraAvailable} - isLogsAvailable={isLogsAvailable} - summary={summary} - /> + <IntegrationGroup summary={summary} /> </EuiPopover> ); }; - -const mapStateToProps = (state: AppState) => ({ - popoverState: isIntegrationsPopupOpen(state), -}); - -const mapDispatchToProps = (dispatch: any) => ({ - togglePopoverIsVisible: (popoverState: PopoverState) => { - return dispatch(toggleIntegrationsPopover(popoverState)); - }, -}); - -export const MonitorListActionsPopover = connect( - mapStateToProps, - mapDispatchToProps -)(MonitorListActionsPopoverComponent); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx index 35b649fa35795..8383596ccc346 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx @@ -4,20 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect } from 'react'; -import { EuiLink, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui'; +import React from 'react'; import styled from 'styled-components'; -import { connect } from 'react-redux'; +import { EuiLink, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui'; import { MonitorSummary } from '../../../../../common/graphql/types'; -import { AppState } from '../../../../state'; -import { fetchMonitorDetails } from '../../../../state/actions/monitor'; import { MostRecentError } from './most_recent_error'; -import { getMonitorDetails } from '../../../../state/selectors'; import { MonitorStatusList } from './monitor_status_list'; import { MonitorDetails } from '../../../../../common/runtime_types'; -import { useUrlParams } from '../../../../hooks'; -import { MonitorDetailsActionPayload } from '../../../../state/actions/types'; -import { MonitorListActionsPopover } from '../monitor_list_actions_popover'; +import { MonitorListActionsPopover } from '../../../connected'; const ContainerDiv = styled.div` padding: 10px; @@ -34,34 +28,13 @@ interface MonitorListDrawerProps { * Monitor details to be fetched from rest api using monitorId */ monitorDetails: MonitorDetails; - - /** - * Redux action to trigger , loading monitor details - */ - loadMonitorDetails: typeof fetchMonitorDetails; } /** * The elements shown when the user expands the monitor list rows. */ -export function MonitorListDrawerComponent({ - summary, - loadMonitorDetails, - monitorDetails, -}: MonitorListDrawerProps) { - const monitorId = summary?.monitor_id; - const [getUrlParams] = useUrlParams(); - const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = getUrlParams(); - - useEffect(() => { - loadMonitorDetails({ - dateStart, - dateEnd, - monitorId, - }); - }, [dateStart, dateEnd, monitorId, loadMonitorDetails]); - +export function MonitorListDrawerComponent({ summary, monitorDetails }: MonitorListDrawerProps) { const monitorUrl = summary?.state?.url?.full || ''; return summary && summary.state.checks ? ( @@ -91,17 +64,3 @@ export function MonitorListDrawerComponent({ </ContainerDiv> ) : null; } - -const mapStateToProps = (state: AppState, { summary }: any) => ({ - monitorDetails: getMonitorDetails(state, summary), -}); - -const mapDispatchToProps = (dispatch: any) => ({ - loadMonitorDetails: (actionPayload: MonitorDetailsActionPayload) => - dispatch(fetchMonitorDetails(actionPayload)), -}); - -export const MonitorListDrawer = connect( - mapStateToProps, - mapDispatchToProps -)(MonitorListDrawerComponent); diff --git a/x-pack/legacy/plugins/uptime/public/hooks/use_url_params.ts b/x-pack/legacy/plugins/uptime/public/hooks/use_url_params.ts index e509e14223006..dc309943d7cf9 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/use_url_params.ts +++ b/x-pack/legacy/plugins/uptime/public/hooks/use_url_params.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import qs from 'querystring'; +import { parse, stringify } from 'query-string'; import { useLocation, useHistory } from 'react-router-dom'; import { UptimeUrlParams, getSupportedUrlParams } from '../lib/helper'; @@ -23,14 +23,17 @@ export const useUrlParams: UptimeUrlParamsHook = () => { search = location.search; } - const params = search ? { ...qs.parse(search[0] === '?' ? search.slice(1) : search) } : {}; + const params = search + ? parse(search[0] === '?' ? search.slice(1) : search, { sort: false }) + : {}; + return getSupportedUrlParams(params); }; const updateUrlParams: UpdateUrlParams = updatedParams => { if (!history || !location) return; const { pathname, search } = location; - const currentParams: any = qs.parse(search[0] === '?' ? search.slice(1) : search); + const currentParams = parse(search[0] === '?' ? search.slice(1) : search, { sort: false }); const mergedParams = { ...currentParams, ...updatedParams, @@ -38,7 +41,7 @@ export const useUrlParams: UptimeUrlParamsHook = () => { history.push({ pathname, - search: qs.stringify( + search: stringify( // drop any parameters that have no value Object.keys(mergedParams).reduce((params, key) => { const value = mergedParams[key]; @@ -49,7 +52,8 @@ export const useUrlParams: UptimeUrlParamsHook = () => { ...params, [key]: value, }; - }, {}) + }, {}), + { sort: false } ), }); }; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/stringify_url_params.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/stringify_url_params.ts index 7d00a27d69032..a8ce86c4399e2 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/stringify_url_params.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/stringify_url_params.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import qs from 'querystring'; +import { stringify } from 'query-string'; import { UptimeUrlParams } from './url_params'; import { CLIENT_DEFAULTS } from '../../../common/constants'; @@ -38,5 +38,5 @@ export const stringifyUrlParams = (params: Partial<UptimeUrlParams>, ignoreEmpty } }); } - return `?${qs.stringify(params)}`; + return `?${stringify(params, { sort: false })}`; }; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts index f01448d9e37ac..11dfc3f21b1bf 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts @@ -46,7 +46,7 @@ const { * require further development. */ export const getSupportedUrlParams = (params: { - [key: string]: string | string[] | undefined; + [key: string]: string | string[] | undefined | null; }): UptimeUrlParams => { const filteredParams: { [key: string]: string | undefined } = {}; Object.keys(params).forEach(key => { diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/overview.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/overview.test.tsx.snap index fff947bd96024..71b3fb5c7146a 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/overview.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/overview.test.tsx.snap @@ -54,6 +54,7 @@ exports[`MonitorPage shallow renders expected elements for valid props 1`] = ` <OverviewPageComponent autocomplete={ Object { + "addQuerySuggestionProvider": [MockFunction], "getQuerySuggestions": [MockFunction], "getValueSuggestions": [MockFunction], "hasQuerySuggestions": [Function], diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/overview.test.tsx b/x-pack/legacy/plugins/uptime/public/pages/__tests__/overview.test.tsx index 365e96788bbbf..86713c606c1bb 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/__tests__/overview.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/__tests__/overview.test.tsx @@ -92,6 +92,7 @@ describe('MonitorPage', () => { getQuerySuggestions: jest.fn(), hasQuerySuggestions: () => true, getValueSuggestions: jest.fn(), + addQuerySuggestionProvider: jest.fn(), }; it('shallow renders expected elements for valid props', () => { diff --git a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx index ae7457e835c94..5360d66f87e99 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx @@ -16,13 +16,13 @@ import { import { useUrlParams, useUptimeTelemetry, UptimePage } from '../hooks'; import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; import { useTrackPageview } from '../../../infra/public'; -import { DataPublicPluginStart, IIndexPattern } from '../../../../../../src/plugins/data/public'; +import { DataPublicPluginSetup, IIndexPattern } from '../../../../../../src/plugins/data/public'; import { UptimeThemeContext } from '../contexts'; import { FilterGroup, KueryBar } from '../components/connected'; import { useUpdateKueryString } from '../hooks'; interface OverviewPageProps { - autocomplete: DataPublicPluginStart['autocomplete']; + autocomplete: DataPublicPluginSetup['autocomplete']; indexPattern: IIndexPattern; setEsKueryFilters: (esFilters: string) => void; } diff --git a/x-pack/legacy/plugins/uptime/public/routes.tsx b/x-pack/legacy/plugins/uptime/public/routes.tsx index 0f726d89e0d28..83be45083b645 100644 --- a/x-pack/legacy/plugins/uptime/public/routes.tsx +++ b/x-pack/legacy/plugins/uptime/public/routes.tsx @@ -6,13 +6,13 @@ import React, { FC } from 'react'; import { Route, Switch } from 'react-router-dom'; -import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; +import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public'; import { OverviewPage } from './components/connected/pages/overview_container'; import { MONITOR_ROUTE, OVERVIEW_ROUTE } from '../common/constants'; import { MonitorPage, NotFoundPage } from './pages'; interface RouterProps { - autocomplete: DataPublicPluginStart['autocomplete']; + autocomplete: DataPublicPluginSetup['autocomplete']; } export const PageRouter: FC<RouterProps> = ({ autocomplete }) => ( diff --git a/x-pack/legacy/plugins/uptime/public/state/api/ping.ts b/x-pack/legacy/plugins/uptime/public/state/api/ping.ts index e0c358fe40e71..c61bf42c8c90e 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/ping.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/ping.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import qs from 'querystring'; +import { stringify } from 'query-string'; import { getApiPath } from '../../lib/helper'; import { APIFn } from './types'; import { GetPingHistogramParams, HistogramResult } from '../../../common/types'; @@ -25,7 +25,7 @@ export const fetchPingHistogram: APIFn<GetPingHistogramParams, HistogramResult> ...(statusFilter && { statusFilter }), ...(filters && { filters }), }; - const urlParams = qs.stringify(params).toString(); + const urlParams = stringify(params, { sort: false }); const response = await fetch(`${url}?${urlParams}`); if (!response.ok) { throw new Error(response.statusText); diff --git a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx index dbde9f8b6a8c0..db34566d6c148 100644 --- a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx +++ b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx @@ -100,7 +100,6 @@ const Application = (props: UptimeAppProps) => { <EuiPage className="app-wrapper-panel " data-test-subj="uptimeApp"> <main> <PageHeader setBreadcrumbs={setBreadcrumbs} /> - // @ts-ignore we need to update the type of this prop <PageRouter autocomplete={plugins.data.autocomplete} /> </main> </EuiPage> diff --git a/x-pack/legacy/plugins/watcher/index.ts b/x-pack/legacy/plugins/watcher/index.ts deleted file mode 100644 index fdf9ba1bad6e4..0000000000000 --- a/x-pack/legacy/plugins/watcher/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { resolve } from 'path'; - -const pluginDefinition = { - id: 'watcher', - configPrefix: 'xpack.watcher', - publicDir: resolve(__dirname, 'public'), - require: ['kibana'], - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }, - init(server: any) {}, -}; - -export const watcher = (kibana: any) => new kibana.Plugin(pluginDefinition); diff --git a/x-pack/legacy/plugins/xpack_main/index.js b/x-pack/legacy/plugins/xpack_main/index.js index f3994f7ebcc34..809d90d58d796 100644 --- a/x-pack/legacy/plugins/xpack_main/index.js +++ b/x-pack/legacy/plugins/xpack_main/index.js @@ -11,8 +11,6 @@ import { replaceInjectedVars } from './server/lib/replace_injected_vars'; import { setupXPackMain } from './server/lib/setup_xpack_main'; import { xpackInfoRoute, settingsRoute } from './server/routes/api/v1'; -import { has } from 'lodash'; - export { callClusterFactory } from './server/lib/call_cluster_factory'; import { registerMonitoringCollection } from './server/telemetry_collection'; @@ -82,21 +80,5 @@ export const xpackMain = kibana => { xpackInfoRoute(server); settingsRoute(server, this.kbnServer); }, - deprecations: () => { - function movedToTelemetry(configPath) { - return (settings, log) => { - if (has(settings, configPath)) { - log( - `Config key "xpack.xpack_main.${configPath}" is deprecated. Use "telemetry.${configPath}" instead.` - ); - } - }; - } - return [ - movedToTelemetry('telemetry.config'), - movedToTelemetry('telemetry.url'), - movedToTelemetry('telemetry.enabled'), - ]; - }, }); }; diff --git a/x-pack/package.json b/x-pack/package.json index 921f6ad991188..43df763c22bdc 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -153,7 +153,7 @@ "react-docgen-typescript-loader": "^3.1.1", "react-test-renderer": "^16.12.0", "rxjs-marbles": "^5.0.3", - "sass-loader": "^7.3.1", + "sass-loader": "^8.0.2", "sass-resources-loader": "^2.0.1", "simple-git": "1.116.0", "sinon": "^7.4.2", @@ -292,7 +292,9 @@ "proper-lockfile": "^3.2.0", "puid": "1.0.7", "puppeteer-core": "^1.19.0", + "query-string": "6.10.1", "raw-loader": "3.1.0", + "re-resizable": "^6.1.1", "react": "^16.12.0", "react-apollo": "^2.1.4", "react-beautiful-dnd": "^8.0.7", @@ -324,7 +326,6 @@ "request": "^2.88.0", "reselect": "3.0.1", "resize-observer-polyfill": "^1.5.0", - "re-resizable": "^6.1.1", "rison-node": "0.3.1", "rxjs": "^6.5.3", "semver": "5.7.0", @@ -345,7 +346,7 @@ "uuid": "3.3.2", "venn.js": "0.2.20", "vscode-languageserver": "^5.2.1", - "webpack": "4.41.0", + "webpack": "^4.41.5", "wellknown": "^0.5.0", "xml2js": "^0.4.22", "xregexp": "4.2.4" diff --git a/x-pack/plugins/actions/server/action_type_registry.test.ts b/x-pack/plugins/actions/server/action_type_registry.test.ts index 79c9a44901079..bced8841138f2 100644 --- a/x-pack/plugins/actions/server/action_type_registry.test.ts +++ b/x-pack/plugins/actions/server/action_type_registry.test.ts @@ -13,7 +13,9 @@ import { configUtilsMock } from './actions_config.mock'; const mockTaskManager = taskManagerMock.setup(); const actionTypeRegistryParams = { taskManager: mockTaskManager, - taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor()), + taskRunnerFactory: new TaskRunnerFactory( + new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) + ), actionsConfigUtils: configUtilsMock, }; diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index feca08fad922c..cafad6313d2e4 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -27,7 +27,9 @@ const mockTaskManager = taskManagerMock.setup(); const actionTypeRegistryParams = { taskManager: mockTaskManager, - taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor()), + taskRunnerFactory: new TaskRunnerFactory( + new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) + ), actionsConfigUtils: configUtilsMock, }; @@ -204,7 +206,9 @@ describe('create()', () => { const localActionTypeRegistryParams = { taskManager: mockTaskManager, - taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor()), + taskRunnerFactory: new TaskRunnerFactory( + new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) + ), actionsConfigUtils: localConfigUtils, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts index ff732f58d6c94..db6375fe18193 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts @@ -21,7 +21,9 @@ export function createActionTypeRegistry(): { const logger = loggingServiceMock.create().get() as jest.Mocked<Logger>; const actionTypeRegistry = new ActionTypeRegistry({ taskManager: taskManagerMock.setup(), - taskRunnerFactory: new TaskRunnerFactory(new ActionExecutor()), + taskRunnerFactory: new TaskRunnerFactory( + new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) + ), actionsConfigUtils: configUtilsMock, }); registerBuiltInActionTypes({ diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts index 258efb1500ab2..6d2a234639532 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -20,6 +20,7 @@ describe('execute()', () => { getBasePath, taskManager: mockTaskManager, getScopedSavedObjectsClient: jest.fn().mockReturnValueOnce(savedObjectsClient), + isESOUsingEphemeralEncryptionKey: false, }); savedObjectsClient.get.mockResolvedValueOnce({ id: '123', @@ -71,6 +72,7 @@ describe('execute()', () => { getBasePath, taskManager: mockTaskManager, getScopedSavedObjectsClient, + isESOUsingEphemeralEncryptionKey: false, }); savedObjectsClient.get.mockResolvedValueOnce({ id: '123', @@ -118,6 +120,7 @@ describe('execute()', () => { getBasePath, taskManager: mockTaskManager, getScopedSavedObjectsClient, + isESOUsingEphemeralEncryptionKey: false, }); savedObjectsClient.get.mockResolvedValueOnce({ id: '123', @@ -155,4 +158,24 @@ describe('execute()', () => { }, }); }); + + test('throws when passing isESOUsingEphemeralEncryptionKey with true as a value', async () => { + const getScopedSavedObjectsClient = jest.fn().mockReturnValueOnce(savedObjectsClient); + const executeFn = createExecuteFunction({ + getBasePath, + taskManager: mockTaskManager, + getScopedSavedObjectsClient, + isESOUsingEphemeralEncryptionKey: true, + }); + await expect( + executeFn({ + id: '123', + params: { baz: false }, + spaceId: 'default', + apiKey: null, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unable to execute action due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml"` + ); + }); }); diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index 42db37bbe89f4..c2e8795b5f133 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -12,6 +12,7 @@ interface CreateExecuteFunctionOptions { taskManager: TaskManagerStartContract; getScopedSavedObjectsClient: (request: any) => SavedObjectsClientContract; getBasePath: GetBasePathFunction; + isESOUsingEphemeralEncryptionKey: boolean; } export interface ExecuteOptions { @@ -25,8 +26,15 @@ export function createExecuteFunction({ getBasePath, taskManager, getScopedSavedObjectsClient, + isESOUsingEphemeralEncryptionKey, }: CreateExecuteFunctionOptions) { return async function execute({ id, params, spaceId, apiKey }: ExecuteOptions) { + if (isESOUsingEphemeralEncryptionKey === true) { + throw new Error( + `Unable to execute action due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml` + ); + } + const requestHeaders: Record<string, string> = {}; if (apiKey) { diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index 27098bc541790..eee2ae352fe3d 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -7,9 +7,11 @@ import { PluginInitializerContext } from '../../../../src/core/server'; import { ActionsPlugin } from './plugin'; import { configSchema } from './config'; +import { ActionsClient as ActionsClientClass } from './actions_client'; + +export type ActionsClient = PublicMethodsOf<ActionsClientClass>; export { ActionsPlugin, ActionResult, ActionTypeExecutorOptions, ActionType } from './types'; -export { ActionsClient } from './actions_client'; export { PluginSetupContract, PluginStartContract } from './plugin'; export const plugin = (initContext: PluginInitializerContext) => new ActionsPlugin(initContext); diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index 8301a13c82469..6ab5b812161c3 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -11,8 +11,9 @@ import { actionTypeRegistryMock } from '../action_type_registry.mock'; import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; import { savedObjectsClientMock, loggingServiceMock } from '../../../../../src/core/server/mocks'; import { eventLoggerMock } from '../../../event_log/server/mocks'; +import { spacesServiceMock } from '../../../spaces/server/spaces_service/spaces_service.mock'; -const actionExecutor = new ActionExecutor(); +const actionExecutor = new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }); const savedObjectsClient = savedObjectsClientMock.create(); function getServices() { @@ -33,18 +34,20 @@ const executeParams = { request: {} as KibanaRequest, }; +const spacesMock = spacesServiceMock.createSetupContract(); actionExecutor.initialize({ logger: loggingServiceMock.create().get(), - spaces: { - getSpaceId: () => 'some-namespace', - } as any, + spaces: spacesMock, getServices, actionTypeRegistry, encryptedSavedObjectsPlugin, eventLogger: eventLoggerMock.create(), }); -beforeEach(() => jest.resetAllMocks()); +beforeEach(() => { + jest.resetAllMocks(); + spacesMock.getSpaceId.mockReturnValue('some-namespace'); +}); test('successfully executes', async () => { const actionType = { @@ -219,3 +222,20 @@ test('returns an error if actionType is not enabled', async () => { } `); }); + +test('throws an error when passing isESOUsingEphemeralEncryptionKey with value of true', async () => { + const customActionExecutor = new ActionExecutor({ isESOUsingEphemeralEncryptionKey: true }); + customActionExecutor.initialize({ + logger: loggingServiceMock.create().get(), + spaces: spacesMock, + getServices, + actionTypeRegistry, + encryptedSavedObjectsPlugin, + eventLogger: eventLoggerMock.create(), + }); + await expect( + customActionExecutor.execute(executeParams) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unable to execute action due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml"` + ); +}); diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index 03a892a42792e..e42a69812b7da 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -37,6 +37,11 @@ export type ActionExecutorContract = PublicMethodsOf<ActionExecutor>; export class ActionExecutor { private isInitialized = false; private actionExecutorContext?: ActionExecutorContext; + private readonly isESOUsingEphemeralEncryptionKey: boolean; + + constructor({ isESOUsingEphemeralEncryptionKey }: { isESOUsingEphemeralEncryptionKey: boolean }) { + this.isESOUsingEphemeralEncryptionKey = isESOUsingEphemeralEncryptionKey; + } public initialize(actionExecutorContext: ActionExecutorContext) { if (this.isInitialized) { @@ -55,6 +60,12 @@ export class ActionExecutor { throw new Error('ActionExecutor not initialized'); } + if (this.isESOUsingEphemeralEncryptionKey === true) { + throw new Error( + `Unable to execute action due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml` + ); + } + const { spaces, getServices, diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts index fda1e2f5d2456..6be5e1f79ee82 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts @@ -78,14 +78,18 @@ beforeEach(() => { }); test(`throws an error if factory isn't initialized`, () => { - const factory = new TaskRunnerFactory(new ActionExecutor()); + const factory = new TaskRunnerFactory( + new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) + ); expect(() => factory.create({ taskInstance: mockedTaskInstance }) ).toThrowErrorMatchingInlineSnapshot(`"TaskRunnerFactory not initialized"`); }); test(`throws an error if factory is already initialized`, () => { - const factory = new TaskRunnerFactory(new ActionExecutor()); + const factory = new TaskRunnerFactory( + new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) + ); factory.initialize(taskRunnerFactoryInitializerParams); expect(() => factory.initialize(taskRunnerFactoryInitializerParams) diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts new file mode 100644 index 0000000000000..ba6fbcd32daee --- /dev/null +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ActionsPlugin, ActionsPluginsSetup, ActionsPluginsStart } from './plugin'; +import { PluginInitializerContext } from '../../../../src/core/server'; +import { coreMock, httpServerMock } from '../../../../src/core/server/mocks'; +import { licensingMock } from '../../licensing/server/mocks'; +import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; +import { taskManagerMock } from '../../task_manager/server/mocks'; +import { eventLogMock } from '../../event_log/server/mocks'; + +describe('Actions Plugin', () => { + describe('setup()', () => { + let context: PluginInitializerContext; + let plugin: ActionsPlugin; + let coreSetup: ReturnType<typeof coreMock.createSetup>; + let pluginsSetup: jest.Mocked<ActionsPluginsSetup>; + + beforeEach(() => { + context = coreMock.createPluginInitializerContext(); + plugin = new ActionsPlugin(context); + coreSetup = coreMock.createSetup(); + pluginsSetup = { + taskManager: taskManagerMock.createSetup(), + encryptedSavedObjects: encryptedSavedObjectsMock.createSetup(), + licensing: licensingMock.createSetup(), + event_log: eventLogMock.createSetup(), + }; + }); + + it('should log warning when Encrypted Saved Objects plugin is using an ephemeral encryption key', async () => { + await plugin.setup(coreSetup, pluginsSetup); + expect(pluginsSetup.encryptedSavedObjects.usingEphemeralEncryptionKey).toEqual(true); + expect(context.logger.get().warn).toHaveBeenCalledWith( + 'APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.' + ); + }); + + describe('routeHandlerContext.getActionsClient()', () => { + it('should not throw error when ESO plugin not using a generated key', async () => { + await plugin.setup(coreSetup, { + ...pluginsSetup, + encryptedSavedObjects: { + ...pluginsSetup.encryptedSavedObjects, + usingEphemeralEncryptionKey: false, + }, + }); + + expect(coreSetup.http.registerRouteHandlerContext).toHaveBeenCalledTimes(1); + const handler = coreSetup.http.registerRouteHandlerContext.mock.calls[0]; + expect(handler[0]).toEqual('actions'); + + const actionsContextHandler = (await handler[1]( + { + core: { + savedObjects: { + client: {}, + }, + }, + } as any, + httpServerMock.createKibanaRequest(), + httpServerMock.createResponseFactory() + )) as any; + actionsContextHandler.getActionsClient(); + }); + + it('should throw error when ESO plugin using a generated key', async () => { + await plugin.setup(coreSetup, pluginsSetup); + + expect(coreSetup.http.registerRouteHandlerContext).toHaveBeenCalledTimes(1); + const handler = coreSetup.http.registerRouteHandlerContext.mock.calls[0]; + expect(handler[0]).toEqual('actions'); + + const actionsContextHandler = (await handler[1]( + { + core: { + savedObjects: { + client: {}, + }, + }, + } as any, + httpServerMock.createKibanaRequest(), + httpServerMock.createResponseFactory() + )) as any; + expect(() => actionsContextHandler.getActionsClient()).toThrowErrorMatchingInlineSnapshot( + `"Unable to create actions client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml"` + ); + }); + }); + }); + describe('start()', () => { + let plugin: ActionsPlugin; + let coreSetup: ReturnType<typeof coreMock.createSetup>; + let coreStart: ReturnType<typeof coreMock.createStart>; + let pluginsSetup: jest.Mocked<ActionsPluginsSetup>; + let pluginsStart: jest.Mocked<ActionsPluginsStart>; + + beforeEach(() => { + const context = coreMock.createPluginInitializerContext(); + plugin = new ActionsPlugin(context); + coreSetup = coreMock.createSetup(); + coreStart = coreMock.createStart(); + pluginsSetup = { + taskManager: taskManagerMock.createSetup(), + encryptedSavedObjects: encryptedSavedObjectsMock.createSetup(), + licensing: licensingMock.createSetup(), + event_log: eventLogMock.createSetup(), + }; + pluginsStart = { + taskManager: taskManagerMock.createStart(), + encryptedSavedObjects: encryptedSavedObjectsMock.createStart(), + }; + }); + + describe('getActionsClientWithRequest()', () => { + it('should not throw error when ESO plugin not using a generated key', async () => { + await plugin.setup(coreSetup, { + ...pluginsSetup, + encryptedSavedObjects: { + ...pluginsSetup.encryptedSavedObjects, + usingEphemeralEncryptionKey: false, + }, + }); + const pluginStart = plugin.start(coreStart, pluginsStart); + + await pluginStart.getActionsClientWithRequest(httpServerMock.createKibanaRequest()); + }); + + it('should throw error when ESO plugin using generated key', async () => { + await plugin.setup(coreSetup, pluginsSetup); + const pluginStart = plugin.start(coreStart, pluginsStart); + + expect(pluginsSetup.encryptedSavedObjects.usingEphemeralEncryptionKey).toEqual(true); + await expect( + pluginStart.getActionsClientWithRequest(httpServerMock.createKibanaRequest()) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unable to create actions client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml"` + ); + }); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index dab09fc455ecf..1714666882b05 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -62,7 +62,7 @@ export interface PluginSetupContract { export interface PluginStartContract { execute(options: ExecuteOptions): Promise<void>; - getActionsClientWithRequest(request: KibanaRequest): Promise<ActionsClient>; + getActionsClientWithRequest(request: KibanaRequest): Promise<PublicMethodsOf<ActionsClient>>; } export interface ActionsPluginsSetup { @@ -90,6 +90,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi private licenseState: LicenseState | null = null; private spaces?: SpacesServiceSetup; private eventLogger?: IEventLogger; + private isESOUsingEphemeralEncryptionKey?: boolean; constructor(initContext: PluginInitializerContext) { this.config = initContext.config @@ -108,6 +109,15 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi } public async setup(core: CoreSetup, plugins: ActionsPluginsSetup): Promise<PluginSetupContract> { + this.isESOUsingEphemeralEncryptionKey = + plugins.encryptedSavedObjects.usingEphemeralEncryptionKey; + + if (this.isESOUsingEphemeralEncryptionKey) { + this.logger.warn( + 'APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.' + ); + } + // Encrypted attributes // - `secrets` properties will be encrypted // - `config` will be included in AAD @@ -127,7 +137,9 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi event: { provider: EVENT_LOG_PROVIDER }, }); - const actionExecutor = new ActionExecutor(); + const actionExecutor = new ActionExecutor({ + isESOUsingEphemeralEncryptionKey: this.isESOUsingEphemeralEncryptionKey, + }); const taskRunnerFactory = new TaskRunnerFactory(actionExecutor); const actionsConfigUtils = getActionsConfigurationUtilities( (await this.config) as ActionsConfig @@ -179,6 +191,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi taskRunnerFactory, kibanaIndex, adminClient, + isESOUsingEphemeralEncryptionKey, } = this; actionExecutor!.initialize({ @@ -203,9 +216,15 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi taskManager: plugins.taskManager, getScopedSavedObjectsClient: core.savedObjects.getScopedClient, getBasePath: this.getBasePath, + isESOUsingEphemeralEncryptionKey: isESOUsingEphemeralEncryptionKey!, }), // Ability to get an actions client from legacy code async getActionsClientWithRequest(request: KibanaRequest) { + if (isESOUsingEphemeralEncryptionKey === true) { + throw new Error( + `Unable to create actions client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml` + ); + } return new ActionsClient({ savedObjectsClient: core.savedObjects.getScopedClient(request), actionTypeRegistry: actionTypeRegistry!, @@ -229,10 +248,15 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi private createRouteHandlerContext = ( defaultKibanaIndex: string ): IContextProvider<RequestHandler<any, any, any>, 'actions'> => { - const { actionTypeRegistry, adminClient } = this; + const { actionTypeRegistry, adminClient, isESOUsingEphemeralEncryptionKey } = this; return async function actionsRouteHandlerContext(context, request) { return { getActionsClient: () => { + if (isESOUsingEphemeralEncryptionKey === true) { + throw new Error( + `Unable to create actions client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml` + ); + } return new ActionsClient({ savedObjectsClient: context.core!.savedObjects.client, actionTypeRegistry: actionTypeRegistry!, diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index e301d157d2c7c..d936e2a467f52 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -15,8 +15,8 @@ export const config = { ui: true, }, schema: schema.object({ + enabled: schema.boolean({ defaultValue: true }), serviceMapEnabled: schema.boolean({ defaultValue: false }), - serviceMapInitialTimeRange: schema.number({ defaultValue: 60 * 1000 * 60 }), // last 1 hour autocreateApmIndexPattern: schema.boolean({ defaultValue: true }), ui: schema.object({ enabled: schema.boolean({ defaultValue: true }), @@ -38,7 +38,6 @@ export function mergeConfigs(apmOssConfig: APMOSSConfig, apmConfig: APMXPackConf 'apm_oss.onboardingIndices': apmOssConfig.onboardingIndices, 'apm_oss.indexPattern': apmOssConfig.indexPattern, 'xpack.apm.serviceMapEnabled': apmConfig.serviceMapEnabled, - 'xpack.apm.serviceMapInitialTimeRange': apmConfig.serviceMapInitialTimeRange, 'xpack.apm.ui.enabled': apmConfig.ui.enabled, 'xpack.apm.ui.maxTraceItems': apmConfig.ui.maxTraceItems, 'xpack.apm.ui.transactionGroupBucketSize': apmConfig.ui.transactionGroupBucketSize, diff --git a/x-pack/plugins/case/server/index.ts b/x-pack/plugins/case/server/index.ts index 3963debea9795..990aef19b74f7 100644 --- a/x-pack/plugins/case/server/index.ts +++ b/x-pack/plugins/case/server/index.ts @@ -7,7 +7,7 @@ import { PluginInitializerContext } from '../../../../src/core/server'; import { ConfigSchema } from './config'; import { CasePlugin } from './plugin'; -export { NewCaseFormatted, NewCommentFormatted } from './routes/api/types'; +export { CaseAttributes, CommentAttributes } from './routes/api/types'; export const config = { schema: ConfigSchema }; export const plugin = (initializerContext: PluginInitializerContext) => diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts index 360c6de67b2a8..eb9afb27a749e 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts @@ -21,6 +21,8 @@ export const createMockSavedObjectsRepository = (savedObject: any[] = []) => { throw SavedObjectsErrorHelpers.createBadRequestError('Error thrown for testing'); } return { + page: 1, + per_page: 5, total: savedObject.length, saved_objects: savedObject, }; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts index 84889c3ac49be..ac9eddd6dd2cb 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_router.ts @@ -12,7 +12,7 @@ import { RouteDeps } from '../index'; export const createRoute = async ( api: (deps: RouteDeps) => void, - method: 'get' | 'post' | 'delete', + method: 'get' | 'post' | 'delete' | 'patch', badAuth = false ) => { const httpService = httpServiceMock.createSetupContract(); diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts index d59f0977e6993..c7f6b6fad7d1a 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -9,17 +9,16 @@ export const mockCases = [ type: 'case-workflow', id: 'mock-id-1', attributes: { - created_at: 1574718888885, + created_at: '2019-11-25T21:54:48.952Z', created_by: { - full_name: null, + full_name: 'elastic', username: 'elastic', }, description: 'This is a brand new case of a bad meanie defacing data', title: 'Super Bad Security Issue', state: 'open', tags: ['defacement'], - case_type: 'security', - assignees: [], + updated_at: '2019-11-25T21:54:48.952Z', }, references: [], updated_at: '2019-11-25T21:54:48.952Z', @@ -29,17 +28,16 @@ export const mockCases = [ type: 'case-workflow', id: 'mock-id-2', attributes: { - created_at: 1574721120834, + created_at: '2019-11-25T22:32:00.900Z', created_by: { - full_name: null, + full_name: 'elastic', username: 'elastic', }, description: 'Oh no, a bad meanie destroying data!', title: 'Damaging Data Destruction Detected', state: 'open', tags: ['Data Destruction'], - case_type: 'security', - assignees: [], + updated_at: '2019-11-25T22:32:00.900Z', }, references: [], updated_at: '2019-11-25T22:32:00.900Z', @@ -49,17 +47,16 @@ export const mockCases = [ type: 'case-workflow', id: 'mock-id-3', attributes: { - created_at: 1574721137881, + created_at: '2019-11-25T22:32:17.947Z', created_by: { - full_name: null, + full_name: 'elastic', username: 'elastic', }, description: 'Oh no, a bad meanie going LOLBins all over the place!', title: 'Another bad one', state: 'open', tags: ['LOLBins'], - case_type: 'security', - assignees: [], + updated_at: '2019-11-25T22:32:17.947Z', }, references: [], updated_at: '2019-11-25T22:32:17.947Z', @@ -82,11 +79,12 @@ export const mockCaseComments = [ id: 'mock-comment-1', attributes: { comment: 'Wow, good luck catching that bad meanie!', - created_at: 1574718900112, + created_at: '2019-11-25T21:55:00.177Z', created_by: { - full_name: null, + full_name: 'elastic', username: 'elastic', }, + updated_at: '2019-11-25T21:55:00.177Z', }, references: [ { @@ -103,11 +101,12 @@ export const mockCaseComments = [ id: 'mock-comment-2', attributes: { comment: 'Well I decided to update my comment. So what? Deal with it.', - created_at: 1574718902724, + created_at: '2019-11-25T21:55:14.633Z', created_by: { - full_name: null, + full_name: 'elastic', username: 'elastic', }, + updated_at: '2019-11-25T21:55:14.633Z', }, references: [ { @@ -124,11 +123,12 @@ export const mockCaseComments = [ id: 'mock-comment-3', attributes: { comment: 'Wow, good luck catching that bad meanie!', - created_at: 1574721150542, + created_at: '2019-11-25T22:32:30.608Z', created_by: { - full_name: null, + full_name: 'elastic', username: 'elastic', }, + updated_at: '2019-11-25T22:32:30.608Z', }, references: [ { diff --git a/x-pack/plugins/case/server/routes/api/__tests__/get_all_cases.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/get_all_cases.test.ts index 2f8a229c08f29..96c411a746d49 100644 --- a/x-pack/plugins/case/server/routes/api/__tests__/get_all_cases.test.ts +++ b/x-pack/plugins/case/server/routes/api/__tests__/get_all_cases.test.ts @@ -19,7 +19,7 @@ describe('GET all cases', () => { beforeAll(async () => { routeHandler = await createRoute(initGetAllCasesApi, 'get'); }); - it(`returns the case without case comments when includeComments is false`, async () => { + it(`gets all the cases`, async () => { const request = httpServerMock.createKibanaRequest({ path: '/api/cases', method: 'get', @@ -29,6 +29,6 @@ describe('GET all cases', () => { const response = await routeHandler(theContext, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload.saved_objects).toHaveLength(3); + expect(response.payload.cases).toHaveLength(3); }); }); diff --git a/x-pack/plugins/case/server/routes/api/__tests__/get_case.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/get_case.test.ts index 3c5f8e52d1946..60becf1228a0c 100644 --- a/x-pack/plugins/case/server/routes/api/__tests__/get_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/__tests__/get_case.test.ts @@ -12,15 +12,17 @@ import { mockCasesErrorTriggerData, } from '../__fixtures__'; import { initGetCaseApi } from '../get_case'; -import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; +import { kibanaResponseFactory, RequestHandler, SavedObject } from 'src/core/server'; import { httpServerMock } from 'src/core/server/mocks'; +import { flattenCaseSavedObject } from '../utils'; +import { CaseAttributes } from '../types'; describe('GET case', () => { let routeHandler: RequestHandler<any, any, any>; beforeAll(async () => { routeHandler = await createRoute(initGetCaseApi, 'get'); }); - it(`returns the case without case comments when includeComments is false`, async () => { + it(`returns the case with empty case comments when includeComments is false`, async () => { const request = httpServerMock.createKibanaRequest({ path: '/api/cases/{id}', params: { @@ -37,8 +39,13 @@ describe('GET case', () => { const response = await routeHandler(theContext, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload).toEqual(mockCases.find(s => s.id === 'mock-id-1')); - expect(response.payload.comments).toBeUndefined(); + expect(response.payload).toEqual( + flattenCaseSavedObject( + (mockCases.find(s => s.id === 'mock-id-1') as unknown) as SavedObject<CaseAttributes>, + [] + ) + ); + expect(response.payload.comments).toEqual([]); }); it(`returns an error when thrown from getCase`, async () => { const request = httpServerMock.createKibanaRequest({ @@ -76,7 +83,7 @@ describe('GET case', () => { const response = await routeHandler(theContext, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload.comments.saved_objects).toHaveLength(3); + expect(response.payload.comments).toHaveLength(3); }); it(`returns an error when thrown from getAllCaseComments`, async () => { const request = httpServerMock.createKibanaRequest({ diff --git a/x-pack/plugins/case/server/routes/api/__tests__/get_comment.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/get_comment.test.ts index 9b6a1e435838b..3add93acc641f 100644 --- a/x-pack/plugins/case/server/routes/api/__tests__/get_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/__tests__/get_comment.test.ts @@ -11,8 +11,10 @@ import { mockCaseComments, } from '../__fixtures__'; import { initGetCommentApi } from '../get_comment'; -import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; +import { kibanaResponseFactory, RequestHandler, SavedObject } from 'src/core/server'; import { httpServerMock } from 'src/core/server/mocks'; +import { flattenCommentSavedObject } from '../utils'; +import { CommentAttributes } from '../types'; describe('GET comment', () => { let routeHandler: RequestHandler<any, any, any>; @@ -32,7 +34,11 @@ describe('GET comment', () => { const response = await routeHandler(theContext, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload).toEqual(mockCaseComments.find(s => s.id === 'mock-comment-1')); + expect(response.payload).toEqual( + flattenCommentSavedObject( + mockCaseComments.find(s => s.id === 'mock-comment-1') as SavedObject<CommentAttributes> + ) + ); }); it(`returns an error when getComment throws`, async () => { const request = httpServerMock.createKibanaRequest({ diff --git a/x-pack/plugins/case/server/routes/api/__tests__/post_case.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/post_case.test.ts index bb688dde4c58f..32c7c5a015af0 100644 --- a/x-pack/plugins/case/server/routes/api/__tests__/post_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/__tests__/post_case.test.ts @@ -28,7 +28,6 @@ describe('POST cases', () => { title: 'Super Bad Security Issue', state: 'open', tags: ['defacement'], - case_type: 'security', }, }); @@ -36,8 +35,8 @@ describe('POST cases', () => { const response = await routeHandler(theContext, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload.id).toEqual('mock-it'); - expect(response.payload.attributes.created_by.username).toEqual('awesome'); + expect(response.payload.case_id).toEqual('mock-it'); + expect(response.payload.created_by.username).toEqual('awesome'); }); it(`Returns an error if postNewCase throws`, async () => { const request = httpServerMock.createKibanaRequest({ @@ -48,7 +47,6 @@ describe('POST cases', () => { title: 'Super Bad Security Issue', state: 'open', tags: ['error'], - case_type: 'security', }, }); @@ -69,7 +67,6 @@ describe('POST cases', () => { title: 'Super Bad Security Issue', state: 'open', tags: ['defacement'], - case_type: 'security', }, }); diff --git a/x-pack/plugins/case/server/routes/api/__tests__/post_comment.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/post_comment.test.ts index 0c059b7f15ea4..653140af2a7cf 100644 --- a/x-pack/plugins/case/server/routes/api/__tests__/post_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/__tests__/post_comment.test.ts @@ -35,8 +35,7 @@ describe('POST comment', () => { const response = await routeHandler(theContext, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload.id).toEqual('mock-comment'); - expect(response.payload.references[0].id).toEqual('mock-id-1'); + expect(response.payload.comment_id).toEqual('mock-comment'); }); it(`Returns an error if the case does not exist`, async () => { const request = httpServerMock.createKibanaRequest({ diff --git a/x-pack/plugins/case/server/routes/api/__tests__/update_case.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/update_case.test.ts index 7ed478d2e7c01..23283d7f8a5be 100644 --- a/x-pack/plugins/case/server/routes/api/__tests__/update_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/__tests__/update_case.test.ts @@ -17,12 +17,12 @@ import { httpServerMock } from 'src/core/server/mocks'; describe('UPDATE case', () => { let routeHandler: RequestHandler<any, any, any>; beforeAll(async () => { - routeHandler = await createRoute(initUpdateCaseApi, 'post'); + routeHandler = await createRoute(initUpdateCaseApi, 'patch'); }); it(`Updates a case`, async () => { const request = httpServerMock.createKibanaRequest({ path: '/api/cases/{id}', - method: 'post', + method: 'patch', params: { id: 'mock-id-1', }, @@ -35,13 +35,13 @@ describe('UPDATE case', () => { const response = await routeHandler(theContext, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload.id).toEqual('mock-id-1'); - expect(response.payload.attributes.state).toEqual('closed'); + expect(typeof response.payload.updated_at).toBe('string'); + expect(response.payload.state).toEqual('closed'); }); it(`Returns an error if updateCase throws`, async () => { const request = httpServerMock.createKibanaRequest({ path: '/api/cases/{id}', - method: 'post', + method: 'patch', params: { id: 'mock-id-does-not-exist', }, diff --git a/x-pack/plugins/case/server/routes/api/__tests__/update_comment.test.ts b/x-pack/plugins/case/server/routes/api/__tests__/update_comment.test.ts index 8aa84b45b7dbb..5bfd121691ab4 100644 --- a/x-pack/plugins/case/server/routes/api/__tests__/update_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/__tests__/update_comment.test.ts @@ -17,12 +17,12 @@ import { httpServerMock } from 'src/core/server/mocks'; describe('UPDATE comment', () => { let routeHandler: RequestHandler<any, any, any>; beforeAll(async () => { - routeHandler = await createRoute(initUpdateCommentApi, 'post'); + routeHandler = await createRoute(initUpdateCommentApi, 'patch'); }); it(`Updates a comment`, async () => { const request = httpServerMock.createKibanaRequest({ path: '/api/cases/comment/{id}', - method: 'post', + method: 'patch', params: { id: 'mock-comment-1', }, @@ -35,13 +35,12 @@ describe('UPDATE comment', () => { const response = await routeHandler(theContext, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload.id).toEqual('mock-comment-1'); - expect(response.payload.attributes.comment).toEqual('Update my comment'); + expect(response.payload.comment).toEqual('Update my comment'); }); it(`Returns an error if updateComment throws`, async () => { const request = httpServerMock.createKibanaRequest({ path: '/api/cases/comment/{id}', - method: 'post', + method: 'patch', params: { id: 'mock-comment-does-not-exist', }, diff --git a/x-pack/plugins/case/server/routes/api/get_all_case_comments.ts b/x-pack/plugins/case/server/routes/api/get_all_case_comments.ts index cc4956ead1bd7..b74227fa8d983 100644 --- a/x-pack/plugins/case/server/routes/api/get_all_case_comments.ts +++ b/x-pack/plugins/case/server/routes/api/get_all_case_comments.ts @@ -6,7 +6,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '.'; -import { wrapError } from './utils'; +import { formatAllComments, wrapError } from './utils'; export function initGetAllCaseCommentsApi({ caseService, router }: RouteDeps) { router.get( @@ -24,7 +24,7 @@ export function initGetAllCaseCommentsApi({ caseService, router }: RouteDeps) { client: context.core.savedObjects.client, caseId: request.params.id, }); - return response.ok({ body: theComments }); + return response.ok({ body: formatAllComments(theComments) }); } catch (error) { return response.customError(wrapError(error)); } diff --git a/x-pack/plugins/case/server/routes/api/get_all_cases.ts b/x-pack/plugins/case/server/routes/api/get_all_cases.ts index 749a183dfe980..09075a32ac377 100644 --- a/x-pack/plugins/case/server/routes/api/get_all_cases.ts +++ b/x-pack/plugins/case/server/routes/api/get_all_cases.ts @@ -4,21 +4,35 @@ * you may not use this file except in compliance with the Elastic License. */ +import { schema } from '@kbn/config-schema'; import { RouteDeps } from '.'; -import { wrapError } from './utils'; +import { formatAllCases, wrapError } from './utils'; +import { SavedObjectsFindOptionsSchema } from './schema'; +import { AllCases } from './types'; export function initGetAllCasesApi({ caseService, router }: RouteDeps) { router.get( { path: '/api/cases', - validate: false, + validate: { + query: schema.nullable(SavedObjectsFindOptionsSchema), + }, }, async (context, request, response) => { try { - const cases = await caseService.getAllCases({ - client: context.core.savedObjects.client, + const args = request.query + ? { + client: context.core.savedObjects.client, + options: request.query, + } + : { + client: context.core.savedObjects.client, + }; + const cases = await caseService.getAllCases(args); + const body: AllCases = formatAllCases(cases); + return response.ok({ + body, }); - return response.ok({ body: cases }); } catch (error) { return response.customError(wrapError(error)); } diff --git a/x-pack/plugins/case/server/routes/api/get_case.ts b/x-pack/plugins/case/server/routes/api/get_case.ts index 6aad22a1ebf1b..2481197000beb 100644 --- a/x-pack/plugins/case/server/routes/api/get_case.ts +++ b/x-pack/plugins/case/server/routes/api/get_case.ts @@ -6,7 +6,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '.'; -import { wrapError } from './utils'; +import { flattenCaseSavedObject, wrapError } from './utils'; export function initGetCaseApi({ caseService, router }: RouteDeps) { router.get( @@ -33,14 +33,16 @@ export function initGetCaseApi({ caseService, router }: RouteDeps) { return response.customError(wrapError(error)); } if (!includeComments) { - return response.ok({ body: theCase }); + return response.ok({ body: flattenCaseSavedObject(theCase, []) }); } try { const theComments = await caseService.getAllCaseComments({ client: context.core.savedObjects.client, caseId: request.params.id, }); - return response.ok({ body: { ...theCase, comments: theComments } }); + return response.ok({ + body: { ...flattenCaseSavedObject(theCase, theComments.saved_objects) }, + }); } catch (error) { return response.customError(wrapError(error)); } diff --git a/x-pack/plugins/case/server/routes/api/get_comment.ts b/x-pack/plugins/case/server/routes/api/get_comment.ts index 6fd507d89738d..d892b4cfebc3b 100644 --- a/x-pack/plugins/case/server/routes/api/get_comment.ts +++ b/x-pack/plugins/case/server/routes/api/get_comment.ts @@ -6,7 +6,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '.'; -import { wrapError } from './utils'; +import { flattenCommentSavedObject, wrapError } from './utils'; export function initGetCommentApi({ caseService, router }: RouteDeps) { router.get( @@ -24,7 +24,7 @@ export function initGetCommentApi({ caseService, router }: RouteDeps) { client: context.core.savedObjects.client, commentId: request.params.id, }); - return response.ok({ body: theComment }); + return response.ok({ body: flattenCommentSavedObject(theComment) }); } catch (error) { return response.customError(wrapError(error)); } diff --git a/x-pack/plugins/case/server/routes/api/get_tags.ts b/x-pack/plugins/case/server/routes/api/get_tags.ts new file mode 100644 index 0000000000000..1d714db4c0c28 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/get_tags.ts @@ -0,0 +1,28 @@ +/* + * 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 { RouteDeps } from './index'; +import { wrapError } from './utils'; + +export function initGetTagsApi({ caseService, router }: RouteDeps) { + router.get( + { + path: '/api/cases/tags', + validate: {}, + }, + async (context, request, response) => { + let theCase; + try { + theCase = await caseService.getTags({ + client: context.core.savedObjects.client, + }); + return response.ok({ body: theCase }); + } catch (error) { + return response.customError(wrapError(error)); + } + } + ); +} diff --git a/x-pack/plugins/case/server/routes/api/index.ts b/x-pack/plugins/case/server/routes/api/index.ts index 11ef91d539e87..32dfd6a78d1c2 100644 --- a/x-pack/plugins/case/server/routes/api/index.ts +++ b/x-pack/plugins/case/server/routes/api/index.ts @@ -5,17 +5,18 @@ */ import { IRouter } from 'src/core/server'; -import { initDeleteCommentApi } from './delete_comment'; +import { CaseServiceSetup } from '../../services'; import { initDeleteCaseApi } from './delete_case'; +import { initDeleteCommentApi } from './delete_comment'; import { initGetAllCaseCommentsApi } from './get_all_case_comments'; import { initGetAllCasesApi } from './get_all_cases'; import { initGetCaseApi } from './get_case'; import { initGetCommentApi } from './get_comment'; +import { initGetTagsApi } from './get_tags'; import { initPostCaseApi } from './post_case'; import { initPostCommentApi } from './post_comment'; import { initUpdateCaseApi } from './update_case'; import { initUpdateCommentApi } from './update_comment'; -import { CaseServiceSetup } from '../../services'; export interface RouteDeps { caseService: CaseServiceSetup; @@ -23,12 +24,13 @@ export interface RouteDeps { } export function initCaseApi(deps: RouteDeps) { + initDeleteCaseApi(deps); + initDeleteCommentApi(deps); initGetAllCaseCommentsApi(deps); initGetAllCasesApi(deps); initGetCaseApi(deps); initGetCommentApi(deps); - initDeleteCaseApi(deps); - initDeleteCommentApi(deps); + initGetTagsApi(deps); initPostCaseApi(deps); initPostCommentApi(deps); initUpdateCaseApi(deps); diff --git a/x-pack/plugins/case/server/routes/api/post_case.ts b/x-pack/plugins/case/server/routes/api/post_case.ts index e5aa0a3548b48..948bf02d5b3c1 100644 --- a/x-pack/plugins/case/server/routes/api/post_case.ts +++ b/x-pack/plugins/case/server/routes/api/post_case.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { formatNewCase, wrapError } from './utils'; +import { flattenCaseSavedObject, formatNewCase, wrapError } from './utils'; import { NewCaseSchema } from './schema'; import { RouteDeps } from '.'; @@ -31,7 +31,7 @@ export function initPostCaseApi({ caseService, router }: RouteDeps) { ...createdBy, }), }); - return response.ok({ body: newCase }); + return response.ok({ body: flattenCaseSavedObject(newCase, []) }); } catch (error) { return response.customError(wrapError(error)); } diff --git a/x-pack/plugins/case/server/routes/api/post_comment.ts b/x-pack/plugins/case/server/routes/api/post_comment.ts index 3f4592f5bb11f..f3f21becddfad 100644 --- a/x-pack/plugins/case/server/routes/api/post_comment.ts +++ b/x-pack/plugins/case/server/routes/api/post_comment.ts @@ -5,7 +5,7 @@ */ import { schema } from '@kbn/config-schema'; -import { formatNewComment, wrapError } from './utils'; +import { flattenCommentSavedObject, formatNewComment, wrapError } from './utils'; import { NewCommentSchema } from './schema'; import { RouteDeps } from '.'; import { CASE_SAVED_OBJECT } from '../../constants'; @@ -53,7 +53,7 @@ export function initPostCommentApi({ caseService, router }: RouteDeps) { ], }); - return response.ok({ body: newComment }); + return response.ok({ body: flattenCommentSavedObject(newComment) }); } catch (error) { return response.customError(wrapError(error)); } diff --git a/x-pack/plugins/case/server/routes/api/schema.ts b/x-pack/plugins/case/server/routes/api/schema.ts index 4a4a0c3a11e36..962dc474254f0 100644 --- a/x-pack/plugins/case/server/routes/api/schema.ts +++ b/x-pack/plugins/case/server/routes/api/schema.ts @@ -7,8 +7,8 @@ import { schema } from '@kbn/config-schema'; export const UserSchema = schema.object({ - username: schema.string(), full_name: schema.maybe(schema.string()), + username: schema.string(), }); export const NewCommentSchema = schema.object({ @@ -17,28 +17,38 @@ export const NewCommentSchema = schema.object({ export const CommentSchema = schema.object({ comment: schema.string(), - created_at: schema.number(), + created_at: schema.string(), created_by: UserSchema, + updated_at: schema.string(), }); export const UpdatedCommentSchema = schema.object({ comment: schema.string(), + updated_at: schema.string(), }); export const NewCaseSchema = schema.object({ - assignees: schema.arrayOf(UserSchema, { defaultValue: [] }), description: schema.string(), - title: schema.string(), state: schema.oneOf([schema.literal('open'), schema.literal('closed')], { defaultValue: 'open' }), tags: schema.arrayOf(schema.string(), { defaultValue: [] }), - case_type: schema.string(), + title: schema.string(), }); export const UpdatedCaseSchema = schema.object({ - assignees: schema.maybe(schema.arrayOf(UserSchema)), description: schema.maybe(schema.string()), - title: schema.maybe(schema.string()), state: schema.maybe(schema.oneOf([schema.literal('open'), schema.literal('closed')])), tags: schema.maybe(schema.arrayOf(schema.string())), - case_type: schema.maybe(schema.string()), + title: schema.maybe(schema.string()), +}); + +export const SavedObjectsFindOptionsSchema = schema.object({ + defaultSearchOperator: schema.maybe(schema.oneOf([schema.literal('AND'), schema.literal('OR')])), + fields: schema.maybe(schema.arrayOf(schema.string())), + filter: schema.maybe(schema.string()), + page: schema.maybe(schema.number()), + perPage: schema.maybe(schema.number()), + search: schema.maybe(schema.string()), + searchFields: schema.maybe(schema.arrayOf(schema.string())), + sortField: schema.maybe(schema.string()), + sortOrder: schema.maybe(schema.oneOf([schema.literal('desc'), schema.literal('asc')])), }); diff --git a/x-pack/plugins/case/server/routes/api/types.ts b/x-pack/plugins/case/server/routes/api/types.ts index d943e4e5fd7dd..2d1a88bcf1429 100644 --- a/x-pack/plugins/case/server/routes/api/types.ts +++ b/x-pack/plugins/case/server/routes/api/types.ts @@ -9,28 +9,63 @@ import { CommentSchema, NewCaseSchema, NewCommentSchema, + SavedObjectsFindOptionsSchema, UpdatedCaseSchema, UpdatedCommentSchema, UserSchema, } from './schema'; +import { SavedObjectAttributes } from '../../../../../../src/core/types'; export type NewCaseType = TypeOf<typeof NewCaseSchema>; -export type NewCommentFormatted = TypeOf<typeof CommentSchema>; +export type CommentAttributes = TypeOf<typeof CommentSchema> & SavedObjectAttributes; export type NewCommentType = TypeOf<typeof NewCommentSchema>; +export type SavedObjectsFindOptionsType = TypeOf<typeof SavedObjectsFindOptionsSchema>; export type UpdatedCaseTyped = TypeOf<typeof UpdatedCaseSchema>; export type UpdatedCommentType = TypeOf<typeof UpdatedCommentSchema>; export type UserType = TypeOf<typeof UserSchema>; -export interface NewCaseFormatted extends NewCaseType { - created_at: number; +export interface CaseAttributes extends NewCaseType, SavedObjectAttributes { + created_at: string; created_by: UserType; + updated_at: string; +} + +export type FlattenedCaseSavedObject = CaseAttributes & { + case_id: string; + comments: FlattenedCommentSavedObject[]; +}; + +export type FlattenedCasesSavedObject = Array< + CaseAttributes & { + case_id: string; + // TO DO it is partial because we need to add it the commentCount + commentCount?: number; + } +>; + +export interface AllCases { + cases: FlattenedCasesSavedObject; + page: number; + per_page: number; + total: number; +} + +export type FlattenedCommentSavedObject = CommentAttributes & { + comment_id: string; + // TO DO We might want to add the case_id where this comment is related too +}; + +export interface AllComments { + comments: FlattenedCommentSavedObject[]; + page: number; + per_page: number; + total: number; } export interface UpdatedCaseType { - assignees?: UpdatedCaseTyped['assignees']; description?: UpdatedCaseTyped['description']; - title?: UpdatedCaseTyped['title']; state?: UpdatedCaseTyped['state']; tags?: UpdatedCaseTyped['tags']; - case_type?: UpdatedCaseTyped['case_type']; + title?: UpdatedCaseTyped['title']; + updated_at: string; } diff --git a/x-pack/plugins/case/server/routes/api/update_case.ts b/x-pack/plugins/case/server/routes/api/update_case.ts index 52c8cab0022dd..2a814c7259e4a 100644 --- a/x-pack/plugins/case/server/routes/api/update_case.ts +++ b/x-pack/plugins/case/server/routes/api/update_case.ts @@ -10,7 +10,7 @@ import { RouteDeps } from '.'; import { UpdatedCaseSchema } from './schema'; export function initUpdateCaseApi({ caseService, router }: RouteDeps) { - router.post( + router.patch( { path: '/api/cases/{id}', validate: { @@ -25,9 +25,12 @@ export function initUpdateCaseApi({ caseService, router }: RouteDeps) { const updatedCase = await caseService.updateCase({ client: context.core.savedObjects.client, caseId: request.params.id, - updatedAttributes: request.body, + updatedAttributes: { + ...request.body, + updated_at: new Date().toISOString(), + }, }); - return response.ok({ body: updatedCase }); + return response.ok({ body: updatedCase.attributes }); } catch (error) { return response.customError(wrapError(error)); } diff --git a/x-pack/plugins/case/server/routes/api/update_comment.ts b/x-pack/plugins/case/server/routes/api/update_comment.ts index e1ee6029e8e4f..815f44a14e2e7 100644 --- a/x-pack/plugins/case/server/routes/api/update_comment.ts +++ b/x-pack/plugins/case/server/routes/api/update_comment.ts @@ -10,7 +10,7 @@ import { NewCommentSchema } from './schema'; import { RouteDeps } from '.'; export function initUpdateCommentApi({ caseService, router }: RouteDeps) { - router.post( + router.patch( { path: '/api/cases/comment/{id}', validate: { @@ -25,9 +25,12 @@ export function initUpdateCommentApi({ caseService, router }: RouteDeps) { const updatedComment = await caseService.updateComment({ client: context.core.savedObjects.client, commentId: request.params.id, - updatedAttributes: request.body, + updatedAttributes: { + ...request.body, + updated_at: new Date().toISOString(), + }, }); - return response.ok({ body: updatedComment }); + return response.ok({ body: updatedComment.attributes }); } catch (error) { return response.customError(wrapError(error)); } diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index c6e33dbb8433b..51944b04836ab 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -5,21 +5,31 @@ */ import { boomify, isBoom } from 'boom'; -import { CustomHttpResponseOptions, ResponseError } from 'kibana/server'; import { + CustomHttpResponseOptions, + ResponseError, + SavedObject, + SavedObjectsFindResponse, +} from 'kibana/server'; +import { + AllComments, + CaseAttributes, + CommentAttributes, + FlattenedCaseSavedObject, + FlattenedCommentSavedObject, + AllCases, NewCaseType, - NewCaseFormatted, NewCommentType, - NewCommentFormatted, UserType, } from './types'; export const formatNewCase = ( newCase: NewCaseType, { full_name, username }: { full_name?: string; username: string } -): NewCaseFormatted => ({ - created_at: new Date().valueOf(), +): CaseAttributes => ({ + created_at: new Date().toISOString(), created_by: { full_name, username }, + updated_at: new Date().toISOString(), ...newCase, }); @@ -32,10 +42,11 @@ export const formatNewComment = ({ newComment, full_name, username, -}: NewCommentArgs): NewCommentFormatted => ({ +}: NewCommentArgs): CommentAttributes => ({ ...newComment, - created_at: new Date().valueOf(), + created_at: new Date().toISOString(), created_by: { full_name, username }, + updated_at: new Date().toISOString(), }); export function wrapError(error: any): CustomHttpResponseOptions<ResponseError> { @@ -46,3 +57,55 @@ export function wrapError(error: any): CustomHttpResponseOptions<ResponseError> statusCode: boom.output.statusCode, }; } + +export const formatAllCases = (cases: SavedObjectsFindResponse<CaseAttributes>): AllCases => ({ + page: cases.page, + per_page: cases.per_page, + total: cases.total, + cases: flattenCaseSavedObjects(cases.saved_objects), +}); + +export const flattenCaseSavedObjects = ( + savedObjects: SavedObjectsFindResponse<CaseAttributes>['saved_objects'] +): FlattenedCaseSavedObject[] => + savedObjects.reduce( + (acc: FlattenedCaseSavedObject[], savedObject: SavedObject<CaseAttributes>) => { + return [...acc, flattenCaseSavedObject(savedObject, [])]; + }, + [] + ); + +export const flattenCaseSavedObject = ( + savedObject: SavedObject<CaseAttributes>, + comments: Array<SavedObject<CommentAttributes>> +): FlattenedCaseSavedObject => ({ + case_id: savedObject.id, + comments: flattenCommentSavedObjects(comments), + ...savedObject.attributes, +}); + +export const formatAllComments = ( + comments: SavedObjectsFindResponse<CommentAttributes> +): AllComments => ({ + page: comments.page, + per_page: comments.per_page, + total: comments.total, + comments: flattenCommentSavedObjects(comments.saved_objects), +}); + +export const flattenCommentSavedObjects = ( + savedObjects: SavedObjectsFindResponse<CommentAttributes>['saved_objects'] +): FlattenedCommentSavedObject[] => + savedObjects.reduce( + (acc: FlattenedCommentSavedObject[], savedObject: SavedObject<CommentAttributes>) => { + return [...acc, flattenCommentSavedObject(savedObject)]; + }, + [] + ); + +export const flattenCommentSavedObject = ( + savedObject: SavedObject<CommentAttributes> +): FlattenedCommentSavedObject => ({ + comment_id: savedObject.id, + ...savedObject.attributes, +}); diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts index 531d5fa5b87e5..d6d4bd606676c 100644 --- a/x-pack/plugins/case/server/services/index.ts +++ b/x-pack/plugins/case/server/services/index.ts @@ -16,12 +16,14 @@ import { } from 'kibana/server'; import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT } from '../constants'; import { - NewCaseFormatted, - NewCommentFormatted, + CaseAttributes, + CommentAttributes, + SavedObjectsFindOptionsType, UpdatedCaseType, UpdatedCommentType, } from '../routes/api/types'; import { AuthenticatedUser, SecurityPluginSetup } from '../../../security/server'; +import { readTags } from './tags/read_tags'; interface ClientArgs { client: SavedObjectsClientContract; @@ -30,15 +32,19 @@ interface ClientArgs { interface GetCaseArgs extends ClientArgs { caseId: string; } + +interface GetCasesArgs extends ClientArgs { + options?: SavedObjectsFindOptionsType; +} interface GetCommentArgs extends ClientArgs { commentId: string; } interface PostCaseArgs extends ClientArgs { - attributes: NewCaseFormatted; + attributes: CaseAttributes; } interface PostCommentArgs extends ClientArgs { - attributes: NewCommentFormatted; + attributes: CommentAttributes; references: SavedObjectReference[]; } interface UpdateCaseArgs extends ClientArgs { @@ -61,15 +67,16 @@ interface CaseServiceDeps { export interface CaseServiceSetup { deleteCase(args: GetCaseArgs): Promise<{}>; deleteComment(args: GetCommentArgs): Promise<{}>; - getAllCases(args: ClientArgs): Promise<SavedObjectsFindResponse>; - getAllCaseComments(args: GetCaseArgs): Promise<SavedObjectsFindResponse>; - getCase(args: GetCaseArgs): Promise<SavedObject>; - getComment(args: GetCommentArgs): Promise<SavedObject>; + getAllCases(args: GetCasesArgs): Promise<SavedObjectsFindResponse<CaseAttributes>>; + getAllCaseComments(args: GetCaseArgs): Promise<SavedObjectsFindResponse<CommentAttributes>>; + getCase(args: GetCaseArgs): Promise<SavedObject<CaseAttributes>>; + getComment(args: GetCommentArgs): Promise<SavedObject<CommentAttributes>>; + getTags(args: ClientArgs): Promise<string[]>; getUser(args: GetUserArgs): Promise<AuthenticatedUser>; - postNewCase(args: PostCaseArgs): Promise<SavedObject>; - postNewComment(args: PostCommentArgs): Promise<SavedObject>; - updateCase(args: UpdateCaseArgs): Promise<SavedObjectsUpdateResponse>; - updateComment(args: UpdateCommentArgs): Promise<SavedObjectsUpdateResponse>; + postNewCase(args: PostCaseArgs): Promise<SavedObject<CaseAttributes>>; + postNewComment(args: PostCommentArgs): Promise<SavedObject<CommentAttributes>>; + updateCase(args: UpdateCaseArgs): Promise<SavedObjectsUpdateResponse<CaseAttributes>>; + updateComment(args: UpdateCommentArgs): Promise<SavedObjectsUpdateResponse<CommentAttributes>>; } export class CaseService { @@ -111,10 +118,10 @@ export class CaseService { throw error; } }, - getAllCases: async ({ client }: ClientArgs) => { + getAllCases: async ({ client, options }: GetCasesArgs) => { try { this.log.debug(`Attempting to GET all cases`); - return await client.find({ type: CASE_SAVED_OBJECT }); + return await client.find({ ...options, type: CASE_SAVED_OBJECT }); } catch (error) { this.log.debug(`Error on GET cases: ${error}`); throw error; @@ -132,6 +139,15 @@ export class CaseService { throw error; } }, + getTags: async ({ client }: ClientArgs) => { + try { + this.log.debug(`Attempting to GET all cases`); + return await readTags({ client }); + } catch (error) { + this.log.debug(`Error on GET cases: ${error}`); + throw error; + } + }, getUser: async ({ request, response }: GetUserArgs) => { let user; try { diff --git a/x-pack/plugins/case/server/services/tags/read_tags.ts b/x-pack/plugins/case/server/services/tags/read_tags.ts new file mode 100644 index 0000000000000..58ab99b164cfb --- /dev/null +++ b/x-pack/plugins/case/server/services/tags/read_tags.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObject, SavedObjectsClientContract } from 'kibana/server'; +import { CASE_SAVED_OBJECT } from '../../constants'; +import { CaseAttributes } from '../..'; + +const DEFAULT_PER_PAGE: number = 1000; + +export const convertToTags = (tagObjects: Array<SavedObject<CaseAttributes>>): string[] => + tagObjects.reduce<string[]>((accum, tagObj) => { + if (tagObj && tagObj.attributes && tagObj.attributes.tags) { + return [...accum, ...tagObj.attributes.tags]; + } else { + return accum; + } + }, []); + +export const convertTagsToSet = (tagObjects: Array<SavedObject<CaseAttributes>>): Set<string> => { + return new Set(convertToTags(tagObjects)); +}; + +// Note: This is doing an in-memory aggregation of the tags by calling each of the alerting +// records in batches of this const setting and uses the fields to try to get the least +// amount of data per record back. If saved objects at some point supports aggregations +// then this should be replaced with a an aggregation call. +// Ref: https://www.elastic.co/guide/en/kibana/master/saved-objects-api.html +export const readTags = async ({ + client, + perPage = DEFAULT_PER_PAGE, +}: { + client: SavedObjectsClientContract; + perPage?: number; +}): Promise<string[]> => { + const tags = await readRawTags({ client, perPage }); + return tags; +}; + +export const readRawTags = async ({ + client, + perPage = DEFAULT_PER_PAGE, +}: { + client: SavedObjectsClientContract; + perPage?: number; +}): Promise<string[]> => { + const firstTags = await client.find({ + type: CASE_SAVED_OBJECT, + fields: ['tags'], + page: 1, + perPage, + }); + const tags = await client.find({ + type: CASE_SAVED_OBJECT, + fields: ['tags'], + page: 1, + perPage: firstTags.total, + }); + + return Array.from(convertTagsToSet(tags.saved_objects)); +}; diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index 0128cd3dd6df7..0dc3fc29ca805 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -118,4 +118,4 @@ export interface EndpointMetadata { /** * The PageId type is used for the payload when firing userNavigatedToPage actions */ -export type PageId = 'alertsPage' | 'endpointListPage'; +export type PageId = 'alertsPage' | 'managementPage'; diff --git a/x-pack/plugins/endpoint/package.json b/x-pack/plugins/endpoint/package.json index 8efd0eab0eee0..25afe2c8442ba 100644 --- a/x-pack/plugins/endpoint/package.json +++ b/x-pack/plugins/endpoint/package.json @@ -9,6 +9,7 @@ "react-redux": "^7.1.0" }, "devDependencies": { - "@types/react-redux": "^7.1.0" + "@types/react-redux": "^7.1.0", + "redux-devtools-extension": "^2.13.8" } } diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx index 9bea41126d296..a86c647e771d4 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx @@ -13,6 +13,7 @@ import { Provider } from 'react-redux'; import { Store } from 'redux'; import { appStoreFactory } from './store'; import { AlertIndex } from './view/alerts'; +import { ManagementList } from './view/managing'; /** * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle. @@ -20,13 +21,12 @@ import { AlertIndex } from './view/alerts'; export function renderApp(coreStart: CoreStart, { appBasePath, element }: AppMountParameters) { coreStart.http.get('/api/endpoint/hello-world'); - const [store, stopSagas] = appStoreFactory(coreStart); + const store = appStoreFactory(coreStart); ReactDOM.render(<AppRoot basename={appBasePath} store={store} />, element); return () => { ReactDOM.unmountComponentAtNode(element); - stopSagas(); }; } @@ -49,22 +49,7 @@ const AppRoot: React.FunctionComponent<RouterProps> = React.memo(({ basename, st </h1> )} /> - <Route - path="/management" - render={() => { - // FIXME: This is temporary. Will be removed in next PR for endpoint list - store.dispatch({ type: 'userEnteredEndpointListPage' }); - - return ( - <h1 data-test-subj="endpointManagement"> - <FormattedMessage - id="xpack.endpoint.endpointManagement" - defaultMessage="Manage Endpoints" - /> - </h1> - ); - }} - /> + <Route path="/management" component={ManagementList} /> <Route path="/alerts" component={AlertIndex} /> <Route render={() => ( diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/action.ts index 593041af75c05..04c6cf7fc4634 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/action.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/action.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EndpointListAction } from './endpoint_list'; +import { ManagementAction } from './managing'; import { AlertAction } from './alerts'; import { RoutingAction } from './routing'; -export type AppAction = EndpointListAction | AlertAction | RoutingAction; +export type AppAction = ManagementAction | AlertAction | RoutingAction; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts index 11bac195653c6..4a7fac147852b 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import qs from 'querystring'; +import { parse } from 'query-string'; import { HttpFetchQuery } from 'src/core/public'; import { AppAction } from '../action'; import { MiddlewareFactory, AlertListData } from '../../types'; export const alertMiddlewareFactory: MiddlewareFactory = coreStart => { - const qp = qs.parse(window.location.search.slice(1)); + const qp = parse(window.location.search.slice(1), { sort: false }); return api => next => async (action: AppAction) => { next(action); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/action.ts deleted file mode 100644 index 02ec0f9d09035..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/action.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EndpointListData } from './types'; - -interface ServerReturnedEndpointList { - type: 'serverReturnedEndpointList'; - payload: EndpointListData; -} - -interface UserEnteredEndpointListPage { - type: 'userEnteredEndpointListPage'; -} - -interface UserExitedEndpointListPage { - type: 'userExitedEndpointListPage'; -} - -export type EndpointListAction = - | ServerReturnedEndpointList - | UserEnteredEndpointListPage - | UserExitedEndpointListPage; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.ts deleted file mode 100644 index bdf0708457bb0..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { endpointListReducer } from './reducer'; -export { EndpointListAction } from './action'; -export { endpointListSaga } from './saga'; -export * from './types'; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/reducer.ts deleted file mode 100644 index e57d9683e4707..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/reducer.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Reducer } from 'redux'; -import { EndpointListState } from './types'; -import { AppAction } from '../action'; - -const initialState = (): EndpointListState => { - return { - endpoints: [], - request_page_size: 10, - request_index: 0, - total: 0, - }; -}; - -export const endpointListReducer: Reducer<EndpointListState, AppAction> = ( - state = initialState(), - action -) => { - if (action.type === 'serverReturnedEndpointList') { - return { - ...state, - ...action.payload, - }; - } - - if (action.type === 'userExitedEndpointListPage') { - return initialState(); - } - - return state; -}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.test.ts deleted file mode 100644 index 6bf946873e179..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { CoreStart, HttpSetup } from 'kibana/public'; -import { applyMiddleware, createStore, Dispatch, Store } from 'redux'; -import { createSagaMiddleware, SagaContext } from '../../lib'; -import { endpointListSaga } from './saga'; -import { coreMock } from '../../../../../../../../src/core/public/mocks'; -import { - EndpointData, - EndpointListAction, - EndpointListData, - endpointListReducer, - EndpointListState, -} from './index'; -import { endpointListData } from './selectors'; - -describe('endpoint list saga', () => { - const sleep = (ms = 100) => new Promise(wakeup => setTimeout(wakeup, ms)); - let fakeCoreStart: jest.Mocked<CoreStart>; - let fakeHttpServices: jest.Mocked<HttpSetup>; - let store: Store<EndpointListState>; - let dispatch: Dispatch<EndpointListAction>; - let stopSagas: () => void; - - // TODO: consolidate the below ++ helpers in `index.test.ts` into a `test_helpers.ts`?? - const generateEndpoint = (): EndpointData => { - return { - machine_id: Math.random() - .toString(16) - .substr(2), - created_at: new Date(), - host: { - name: '', - hostname: '', - ip: '', - mac_address: '', - os: { - name: '', - full: '', - }, - }, - endpoint: { - domain: '', - is_base_image: true, - active_directory_distinguished_name: '', - active_directory_hostname: '', - upgrade: { - status: '', - updated_at: new Date(), - }, - isolation: { - status: false, - request_status: true, - updated_at: new Date(), - }, - policy: { - name: '', - id: '', - }, - sensor: { - persistence: true, - status: {}, - }, - }, - }; - }; - const getEndpointListApiResponse = (): EndpointListData => { - return { - endpoints: [generateEndpoint()], - request_page_size: 1, - request_index: 1, - total: 10, - }; - }; - - const endpointListSagaFactory = () => { - return async (sagaContext: SagaContext) => { - await endpointListSaga(sagaContext, fakeCoreStart).catch((e: Error) => { - // eslint-disable-next-line no-console - console.error(e); - return Promise.reject(e); - }); - }; - }; - - beforeEach(() => { - fakeCoreStart = coreMock.createStart({ basePath: '/mock' }); - fakeHttpServices = fakeCoreStart.http as jest.Mocked<HttpSetup>; - - const sagaMiddleware = createSagaMiddleware(endpointListSagaFactory()); - store = createStore(endpointListReducer, applyMiddleware(sagaMiddleware)); - - sagaMiddleware.start(); - stopSagas = sagaMiddleware.stop; - dispatch = store.dispatch; - }); - - afterEach(() => { - stopSagas(); - }); - - test('it handles `userEnteredEndpointListPage`', async () => { - const apiResponse = getEndpointListApiResponse(); - - fakeHttpServices.post.mockResolvedValue(apiResponse); - expect(fakeHttpServices.post).not.toHaveBeenCalled(); - - dispatch({ type: 'userEnteredEndpointListPage' }); - await sleep(); - - expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/endpoints'); - expect(endpointListData(store.getState())).toEqual(apiResponse.endpoints); - }); -}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.ts deleted file mode 100644 index cc156cfa88002..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/saga.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { CoreStart } from 'kibana/public'; -import { SagaContext } from '../../lib'; -import { EndpointListAction } from './action'; - -export const endpointListSaga = async ( - { actionsAndState, dispatch }: SagaContext<EndpointListAction>, - coreStart: CoreStart -) => { - const { post: httpPost } = coreStart.http; - - for await (const { action } of actionsAndState()) { - if (action.type === 'userEnteredEndpointListPage') { - const response = await httpPost('/api/endpoint/endpoints'); - dispatch({ - type: 'serverReturnedEndpointList', - payload: response, - }); - } - } -}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/types.ts deleted file mode 100644 index f2810dd89f857..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/types.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -// FIXME: temporary until server defined `interface` is moved -export interface EndpointData { - machine_id: string; - created_at: Date; - host: { - name: string; - hostname: string; - ip: string; - mac_address: string; - os: { - name: string; - full: string; - }; - }; - endpoint: { - domain: string; - is_base_image: boolean; - active_directory_distinguished_name: string; - active_directory_hostname: string; - upgrade: { - status?: string; - updated_at?: Date; - }; - isolation: { - status: boolean; - request_status?: string | boolean; - updated_at?: Date; - }; - policy: { - name: string; - id: string; - }; - sensor: { - persistence: boolean; - status: object; - }; - }; -} - -// FIXME: temporary until server defined `interface` is moved to a module we can reference -export interface EndpointListData { - endpoints: EndpointData[]; - request_page_size: number; - request_index: number; - total: number; -} - -export type EndpointListState = EndpointListData; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts index a32f310392ca9..3bbcc3f25a6d8 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/index.ts @@ -4,25 +4,62 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createStore, compose, applyMiddleware, Store } from 'redux'; +import { + createStore, + compose, + applyMiddleware, + Store, + MiddlewareAPI, + Dispatch, + Middleware, +} from 'redux'; import { CoreStart } from 'kibana/public'; -import { appSagaFactory } from './saga'; import { appReducer } from './reducer'; import { alertMiddlewareFactory } from './alerts/middleware'; +import { managementMiddlewareFactory } from './managing'; +import { GlobalState } from '../types'; +import { AppAction } from './action'; const composeWithReduxDevTools = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ name: 'EndpointApp' }) : compose; -export const appStoreFactory = (coreStart: CoreStart): [Store, () => void] => { - const sagaReduxMiddleware = appSagaFactory(coreStart); +export type Selector<S, R> = (state: S) => R; + +/** + * Wrap Redux Middleware and adjust 'getState()' to return the namespace from 'GlobalState that applies to the given Middleware concern. + * + * @param selector + * @param middleware + */ +export const substateMiddlewareFactory = <Substate>( + selector: Selector<GlobalState, Substate>, + middleware: Middleware<{}, Substate, Dispatch<AppAction>> +): Middleware<{}, GlobalState, Dispatch<AppAction>> => { + return api => { + const substateAPI: MiddlewareAPI<Dispatch<AppAction>, Substate> = { + ...api, + getState() { + return selector(api.getState()); + }, + }; + return middleware(substateAPI); + }; +}; + +export const appStoreFactory = (coreStart: CoreStart): Store => { const store = createStore( appReducer, composeWithReduxDevTools( - applyMiddleware(alertMiddlewareFactory(coreStart), appSagaFactory(coreStart)) + applyMiddleware( + alertMiddlewareFactory(coreStart), + substateMiddlewareFactory( + globalState => globalState.managementList, + managementMiddlewareFactory(coreStart) + ) + ) ) ); - sagaReduxMiddleware.start(); - return [store, sagaReduxMiddleware.stop]; + return store; }; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/action.ts new file mode 100644 index 0000000000000..e916dc66c59f0 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/action.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ManagementListPagination } from '../../types'; +import { EndpointResultList } from '../../../../../common/types'; + +interface ServerReturnedManagementList { + type: 'serverReturnedManagementList'; + payload: EndpointResultList; +} + +interface UserExitedManagementList { + type: 'userExitedManagementList'; +} + +interface UserPaginatedManagementList { + type: 'userPaginatedManagementList'; + payload: ManagementListPagination; +} + +export type ManagementAction = + | ServerReturnedManagementList + | UserExitedManagementList + | UserPaginatedManagementList; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.test.ts similarity index 51% rename from x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.test.ts rename to x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.test.ts index a46653f82ee45..dde0ba1e96a8a 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/endpoint_list/index.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.test.ts @@ -5,64 +5,52 @@ */ import { createStore, Dispatch, Store } from 'redux'; -import { EndpointListAction, EndpointData, endpointListReducer, EndpointListState } from './index'; -import { endpointListData } from './selectors'; +import { ManagementAction, managementListReducer } from './index'; +import { EndpointMetadata } from '../../../../../common/types'; +import { ManagementListState } from '../../types'; +import { listData } from './selectors'; describe('endpoint_list store concerns', () => { - let store: Store<EndpointListState>; - let dispatch: Dispatch<EndpointListAction>; + let store: Store<ManagementListState>; + let dispatch: Dispatch<ManagementAction>; const createTestStore = () => { - store = createStore(endpointListReducer); + store = createStore(managementListReducer); dispatch = store.dispatch; }; - const generateEndpoint = (): EndpointData => { + const generateEndpoint = (): EndpointMetadata => { return { - machine_id: Math.random() - .toString(16) - .substr(2), - created_at: new Date(), - host: { - name: '', - hostname: '', - ip: '', - mac_address: '', - os: { - name: '', - full: '', - }, + event: { + created: new Date(0), }, endpoint: { - domain: '', - is_base_image: true, - active_directory_distinguished_name: '', - active_directory_hostname: '', - upgrade: { - status: '', - updated_at: new Date(), - }, - isolation: { - status: false, - request_status: true, - updated_at: new Date(), - }, policy: { - name: '', id: '', }, - sensor: { - persistence: true, - status: {}, + }, + agent: { + version: '', + id: '', + }, + host: { + id: '', + hostname: '', + ip: [''], + mac: [''], + os: { + name: '', + full: '', + version: '', }, }, }; }; const loadDataToStore = () => { dispatch({ - type: 'serverReturnedEndpointList', + type: 'serverReturnedManagementList', payload: { endpoints: [generateEndpoint()], request_page_size: 1, - request_index: 1, + request_page_index: 1, total: 10, }, }); @@ -76,39 +64,40 @@ describe('endpoint_list store concerns', () => { test('it creates default state', () => { expect(store.getState()).toEqual({ endpoints: [], - request_page_size: 10, - request_index: 0, + pageSize: 10, + pageIndex: 0, total: 0, + loading: false, }); }); - test('it handles `serverReturnedEndpointList', () => { + test('it handles `serverReturnedManagementList', () => { const payload = { endpoints: [generateEndpoint()], request_page_size: 1, - request_index: 1, + request_page_index: 1, total: 10, }; dispatch({ - type: 'serverReturnedEndpointList', + type: 'serverReturnedManagementList', payload, }); const currentState = store.getState(); expect(currentState.endpoints).toEqual(payload.endpoints); - expect(currentState.request_page_size).toEqual(payload.request_page_size); - expect(currentState.request_index).toEqual(payload.request_index); + expect(currentState.pageSize).toEqual(payload.request_page_size); + expect(currentState.pageIndex).toEqual(payload.request_page_index); expect(currentState.total).toEqual(payload.total); }); - test('it handles `userExitedEndpointListPage`', () => { + test('it handles `userExitedManagementListPage`', () => { loadDataToStore(); expect(store.getState().total).toEqual(10); - dispatch({ type: 'userExitedEndpointListPage' }); + dispatch({ type: 'userExitedManagementList' }); expect(store.getState().endpoints.length).toEqual(0); - expect(store.getState().request_index).toEqual(0); + expect(store.getState().pageIndex).toEqual(0); }); }); @@ -118,9 +107,9 @@ describe('endpoint_list store concerns', () => { loadDataToStore(); }); - test('it selects `endpointListData`', () => { + test('it selects `managementListData`', () => { const currentState = store.getState(); - expect(endpointListData(currentState)).toEqual(currentState.endpoints); + expect(listData(currentState)).toEqual(currentState.endpoints); }); }); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.ts new file mode 100644 index 0000000000000..f0bfe27c9e30f --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { managementListReducer } from './reducer'; +export { ManagementAction } from './action'; +export { managementMiddlewareFactory } from './middleware'; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts new file mode 100644 index 0000000000000..095e49a6c4306 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { CoreStart, HttpSetup } from 'kibana/public'; +import { applyMiddleware, createStore, Dispatch, Store } from 'redux'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { managementListReducer, managementMiddlewareFactory } from './index'; +import { EndpointMetadata, EndpointResultList } from '../../../../../common/types'; +import { ManagementListState } from '../../types'; +import { AppAction } from '../action'; +import { listData } from './selectors'; +describe('endpoint list saga', () => { + const sleep = (ms = 100) => new Promise(wakeup => setTimeout(wakeup, ms)); + let fakeCoreStart: jest.Mocked<CoreStart>; + let fakeHttpServices: jest.Mocked<HttpSetup>; + let store: Store<ManagementListState>; + let getState: typeof store['getState']; + let dispatch: Dispatch<AppAction>; + // https://github.com/elastic/endpoint-app-team/issues/131 + const generateEndpoint = (): EndpointMetadata => { + return { + event: { + created: new Date(0), + }, + endpoint: { + policy: { + id: '', + }, + }, + agent: { + version: '', + id: '', + }, + host: { + id: '', + hostname: '', + ip: [''], + mac: [''], + os: { + name: '', + full: '', + version: '', + }, + }, + }; + }; + const getEndpointListApiResponse = (): EndpointResultList => { + return { + endpoints: [generateEndpoint()], + request_page_size: 1, + request_page_index: 1, + total: 10, + }; + }; + beforeEach(() => { + fakeCoreStart = coreMock.createStart({ basePath: '/mock' }); + fakeHttpServices = fakeCoreStart.http as jest.Mocked<HttpSetup>; + store = createStore( + managementListReducer, + applyMiddleware(managementMiddlewareFactory(fakeCoreStart)) + ); + getState = store.getState; + dispatch = store.dispatch; + }); + test('it handles `userNavigatedToPage`', async () => { + const apiResponse = getEndpointListApiResponse(); + fakeHttpServices.post.mockResolvedValue(apiResponse); + expect(fakeHttpServices.post).not.toHaveBeenCalled(); + dispatch({ type: 'userNavigatedToPage', payload: 'managementPage' }); + await sleep(); + expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/endpoints', { + body: JSON.stringify({ + paging_properties: [{ page_index: 0 }, { page_size: 10 }], + }), + }); + expect(listData(getState())).toEqual(apiResponse.endpoints); + }); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.ts new file mode 100644 index 0000000000000..ae756caf5aa35 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MiddlewareFactory } from '../../types'; +import { pageIndex, pageSize } from './selectors'; +import { ManagementListState } from '../../types'; +import { AppAction } from '../action'; + +export const managementMiddlewareFactory: MiddlewareFactory<ManagementListState> = coreStart => { + return ({ getState, dispatch }) => next => async (action: AppAction) => { + next(action); + if ( + (action.type === 'userNavigatedToPage' && action.payload === 'managementPage') || + action.type === 'userPaginatedManagementList' + ) { + const managementPageIndex = pageIndex(getState()); + const managementPageSize = pageSize(getState()); + const response = await coreStart.http.post('/api/endpoint/endpoints', { + body: JSON.stringify({ + paging_properties: [ + { page_index: managementPageIndex }, + { page_size: managementPageSize }, + ], + }), + }); + response.request_page_index = managementPageIndex; + dispatch({ + type: 'serverReturnedManagementList', + payload: response, + }); + } + }; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/reducer.ts new file mode 100644 index 0000000000000..bbbbdc4d17ce6 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/reducer.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Reducer } from 'redux'; +import { ManagementListState } from '../../types'; +import { AppAction } from '../action'; + +const initialState = (): ManagementListState => { + return { + endpoints: [], + pageSize: 10, + pageIndex: 0, + total: 0, + loading: false, + }; +}; + +export const managementListReducer: Reducer<ManagementListState, AppAction> = ( + state = initialState(), + action +) => { + if (action.type === 'serverReturnedManagementList') { + const { + endpoints, + total, + request_page_size: pageSize, + request_page_index: pageIndex, + } = action.payload; + return { + ...state, + endpoints, + total, + pageSize, + pageIndex, + loading: false, + }; + } + + if (action.type === 'userExitedManagementList') { + return initialState(); + } + + if (action.type === 'userPaginatedManagementList') { + return { + ...state, + ...action.payload, + loading: true, + }; + } + + return state; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/selectors.ts new file mode 100644 index 0000000000000..3dcb144c2bade --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/selectors.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ManagementListState } from '../../types'; + +export const listData = (state: ManagementListState) => state.endpoints; + +export const pageIndex = (state: ManagementListState) => state.pageIndex; + +export const pageSize = (state: ManagementListState) => state.pageSize; + +export const totalHits = (state: ManagementListState) => state.total; + +export const isLoading = (state: ManagementListState) => state.loading; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts index a9cf6d9980519..7d738c266fae0 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/reducer.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import { combineReducers, Reducer } from 'redux'; -import { endpointListReducer } from './endpoint_list'; +import { managementListReducer } from './managing'; import { AppAction } from './action'; import { alertListReducer } from './alerts'; import { GlobalState } from '../types'; export const appReducer: Reducer<GlobalState, AppAction> = combineReducers({ - endpointList: endpointListReducer, + managementList: managementListReducer, alertList: alertListReducer, }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/saga.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/saga.ts deleted file mode 100644 index 3b7de79d5443c..0000000000000 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/saga.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { CoreStart } from 'kibana/public'; -import { createSagaMiddleware, SagaContext } from '../lib'; -import { endpointListSaga } from './endpoint_list'; - -export const appSagaFactory = (coreStart: CoreStart) => { - return createSagaMiddleware(async (sagaContext: SagaContext) => { - await Promise.all([ - // Concerns specific sagas here - endpointListSaga(sagaContext, coreStart), - ]); - }); -}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts index 5f02d36308053..02a7793fc38b0 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts @@ -6,20 +6,42 @@ import { Dispatch, MiddlewareAPI } from 'redux'; import { CoreStart } from 'kibana/public'; -import { EndpointListState } from './store/endpoint_list'; +import { EndpointMetadata } from '../../../common/types'; import { AppAction } from './store/action'; import { AlertResultList } from '../../../common/types'; -export type MiddlewareFactory = ( +export type MiddlewareFactory<S = GlobalState> = ( coreStart: CoreStart ) => ( - api: MiddlewareAPI<Dispatch<AppAction>, GlobalState> + api: MiddlewareAPI<Dispatch<AppAction>, S> ) => (next: Dispatch<AppAction>) => (action: AppAction) => unknown; +export interface ManagementListState { + endpoints: EndpointMetadata[]; + total: number; + pageSize: number; + pageIndex: number; + loading: boolean; +} + +export interface ManagementListPagination { + pageIndex: number; + pageSize: number; +} + export interface GlobalState { - readonly endpointList: EndpointListState; + readonly managementList: ManagementListState; readonly alertList: AlertListState; } export type AlertListData = AlertResultList; export type AlertListState = AlertResultList; +export type CreateStructuredSelector = < + SelectorMap extends { [key: string]: (...args: never[]) => unknown } +>( + selectorMap: SelectorMap +) => ( + state: SelectorMap[keyof SelectorMap] extends (state: infer State) => unknown ? State : never +) => { + [Key in keyof SelectorMap]: ReturnType<SelectorMap[Key]>; +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/hooks.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/hooks.ts new file mode 100644 index 0000000000000..a0720fbd8aeeb --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/hooks.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useSelector } from 'react-redux'; +import { GlobalState, ManagementListState } from '../../types'; + +export function useManagementListSelector<TSelected>( + selector: (state: ManagementListState) => TSelected +) { + return useSelector(function(state: GlobalState) { + return selector(state.managementList); + }); +} diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/index.tsx new file mode 100644 index 0000000000000..44b08f25c7653 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/managing/index.tsx @@ -0,0 +1,167 @@ +/* + * 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, { useMemo, useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiTitle, + EuiBasicTable, + EuiTextColor, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { createStructuredSelector } from 'reselect'; +import * as selectors from '../../store/managing/selectors'; +import { ManagementAction } from '../../store/managing/action'; +import { useManagementListSelector } from './hooks'; +import { usePageId } from '../use_page_id'; +import { CreateStructuredSelector } from '../../types'; + +const selector = (createStructuredSelector as CreateStructuredSelector)(selectors); +export const ManagementList = () => { + usePageId('managementPage'); + const dispatch = useDispatch<(a: ManagementAction) => void>(); + const { + listData, + pageIndex, + pageSize, + totalHits: totalItemCount, + isLoading, + } = useManagementListSelector(selector); + + const paginationSetup = useMemo(() => { + return { + pageIndex, + pageSize, + totalItemCount, + pageSizeOptions: [10, 20, 50], + hidePerPageOptions: false, + }; + }, [pageIndex, pageSize, totalItemCount]); + + const onTableChange = useCallback( + ({ page }: { page: { index: number; size: number } }) => { + const { index, size } = page; + dispatch({ + type: 'userPaginatedManagementList', + payload: { pageIndex: index, pageSize: size }, + }); + }, + [dispatch] + ); + + const columns = [ + { + field: 'host.hostname', + name: i18n.translate('xpack.endpoint.management.list.host', { + defaultMessage: 'Hostname', + }), + }, + { + field: '', + name: i18n.translate('xpack.endpoint.management.list.policy', { + defaultMessage: 'Policy', + }), + render: () => { + return 'Policy Name'; + }, + }, + { + field: '', + name: i18n.translate('xpack.endpoint.management.list.policyStatus', { + defaultMessage: 'Policy Status', + }), + render: () => { + return 'Policy Status'; + }, + }, + { + field: '', + name: i18n.translate('xpack.endpoint.management.list.alerts', { + defaultMessage: 'Alerts', + }), + render: () => { + return '0'; + }, + }, + { + field: 'host.os.name', + name: i18n.translate('xpack.endpoint.management.list.os', { + defaultMessage: 'Operating System', + }), + }, + { + field: 'host.ip', + name: i18n.translate('xpack.endpoint.management.list.ip', { + defaultMessage: 'IP Address', + }), + }, + { + field: '', + name: i18n.translate('xpack.endpoint.management.list.sensorVersion', { + defaultMessage: 'Sensor Version', + }), + render: () => { + return 'version'; + }, + }, + { + field: '', + name: i18n.translate('xpack.endpoint.management.list.lastActive', { + defaultMessage: 'Last Active', + }), + render: () => { + return 'xxxx'; + }, + }, + ]; + + return ( + <EuiPage> + <EuiPageBody> + <EuiPageContent> + <EuiPageContentHeader> + <EuiPageContentHeaderSection> + <EuiTitle> + <h2 data-test-subj="managementViewTitle"> + <FormattedMessage + id="xpack.endpoint.managementList.hosts" + defaultMessage="Hosts" + /> + </h2> + </EuiTitle> + <h4> + <EuiTextColor color="subdued"> + <FormattedMessage + id="xpack.endpoint.managementList.totalCount" + defaultMessage="{totalItemCount} Hosts" + values={{ totalItemCount }} + /> + </EuiTextColor> + </h4> + </EuiPageContentHeaderSection> + </EuiPageContentHeader> + <EuiPageContentBody> + <EuiBasicTable + data-test-subj="managementListTable" + items={listData} + columns={columns} + loading={isLoading} + pagination={paginationSetup} + onChange={onTableChange} + /> + </EuiPageContentBody> + </EuiPageContent> + </EuiPageBody> + </EuiPage> + ); +}; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts deleted file mode 100644 index c7f790588a739..0000000000000 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/actions.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { CameraAction } from './store/camera'; -import { DataAction } from './store/data'; - -export type ResolverAction = CameraAction | DataAction; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/documentation/camera.md b/x-pack/plugins/endpoint/public/embeddables/resolver/documentation/camera.md new file mode 100644 index 0000000000000..aeca76fad916f --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/documentation/camera.md @@ -0,0 +1,26 @@ +# Introduction + +Resolver renders a map in a DOM element. Items on the map are placed in 2 dimensions using arbitrary units. Like other mapping software, the map can show things at different scales. The 'camera' determines what is shown on the map. + +The camera is positioned. When the user clicks-and-drags the map, the camera's position is changed. This allows the user to pan around the map and see things that would otherwise be out of view, at a given scale. + +The camera determines the scale. If the scale is smaller, the viewport of the map is larger and more is visible. This allows the user to zoom in an out. On screen controls and gestures (trackpad-pinch, or CTRL-mousewheel) change the scale. + +# Concepts + +## Scaling +The camera scale is controlled both by the user and programatically by Resolver. There is a maximum and minimum scale value (at the time of this writing they are 0.5 and 6.) This means that the map, and things on the map, will be rendered at between 0.5 and 6 times their instrinsic dimensions. + +A range control is provided so that the user can change the scale. The user can also pinch-to-zoom on Mac OS X (or use ctrl-mousewheel otherwise) to change the scale. These interactions change the `scalingFactor`. This number is between 0 and 1. It represents how zoomed-in things should be. When the `scalingFactor` is 1, the scale will be the maximum scale value. When `scalingFactor` is 0, the scale will be the minimum scale value. Otherwise we interpolate between the minimum and maximum scale factor. The rate that the scale increases between the two is controlled by `scalingFactor**zoomCurveRate` The zoom curve rate is 4 at the time of this writing. This makes it so that the change in scale is more pronounced when the user is zoomed in. + +``` +renderScale = minimumScale * (1 - scalingFactor**curveRate) + maximumScale * scalingFactor**curveRate; +``` + +## Panning +When the user clicks and drags the map, the camera is 'moved' around. This allows the user to see different things on the map. The on-screen controls provide 4 directional buttons which nudge the camera, as well as a reset button. The reset button brings the camera back where it started (0, 0). + +Resolver may programatically change the position of the camera in order to bring some interesting elements into view. + +## Animation +The camera can animate changes to its position. Animations usually have a short, fixed duration, such as 1 second. If the camera is moving a great deal during the animation, then things could end up moving across the screen too quickly. In this case, looking at Resolver might be disorienting. In order to combat this, Resolver may temporarily decrease the scale. By decreasing the scale, objects look futher away. Far away objects appear to move slower. diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx index 9539162f9cfb6..6680ba615e353 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx @@ -6,7 +6,8 @@ import ReactDOM from 'react-dom'; import React from 'react'; -import { AppRoot } from './view'; +import { Provider } from 'react-redux'; +import { Resolver } from './view'; import { storeFactory } from './store'; import { Embeddable } from '../../../../../../src/plugins/embeddable/public'; @@ -20,7 +21,12 @@ export class ResolverEmbeddable extends Embeddable { } this.lastRenderTarget = node; const { store } = storeFactory(); - ReactDOM.render(<AppRoot store={store} />, node); + ReactDOM.render( + <Provider store={store}> + <Resolver /> + </Provider>, + node + ); } public reload(): void { diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/math.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/math.ts index c59db31c39e82..6bf0fedc84dfe 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/math.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/math.ts @@ -10,3 +10,10 @@ export function clamp(value: number, minimum: number, maximum: number) { return Math.max(Math.min(value, maximum), minimum); } + +/** + * linearly interpolate between `a` and `b` at a ratio of `ratio`. If `ratio` is `0`, return `a`, if ratio is `1`, return `b`. + */ +export function lerp(a: number, b: number, ratio: number): number { + return a * (1 - ratio) + b * ratio; +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts index 3084ce0eacdb4..bd7d1bf743df8 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/transformation.ts @@ -15,11 +15,32 @@ export function inverseOrthographicProjection( bottom: number, left: number ): Matrix3 { - const m11 = (right - left) / 2; - const m13 = (right + left) / (right - left); + let m11: number; + let m13: number; + let m22: number; + let m23: number; - const m22 = (top - bottom) / 2; - const m23 = (top + bottom) / (top - bottom); + /** + * If `right - left` is 0, the width is 0, so scale everything to 0 + */ + if (right - left === 0) { + m11 = 0; + m13 = 0; + } else { + m11 = (right - left) / 2; + m13 = (right + left) / (right - left); + } + + /** + * If `top - bottom` is 0, the height is 0, so scale everything to 0 + */ + if (top - bottom === 0) { + m22 = 0; + m23 = 0; + } else { + m22 = (top - bottom) / 2; + m23 = (top + bottom) / (top - bottom); + } return [m11, 0, m13, 0, m22, m23, 0, 0, 0]; } @@ -37,11 +58,32 @@ export function orthographicProjection( bottom: number, left: number ): Matrix3 { - const m11 = 2 / (right - left); // adjust x scale to match ndc (-1, 1) bounds - const m13 = -((right + left) / (right - left)); + let m11: number; + let m13: number; + let m22: number; + let m23: number; + + /** + * If `right - left` is 0, the width is 0, so scale everything to 0 + */ + if (right - left === 0) { + m11 = 0; + m13 = 0; + } else { + m11 = 2 / (right - left); // adjust x scale to match ndc (-1, 1) bounds + m13 = -((right + left) / (right - left)); + } - const m22 = 2 / (top - bottom); // adjust y scale to match ndc (-1, 1) bounds - const m23 = -((top + bottom) / (top - bottom)); + /** + * If `top - bottom` is 0, the height is 0, so scale everything to 0 + */ + if (top - bottom === 0) { + m22 = 0; + m23 = 0; + } else { + m22 = top - bottom === 0 ? 0 : 2 / (top - bottom); // adjust y scale to match ndc (-1, 1) bounds + m23 = top - bottom === 0 ? 0 : -((top + bottom) / (top - bottom)); + } return [m11, 0, m13, 0, m22, m23, 0, 0, 0]; } @@ -68,6 +110,6 @@ export function translationTransformation([x, y]: Vector2): Matrix3 { return [ 1, 0, x, 0, 1, y, - 0, 0, 1 + 0, 0, 0 ] } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts index 3c0681413305e..898ce6f6bacd2 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/lib/vector2.ts @@ -26,6 +26,13 @@ export function divide(a: Vector2, b: Vector2): Vector2 { return [a[0] / b[0], a[1] / b[1]]; } +/** + * Return `[ a[0] * b[0], a[1] * b[1] ]` + */ +export function multiply(a: Vector2, b: Vector2): Vector2 { + return [a[0] * b[0], a[1] * b[1]]; +} + /** * Returns a vector which is the result of applying a 2D transformation matrix to the provided vector. */ @@ -50,3 +57,33 @@ export function angle(a: Vector2, b: Vector2) { const deltaY = b[1] - a[1]; return Math.atan2(deltaY, deltaX); } + +/** + * Clamp `vector`'s components. + */ +export function clamp([x, y]: Vector2, [minX, minY]: Vector2, [maxX, maxY]: Vector2): Vector2 { + return [Math.max(minX, Math.min(maxX, x)), Math.max(minY, Math.min(maxY, y))]; +} + +/** + * Scale vector by number + */ +export function scale(a: Vector2, n: number): Vector2 { + return [a[0] * n, a[1] * n]; +} + +/** + * Linearly interpolate between `a` and `b`. + * `t` represents progress and: + * 0 <= `t` <= 1 + */ +export function lerp(a: Vector2, b: Vector2, t: number): Vector2 { + return add(scale(a, 1 - t), scale(b, t)); +} + +/** + * The length of the vector + */ +export function length([x, y]: Vector2): number { + return Math.sqrt(x * x + y * y); +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts index 67acdbd253f65..9a6f19adcc101 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event_test_helpers.ts @@ -25,6 +25,7 @@ export function mockProcessEvent( machine_id: '', ...parts, data_buffer: { + timestamp_utc: '2019-09-24 01:47:47Z', event_subtype_full: 'creation_event', event_type_full: 'process_event', process_name: '', diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts new file mode 100644 index 0000000000000..25f196c76a290 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ProcessEvent } from '../types'; +import { CameraAction } from './camera'; +import { DataAction } from './data'; + +/** + * When the user wants to bring a process node front-and-center on the map. + */ +interface UserBroughtProcessIntoView { + readonly type: 'userBroughtProcessIntoView'; + readonly payload: { + /** + * Used to identify the process node that should be brought into view. + */ + readonly process: ProcessEvent; + /** + * The time (since epoch in milliseconds) when the action was dispatched. + */ + readonly time: number; + }; +} + +export type ResolverAction = CameraAction | DataAction | UserBroughtProcessIntoView; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/action.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/action.ts index 7d3e64ab34f23..dcc6c2c9c9411 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/action.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/action.ts @@ -4,7 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Vector2, PanDirection } from '../../types'; +import { Vector2 } from '../../types'; + +interface TimestampedPayload { + /** + * Time (since epoch in milliseconds) when this action was dispatched. + */ + readonly time: number; +} interface UserSetZoomLevel { readonly type: 'userSetZoomLevel'; @@ -24,11 +31,13 @@ interface UserClickedZoomIn { interface UserZoomed { readonly type: 'userZoomed'; - /** - * A value to zoom in by. Should be a fraction of `1`. For a `'wheel'` event when `event.deltaMode` is `'pixel'`, - * pass `event.deltaY / -renderHeight` where `renderHeight` is the height of the Resolver element in pixels. - */ - readonly payload: number; + readonly payload: { + /** + * A value to zoom in by. Should be a fraction of `1`. For a `'wheel'` event when `event.deltaMode` is `'pixel'`, + * pass `event.deltaY / -renderHeight` where `renderHeight` is the height of the Resolver element in pixels. + */ + readonly zoomChange: number; + } & TimestampedPayload; } interface UserSetRasterSize { @@ -40,7 +49,7 @@ interface UserSetRasterSize { } /** - * This is currently only used in tests. The 'back to center' button will use this action, and more tests around its behavior will need to be added. + * When the user warps the camera to an exact point instantly. */ interface UserSetPositionOfCamera { readonly type: 'userSetPositionOfCamera'; @@ -52,33 +61,45 @@ interface UserSetPositionOfCamera { interface UserStartedPanning { readonly type: 'userStartedPanning'; - /** - * A vector in screen coordinates (each unit is a pixel and the Y axis increases towards the bottom of the screen) - * relative to the Resolver component. - * Represents a starting position during panning for a pointing device. - */ - readonly payload: Vector2; + + readonly payload: { + /** + * A vector in screen coordinates (each unit is a pixel and the Y axis increases towards the bottom of the screen) + * relative to the Resolver component. + * Represents a starting position during panning for a pointing device. + */ + readonly screenCoordinates: Vector2; + } & TimestampedPayload; } interface UserStoppedPanning { readonly type: 'userStoppedPanning'; + + readonly payload: TimestampedPayload; } -interface UserClickedPanControl { - readonly type: 'userClickedPanControl'; +interface UserNudgedCamera { + readonly type: 'userNudgedCamera'; /** * String that represents the direction in which Resolver can be panned */ - readonly payload: PanDirection; + readonly payload: { + /** + * A cardinal direction to move the users perspective in. + */ + readonly direction: Vector2; + } & TimestampedPayload; } interface UserMovedPointer { readonly type: 'userMovedPointer'; - /** - * A vector in screen coordinates relative to the Resolver component. - * The payload should be contain clientX and clientY minus the client position of the Resolver component. - */ - readonly payload: Vector2; + readonly payload: { + /** + * A vector in screen coordinates relative to the Resolver component. + * The payload should be contain clientX and clientY minus the client position of the Resolver component. + */ + screenCoordinates: Vector2; + } & TimestampedPayload; } export type CameraAction = @@ -91,4 +112,4 @@ export type CameraAction = | UserMovedPointer | UserClickedZoomOut | UserClickedZoomIn - | UserClickedPanControl; + | UserNudgedCamera; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/animation.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/animation.test.ts new file mode 100644 index 0000000000000..795344d8af092 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/animation.test.ts @@ -0,0 +1,193 @@ +/* + * 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 { createStore, Store, Reducer } from 'redux'; +import { cameraReducer, cameraInitialState } from './reducer'; +import { CameraState, Vector2, ResolverAction } from '../../types'; +import * as selectors from './selectors'; +import { animatePanning } from './methods'; +import { lerp } from '../../lib/math'; + +type TestAction = + | ResolverAction + | { + readonly type: 'animatePanning'; + readonly payload: { + /** + * The start time of the animation. + */ + readonly time: number; + /** + * The duration of the animation. + */ + readonly duration: number; + /** + * The target translation the camera will animate towards. + */ + readonly targetTranslation: Vector2; + }; + }; + +describe('when the camera is created', () => { + let store: Store<CameraState, TestAction>; + beforeEach(() => { + const testReducer: Reducer<CameraState, TestAction> = ( + state = cameraInitialState(), + action + ): CameraState => { + // If the test action is fired, call the animatePanning method + if (action.type === 'animatePanning') { + const { + payload: { time, targetTranslation, duration }, + } = action; + return animatePanning(state, time, targetTranslation, duration); + } + return cameraReducer(state, action); + }; + store = createStore(testReducer); + }); + it('should be at 0,0', () => { + expect(selectors.translation(store.getState())(0)).toEqual([0, 0]); + }); + it('should have scale of [1,1]', () => { + expect(selectors.scale(store.getState())(0)).toEqual([1, 1]); + }); + describe('when animation begins', () => { + const duration = 1000; + let targetTranslation: Vector2; + const startTime = 0; + beforeEach(() => { + // The distance the camera moves must be nontrivial in order to trigger a scale animation + targetTranslation = [1000, 1000]; + const action: TestAction = { + type: 'animatePanning', + payload: { + time: startTime, + duration, + targetTranslation, + }, + }; + store.dispatch(action); + }); + describe('when the animation is in progress', () => { + let translationAtIntervals: Vector2[]; + let scaleAtIntervals: Vector2[]; + beforeEach(() => { + translationAtIntervals = []; + scaleAtIntervals = []; + const state = store.getState(); + for (let progress = 0; progress <= 1; progress += 0.1) { + translationAtIntervals.push( + selectors.translation(state)(lerp(startTime, startTime + duration, progress)) + ); + scaleAtIntervals.push( + selectors.scale(state)(lerp(startTime, startTime + duration, progress)) + ); + } + }); + it('should gradually translate to the target', () => { + expect(translationAtIntervals).toMatchInlineSnapshot(` + Array [ + Array [ + 0, + 0, + ], + Array [ + 4.000000000000001, + 4.000000000000001, + ], + Array [ + 32.00000000000001, + 32.00000000000001, + ], + Array [ + 108.00000000000004, + 108.00000000000004, + ], + Array [ + 256.00000000000006, + 256.00000000000006, + ], + Array [ + 500, + 500, + ], + Array [ + 744, + 744, + ], + Array [ + 891.9999999999999, + 891.9999999999999, + ], + Array [ + 968, + 968, + ], + Array [ + 996, + 996, + ], + Array [ + 1000, + 1000, + ], + ] + `); + }); + it('should gradually zoom in and out to the target', () => { + expect(scaleAtIntervals).toMatchInlineSnapshot(` + Array [ + Array [ + 1, + 1, + ], + Array [ + 0.9873589660765236, + 0.9873589660765236, + ], + Array [ + 0.8988717286121894, + 0.8988717286121894, + ], + Array [ + 0.7060959612791753, + 0.7060959612791753, + ], + Array [ + 0.6176087238148411, + 0.6176087238148411, + ], + Array [ + 0.6049676898913647, + 0.6049676898913647, + ], + Array [ + 0.6176087238148411, + 0.6176087238148411, + ], + Array [ + 0.7060959612791753, + 0.7060959612791753, + ], + Array [ + 0.8988717286121893, + 0.8988717286121893, + ], + Array [ + 0.9873589660765237, + 0.9873589660765237, + ], + Array [ + 1, + 1, + ], + ] + `); + }); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/inverse_projection_matrix.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/inverse_projection_matrix.test.ts index 41e3bc025f557..000dbb8d52841 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/inverse_projection_matrix.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/inverse_projection_matrix.test.ts @@ -18,14 +18,27 @@ describe('inverseProjectionMatrix', () => { beforeEach(() => { store = createStore(cameraReducer, undefined); compare = (rasterPosition: [number, number], expectedWorldPosition: [number, number]) => { + // time isn't really relevant as we aren't testing animation + const time = 0; const [worldX, worldY] = applyMatrix3( rasterPosition, - inverseProjectionMatrix(store.getState()) + inverseProjectionMatrix(store.getState())(time) ); expect(worldX).toBeCloseTo(expectedWorldPosition[0]); expect(worldY).toBeCloseTo(expectedWorldPosition[1]); }; }); + + describe('when the raster size is 0x0 pixels', () => { + beforeEach(() => { + const action: CameraAction = { type: 'userSetRasterSize', payload: [0, 0] }; + store.dispatch(action); + }); + it('should convert 0,0 in raster space to 0,0 (center) in world space', () => { + compare([10, 0], [0, 0]); + }); + }); + describe('when the raster size is 300 x 200 pixels', () => { beforeEach(() => { const action: CameraAction = { type: 'userSetRasterSize', payload: [300, 200] }; @@ -69,7 +82,7 @@ describe('inverseProjectionMatrix', () => { }); describe('when the user has panned to the right and up by 50', () => { beforeEach(() => { - const action: CameraAction = { type: 'userSetPositionOfCamera', payload: [-50, -50] }; + const action: CameraAction = { type: 'userSetPositionOfCamera', payload: [50, 50] }; store.dispatch(action); }); it('should convert 100,150 in raster space to 0,0 (center) in world space', () => { @@ -84,7 +97,7 @@ describe('inverseProjectionMatrix', () => { }); describe('when the user has panned to the right by 350 and up by 250', () => { beforeEach(() => { - const action: CameraAction = { type: 'userSetPositionOfCamera', payload: [-350, -250] }; + const action: CameraAction = { type: 'userSetPositionOfCamera', payload: [350, 250] }; store.dispatch(action); }); describe('when the user has scaled to 2', () => { diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/methods.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/methods.ts new file mode 100644 index 0000000000000..4afbacb819b1a --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/methods.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { translation } from './selectors'; +import { CameraState, Vector2 } from '../../types'; + +/** + * Return a new `CameraState` with the `animation` property + * set. The camera will animate to `targetTranslation` over `duration`. + */ +export function animatePanning( + state: CameraState, + startTime: number, + targetTranslation: Vector2, + duration: number +): CameraState { + const nextState: CameraState = { + ...state, + /** + * This cancels panning if any was taking place. + */ + panning: undefined, + translationNotCountingCurrentPanning: targetTranslation, + animation: { + startTime, + targetTranslation, + initialTranslation: translation(state)(startTime), + duration, + }, + }; + + return nextState; +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/panning.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/panning.test.ts index 17401a63b5ae8..9a9a5ea1c0cfc 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/panning.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/panning.test.ts @@ -13,11 +13,14 @@ import { translation } from './selectors'; describe('panning interaction', () => { let store: Store<CameraState, CameraAction>; let translationShouldBeCloseTo: (expectedTranslation: Vector2) => void; + let time: number; beforeEach(() => { + // The time isn't relevant as we don't use animations in this suite. + time = 0; store = createStore(cameraReducer, undefined); translationShouldBeCloseTo = expectedTranslation => { - const actualTranslation = translation(store.getState()); + const actualTranslation = translation(store.getState())(time); expect(expectedTranslation[0]).toBeCloseTo(actualTranslation[0]); expect(expectedTranslation[1]).toBeCloseTo(actualTranslation[1]); }; @@ -30,94 +33,64 @@ describe('panning interaction', () => { it('should have a translation of 0,0', () => { translationShouldBeCloseTo([0, 0]); }); - describe('when the user has started panning', () => { + describe('when the user has started panning at (100, 100)', () => { beforeEach(() => { - const action: CameraAction = { type: 'userStartedPanning', payload: [100, 100] }; + const action: CameraAction = { + type: 'userStartedPanning', + payload: { screenCoordinates: [100, 100], time }, + }; store.dispatch(action); }); it('should have a translation of 0,0', () => { translationShouldBeCloseTo([0, 0]); }); - describe('when the user continues to pan 50px up and to the right', () => { + describe('when the user moves their pointer 50px up and right (towards the top right of the screen)', () => { beforeEach(() => { - const action: CameraAction = { type: 'userMovedPointer', payload: [150, 50] }; + const action: CameraAction = { + type: 'userMovedPointer', + payload: { screenCoordinates: [150, 50], time }, + }; store.dispatch(action); }); - it('should have a translation of 50,50', () => { - translationShouldBeCloseTo([50, 50]); + it('should have a translation of [-50, -50] as the camera is now focused on things lower and to the left.', () => { + translationShouldBeCloseTo([-50, -50]); }); describe('when the user then stops panning', () => { beforeEach(() => { - const action: CameraAction = { type: 'userStoppedPanning' }; + const action: CameraAction = { + type: 'userStoppedPanning', + payload: { time }, + }; store.dispatch(action); }); - it('should have a translation of 50,50', () => { - translationShouldBeCloseTo([50, 50]); + it('should still have a translation of [-50, -50]', () => { + translationShouldBeCloseTo([-50, -50]); }); }); }); }); }); - describe('panning controls', () => { - describe('when user clicks on pan north button', () => { - beforeEach(() => { - const action: CameraAction = { type: 'userClickedPanControl', payload: 'north' }; - store.dispatch(action); - }); - it('moves the camera south so that objects appear closer to the bottom of the screen', () => { - const actual = translation(store.getState()); - expect(actual).toMatchInlineSnapshot(` - Array [ - 0, - -32.49906769231164, - ] - `); - }); - }); - describe('when user clicks on pan south button', () => { - beforeEach(() => { - const action: CameraAction = { type: 'userClickedPanControl', payload: 'south' }; - store.dispatch(action); - }); - it('moves the camera north so that objects appear closer to the top of the screen', () => { - const actual = translation(store.getState()); - expect(actual).toMatchInlineSnapshot(` - Array [ - 0, - 32.49906769231164, - ] - `); - }); - }); - describe('when user clicks on pan east button', () => { - beforeEach(() => { - const action: CameraAction = { type: 'userClickedPanControl', payload: 'east' }; - store.dispatch(action); - }); - it('moves the camera west so that objects appear closer to the left of the screen', () => { - const actual = translation(store.getState()); - expect(actual).toMatchInlineSnapshot(` - Array [ - -32.49906769231164, - 0, - ] - `); - }); + describe('when the user nudges the camera up', () => { + beforeEach(() => { + const action: CameraAction = { + type: 'userNudgedCamera', + payload: { direction: [0, 1], time }, + }; + store.dispatch(action); }); - describe('when user clicks on pan west button', () => { - beforeEach(() => { - const action: CameraAction = { type: 'userClickedPanControl', payload: 'west' }; - store.dispatch(action); - }); - it('moves the camera east so that objects appear closer to the right of the screen', () => { - const actual = translation(store.getState()); - expect(actual).toMatchInlineSnapshot(` - Array [ - 32.49906769231164, - 0, - ] - `); - }); + it('the camera eventually moves up so that objects appear closer to the bottom of the screen', () => { + const aBitIntoTheFuture = time + 100; + + /** + * Check the position once the animation has advanced 100ms + */ + const actual: Vector2 = translation(store.getState())(aBitIntoTheFuture); + expect(actual).toMatchInlineSnapshot(` + Array [ + 0, + 7.4074074074074066, + ] + `); }); }); }); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/projection_matrix.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/projection_matrix.test.ts index e21e3d1001794..e868424d06c94 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/projection_matrix.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/projection_matrix.test.ts @@ -18,11 +18,21 @@ describe('projectionMatrix', () => { beforeEach(() => { store = createStore(cameraReducer, undefined); compare = (worldPosition: [number, number], expectedRasterPosition: [number, number]) => { - const [rasterX, rasterY] = applyMatrix3(worldPosition, projectionMatrix(store.getState())); + // time isn't really relevant as we aren't testing animation + const time = 0; + const [rasterX, rasterY] = applyMatrix3( + worldPosition, + projectionMatrix(store.getState())(time) + ); expect(rasterX).toBeCloseTo(expectedRasterPosition[0]); expect(rasterY).toBeCloseTo(expectedRasterPosition[1]); }; }); + describe('when the raster size is 0 x 0 pixels (unpainted)', () => { + it('should convert 0,0 (center) in world space to 0,0 in raster space', () => { + compare([0, 0], [0, 0]); + }); + }); describe('when the raster size is 300 x 200 pixels', () => { beforeEach(() => { const action: CameraAction = { type: 'userSetRasterSize', payload: [300, 200] }; @@ -66,7 +76,7 @@ describe('projectionMatrix', () => { }); describe('when the user has panned to the right and up by 50', () => { beforeEach(() => { - const action: CameraAction = { type: 'userSetPositionOfCamera', payload: [-50, -50] }; + const action: CameraAction = { type: 'userSetPositionOfCamera', payload: [50, 50] }; store.dispatch(action); }); it('should convert 0,0 (center) in world space to 100,150 in raster space', () => { @@ -83,7 +93,7 @@ describe('projectionMatrix', () => { beforeEach(() => { const action: CameraAction = { type: 'userSetPositionOfCamera', - payload: [-350, -250], + payload: [350, 250], }; store.dispatch(action); }); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/reducer.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/reducer.ts index 7c4678a4f1dc1..0f6ae1b7d904a 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/reducer.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/reducer.ts @@ -5,24 +5,32 @@ */ import { Reducer } from 'redux'; -import { applyMatrix3, subtract } from '../../lib/vector2'; -import { userIsPanning, translation, projectionMatrix, inverseProjectionMatrix } from './selectors'; +import { unitsPerNudge, nudgeAnimationDuration } from './scaling_constants'; +import { animatePanning } from './methods'; +import * as vector2 from '../../lib/vector2'; +import * as selectors from './selectors'; import { clamp } from '../../lib/math'; import { CameraState, ResolverAction, Vector2 } from '../../types'; import { scaleToZoom } from './scale_to_zoom'; -function initialState(): CameraState { - return { +/** + * Used in tests. + */ +export function cameraInitialState(): CameraState { + const state: CameraState = { scalingFactor: scaleToZoom(1), // Defaulted to 1 to 1 scale rasterSize: [0, 0] as const, translationNotCountingCurrentPanning: [0, 0] as const, latestFocusedWorldCoordinates: null, + animation: undefined, + panning: undefined, }; + return state; } export const cameraReducer: Reducer<CameraState, ResolverAction> = ( - state = initialState(), + state = cameraInitialState(), action ) => { if (action.type === 'userSetZoomLevel') { @@ -30,10 +38,11 @@ export const cameraReducer: Reducer<CameraState, ResolverAction> = ( * Handle the scale being explicitly set, for example by a 'reset zoom' feature, or by a range slider with exact scale values */ - return { + const nextState: CameraState = { ...state, scalingFactor: clamp(action.payload, 0, 1), }; + return nextState; } else if (action.type === 'userClickedZoomIn') { return { ...state, @@ -47,7 +56,7 @@ export const cameraReducer: Reducer<CameraState, ResolverAction> = ( } else if (action.type === 'userZoomed') { const stateWithNewScaling: CameraState = { ...state, - scalingFactor: clamp(state.scalingFactor + action.payload, 0, 1), + scalingFactor: clamp(state.scalingFactor + action.payload.zoomChange, 0, 1), }; /** @@ -59,22 +68,41 @@ export const cameraReducer: Reducer<CameraState, ResolverAction> = ( * using CTRL and the mousewheel, or by pinching the trackpad on a Mac. The node will stay under your mouse cursor and other things in the map will get * nearer or further from the mouse cursor. This lets you keep your context when changing zoom levels. */ - if (state.latestFocusedWorldCoordinates !== null) { - const rasterOfLastFocusedWorldCoordinates = applyMatrix3( + if ( + state.latestFocusedWorldCoordinates !== null && + !selectors.isAnimating(state)(action.payload.time) + ) { + const rasterOfLastFocusedWorldCoordinates = vector2.applyMatrix3( state.latestFocusedWorldCoordinates, - projectionMatrix(state) + selectors.projectionMatrix(state)(action.payload.time) + ); + const newWorldCoordinatesAtLastFocusedPosition = vector2.applyMatrix3( + rasterOfLastFocusedWorldCoordinates, + selectors.inverseProjectionMatrix(stateWithNewScaling)(action.payload.time) + ); + + /** + * The change in world position incurred by changing scale. + */ + const delta = vector2.subtract( + newWorldCoordinatesAtLastFocusedPosition, + state.latestFocusedWorldCoordinates ); - const matrix = inverseProjectionMatrix(stateWithNewScaling); - const worldCoordinateThereNow = applyMatrix3(rasterOfLastFocusedWorldCoordinates, matrix); - const delta = subtract(worldCoordinateThereNow, state.latestFocusedWorldCoordinates); - return { + /** + * Adjust for the change in position due to scale. + */ + const translationNotCountingCurrentPanning: Vector2 = vector2.subtract( + stateWithNewScaling.translationNotCountingCurrentPanning, + delta + ); + + const nextState: CameraState = { ...stateWithNewScaling, - translationNotCountingCurrentPanning: [ - stateWithNewScaling.translationNotCountingCurrentPanning[0] + delta[0], - stateWithNewScaling.translationNotCountingCurrentPanning[1] + delta[1], - ], + translationNotCountingCurrentPanning, }; + + return nextState; } else { return stateWithNewScaling; } @@ -82,83 +110,76 @@ export const cameraReducer: Reducer<CameraState, ResolverAction> = ( /** * Handle the case where the position of the camera is explicitly set, for example by a 'back to center' feature. */ - return { + const nextState: CameraState = { ...state, + animation: undefined, translationNotCountingCurrentPanning: action.payload, }; + return nextState; } else if (action.type === 'userStartedPanning') { + if (selectors.isAnimating(state)(action.payload.time)) { + return state; + } /** * When the user begins panning with a mousedown event we mark the starting position for later comparisons. */ - return { + const nextState: CameraState = { ...state, + animation: undefined, panning: { - origin: action.payload, - currentOffset: action.payload, + origin: action.payload.screenCoordinates, + currentOffset: action.payload.screenCoordinates, }, }; + return nextState; } else if (action.type === 'userStoppedPanning') { /** * When the user stops panning (by letting up on the mouse) we calculate the new translation of the camera. */ - if (userIsPanning(state)) { - return { - ...state, - translationNotCountingCurrentPanning: translation(state), - panning: undefined, - }; - } else { - return state; - } - } else if (action.type === 'userClickedPanControl') { - const panDirection = action.payload; + const nextState: CameraState = { + ...state, + translationNotCountingCurrentPanning: selectors.translation(state)(action.payload.time), + panning: undefined, + }; + return nextState; + } else if (action.type === 'userNudgedCamera') { + const { direction, time } = action.payload; /** - * Delta amount will be in the range of 20 -> 40 depending on the scalingFactor + * Nudge less when zoomed in. */ - const deltaAmount = (1 + state.scalingFactor) * 20; - let delta: Vector2; - if (panDirection === 'north') { - delta = [0, -deltaAmount]; - } else if (panDirection === 'south') { - delta = [0, deltaAmount]; - } else if (panDirection === 'east') { - delta = [-deltaAmount, 0]; - } else if (panDirection === 'west') { - delta = [deltaAmount, 0]; - } else { - delta = [0, 0]; - } + const nudge = vector2.multiply( + vector2.divide([unitsPerNudge, unitsPerNudge], selectors.scale(state)(time)), + direction + ); - return { - ...state, - translationNotCountingCurrentPanning: [ - state.translationNotCountingCurrentPanning[0] + delta[0], - state.translationNotCountingCurrentPanning[1] + delta[1], - ], - }; + return animatePanning( + state, + time, + vector2.add(state.translationNotCountingCurrentPanning, nudge), + nudgeAnimationDuration + ); } else if (action.type === 'userSetRasterSize') { /** * Handle resizes of the Resolver component. We need to know the size in order to convert between screen * and world coordinates. */ - return { + const nextState: CameraState = { ...state, rasterSize: action.payload, }; + return nextState; } else if (action.type === 'userMovedPointer') { - const stateWithUpdatedPanning = { - ...state, - /** - * If the user is panning, adjust the panning offset - */ - panning: userIsPanning(state) - ? { - origin: state.panning ? state.panning.origin : action.payload, - currentOffset: action.payload, - } - : state.panning, - }; - return { + let stateWithUpdatedPanning: CameraState = state; + if (state.panning) { + stateWithUpdatedPanning = { + ...state, + panning: { + origin: state.panning.origin, + currentOffset: action.payload.screenCoordinates, + }, + }; + } + const nextState: CameraState = { ...stateWithUpdatedPanning, /** * keep track of the last world coordinates the user moved over. @@ -166,11 +187,12 @@ export const cameraReducer: Reducer<CameraState, ResolverAction> = ( * to keep the same point under the pointer. * In order to do this, we need to know the position of the mouse when changing the scale. */ - latestFocusedWorldCoordinates: applyMatrix3( - action.payload, - inverseProjectionMatrix(stateWithUpdatedPanning) + latestFocusedWorldCoordinates: vector2.applyMatrix3( + action.payload.screenCoordinates, + selectors.inverseProjectionMatrix(stateWithUpdatedPanning)(action.payload.time) ), }; + return nextState; } else { return state; } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/scaling_constants.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/scaling_constants.ts index 93c41fde64f0e..243d8877a8b0d 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/scaling_constants.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/scaling_constants.ts @@ -7,7 +7,7 @@ /** * The minimum allowed value for the camera scale. This is the least scale that we will ever render something at. */ -export const minimum = 0.1; +export const minimum = 0.5; /** * The maximum allowed value for the camera scale. This is greatest scale that we will ever render something at. @@ -18,3 +18,13 @@ export const maximum = 6; * The curve of the zoom function growth rate. The higher the scale factor is, the higher the zoom rate will be. */ export const zoomCurveRate = 4; + +/** + * The size, in world units, of a 'nudge' as caused by clicking the up, right, down, or left panning buttons. + */ +export const unitsPerNudge = 50; + +/** + * The duration a nudge animation lasts. + */ +export const nudgeAnimationDuration = 300; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts index 53ffe6dd073fa..226e36f63d788 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/selectors.ts @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Vector2, CameraState, AABB, Matrix3 } from '../../types'; -import { subtract, divide, add, applyMatrix3 } from '../../lib/vector2'; +import { createSelector, defaultMemoize } from 'reselect'; +import { easing } from 'ts-easing'; +import { clamp, lerp } from '../../lib/math'; +import * as vector2 from '../../lib/vector2'; import { multiply, add as addMatrix } from '../../lib/matrix3'; import { inverseOrthographicProjection, @@ -13,7 +15,8 @@ import { orthographicProjection, translationTransformation, } from '../../lib/transformation'; -import { maximum, minimum, zoomCurveRate } from './scaling_constants'; +import * as scalingConstants from './scaling_constants'; +import { Vector2, CameraState, AABB, Matrix3, CameraAnimationState } from '../../types'; interface ClippingPlanes { renderWidth: number; @@ -24,77 +27,283 @@ interface ClippingPlanes { clippingPlaneBottom: number; } +function animationIsActive(animation: CameraAnimationState, time: number): boolean { + return animation.startTime + animation.duration >= time; +} + /** - * The viewable area in the Resolver map, in world coordinates. + * The scale by which world values are scaled when rendered. + * + * When the camera position (translation) is changed programatically, it may be animated. + * The duration of the animation is generally fixed for a given type of interaction. This way + * the user won't have to wait for a variable amount of time to complete their interaction. + * + * Since the duration is fixed and the amount that the camera position changes is variable, + * the speed at which the camera changes is also variable. If the distance the camera will move + * is very far, the camera will move very fast. + * + * When the camera moves fast, elements will move across the screen quickly. These + * quick moving elements can be distracting to the user. They may also hinder the quality of + * animation. + * + * The speed at which objects move across the screen is dependent on the speed of the camera + * as well as the scale. If the scale is high, the camera is zoomed in, and so objects move + * across the screen faster at a given camera speed. Think of looking into a telephoto lense + * and moving around only a few degrees: many things might pass through your sight. + * + * If the scale is low, the camera is zoomed out, objects look further away, and so they move + * across the screen slower at a given camera speed. Therefore we can control the speed at + * which objects move across the screen without changing the camera speed. We do this by changing scale. + * + * Changing the scale abruptly isn't acceptable because it would be visually jarring. Also, the + * change in scale should be temporary, and the original scale should be resumed after the animation. + * + * In order to change the scale to lower value, and then back, without being jarring to the user, + * we calculate a temporary target scale and animate to it. + * */ -export function viewableBoundingBox(state: CameraState): AABB { - const { renderWidth, renderHeight } = clippingPlanes(state); - const matrix = inverseProjectionMatrix(state); - const bottomLeftCorner: Vector2 = [0, renderHeight]; - const topRightCorner: Vector2 = [renderWidth, 0]; - return { - minimum: applyMatrix3(bottomLeftCorner, matrix), - maximum: applyMatrix3(topRightCorner, matrix), - }; -} +export const scale: (state: CameraState) => (time: number) => Vector2 = createSelector( + state => state.scalingFactor, + state => state.animation, + (scalingFactor, animation) => { + const scaleNotCountingAnimation = scaleFromScalingFactor(scalingFactor); + /** + * If `animation` is defined, an animation may be in progress when the returned function is called + */ + if (animation !== undefined) { + /** + * The distance the camera will move during the animation is used to determine the camera speed. + */ + const panningDistance = vector2.distance( + animation.targetTranslation, + animation.initialTranslation + ); + + const panningDistanceInPixels = panningDistance * scaleNotCountingAnimation; + + /** + * The speed at which pixels move across the screen during animation in pixels per millisecond. + */ + const speed = panningDistanceInPixels / animation.duration; + + /** + * The speed (in pixels per millisecond) at which an animation is triggered is a constant. + * If the camera isn't moving very fast, no change in scale is necessary. + */ + const speedThreshold = 0.4; + + /** + * Growth in speed beyond the threshold is taken to the power of a constant. This limits the + * rate of growth of speed. + */ + const speedGrowthFactor = 0.4; + + /* + * Limit the rate of growth of speed. If the speed is too great, the animation will be + * unpleasant and have poor performance. + * + * gnuplot> plot [x=0:10][y=0:3] threshold=0.4, growthFactor=0.4, x < threshold ? x : x ** growthFactor - (threshold ** growthFactor - threshold) + * + * + * 3 +----------------------------------------------------------------------------+ + * | target speed + + + | + * | | + * | ******* | + * | | + * | | + * 2.5 |-+ +-| + * | | + * | | + * | **| + * | ******* | + * | ****** | + * 2 |-+ ****** +-| + * | ***** | + * | ***** | + * | ***** | + * | ***** | + * 1.5 |-+ ***** +-| + * | **** | + * | **** | + * | **** | + * | *** | + * | *** | + * 1 |-+ ** +-| + * | *** | + * | *** | + * | * | + * | ** | + * | ** | + * 0.5 |-+ * +-| + * | ** | + * | * | + * | * | + * | * | + * |* + + + + | + * 0 +----------------------------------------------------------------------------+ + * 0 2 4 6 8 10 + * camera speed (pixels per ms) + * + **/ + const limitedSpeed = + speed < speedThreshold + ? speed + : speed ** speedGrowthFactor - (speedThreshold ** speedGrowthFactor - speedThreshold); + + /** + * The distance and duration of the animation are independent variables. If the speed was + * limited, only the scale can change. The lower the scale, the further the camera is + * away from things, and therefore the slower things move across the screen. Adjust the + * scale (within its own limits) to match the limited speed. + * + * This will cause the camera to zoom out if it would otherwise move too fast. + */ + const adjustedScale = clamp( + (limitedSpeed * animation.duration) / panningDistance, + scalingConstants.minimum, + scalingConstants.maximum + ); + + return time => { + /** + * If the animation has completed, return the `scaleNotCountingAnimation`, as + * the animation always completes with the scale set back at starting value. + */ + if (animationIsActive(animation, time) === false) { + return [scaleNotCountingAnimation, scaleNotCountingAnimation]; + } else { + /** + * + * Animation is defined by a starting time, duration, starting position, and ending position. The amount of time + * which has passed since the start time, compared to the duration, defines the progress of the animation. + * We represent this process with a number between 0 and 1. As the animation progresses, the value changes from 0 + * to 1, linearly. + */ + const x = animationProgress(animation, time); + /** + * The change in scale over the duration of the animation should not be linear. It should grow to the target value, + * then shrink back down to the original value. We adjust the animation progress so that it reaches its peak + * halfway through the animation and then returns to the beginning value by the end of the animation. + * + * We ease the value so that the change from not-animating-at-all to animating-at-full-speed isn't abrupt. + * See the graph: + * + * gnuplot> plot [x=-0:1][x=0:1.2] eased(t)=t<.5? 4*t**3 : (t-1)*(2*t-2)**2+1, progress(t)=-abs(2*t-1)+1, eased(progress(x)) + * + * + * 1.2 +--------------------------------------------------------------------------------------+ + * | + + + + | + * | e(t)=t<.5? 4*t**3 : (t-1)*(2*t-2)**2+1, t(x)=-abs(2*x-1)+1, e(t(x)) ******* | + * | | + * | | + * | | + * 1 |-+ **************** +-| + * | *** *** | + * | ** ** | + * | ** ** | + * | * * | + * | * * | + * 0.8 |-+ * * +-| + * | * * | + * | * * | + * | * * | + * | * * | + * 0.6 |-+ * * +-| + * | * * | + * | * * | + * | * * | + * | * * | + * | * * | + * 0.4 |-+ * * +-| + * | * * | + * | * * | + * | * * | + * | * * | + * | * * | + * 0.2 |-+ * * +-| + * | * * | + * | * * | + * | ** ** | + * | * * | + * | *** + + + + *** | + * 0 +--------------------------------------------------------------------------------------+ + * 0 0.2 0.4 0.6 0.8 1 + * animation progress + * + */ + const easedInOutAnimationProgress = easing.inOutCubic(-Math.abs(2 * x - 1) + 1); + + /** + * Linearly interpolate between these, using the bell-shaped easing value + */ + const lerpedScale = lerp( + scaleNotCountingAnimation, + adjustedScale, + easedInOutAnimationProgress + ); + + /** + * The scale should be the same in both axes. + */ + return [lerpedScale, lerpedScale]; + } + }; + } else { + /** + * The scale should be the same in both axes. + */ + return () => [scaleNotCountingAnimation, scaleNotCountingAnimation]; + } + + /** + * Interpolate between the minimum and maximum scale, + * using a curved ratio based on `factor`. + */ + function scaleFromScalingFactor(factor: number): number { + return lerp( + scalingConstants.minimum, + scalingConstants.maximum, + Math.pow(factor, scalingConstants.zoomCurveRate) + ); + } + } +); /** * The 2D clipping planes used for the orthographic projection. See https://en.wikipedia.org/wiki/Orthographic_projection */ -function clippingPlanes(state: CameraState): ClippingPlanes { - const renderWidth = state.rasterSize[0]; - const renderHeight = state.rasterSize[1]; - const clippingPlaneRight = renderWidth / 2 / scale(state)[0]; - const clippingPlaneTop = renderHeight / 2 / scale(state)[1]; - - return { - renderWidth, - renderHeight, - clippingPlaneRight, - clippingPlaneTop, - clippingPlaneLeft: -clippingPlaneRight, - clippingPlaneBottom: -clippingPlaneTop, - }; -} +export const clippingPlanes: ( + state: CameraState +) => (time: number) => ClippingPlanes = createSelector( + state => state.rasterSize, + scale, + (rasterSize, scaleAtTime) => (time: number) => { + const [scaleX, scaleY] = scaleAtTime(time); + const renderWidth = rasterSize[0]; + const renderHeight = rasterSize[1]; + const clippingPlaneRight = renderWidth / 2 / scaleX; + const clippingPlaneTop = renderHeight / 2 / scaleY; + + return { + renderWidth, + renderHeight, + clippingPlaneRight, + clippingPlaneTop, + clippingPlaneLeft: -clippingPlaneRight, + clippingPlaneBottom: -clippingPlaneTop, + }; + } +); /** - * A matrix that when applied to a Vector2 will convert it from world coordinates to screen coordinates. - * See https://en.wikipedia.org/wiki/Orthographic_projection + * Whether or not the camera is animating, at a given time. */ -export const projectionMatrix: (state: CameraState) => Matrix3 = state => { - const { - renderWidth, - renderHeight, - clippingPlaneRight, - clippingPlaneTop, - clippingPlaneLeft, - clippingPlaneBottom, - } = clippingPlanes(state); - - return multiply( - // 5. convert from 0->2 to 0->rasterWidth (or height) - scalingTransformation([renderWidth / 2, renderHeight / 2]), - addMatrix( - // 4. add one to change range from -1->1 to 0->2 - [0, 0, 1, 0, 0, 1, 0, 0, 0], - multiply( - // 3. invert y since CSS has inverted y - scalingTransformation([1, -1]), - multiply( - // 2. scale to clipping plane - orthographicProjection( - clippingPlaneTop, - clippingPlaneRight, - clippingPlaneBottom, - clippingPlaneLeft - ), - // 1. adjust for camera - translationTransformation(translation(state)) - ) - ) - ) - ); -}; +export const isAnimating: (state: CameraState) => (time: number) => boolean = createSelector( + state => state.animation, + animation => time => { + return animation !== undefined && animationIsActive(animation, time); + } +); /** * The camera has a translation value (not counting any current panning.) This is initialized to (0, 0) and @@ -108,79 +317,186 @@ export const projectionMatrix: (state: CameraState) => Matrix3 = state => { * * We could update the translation as the user moved the mouse but floating point drift (round-off error) could occur. */ -export function translation(state: CameraState): Vector2 { - if (state.panning) { - return add( - state.translationNotCountingCurrentPanning, - divide(subtract(state.panning.currentOffset, state.panning.origin), [ - scale(state)[0], - // Invert `y` since the `.panning` vectors are in screen coordinates and therefore have backwards `y` - -scale(state)[1], - ]) - ); - } else { - return state.translationNotCountingCurrentPanning; +export const translation: (state: CameraState) => (time: number) => Vector2 = createSelector( + state => state.panning, + state => state.translationNotCountingCurrentPanning, + scale, + state => state.animation, + (panning, translationNotCountingCurrentPanning, scaleAtTime, animation) => { + return (time: number) => { + const [scaleX, scaleY] = scaleAtTime(time); + if (animation !== undefined && animationIsActive(animation, time)) { + return vector2.lerp( + animation.initialTranslation, + animation.targetTranslation, + easing.inOutCubic(animationProgress(animation, time)) + ); + } else if (panning) { + const changeInPanningOffset = vector2.subtract(panning.currentOffset, panning.origin); + /** + * invert the vector since panning moves the perception of the screen, which is inverse of the + * translation of the camera. Inverse the `y` axis again, since `y` is inverted between + * world and screen coordinates. + */ + const changeInTranslation = vector2.divide(changeInPanningOffset, [-scaleX, scaleY]); + return vector2.add(translationNotCountingCurrentPanning, changeInTranslation); + } else { + return translationNotCountingCurrentPanning; + } + }; } -} +); /** * A matrix that when applied to a Vector2 converts it from screen coordinates to world coordinates. * See https://en.wikipedia.org/wiki/Orthographic_projection */ -export const inverseProjectionMatrix: (state: CameraState) => Matrix3 = state => { - const { - renderWidth, - renderHeight, - clippingPlaneRight, - clippingPlaneTop, - clippingPlaneLeft, - clippingPlaneBottom, - } = clippingPlanes(state); - - /* prettier-ignore */ - const screenToNDC = [ - 2 / renderWidth, 0, -1, - 0, 2 / renderHeight, -1, - 0, 0, 0 - ] as const - - const [translationX, translationY] = translation(state); - - return addMatrix( - // 4. Translate for the 'camera' - // prettier-ignore - [ - 0, 0, -translationX, - 0, 0, -translationY, - 0, 0, 0 - ] as const, - multiply( - // 3. make values in range of clipping planes - inverseOrthographicProjection( +export const inverseProjectionMatrix: ( + state: CameraState +) => (time: number) => Matrix3 = createSelector( + clippingPlanes, + translation, + (clippingPlanesAtTime, translationAtTime) => { + return (time: number) => { + const { + renderWidth, + renderHeight, + clippingPlaneRight, + clippingPlaneTop, + clippingPlaneLeft, + clippingPlaneBottom, + } = clippingPlanesAtTime(time); + + /** + * 1. Convert from 0<=n<=screenDimension to -1<=n<=1 + * e.g. for x-axis, divide by renderWidth then multiply by 2 and subtract by one so the value is in range of -1->1 + */ + // prettier-ignore + const screenToNDC: Matrix3 = [ + renderWidth === 0 ? 0 : 2 / renderWidth, 0, -1, + 0, renderHeight === 0 ? 0 : 2 / renderHeight, -1, + 0, 0, 0 + ]; + + /** + * 2. Invert Y since DOM positioning has inverted Y axis + */ + const invertY = scalingTransformation([1, -1]); + + const [translationX, translationY] = translationAtTime(time); + + /** + * 3. Scale values to the clipping plane dimensions. + */ + const scaleToClippingPlaneDimensions = inverseOrthographicProjection( clippingPlaneTop, clippingPlaneRight, clippingPlaneBottom, clippingPlaneLeft - ), - multiply( - // 2 Invert Y since CSS has inverted y - scalingTransformation([1, -1]), - // 1. convert screen coordinates to NDC - // e.g. for x-axis, divide by renderWidth then multiply by 2 and subtract by one so the value is in range of -1->1 - screenToNDC - ) - ) - ); -}; + ); + + /** + * Move the values to accomodate for the perspective of the camera (based on the camera's transform) + */ + const translateForCamera: Matrix3 = [0, 0, translationX, 0, 0, translationY, 0, 0, 0]; + + return addMatrix( + translateForCamera, + multiply(scaleToClippingPlaneDimensions, multiply(invertY, screenToNDC)) + ); + }; + } +); /** - * The scale by which world values are scaled when rendered. + * The viewable area in the Resolver map, in world coordinates. */ -export const scale = (state: CameraState): Vector2 => { - const delta = maximum - minimum; - const value = Math.pow(state.scalingFactor, zoomCurveRate) * delta + minimum; - return [value, value]; -}; +export const viewableBoundingBox: (state: CameraState) => (time: number) => AABB = createSelector( + clippingPlanes, + inverseProjectionMatrix, + (clippingPlanesAtTime, matrixAtTime) => { + return (time: number) => { + const { renderWidth, renderHeight } = clippingPlanesAtTime(time); + const matrix = matrixAtTime(time); + const bottomLeftCorner: Vector2 = [0, renderHeight]; + const topRightCorner: Vector2 = [renderWidth, 0]; + return { + minimum: vector2.applyMatrix3(bottomLeftCorner, matrix), + maximum: vector2.applyMatrix3(topRightCorner, matrix), + }; + }; + } +); + +/** + * A matrix that when applied to a Vector2 will convert it from world coordinates to screen coordinates. + * See https://en.wikipedia.org/wiki/Orthographic_projection + */ +export const projectionMatrix: (state: CameraState) => (time: number) => Matrix3 = createSelector( + clippingPlanes, + translation, + (clippingPlanesAtTime, translationAtTime) => { + return defaultMemoize((time: number) => { + const { + renderWidth, + renderHeight, + clippingPlaneRight, + clippingPlaneTop, + clippingPlaneLeft, + clippingPlaneBottom, + } = clippingPlanesAtTime(time); + + /** + * 1. adjust for camera by subtracting its translation. The closer the camera is to a point, the closer that point + * should be to the center of the screen. + */ + const adjustForCameraPosition = translationTransformation( + vector2.scale(translationAtTime(time), -1) + ); + + /** + * 2. Scale the values based on the dimsension of Resolver on the screen. + */ + const screenToNDC = orthographicProjection( + clippingPlaneTop, + clippingPlaneRight, + clippingPlaneBottom, + clippingPlaneLeft + ); + + /** + * 3. invert y since CSS has inverted y + */ + const invertY = scalingTransformation([1, -1]); + + /** + * 3. Convert values from the scale of -1<=n<=1 to 0<=n<=2 + */ + // prettier-ignore + const fromNDCtoZeroToTwo: Matrix3 = [ + 0, 0, 1, + 0, 0, 1, + 0, 0, 0 + ] + + /** + * 4. convert from 0->2 to 0->rasterDimension by multiplying by rasterDimension/2 + */ + const fromZeroToTwoToRasterDimensions = scalingTransformation([ + renderWidth / 2, + renderHeight / 2, + ]); + + return multiply( + fromZeroToTwoToRasterDimensions, + addMatrix( + fromNDCtoZeroToTwo, + multiply(invertY, multiply(screenToNDC, adjustForCameraPosition)) + ) + ); + }); + } +); /** * Scales the coordinate system, used for zooming. Should always be between 0 and 1 @@ -193,3 +509,12 @@ export const scalingFactor = (state: CameraState): CameraState['scalingFactor'] * Whether or not the user is current panning the map. */ export const userIsPanning = (state: CameraState): boolean => state.panning !== undefined; + +/** + * Returns a number 0<=n<=1 where: + * 0 meaning it just started, + * 1 meaning it is done. + */ +function animationProgress(animation: CameraAnimationState, time: number): number { + return clamp((time - animation.startTime) / animation.duration, 0, 1); +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/zooming.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/zooming.test.ts index abc113d5999ff..fb38c2f526e0b 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/zooming.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/camera/zooming.test.ts @@ -15,12 +15,13 @@ import { applyMatrix3 } from '../../lib/vector2'; describe('zooming', () => { let store: Store<CameraState, CameraAction>; + let time: number; const cameraShouldBeBoundBy = (expectedViewableBoundingBox: AABB): [string, () => void] => { return [ `the camera view should be bound by an AABB with a minimum point of ${expectedViewableBoundingBox.minimum} and a maximum point of ${expectedViewableBoundingBox.maximum}`, () => { - const actual = viewableBoundingBox(store.getState()); + const actual = viewableBoundingBox(store.getState())(time); expect(actual.minimum[0]).toBeCloseTo(expectedViewableBoundingBox.minimum[0]); expect(actual.minimum[1]).toBeCloseTo(expectedViewableBoundingBox.minimum[1]); expect(actual.maximum[0]).toBeCloseTo(expectedViewableBoundingBox.maximum[0]); @@ -29,6 +30,8 @@ describe('zooming', () => { ]; }; beforeEach(() => { + // Time isn't relevant as we aren't testing animation + time = 0; store = createStore(cameraReducer, undefined); }); describe('when the raster size is 300 x 200 pixels', () => { @@ -58,12 +61,12 @@ describe('zooming', () => { beforeEach(() => { const action: CameraAction = { type: 'userZoomed', - payload: 1, + payload: { zoomChange: 1, time }, }; store.dispatch(action); }); it('should zoom to maximum scale factor', () => { - const actual = viewableBoundingBox(store.getState()); + const actual = viewableBoundingBox(store.getState())(time); expect(actual).toMatchInlineSnapshot(` Object { "maximum": Array [ @@ -79,16 +82,16 @@ describe('zooming', () => { }); }); it('the raster position 200, 50 should map to the world position 50, 50', () => { - expectVectorsToBeClose(applyMatrix3([200, 50], inverseProjectionMatrix(store.getState())), [ - 50, - 50, - ]); + expectVectorsToBeClose( + applyMatrix3([200, 50], inverseProjectionMatrix(store.getState())(time)), + [50, 50] + ); }); describe('when the user has moved their mouse to the raster position 200, 50', () => { beforeEach(() => { const action: CameraAction = { type: 'userMovedPointer', - payload: [200, 50], + payload: { screenCoordinates: [200, 50], time }, }; store.dispatch(action); }); @@ -104,13 +107,13 @@ describe('zooming', () => { beforeEach(() => { const action: CameraAction = { type: 'userZoomed', - payload: 0.5, + payload: { zoomChange: 0.5, time }, }; store.dispatch(action); }); it('the raster position 200, 50 should map to the world position 50, 50', () => { expectVectorsToBeClose( - applyMatrix3([200, 50], inverseProjectionMatrix(store.getState())), + applyMatrix3([200, 50], inverseProjectionMatrix(store.getState())(time)), [50, 50] ); }); @@ -118,7 +121,7 @@ describe('zooming', () => { }); describe('when the user pans right by 100 pixels', () => { beforeEach(() => { - const action: CameraAction = { type: 'userSetPositionOfCamera', payload: [-100, 0] }; + const action: CameraAction = { type: 'userSetPositionOfCamera', payload: [100, 0] }; store.dispatch(action); }); it( @@ -130,7 +133,7 @@ describe('zooming', () => { it('should be centered on 100, 0', () => { const worldCenterPoint = applyMatrix3( [150, 100], - inverseProjectionMatrix(store.getState()) + inverseProjectionMatrix(store.getState())(time) ); expect(worldCenterPoint[0]).toBeCloseTo(100); expect(worldCenterPoint[1]).toBeCloseTo(0); @@ -143,7 +146,7 @@ describe('zooming', () => { it('should be centered on 100, 0', () => { const worldCenterPoint = applyMatrix3( [150, 100], - inverseProjectionMatrix(store.getState()) + inverseProjectionMatrix(store.getState())(time) ); expect(worldCenterPoint[0]).toBeCloseTo(100); expect(worldCenterPoint[1]).toBeCloseTo(0); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap index 261ca7e0a7bba..1dc17054b9f47 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/__snapshots__/graphing.test.ts.snap @@ -18,6 +18,7 @@ Object { "node_id": 0, "process_name": "", "process_path": "", + "timestamp_utc": "2019-09-24 01:47:47Z", }, "event_timestamp": 1, "event_type": 1, @@ -172,6 +173,7 @@ Object { "node_id": 0, "process_name": "", "process_path": "", + "timestamp_utc": "2019-09-24 01:47:47Z", }, "event_timestamp": 1, "event_type": 1, @@ -188,6 +190,7 @@ Object { "process_name": "", "process_path": "", "source_id": 0, + "timestamp_utc": "2019-09-24 01:47:47Z", }, "event_timestamp": 1, "event_type": 1, @@ -204,6 +207,7 @@ Object { "process_name": "", "process_path": "", "source_id": 0, + "timestamp_utc": "2019-09-24 01:47:47Z", }, "event_timestamp": 1, "event_type": 1, @@ -220,6 +224,7 @@ Object { "process_name": "", "process_path": "", "source_id": 1, + "timestamp_utc": "2019-09-24 01:47:47Z", }, "event_timestamp": 1, "event_type": 1, @@ -236,6 +241,7 @@ Object { "process_name": "", "process_path": "", "source_id": 1, + "timestamp_utc": "2019-09-24 01:47:47Z", }, "event_timestamp": 1, "event_type": 1, @@ -252,6 +258,7 @@ Object { "process_name": "", "process_path": "", "source_id": 2, + "timestamp_utc": "2019-09-24 01:47:47Z", }, "event_timestamp": 1, "event_type": 1, @@ -268,6 +275,7 @@ Object { "process_name": "", "process_path": "", "source_id": 2, + "timestamp_utc": "2019-09-24 01:47:47Z", }, "event_timestamp": 1, "event_type": 1, @@ -284,6 +292,7 @@ Object { "process_name": "", "process_path": "", "source_id": 6, + "timestamp_utc": "2019-09-24 01:47:47Z", }, "event_timestamp": 1, "event_type": 1, @@ -318,6 +327,7 @@ Object { "node_id": 0, "process_name": "", "process_path": "", + "timestamp_utc": "2019-09-24 01:47:47Z", }, "event_timestamp": 1, "event_type": 1, @@ -334,6 +344,7 @@ Object { "process_name": "", "process_path": "", "source_id": 0, + "timestamp_utc": "2019-09-24 01:47:47Z", }, "event_timestamp": 1, "event_type": 1, diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index 745bd125c151d..75b477dd7c7fc 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -57,11 +57,17 @@ const isometricTransformMatrix: Matrix3 = [ /** * The distance in pixels (at scale 1) between nodes. Change this to space out nodes more */ -export const distanceBetweenNodes = distanceBetweenNodesInUnits * unit; +const distanceBetweenNodes = distanceBetweenNodesInUnits * unit; -export function graphableProcesses(state: DataState) { - return state.results.filter(isGraphableProcess); -} +/** + * Process events that will be graphed. + */ +export const graphableProcesses = createSelector( + ({ results }: DataState) => results, + function(results: DataState['results']) { + return results.filter(isGraphableProcess); + } +); /** * In laying out the graph, we precalculate the 'width' of each subtree. The 'width' of the subtree is determined by its diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts index d043453a8e4cd..b17572bbc4ab4 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/index.ts @@ -4,43 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createStore, StoreEnhancer } from 'redux'; -import { ResolverAction } from '../types'; +import { createStore, applyMiddleware, Store } from 'redux'; +import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly'; +import { ResolverAction, ResolverState } from '../types'; import { resolverReducer } from './reducer'; -export const storeFactory = () => { - /** - * Redux Devtools extension exposes itself via a property on the global object. - * This interface can be used to cast `window` to a type that may expose Redux Devtools. - */ - interface SomethingThatMightHaveReduxDevTools { - __REDUX_DEVTOOLS_EXTENSION__?: (options?: PartialReduxDevToolsOptions) => StoreEnhancer; - } +export const storeFactory = (): { store: Store<ResolverState, ResolverAction> } => { + const actionsBlacklist: Array<ResolverAction['type']> = ['userMovedPointer']; + const composeEnhancers = composeWithDevTools({ + name: 'Resolver', + actionsBlacklist, + }); - /** - * Some of the options that can be passed when configuring Redux Devtools. - */ - interface PartialReduxDevToolsOptions { - /** - * A name for this store - */ - name?: string; - /** - * A list of action types to ignore. This is used to ignore high frequency events created by a mousemove handler - */ - actionsBlacklist?: readonly string[]; - } - const windowWhichMightHaveReduxDevTools = window as SomethingThatMightHaveReduxDevTools; - // Make sure blacklisted action types are valid - const actionsBlacklist: ReadonlyArray<ResolverAction['type']> = ['userMovedPointer']; - const store = createStore( - resolverReducer, - windowWhichMightHaveReduxDevTools.__REDUX_DEVTOOLS_EXTENSION__ && - windowWhichMightHaveReduxDevTools.__REDUX_DEVTOOLS_EXTENSION__({ - name: 'Resolver', - actionsBlacklist, - }) - ); + const middlewareEnhancer = applyMiddleware(); + + const store = createStore(resolverReducer, composeEnhancers(middlewareEnhancer)); return { store, }; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts new file mode 100644 index 0000000000000..8808160c9c631 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts @@ -0,0 +1,30 @@ +/* + * 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 { animatePanning } from './camera/methods'; +import { processNodePositionsAndEdgeLineSegments } from './selectors'; +import { ResolverState, ProcessEvent } from '../types'; + +const animationDuration = 1000; + +/** + * Return new `ResolverState` with the camera animating to focus on `process`. + */ +export function animateProcessIntoView( + state: ResolverState, + startTime: number, + process: ProcessEvent +): ResolverState { + const { processNodePositions } = processNodePositionsAndEdgeLineSegments(state); + const position = processNodePositions.get(process); + if (position) { + return { + ...state, + camera: animatePanning(state.camera, startTime, position, animationDuration), + }; + } + return state; +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts index 97ab51cbd6dea..20c490b8998f9 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/reducer.ts @@ -4,11 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ import { Reducer, combineReducers } from 'redux'; +import { animateProcessIntoView } from './methods'; import { cameraReducer } from './camera/reducer'; import { dataReducer } from './data/reducer'; import { ResolverState, ResolverAction } from '../types'; -export const resolverReducer: Reducer<ResolverState, ResolverAction> = combineReducers({ +const concernReducers = combineReducers({ camera: cameraReducer, data: dataReducer, }); + +export const resolverReducer: Reducer<ResolverState, ResolverAction> = (state, action) => { + const nextState = concernReducers(state, action); + if (action.type === 'userBroughtProcessIntoView') { + return animateProcessIntoView(nextState, action.payload.time, action.payload.process); + } else { + return nextState; + } +}; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts index eb1c1fec36995..4d12e656205fa 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts @@ -17,6 +17,9 @@ export const projectionMatrix = composeSelectors( cameraSelectors.projectionMatrix ); +export const clippingPlanes = composeSelectors(cameraStateSelector, cameraSelectors.clippingPlanes); +export const translation = composeSelectors(cameraStateSelector, cameraSelectors.translation); + /** * A matrix that when applied to a Vector2 converts it from screen coordinates to world coordinates. * See https://en.wikipedia.org/wiki/Orthographic_projection @@ -28,6 +31,7 @@ export const inverseProjectionMatrix = composeSelectors( /** * The scale by which world values are scaled when rendered. + * TODO make it a number */ export const scale = composeSelectors(cameraStateSelector, cameraSelectors.scale); @@ -41,6 +45,11 @@ export const scalingFactor = composeSelectors(cameraStateSelector, cameraSelecto */ export const userIsPanning = composeSelectors(cameraStateSelector, cameraSelectors.userIsPanning); +/** + * Whether or not the camera is animating, at a given time. + */ +export const isAnimating = composeSelectors(cameraStateSelector, cameraSelectors.isAnimating); + export const processNodePositionsAndEdgeLineSegments = composeSelectors( dataStateSelector, dataSelectors.processNodePositionsAndEdgeLineSegments diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index f2ae9785446f7..6c6936d377dea 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -export { ResolverAction } from './actions'; +import { Store } from 'redux'; + +import { ResolverAction } from './store/actions'; +export { ResolverAction } from './store/actions'; /** * Redux state for the Resolver feature. Properties on this interface are populated via multiple reducers using redux's `combineReducers`. @@ -21,27 +24,34 @@ export interface ResolverState { readonly data: DataState; } -interface PanningState { +/** + * Piece of redux state that models an animation for the camera. + */ +export interface CameraAnimationState { + /** + * The time when the animation began. + */ + readonly startTime: number; /** - * Screen coordinate vector representing the starting point when panning. + * The final translation when the animation is complete. */ - readonly origin: Vector2; + readonly targetTranslation: Vector2; + /** + * The effective camera position (including an in-progress user panning) at the time + * when the animation began. + */ + readonly initialTranslation: Vector2; /** - * Screen coordinate vector representing the current point when panning. + * The duration, in milliseconds, that the animation should last. Should be > 0 */ - readonly currentOffset: Vector2; + readonly duration: number; } /** - * Redux state for the virtual 'camera' used by Resolver. + * The redux state for the `useCamera` hook. */ -export interface CameraState { - /** - * Contains the starting and current position of the pointer when the user is panning the map. - */ - readonly panning?: PanningState; - +export type CameraState = { /** * Scales the coordinate system, used for zooming. Should always be between 0 and 1 */ @@ -54,7 +64,7 @@ export interface CameraState { /** * The camera world transform not counting any change from panning. When panning finishes, this value is updated to account for it. - * Use the `transform` selector to get the transform adjusted for panning. + * Use the `translation` selector to get the effective translation adjusted for panning. */ readonly translationNotCountingCurrentPanning: Vector2; @@ -62,7 +72,43 @@ export interface CameraState { * The world coordinates that the pointing device was last over. This is used during mousewheel zoom. */ readonly latestFocusedWorldCoordinates: Vector2 | null; -} +} & ( + | { + /** + * Contains the animation start time and target translation. This doesn't model the instantaneous + * progress of an animation. Instead, animation is model as functions-of-time. + */ + readonly animation: CameraAnimationState; + /** + * If the camera is animating, it must not be panning. + */ + readonly panning: undefined; + } + | { + /** + * If the camera is panning, it must not be animating. + */ + readonly animation: undefined; + /** + * Contains the starting and current position of the pointer when the user is panning the map. + */ + readonly panning: { + /** + * Screen coordinate vector representing the starting point when panning. + */ + readonly origin: Vector2; + + /** + * Screen coordinate vector representing the current point when panning. + */ + readonly currentOffset: Vector2; + }; + } + | { + readonly animation: undefined; + readonly panning: undefined; + } +); /** * State for `data` reducer which handles receiving Resolver data from the backend. @@ -73,8 +119,6 @@ export interface DataState { export type Vector2 = readonly [number, number]; -export type Vector3 = readonly [number, number, number]; - /** * A rectangle with sides that align with the `x` and `y` axises. */ @@ -121,6 +165,7 @@ export interface ProcessEvent { readonly event_type: number; readonly machine_id: string; readonly data_buffer: { + timestamp_utc: string; event_subtype_full: eventSubtypeFull; event_type_full: eventTypeFull; node_id: number; @@ -184,6 +229,48 @@ export type ProcessWithWidthMetadata = { ); /** - * String that represents the direction in which Resolver can be panned + * The constructor for a ResizeObserver */ -export type PanDirection = 'north' | 'south' | 'east' | 'west'; +interface ResizeObserverConstructor { + prototype: ResizeObserver; + new (callback: ResizeObserverCallback): ResizeObserver; +} + +/** + * Functions that introduce side effects. A React context provides these, and they may be mocked in tests. + */ +export interface SideEffectors { + /** + * A function which returns the time since epoch in milliseconds. Injected because mocking Date is tedious. + */ + timestamp: () => number; + requestAnimationFrame: typeof window.requestAnimationFrame; + cancelAnimationFrame: typeof window.cancelAnimationFrame; + ResizeObserver: ResizeObserverConstructor; +} + +export interface SideEffectSimulator { + /** + * Control the mock `SideEffectors`. + */ + controls: { + /** + * Set or get the `time` number used for `timestamp` and `requestAnimationFrame` callbacks. + */ + time: number; + /** + * Call any pending `requestAnimationFrame` callbacks. + */ + provideAnimationFrame: () => void; + /** + * Trigger `ResizeObserver` callbacks for `element` and update the mocked value for `getBoundingClientRect`. + */ + simulateElementResize: (element: Element, contentRect: DOMRect) => void; + }; + /** + * Mocked `SideEffectors`. + */ + mock: jest.Mocked<Omit<SideEffectors, 'ResizeObserver'>> & Pick<SideEffectors, 'ResizeObserver'>; +} + +export type ResolverStore = Store<ResolverState, ResolverAction>; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx index cdecd3e02bde1..3386ed4a448d5 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/edge_line.tsx @@ -6,10 +6,8 @@ import React from 'react'; import styled from 'styled-components'; -import { useSelector } from 'react-redux'; import { applyMatrix3, distance, angle } from '../lib/vector2'; -import { Vector2 } from '../types'; -import * as selectors from '../store/selectors'; +import { Vector2, Matrix3 } from '../types'; /** * A placeholder line segment view that connects process nodes. @@ -20,6 +18,7 @@ export const EdgeLine = styled( className, startPosition, endPosition, + projectionMatrix, }: { /** * A className string provided by `styled` @@ -33,12 +32,15 @@ export const EdgeLine = styled( * The postion of second point in the line segment. In 'world' coordinates. */ endPosition: Vector2; + /** + * projectionMatrix which can be used to convert `startPosition` and `endPosition` to screen coordinates. + */ + projectionMatrix: Matrix3; }) => { /** * Convert the start and end positions, which are in 'world' coordinates, * to `left` and `top` css values. */ - const projectionMatrix = useSelector(selectors.projectionMatrix); const screenStart = applyMatrix3(startPosition, projectionMatrix); const screenEnd = applyMatrix3(endPosition, projectionMatrix); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/graph_controls.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/graph_controls.tsx index 3170f8bdf867e..a1cd003949a22 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/graph_controls.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/graph_controls.tsx @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo, useContext } from 'react'; import styled from 'styled-components'; import { EuiRange, EuiPanel, EuiIcon } from '@elastic/eui'; import { useSelector, useDispatch } from 'react-redux'; -import { ResolverAction, PanDirection } from '../types'; +import { SideEffectContext } from './side_effect_context'; +import { ResolverAction, Vector2 } from '../types'; import * as selectors from '../store/selectors'; /** @@ -26,6 +27,7 @@ export const GraphControls = styled( }) => { const dispatch: (action: ResolverAction) => unknown = useDispatch(); const scalingFactor = useSelector(selectors.scalingFactor); + const { timestamp } = useContext(SideEffectContext); const handleZoomAmountChange = useCallback( (event: React.ChangeEvent<HTMLInputElement> | React.MouseEvent<HTMLButtonElement>) => { @@ -61,36 +63,45 @@ export const GraphControls = styled( }); }, [dispatch]); - const handlePanClick = (panDirection: PanDirection) => { - return () => { - dispatch({ - type: 'userClickedPanControl', - payload: panDirection, - }); - }; - }; + const [handleNorth, handleEast, handleSouth, handleWest] = useMemo(() => { + const directionVectors: readonly Vector2[] = [ + [0, 1], + [1, 0], + [0, -1], + [-1, 0], + ]; + return directionVectors.map(direction => { + return () => { + const action: ResolverAction = { + type: 'userNudgedCamera', + payload: { direction, time: timestamp() }, + }; + dispatch(action); + }; + }); + }, [dispatch, timestamp]); return ( <div className={className}> <EuiPanel className="panning-controls" paddingSize="none" hasShadow> <div className="panning-controls-top"> - <button className="north-button" title="North" onClick={handlePanClick('north')}> + <button className="north-button" title="North" onClick={handleNorth}> <EuiIcon type="arrowUp" /> </button> </div> <div className="panning-controls-middle"> - <button className="west-button" title="West" onClick={handlePanClick('west')}> + <button className="west-button" title="West" onClick={handleWest}> <EuiIcon type="arrowLeft" /> </button> <button className="center-button" title="Center" onClick={handleCenterClick}> <EuiIcon type="bullseye" /> </button> - <button className="east-button" title="East" onClick={handlePanClick('east')}> + <button className="east-button" title="East" onClick={handleEast}> <EuiIcon type="arrowRight" /> </button> </div> <div className="panning-controls-bottom"> - <button className="south-button" title="South" onClick={handlePanClick('south')}> + <button className="south-button" title="South" onClick={handleSouth}> <EuiIcon type="arrowDown" /> </button> </div> @@ -116,10 +127,6 @@ export const GraphControls = styled( } ) )` - position: absolute; - top: 5px; - left: 5px; - z-index: 1; background-color: #d4d4d4; color: #333333; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx index a69504e3a5db1..d71a4d87b7eab 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -4,151 +4,62 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useState, useEffect } from 'react'; -import { Store } from 'redux'; -import { Provider, useSelector, useDispatch } from 'react-redux'; +import React from 'react'; +import { useSelector } from 'react-redux'; import styled from 'styled-components'; -import { ResolverState, ResolverAction } from '../types'; import * as selectors from '../store/selectors'; -import { useAutoUpdatingClientRect } from './use_autoupdating_client_rect'; -import { useNonPassiveWheelHandler } from './use_nonpassive_wheel_handler'; -import { ProcessEventDot } from './process_event_dot'; import { EdgeLine } from './edge_line'; +import { Panel } from './panel'; import { GraphControls } from './graph_controls'; +import { ProcessEventDot } from './process_event_dot'; +import { useCamera } from './use_camera'; + +const StyledPanel = styled(Panel)` + position: absolute; + left: 1em; + top: 1em; + max-height: calc(100% - 2em); + overflow: auto; + width: 25em; + max-width: 50%; +`; -export const AppRoot = React.memo(({ store }: { store: Store<ResolverState, ResolverAction> }) => { - return ( - <Provider store={store}> - <Resolver /> - </Provider> - ); -}); - -const Resolver = styled( - React.memo(({ className }: { className?: string }) => { - const dispatch: (action: ResolverAction) => unknown = useDispatch(); +const StyledGraphControls = styled(GraphControls)` + position: absolute; + top: 5px; + right: 5px; +`; +export const Resolver = styled( + React.memo(function Resolver({ className }: { className?: string }) { const { processNodePositions, edgeLineSegments } = useSelector( selectors.processNodePositionsAndEdgeLineSegments ); - const [ref, setRef] = useState<null | HTMLDivElement>(null); - - const userIsPanning = useSelector(selectors.userIsPanning); - - const [elementBoundingClientRect, clientRectCallback] = useAutoUpdatingClientRect(); - - const relativeCoordinatesFromMouseEvent = useCallback( - (event: { clientX: number; clientY: number }): null | [number, number] => { - if (elementBoundingClientRect === null) { - return null; - } - return [ - event.clientX - elementBoundingClientRect.x, - event.clientY - elementBoundingClientRect.y, - ]; - }, - [elementBoundingClientRect] - ); - - useEffect(() => { - if (elementBoundingClientRect !== null) { - dispatch({ - type: 'userSetRasterSize', - payload: [elementBoundingClientRect.width, elementBoundingClientRect.height], - }); - } - }, [dispatch, elementBoundingClientRect]); - - const handleMouseDown = useCallback( - (event: React.MouseEvent<HTMLDivElement>) => { - const maybeCoordinates = relativeCoordinatesFromMouseEvent(event); - if (maybeCoordinates !== null) { - dispatch({ - type: 'userStartedPanning', - payload: maybeCoordinates, - }); - } - }, - [dispatch, relativeCoordinatesFromMouseEvent] - ); - - const handleMouseMove = useCallback( - (event: MouseEvent) => { - const maybeCoordinates = relativeCoordinatesFromMouseEvent(event); - if (maybeCoordinates) { - dispatch({ - type: 'userMovedPointer', - payload: maybeCoordinates, - }); - } - }, - [dispatch, relativeCoordinatesFromMouseEvent] - ); - - const handleMouseUp = useCallback(() => { - if (userIsPanning) { - dispatch({ - type: 'userStoppedPanning', - }); - } - }, [dispatch, userIsPanning]); - - const handleWheel = useCallback( - (event: WheelEvent) => { - if ( - elementBoundingClientRect !== null && - event.ctrlKey && - event.deltaY !== 0 && - event.deltaMode === 0 - ) { - event.preventDefault(); - dispatch({ - type: 'userZoomed', - // we use elementBoundingClientRect to interpret pixel deltas as a fraction of the element's height - // when pinch-zooming in on a mac, deltaY is a negative number but we want the payload to be positive - payload: event.deltaY / -elementBoundingClientRect.height, - }); - } - }, - [elementBoundingClientRect, dispatch] - ); - - useEffect(() => { - window.addEventListener('mouseup', handleMouseUp, { passive: true }); - return () => { - window.removeEventListener('mouseup', handleMouseUp); - }; - }, [handleMouseUp]); - - useEffect(() => { - window.addEventListener('mousemove', handleMouseMove, { passive: true }); - return () => { - window.removeEventListener('mousemove', handleMouseMove); - }; - }, [handleMouseMove]); - - const refCallback = useCallback( - (node: null | HTMLDivElement) => { - setRef(node); - clientRectCallback(node); - }, - [clientRectCallback] - ); - - useNonPassiveWheelHandler(handleWheel, ref); + const { projectionMatrix, ref, onMouseDown } = useCamera(); return ( <div data-test-subj="resolverEmbeddable" className={className}> - <GraphControls /> - <div className="resolver-graph" onMouseDown={handleMouseDown} ref={refCallback}> + <div className="resolver-graph" onMouseDown={onMouseDown} ref={ref}> {Array.from(processNodePositions).map(([processEvent, position], index) => ( - <ProcessEventDot key={index} position={position} event={processEvent} /> + <ProcessEventDot + key={index} + position={position} + projectionMatrix={projectionMatrix} + event={processEvent} + /> ))} {edgeLineSegments.map(([startPosition, endPosition], index) => ( - <EdgeLine key={index} startPosition={startPosition} endPosition={endPosition} /> + <EdgeLine + key={index} + startPosition={startPosition} + endPosition={endPosition} + projectionMatrix={projectionMatrix} + /> ))} </div> + <StyledPanel /> + <StyledGraphControls /> </div> ); }) @@ -156,8 +67,11 @@ const Resolver = styled( /** * Take up all availble space */ - display: flex; - flex-grow: 1; + &, + .resolver-graph { + display: flex; + flex-grow: 1; + } /** * The placeholder components use absolute positioning. */ @@ -166,9 +80,4 @@ const Resolver = styled( * Prevent partially visible components from showing up outside the bounds of Resolver. */ overflow: hidden; - - .resolver-graph { - display: flex; - flex-grow: 1; - } `; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx new file mode 100644 index 0000000000000..c75b73b4bceaf --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx @@ -0,0 +1,165 @@ +/* + * 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, { memo, useCallback, useMemo, useContext } from 'react'; +import { EuiPanel, EuiBadge, EuiBasicTableColumn } from '@elastic/eui'; +import { EuiTitle } from '@elastic/eui'; +import { EuiHorizontalRule, EuiInMemoryTable } from '@elastic/eui'; +import euiVars from '@elastic/eui/dist/eui_theme_light.json'; +import { useSelector } from 'react-redux'; +import { i18n } from '@kbn/i18n'; +import { SideEffectContext } from './side_effect_context'; +import { ProcessEvent } from '../types'; +import { useResolverDispatch } from './use_resolver_dispatch'; +import * as selectors from '../store/selectors'; + +const HorizontalRule = memo(function HorizontalRule() { + return ( + <EuiHorizontalRule + style={{ + /** + * Cannot use `styled` to override this because the specificity of EuiHorizontalRule's + * CSS selectors is too high. + */ + marginLeft: `-${euiVars.euiPanelPaddingModifiers.paddingMedium}`, + marginRight: `-${euiVars.euiPanelPaddingModifiers.paddingMedium}`, + /** + * The default width is 100%, but this should be greater. + */ + width: 'auto', + }} + /> + ); +}); + +export const Panel = memo(function Event({ className }: { className?: string }) { + interface ProcessTableView { + name: string; + timestamp?: Date; + event: ProcessEvent; + } + + const { processNodePositions } = useSelector(selectors.processNodePositionsAndEdgeLineSegments); + const { timestamp } = useContext(SideEffectContext); + + const processTableView: ProcessTableView[] = useMemo( + () => + [...processNodePositions.keys()].map(processEvent => { + const { data_buffer } = processEvent; + const date = new Date(data_buffer.timestamp_utc); + return { + name: data_buffer.process_name, + timestamp: isFinite(date.getTime()) ? date : undefined, + event: processEvent, + }; + }), + [processNodePositions] + ); + + const formatter = new Intl.DateTimeFormat(i18n.getLocale(), { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }); + + const dispatch = useResolverDispatch(); + + const handleBringIntoViewClick = useCallback( + processTableViewItem => { + dispatch({ + type: 'userBroughtProcessIntoView', + payload: { + time: timestamp(), + process: processTableViewItem.event, + }, + }); + }, + [dispatch, timestamp] + ); + + const columns = useMemo<Array<EuiBasicTableColumn<ProcessTableView>>>( + () => [ + { + field: 'name', + name: i18n.translate('xpack.endpoint.resolver.panel.tabel.row.processNameTitle', { + defaultMessage: 'Process Name', + }), + sortable: true, + truncateText: true, + render(name: string) { + return name === '' ? ( + <EuiBadge color="warning"> + {i18n.translate('xpack.endpoint.resolver.panel.table.row.valueMissingDescription', { + defaultMessage: 'Value is missing', + })} + </EuiBadge> + ) : ( + name + ); + }, + }, + { + field: 'timestamp', + name: i18n.translate('xpack.endpoint.resolver.panel.tabel.row.timestampTitle', { + defaultMessage: 'Timestamp', + }), + dataType: 'date', + sortable: true, + render(eventTimestamp?: Date) { + return eventTimestamp ? ( + formatter.format(eventTimestamp) + ) : ( + <EuiBadge color="warning"> + {i18n.translate('xpack.endpoint.resolver.panel.tabel.row.timestampInvalidLabel', { + defaultMessage: 'invalid', + })} + </EuiBadge> + ); + }, + }, + { + name: i18n.translate('xpack.endpoint.resolver.panel.tabel.row.actionsTitle', { + defaultMessage: 'Actions', + }), + actions: [ + { + name: i18n.translate( + 'xpack.endpoint.resolver.panel.tabel.row.actions.bringIntoViewButtonLabel', + { + defaultMessage: 'Bring into view', + } + ), + description: i18n.translate( + 'xpack.endpoint.resolver.panel.tabel.row.bringIntoViewLabel', + { + defaultMessage: 'Bring the process into view on the map.', + } + ), + type: 'icon', + icon: 'flag', + onClick: handleBringIntoViewClick, + }, + ], + }, + ], + [formatter, handleBringIntoViewClick] + ); + return ( + <EuiPanel className={className}> + <EuiTitle size="xs"> + <h4> + {i18n.translate('xpack.endpoint.resolver.panel.title', { + defaultMessage: 'Processes', + })} + </h4> + </EuiTitle> + <HorizontalRule /> + <EuiInMemoryTable<ProcessTableView> items={processTableView} columns={columns} sorting /> + </EuiPanel> + ); +}); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx index 5c3a253d619ef..384fbf90ed984 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx @@ -6,10 +6,8 @@ import React from 'react'; import styled from 'styled-components'; -import { useSelector } from 'react-redux'; import { applyMatrix3 } from '../lib/vector2'; -import { Vector2, ProcessEvent } from '../types'; -import * as selectors from '../store/selectors'; +import { Vector2, ProcessEvent, Matrix3 } from '../types'; /** * A placeholder view for a process node. @@ -20,6 +18,7 @@ export const ProcessEventDot = styled( className, position, event, + projectionMatrix, }: { /** * A `className` string provided by `styled` @@ -33,12 +32,16 @@ export const ProcessEventDot = styled( * An event which contains details about the process node. */ event: ProcessEvent; + /** + * projectionMatrix which can be used to convert `position` to screen coordinates. + */ + projectionMatrix: Matrix3; }) => { /** * Convert the position, which is in 'world' coordinates, to screen coordinates. */ - const projectionMatrix = useSelector(selectors.projectionMatrix); const [left, top] = applyMatrix3(position, projectionMatrix); + const style = { left: (left - 20).toString() + 'px', top: (top - 20).toString() + 'px', diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/side_effect_context.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/view/side_effect_context.ts new file mode 100644 index 0000000000000..ab7f41d815026 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/side_effect_context.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { createContext, Context } from 'react'; +import ResizeObserver from 'resize-observer-polyfill'; +import { SideEffectors } from '../types'; + +/** + * React context that provides 'side-effectors' which we need to mock during testing. + */ +const sideEffectors: SideEffectors = { + timestamp: () => Date.now(), + requestAnimationFrame(...args) { + return window.requestAnimationFrame(...args); + }, + cancelAnimationFrame(...args) { + return window.cancelAnimationFrame(...args); + }, + ResizeObserver, +}; + +/** + * The default values are used in production, tests can provide mock values using `SideEffectSimulator`. + */ +export const SideEffectContext: Context<SideEffectors> = createContext(sideEffectors); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/side_effect_simulator.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/view/side_effect_simulator.ts new file mode 100644 index 0000000000000..3e80b6a8459f7 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/side_effect_simulator.ts @@ -0,0 +1,170 @@ +/* + * 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 { act } from '@testing-library/react'; +import { SideEffectSimulator } from '../types'; + +/** + * Create mock `SideEffectors` for `SideEffectContext.Provider`. The `control` + * object is used to control the mocks. + */ +export const sideEffectSimulator: () => SideEffectSimulator = () => { + // The set of mock `ResizeObserver` instances that currently exist + const resizeObserverInstances: Set<MockResizeObserver> = new Set(); + + // A map of `Element`s to their fake `DOMRect`s + const contentRects: Map<Element, DOMRect> = new Map(); + + /** + * Simulate an element's size changing. This will trigger any `ResizeObserverCallback`s which + * are listening for this element's size changes. It will also cause `element.getBoundingClientRect` to + * return `contentRect` + */ + const simulateElementResize: (target: Element, contentRect: DOMRect) => void = ( + target, + contentRect + ) => { + contentRects.set(target, contentRect); + for (const instance of resizeObserverInstances) { + instance.simulateElementResize(target, contentRect); + } + }; + + /** + * Get the simulate `DOMRect` for `element`. + */ + const contentRectForElement: (target: Element) => DOMRect = target => { + if (contentRects.has(target)) { + return contentRects.get(target)!; + } + const domRect: DOMRect = { + x: 0, + y: 0, + top: 0, + right: 0, + bottom: 0, + left: 0, + width: 0, + height: 0, + toJSON() { + return this; + }, + }; + return domRect; + }; + + /** + * Change `Element.prototype.getBoundingClientRect` to return our faked values. + */ + jest + .spyOn(Element.prototype, 'getBoundingClientRect') + .mockImplementation(function(this: Element) { + return contentRectForElement(this); + }); + + /** + * A mock implementation of `ResizeObserver` that works with our fake `getBoundingClientRect` and `simulateElementResize` + */ + class MockResizeObserver implements ResizeObserver { + constructor(private readonly callback: ResizeObserverCallback) { + resizeObserverInstances.add(this); + } + private elements: Set<Element> = new Set(); + /** + * Simulate `target` changing it size to `contentRect`. + */ + simulateElementResize(target: Element, contentRect: DOMRect) { + if (this.elements.has(target)) { + const entries: ResizeObserverEntry[] = [{ target, contentRect }]; + this.callback(entries, this); + } + } + observe(target: Element) { + this.elements.add(target); + } + unobserve(target: Element) { + this.elements.delete(target); + } + disconnect() { + this.elements.clear(); + } + } + + /** + * milliseconds since epoch, faked. + */ + let mockTime: number = 0; + + /** + * A counter allowing us to give a unique ID for each call to `requestAnimationFrame`. + */ + let frameRequestedCallbacksIDCounter: number = 0; + + /** + * A map of requestAnimationFrame IDs to the related callbacks. + */ + const frameRequestedCallbacks: Map<number, FrameRequestCallback> = new Map(); + + /** + * Trigger any pending `requestAnimationFrame` callbacks. Passes `mockTime` as the timestamp. + */ + const provideAnimationFrame: () => void = () => { + act(() => { + // Iterate the values, and clear the data set before calling the callbacks because the callbacks will repopulate the dataset synchronously in this testing framework. + const values = [...frameRequestedCallbacks.values()]; + frameRequestedCallbacks.clear(); + for (const callback of values) { + callback(mockTime); + } + }); + }; + + /** + * Provide a fake ms timestamp + */ + const timestamp = jest.fn(() => mockTime); + + /** + * Fake `requestAnimationFrame`. + */ + const requestAnimationFrame = jest.fn((callback: FrameRequestCallback): number => { + const id = frameRequestedCallbacksIDCounter++; + frameRequestedCallbacks.set(id, callback); + return id; + }); + + /** + * fake `cancelAnimationFrame`. + */ + const cancelAnimationFrame = jest.fn((id: number) => { + frameRequestedCallbacks.delete(id); + }); + + const retval: SideEffectSimulator = { + controls: { + provideAnimationFrame, + + /** + * Change the mock time value + */ + set time(nextTime: number) { + mockTime = nextTime; + }, + get time() { + return mockTime; + }, + + simulateElementResize, + }, + mock: { + requestAnimationFrame, + cancelAnimationFrame, + timestamp, + ResizeObserver: MockResizeObserver, + }, + }; + return retval; +}; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx deleted file mode 100644 index 5f13995de1c2a..0000000000000 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_autoupdating_client_rect.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { useCallback, useState, useEffect, useRef } from 'react'; -import ResizeObserver from 'resize-observer-polyfill'; - -/** - * Returns a nullable DOMRect and a ref callback. Pass the refCallback to the - * `ref` property of a native element and this hook will return a DOMRect for - * it by calling `getBoundingClientRect`. This hook will observe the element - * with a resize observer and call getBoundingClientRect again after resizes. - * - * Note that the changes to the position of the element aren't automatically - * tracked. So if the element's position moves for some reason, be sure to - * handle that. - */ -export function useAutoUpdatingClientRect(): [DOMRect | null, (node: Element | null) => void] { - const [rect, setRect] = useState<DOMRect | null>(null); - const nodeRef = useRef<Element | null>(null); - const ref = useCallback((node: Element | null) => { - nodeRef.current = node; - if (node !== null) { - setRect(node.getBoundingClientRect()); - } - }, []); - useEffect(() => { - if (nodeRef.current !== null) { - const resizeObserver = new ResizeObserver(entries => { - if (nodeRef.current !== null && nodeRef.current === entries[0].target) { - setRect(nodeRef.current.getBoundingClientRect()); - } - }); - resizeObserver.observe(nodeRef.current); - return () => { - resizeObserver.disconnect(); - }; - } - }, [nodeRef]); - return [rect, ref]; -} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx new file mode 100644 index 0000000000000..85e1d4e694b15 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx @@ -0,0 +1,197 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * This import must be hoisted as it uses `jest.mock`. Is there a better way? Mocking is not good. + */ +import React from 'react'; +import { render, act, RenderResult, fireEvent } from '@testing-library/react'; +import { useCamera } from './use_camera'; +import { Provider } from 'react-redux'; +import * as selectors from '../store/selectors'; +import { storeFactory } from '../store'; +import { + Matrix3, + ResolverAction, + ResolverStore, + ProcessEvent, + SideEffectSimulator, +} from '../types'; +import { SideEffectContext } from './side_effect_context'; +import { applyMatrix3 } from '../lib/vector2'; +import { sideEffectSimulator } from './side_effect_simulator'; + +describe('useCamera on an unpainted element', () => { + let element: HTMLElement; + let projectionMatrix: Matrix3; + const testID = 'camera'; + let reactRenderResult: RenderResult; + let store: ResolverStore; + let simulator: SideEffectSimulator; + beforeEach(async () => { + ({ store } = storeFactory()); + + const Test = function Test() { + const camera = useCamera(); + const { ref, onMouseDown } = camera; + projectionMatrix = camera.projectionMatrix; + return <div data-testid={testID} onMouseDown={onMouseDown} ref={ref} />; + }; + + simulator = sideEffectSimulator(); + + reactRenderResult = render( + <Provider store={store}> + <SideEffectContext.Provider value={simulator.mock}> + <Test /> + </SideEffectContext.Provider> + </Provider> + ); + + const { findByTestId } = reactRenderResult; + element = await findByTestId(testID); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('should be usable in React', async () => { + expect(element).toBeInTheDocument(); + }); + test('returns a projectionMatrix that changes everything to 0', () => { + expect(applyMatrix3([0, 0], projectionMatrix)).toEqual([0, 0]); + }); + describe('which has been resized to 800x600', () => { + const width = 800; + const height = 600; + const leftMargin = 20; + const topMargin = 20; + const centerX = width / 2 + leftMargin; + const centerY = height / 2 + topMargin; + beforeEach(() => { + act(() => { + simulator.controls.simulateElementResize(element, { + width, + height, + left: leftMargin, + top: topMargin, + right: leftMargin + width, + bottom: topMargin + height, + x: leftMargin, + y: topMargin, + toJSON() { + return this; + }, + }); + }); + }); + test('provides a projection matrix that inverts the y axis and translates 400,300 (center of the element)', () => { + expect(applyMatrix3([0, 0], projectionMatrix)).toEqual([400, 300]); + }); + describe('when the user presses the mousedown button in the middle of the element', () => { + beforeEach(() => { + fireEvent.mouseDown(element, { + clientX: centerX, + clientY: centerY, + }); + }); + describe('when the user moves the mouse 50 pixels to the right', () => { + beforeEach(() => { + fireEvent.mouseMove(element, { + clientX: centerX + 50, + clientY: centerY, + }); + }); + it('should project [0, 0] in world corrdinates 50 pixels to the right of the center of the element', () => { + expect(applyMatrix3([0, 0], projectionMatrix)).toEqual([450, 300]); + }); + }); + }); + + describe('when the user uses the mousewheel w/ ctrl held down', () => { + beforeEach(() => { + fireEvent.wheel(element, { + ctrlKey: true, + deltaY: -10, + deltaMode: 0, + }); + }); + it('should zoom in', () => { + expect(projectionMatrix).toMatchInlineSnapshot(` + Array [ + 1.0635255481707058, + 0, + 400, + 0, + -1.0635255481707058, + 300, + 0, + 0, + 0, + ] + `); + }); + }); + + it('should not initially request an animation frame', () => { + expect(simulator.mock.requestAnimationFrame).not.toHaveBeenCalled(); + }); + describe('when the camera begins animation', () => { + let process: ProcessEvent; + beforeEach(() => { + // At this time, processes are provided via mock data. In the future, this test will have to provide those mocks. + const processes: ProcessEvent[] = [ + ...selectors + .processNodePositionsAndEdgeLineSegments(store.getState()) + .processNodePositions.keys(), + ]; + process = processes[processes.length - 1]; + simulator.controls.time = 0; + const action: ResolverAction = { + type: 'userBroughtProcessIntoView', + payload: { + time: simulator.controls.time, + process, + }, + }; + act(() => { + store.dispatch(action); + }); + }); + + it('should request animation frames in a loop', () => { + const animationDuration = 1000; + // When the animation begins, the camera should request an animation frame. + expect(simulator.mock.requestAnimationFrame).toHaveBeenCalledTimes(1); + + // Update the time so that the animation is partially complete. + simulator.controls.time = animationDuration / 5; + // Provide the animation frame, allowing the camera to rerender. + simulator.controls.provideAnimationFrame(); + + // The animation is not complete, so the camera should request another animation frame. + expect(simulator.mock.requestAnimationFrame).toHaveBeenCalledTimes(2); + + // Update the camera so that the animation is nearly complete. + simulator.controls.time = (animationDuration / 10) * 9; + + // Provide the animation frame + simulator.controls.provideAnimationFrame(); + + // Since the animation isn't complete, it should request another frame + expect(simulator.mock.requestAnimationFrame).toHaveBeenCalledTimes(3); + + // Animation lasts 1000ms, so this should end it + simulator.controls.time = animationDuration * 1.1; + + // Provide the last frame + simulator.controls.provideAnimationFrame(); + + // Since animation is complete, it should not have requseted another frame + expect(simulator.mock.requestAnimationFrame).toHaveBeenCalledTimes(3); + }); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.ts new file mode 100644 index 0000000000000..54940b8383f7a --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.ts @@ -0,0 +1,307 @@ +/* + * 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, { + useCallback, + useState, + useEffect, + useRef, + useLayoutEffect, + useContext, +} from 'react'; +import { useSelector } from 'react-redux'; +import { SideEffectContext } from './side_effect_context'; +import { Matrix3 } from '../types'; +import { useResolverDispatch } from './use_resolver_dispatch'; +import * as selectors from '../store/selectors'; + +export function useCamera(): { + /** + * A function to pass to a React element's `ref` property. Used to attach + * native event listeners and to measure the DOM node. + */ + ref: (node: HTMLDivElement | null) => void; + onMouseDown: React.MouseEventHandler<HTMLElement>; + /** + * A 3x3 transformation matrix used to convert a `vector2` from 'world' coordinates + * to screen coordinates. + */ + projectionMatrix: Matrix3; +} { + const dispatch = useResolverDispatch(); + const sideEffectors = useContext(SideEffectContext); + + const [ref, setRef] = useState<null | HTMLDivElement>(null); + + /** + * The position of a thing, as a `Vector2`, is multiplied by the projection matrix + * to determine where it belongs on the screen. + * The projection matrix changes over time if the camera is currently animating. + */ + const projectionMatrixAtTime = useSelector(selectors.projectionMatrix); + + /** + * Use a ref to refer to the `projectionMatrixAtTime` function. The rAF loop + * accesses this and sets state during the rAF cycle. If the rAF loop + * effect read this directly from the selector, the rAF loop would need to + * be re-inited each time this function changed. The `projectionMatrixAtTime` function + * changes each frame during an animation, so the rAF loop would be causing + * itself to reinit on each frame. This would necessarily cause a drop in FPS as there + * would be a dead zone between when the rAF loop stopped and restarted itself. + */ + const projectionMatrixAtTimeRef = useRef<typeof projectionMatrixAtTime>(); + + /** + * The projection matrix is stateful, depending on the current time. + * When the projection matrix changes, the component should be rerendered. + */ + const [projectionMatrix, setProjectionMatrix] = useState<Matrix3>( + projectionMatrixAtTime(sideEffectors.timestamp()) + ); + + const userIsPanning = useSelector(selectors.userIsPanning); + const isAnimatingAtTime = useSelector(selectors.isAnimating); + + const [elementBoundingClientRect, clientRectCallback] = useAutoUpdatingClientRect(); + + /** + * For an event with clientX and clientY, return [clientX, clientY] - the top left corner of the `ref` element + */ + const relativeCoordinatesFromMouseEvent = useCallback( + (event: { clientX: number; clientY: number }): null | [number, number] => { + if (elementBoundingClientRect === null) { + return null; + } + return [ + event.clientX - elementBoundingClientRect.x, + event.clientY - elementBoundingClientRect.y, + ]; + }, + [elementBoundingClientRect] + ); + + const handleMouseDown = useCallback( + (event: React.MouseEvent<HTMLDivElement>) => { + const maybeCoordinates = relativeCoordinatesFromMouseEvent(event); + if (maybeCoordinates !== null) { + dispatch({ + type: 'userStartedPanning', + payload: { screenCoordinates: maybeCoordinates, time: sideEffectors.timestamp() }, + }); + } + }, + [dispatch, relativeCoordinatesFromMouseEvent, sideEffectors] + ); + + const handleMouseMove = useCallback( + (event: MouseEvent) => { + const maybeCoordinates = relativeCoordinatesFromMouseEvent(event); + if (maybeCoordinates) { + dispatch({ + type: 'userMovedPointer', + payload: { + screenCoordinates: maybeCoordinates, + time: sideEffectors.timestamp(), + }, + }); + } + }, + [dispatch, relativeCoordinatesFromMouseEvent, sideEffectors] + ); + + const handleMouseUp = useCallback(() => { + if (userIsPanning) { + dispatch({ + type: 'userStoppedPanning', + payload: { + time: sideEffectors.timestamp(), + }, + }); + } + }, [dispatch, sideEffectors, userIsPanning]); + + const handleWheel = useCallback( + (event: WheelEvent) => { + if ( + elementBoundingClientRect !== null && + event.ctrlKey && + event.deltaY !== 0 && + event.deltaMode === 0 + ) { + event.preventDefault(); + dispatch({ + type: 'userZoomed', + payload: { + /** + * we use elementBoundingClientRect to interpret pixel deltas as a fraction of the element's height + * when pinch-zooming in on a mac, deltaY is a negative number but we want the payload to be positive + */ + zoomChange: event.deltaY / -elementBoundingClientRect.height, + time: sideEffectors.timestamp(), + }, + }); + } + }, + [elementBoundingClientRect, dispatch, sideEffectors] + ); + + const refCallback = useCallback( + (node: null | HTMLDivElement) => { + setRef(node); + clientRectCallback(node); + }, + [clientRectCallback] + ); + + useEffect(() => { + window.addEventListener('mouseup', handleMouseUp, { passive: true }); + return () => { + window.removeEventListener('mouseup', handleMouseUp); + }; + }, [handleMouseUp]); + + useEffect(() => { + window.addEventListener('mousemove', handleMouseMove, { passive: true }); + return () => { + window.removeEventListener('mousemove', handleMouseMove); + }; + }, [handleMouseMove]); + + /** + * Register an event handler directly on `elementRef` for the `wheel` event, with no options + * React sets native event listeners on the `window` and calls provided handlers via event propagation. + * As of Chrome 73, `'wheel'` events on `window` are automatically treated as 'passive'. + * If you don't need to call `event.preventDefault` then you should use regular React event handling instead. + */ + useEffect(() => { + if (ref !== null) { + ref.addEventListener('wheel', handleWheel); + return () => { + ref.removeEventListener('wheel', handleWheel); + }; + } + }, [ref, handleWheel]); + + /** + * Allow rAF loop to indirectly read projectionMatrixAtTime via a ref. Since it also + * sets projectionMatrixAtTime, relying directly on it causes considerable jank. + */ + useLayoutEffect(() => { + projectionMatrixAtTimeRef.current = projectionMatrixAtTime; + }, [projectionMatrixAtTime]); + + /** + * Keep the projection matrix state in sync with the selector. + * This isn't needed during animation. + */ + useLayoutEffect(() => { + // Update the projection matrix that we return, rerendering any component that uses this. + setProjectionMatrix(projectionMatrixAtTime(sideEffectors.timestamp())); + }, [projectionMatrixAtTime, sideEffectors]); + + /** + * When animation is happening, run a rAF loop, when it is done, stop. + */ + useLayoutEffect( + () => { + const startDate = sideEffectors.timestamp(); + if (isAnimatingAtTime(startDate)) { + let rafRef: null | number = null; + const handleFrame = () => { + // Get the current timestamp, now that the frame is ready + const date = sideEffectors.timestamp(); + if (projectionMatrixAtTimeRef.current !== undefined) { + // Update the projection matrix, triggering a rerender + setProjectionMatrix(projectionMatrixAtTimeRef.current(date)); + } + // If we are still animating, request another frame, continuing the loop + if (isAnimatingAtTime(date)) { + rafRef = sideEffectors.requestAnimationFrame(handleFrame); + } else { + /** + * `isAnimatingAtTime` was false, meaning that the animation is complete. + * Do not request another animation frame. + */ + rafRef = null; + } + }; + // Kick off the loop by requestion an animation frame + rafRef = sideEffectors.requestAnimationFrame(handleFrame); + + /** + * This function cancels the animation frame request. The cancel function + * will occur when the component is unmounted. It will also occur when a dependency + * changes. + * + * The `isAnimatingAtTime` dependency will be changed if the animation state changes. The animation + * state only changes when the user animates again (e.g. brings a different node into view, or nudges the + * camera.) + */ + return () => { + // Cancel the animation frame. + if (rafRef !== null) { + sideEffectors.cancelAnimationFrame(rafRef); + } + }; + } + }, + /** + * `isAnimatingAtTime` is a function created with `reselect`. The function reference will be changed when + * the animation state changes. When the function reference has changed, you *might* be animating. + */ + [isAnimatingAtTime, sideEffectors] + ); + + useEffect(() => { + if (elementBoundingClientRect !== null) { + dispatch({ + type: 'userSetRasterSize', + payload: [elementBoundingClientRect.width, elementBoundingClientRect.height], + }); + } + }, [dispatch, elementBoundingClientRect]); + + return { + ref: refCallback, + onMouseDown: handleMouseDown, + projectionMatrix, + }; +} + +/** + * Returns a nullable DOMRect and a ref callback. Pass the refCallback to the + * `ref` property of a native element and this hook will return a DOMRect for + * it by calling `getBoundingClientRect`. This hook will observe the element + * with a resize observer and call getBoundingClientRect again after resizes. + * + * Note that the changes to the position of the element aren't automatically + * tracked. So if the element's position moves for some reason, be sure to + * handle that. + */ +function useAutoUpdatingClientRect(): [DOMRect | null, (node: Element | null) => void] { + const [rect, setRect] = useState<DOMRect | null>(null); + const nodeRef = useRef<Element | null>(null); + const ref = useCallback((node: Element | null) => { + nodeRef.current = node; + if (node !== null) { + setRect(node.getBoundingClientRect()); + } + }, []); + const { ResizeObserver } = useContext(SideEffectContext); + useEffect(() => { + if (nodeRef.current !== null) { + const resizeObserver = new ResizeObserver(entries => { + if (nodeRef.current !== null && nodeRef.current === entries[0].target) { + setRect(nodeRef.current.getBoundingClientRect()); + } + }); + resizeObserver.observe(nodeRef.current); + return () => { + resizeObserver.disconnect(); + }; + } + }, [ResizeObserver, nodeRef]); + return [rect, ref]; +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_nonpassive_wheel_handler.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_nonpassive_wheel_handler.tsx deleted file mode 100644 index a0738bcf4d14c..0000000000000 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_nonpassive_wheel_handler.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { useEffect } from 'react'; -/** - * Register an event handler directly on `elementRef` for the `wheel` event, with no options - * React sets native event listeners on the `window` and calls provided handlers via event propagation. - * As of Chrome 73, `'wheel'` events on `window` are automatically treated as 'passive'. - * If you don't need to call `event.preventDefault` then you should use regular React event handling instead. - */ -export function useNonPassiveWheelHandler( - handler: (event: WheelEvent) => void, - elementRef: HTMLElement | null -) { - useEffect(() => { - if (elementRef !== null) { - elementRef.addEventListener('wheel', handler); - return () => { - elementRef.removeEventListener('wheel', handler); - }; - } - }, [elementRef, handler]); -} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_resolver_dispatch.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_resolver_dispatch.ts new file mode 100644 index 0000000000000..a993a4ed595e1 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_resolver_dispatch.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useDispatch } from 'react-redux'; +import { ResolverAction } from '../types'; + +/** + * Call `useDispatch`, but only accept `ResolverAction` actions. + */ +export const useResolverDispatch: () => (action: ResolverAction) => unknown = useDispatch; diff --git a/x-pack/plugins/graph/config.ts b/x-pack/plugins/graph/config.ts new file mode 100644 index 0000000000000..3838d6ca34ba4 --- /dev/null +++ b/x-pack/plugins/graph/config.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), + savePolicy: schema.oneOf( + [ + schema.literal('none'), + schema.literal('config'), + schema.literal('configAndData'), + schema.literal('configAndDataWithConsent'), + ], + { defaultValue: 'configAndData' } + ), + canEditDrillDownUrls: schema.boolean({ defaultValue: true }), +}); + +export type ConfigSchema = TypeOf<typeof configSchema>; diff --git a/x-pack/plugins/graph/public/index.ts b/x-pack/plugins/graph/public/index.ts index ac9ca960c0c7f..7b2ce67631713 100644 --- a/x-pack/plugins/graph/public/index.ts +++ b/x-pack/plugins/graph/public/index.ts @@ -4,6 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { PluginInitializerContext } from 'kibana/public'; import { GraphPlugin } from './plugin'; +import { ConfigSchema } from '../config'; -export const plugin = () => new GraphPlugin(); +export const plugin = (initializerContext: PluginInitializerContext<ConfigSchema>) => + new GraphPlugin(initializerContext); + +export { GraphSetup } from './plugin'; diff --git a/x-pack/plugins/graph/public/plugin.ts b/x-pack/plugins/graph/public/plugin.ts index c0cec14e04d61..e911b400349f8 100644 --- a/x-pack/plugins/graph/public/plugin.ts +++ b/x-pack/plugins/graph/public/plugin.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart } from 'kibana/public'; import { Plugin } from 'src/core/public'; +import { PluginInitializerContext } from 'kibana/public'; import { toggleNavLink } from './services/toggle_nav_link'; import { LicensingPluginSetup } from '../../licensing/public'; import { checkLicense } from '../common/check_license'; @@ -14,15 +15,18 @@ import { FeatureCatalogueCategory, HomePublicPluginSetup, } from '../../../../src/plugins/home/public'; +import { ConfigSchema } from '../config'; export interface GraphPluginSetupDependencies { licensing: LicensingPluginSetup; home?: HomePublicPluginSetup; } -export class GraphPlugin implements Plugin { +export class GraphPlugin implements Plugin<{ config: Readonly<ConfigSchema> }, void> { private licensing: LicensingPluginSetup | null = null; + constructor(private initializerContext: PluginInitializerContext<ConfigSchema>) {} + setup(core: CoreSetup, { licensing, home }: GraphPluginSetupDependencies) { this.licensing = licensing; @@ -39,6 +43,16 @@ export class GraphPlugin implements Plugin { category: FeatureCatalogueCategory.DATA, }); } + + return { + /** + * The configuration is temporarily exposed to allow the legacy graph plugin to consume + * the setting. Once the graph plugin is migrated completely, this will become an implementation + * detail. + * @deprecated + */ + config: this.initializerContext.config.get(), + }; } start(core: CoreStart) { @@ -52,3 +66,5 @@ export class GraphPlugin implements Plugin { stop() {} } + +export type GraphSetup = ReturnType<GraphPlugin['setup']>; diff --git a/x-pack/plugins/graph/server/index.ts b/x-pack/plugins/graph/server/index.ts index ac9ca960c0c7f..a5900e1778b90 100644 --- a/x-pack/plugins/graph/server/index.ts +++ b/x-pack/plugins/graph/server/index.ts @@ -4,6 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import { PluginConfigDescriptor } from 'kibana/server'; + +import { configSchema, ConfigSchema } from '../config'; import { GraphPlugin } from './plugin'; export const plugin = () => new GraphPlugin(); + +export const config: PluginConfigDescriptor<ConfigSchema> = { + exposeToBrowser: { + canEditDrillDownUrls: true, + savePolicy: true, + }, + schema: configSchema, +}; diff --git a/x-pack/plugins/searchprofiler/public/README.md b/x-pack/plugins/searchprofiler/public/README.md deleted file mode 100644 index 3cf79162b3965..0000000000000 --- a/x-pack/plugins/searchprofiler/public/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## Please note - -See x-pack/legacy/plugins/searchprofiler/public for styles. diff --git a/x-pack/plugins/searchprofiler/public/index.scss b/x-pack/plugins/searchprofiler/public/index.scss new file mode 100644 index 0000000000000..370ec54a85539 --- /dev/null +++ b/x-pack/plugins/searchprofiler/public/index.scss @@ -0,0 +1 @@ +@import 'styles/index' diff --git a/x-pack/plugins/searchprofiler/public/index.ts b/x-pack/plugins/searchprofiler/public/index.ts index 3d77f703b42cd..33952a747018e 100644 --- a/x-pack/plugins/searchprofiler/public/index.ts +++ b/x-pack/plugins/searchprofiler/public/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import './styles/_index.scss'; import { PluginInitializerContext } from 'src/core/public'; import { SearchProfilerUIPlugin } from './plugin'; diff --git a/x-pack/legacy/plugins/searchprofiler/public/styles/_index.scss b/x-pack/plugins/searchprofiler/public/styles/_index.scss similarity index 100% rename from x-pack/legacy/plugins/searchprofiler/public/styles/_index.scss rename to x-pack/plugins/searchprofiler/public/styles/_index.scss diff --git a/x-pack/legacy/plugins/searchprofiler/public/styles/_mixins.scss b/x-pack/plugins/searchprofiler/public/styles/_mixins.scss similarity index 100% rename from x-pack/legacy/plugins/searchprofiler/public/styles/_mixins.scss rename to x-pack/plugins/searchprofiler/public/styles/_mixins.scss diff --git a/x-pack/legacy/plugins/searchprofiler/public/styles/components/_highlight_details_flyout.scss b/x-pack/plugins/searchprofiler/public/styles/components/_highlight_details_flyout.scss similarity index 100% rename from x-pack/legacy/plugins/searchprofiler/public/styles/components/_highlight_details_flyout.scss rename to x-pack/plugins/searchprofiler/public/styles/components/_highlight_details_flyout.scss diff --git a/x-pack/legacy/plugins/searchprofiler/public/styles/components/_percentage_badge.scss b/x-pack/plugins/searchprofiler/public/styles/components/_percentage_badge.scss similarity index 100% rename from x-pack/legacy/plugins/searchprofiler/public/styles/components/_percentage_badge.scss rename to x-pack/plugins/searchprofiler/public/styles/components/_percentage_badge.scss diff --git a/x-pack/legacy/plugins/searchprofiler/public/styles/components/_profile_tree.scss b/x-pack/plugins/searchprofiler/public/styles/components/_profile_tree.scss similarity index 100% rename from x-pack/legacy/plugins/searchprofiler/public/styles/components/_profile_tree.scss rename to x-pack/plugins/searchprofiler/public/styles/components/_profile_tree.scss diff --git a/x-pack/legacy/plugins/searchprofiler/public/styles/containers/_main.scss b/x-pack/plugins/searchprofiler/public/styles/containers/_main.scss similarity index 100% rename from x-pack/legacy/plugins/searchprofiler/public/styles/containers/_main.scss rename to x-pack/plugins/searchprofiler/public/styles/containers/_main.scss diff --git a/x-pack/legacy/plugins/searchprofiler/public/styles/containers/_profile_query_editor.scss b/x-pack/plugins/searchprofiler/public/styles/containers/_profile_query_editor.scss similarity index 100% rename from x-pack/legacy/plugins/searchprofiler/public/styles/containers/_profile_query_editor.scss rename to x-pack/plugins/searchprofiler/public/styles/containers/_profile_query_editor.scss diff --git a/x-pack/plugins/security/public/_index.scss b/x-pack/plugins/security/public/index.scss similarity index 68% rename from x-pack/plugins/security/public/_index.scss rename to x-pack/plugins/security/public/index.scss index 9fa81bad7c3f4..1bdb8cc178fdf 100644 --- a/x-pack/plugins/security/public/_index.scss +++ b/x-pack/plugins/security/public/index.scss @@ -1,2 +1,4 @@ +$secFormWidth: 460px; + // Management styles @import './management/index'; diff --git a/x-pack/plugins/security/public/index.ts b/x-pack/plugins/security/public/index.ts index 712f49afd306e..1c525dc6b9187 100644 --- a/x-pack/plugins/security/public/index.ts +++ b/x-pack/plugins/security/public/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import './index.scss'; import { PluginInitializer } from 'src/core/public'; import { SecurityPlugin, SecurityPluginSetup, SecurityPluginStart } from './plugin'; diff --git a/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx index 6dcf330ec6f9e..75621762b1b85 100644 --- a/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx +++ b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx @@ -72,6 +72,7 @@ export class ChangePasswordForm extends Component<Props, State> { } > <EuiFieldText + autoComplete="off" data-test-subj="currentPassword" type="password" value={this.state.currentPassword} @@ -99,6 +100,7 @@ export class ChangePasswordForm extends Component<Props, State> { } > <EuiFieldText + autoComplete="new-password" data-test-subj="newPassword" type="password" value={this.state.newPassword} @@ -118,6 +120,7 @@ export class ChangePasswordForm extends Component<Props, State> { } > <EuiFieldText + autoComplete="new-password" data-test-subj="confirmNewPassword" type="password" value={this.state.confirmPassword} diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx index 576f3ff9e6008..8e7d9fb2dac08 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx @@ -204,6 +204,7 @@ export class EditUserPage extends Component<Props, State> { {...this.validator.validatePassword(this.state.user)} > <EuiFieldText + autoComplete="new-password" data-test-subj="passwordInput" name="password" type="password" @@ -218,6 +219,7 @@ export class EditUserPage extends Component<Props, State> { {...this.validator.validateConfirmPassword(this.state.user)} > <EuiFieldText + autoComplete="new-password" data-test-subj="passwordConfirmationInput" onChange={this.onConfirmPasswordChange} name="confirm_password" diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index 8be1762133db6..65874ba3a461e 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -28,7 +28,6 @@ function getMockOptions(config: Partial<AuthenticatorOptions['config']> = {}) { clusterClient: elasticsearchServiceMock.createClusterClient(), basePath: httpServiceMock.createSetupContract().basePath, loggers: loggingServiceMock.create(), - isSystemAPIRequest: jest.fn(), config: { session: { idleTimeout: null, lifespan: null }, authc: { providers: [], oidc: {}, saml: {} }, @@ -286,10 +285,11 @@ describe('Authenticator', () => { it('creates session whenever authentication provider returns state for system API requests', async () => { const user = mockAuthenticatedUser(); - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'true' }, + }); const authorization = `Basic ${Buffer.from('foo:bar').toString('base64')}`; - mockOptions.isSystemAPIRequest.mockReturnValue(true); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user, { state: { authorization } }) ); @@ -307,10 +307,11 @@ describe('Authenticator', () => { it('creates session whenever authentication provider returns state for non-system API requests', async () => { const user = mockAuthenticatedUser(); - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'false' }, + }); const authorization = `Basic ${Buffer.from('foo:bar').toString('base64')}`; - mockOptions.isSystemAPIRequest.mockReturnValue(false); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user, { state: { authorization } }) ); @@ -328,9 +329,10 @@ describe('Authenticator', () => { it('does not extend session for system API calls.', async () => { const user = mockAuthenticatedUser(); - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'true' }, + }); - mockOptions.isSystemAPIRequest.mockReturnValue(true); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user) ); @@ -346,9 +348,10 @@ describe('Authenticator', () => { it('extends session for non-system API calls.', async () => { const user = mockAuthenticatedUser(); - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'false' }, + }); - mockOptions.isSystemAPIRequest.mockReturnValue(false); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user) ); @@ -510,9 +513,10 @@ describe('Authenticator', () => { }); it('does not touch session for system API calls if authentication fails with non-401 reason.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'true' }, + }); - mockOptions.isSystemAPIRequest.mockReturnValue(true); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(new Error('some error')) ); @@ -526,9 +530,10 @@ describe('Authenticator', () => { }); it('does not touch session for non-system API calls if authentication fails with non-401 reason.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'false' }, + }); - mockOptions.isSystemAPIRequest.mockReturnValue(false); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(new Error('some error')) ); @@ -544,9 +549,10 @@ describe('Authenticator', () => { it('replaces existing session with the one returned by authentication provider for system API requests', async () => { const user = mockAuthenticatedUser(); const newState = { authorization: 'Basic yyy' }; - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'true' }, + }); - mockOptions.isSystemAPIRequest.mockReturnValue(true); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user, { state: newState }) ); @@ -567,9 +573,10 @@ describe('Authenticator', () => { it('replaces existing session with the one returned by authentication provider for non-system API requests', async () => { const user = mockAuthenticatedUser(); const newState = { authorization: 'Basic yyy' }; - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'false' }, + }); - mockOptions.isSystemAPIRequest.mockReturnValue(false); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.succeeded(user, { state: newState }) ); @@ -588,9 +595,10 @@ describe('Authenticator', () => { }); it('clears session if provider failed to authenticate system API request with 401 with active session.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'true' }, + }); - mockOptions.isSystemAPIRequest.mockReturnValue(true); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(Boom.unauthorized()) ); @@ -604,9 +612,10 @@ describe('Authenticator', () => { }); it('clears session if provider failed to authenticate non-system API request with 401 with active session.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'false' }, + }); - mockOptions.isSystemAPIRequest.mockReturnValue(false); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.failed(Boom.unauthorized()) ); @@ -635,9 +644,10 @@ describe('Authenticator', () => { }); it('does not clear session if provider can not handle system API request authentication with active session.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'true' }, + }); - mockOptions.isSystemAPIRequest.mockReturnValue(true); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); @@ -651,9 +661,10 @@ describe('Authenticator', () => { }); it('does not clear session if provider can not handle non-system API request authentication with active session.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'false' }, + }); - mockOptions.isSystemAPIRequest.mockReturnValue(false); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); @@ -667,9 +678,10 @@ describe('Authenticator', () => { }); it('clears session for system API request if it belongs to not configured provider.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'true' }, + }); - mockOptions.isSystemAPIRequest.mockReturnValue(true); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); @@ -683,9 +695,10 @@ describe('Authenticator', () => { }); it('clears session for non-system API request if it belongs to not configured provider.', async () => { - const request = httpServerMock.createKibanaRequest(); + const request = httpServerMock.createKibanaRequest({ + headers: { 'kbn-system-request': 'false' }, + }); - mockOptions.isSystemAPIRequest.mockReturnValue(false); mockBasicAuthenticationProvider.authenticate.mockResolvedValue( AuthenticationResult.notHandled() ); diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index ea7792e902ec1..3ab49d3c5b124 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -88,7 +88,6 @@ export interface AuthenticatorOptions { loggers: LoggerFactory; clusterClient: IClusterClient; sessionStorageFactory: SessionStorageFactory<ProviderSession>; - isSystemAPIRequest: (request: KibanaRequest) => boolean; } // Mapping between provider key defined in the config and authentication @@ -310,7 +309,7 @@ export class Authenticator { this.updateSessionValue(sessionStorage, { providerType, - isSystemAPIRequest: this.options.isSystemAPIRequest(request), + isSystemRequest: request.isSystemRequest, authenticationResult, existingSession: ownsSession ? existingSession : null, }); @@ -434,12 +433,12 @@ export class Authenticator { providerType, authenticationResult, existingSession, - isSystemAPIRequest, + isSystemRequest, }: { providerType: string; authenticationResult: AuthenticationResult; existingSession: ProviderSession | null; - isSystemAPIRequest: boolean; + isSystemRequest: boolean; } ) { if (!existingSession && !authenticationResult.shouldUpdateState()) { @@ -451,7 +450,7 @@ export class Authenticator { // state we should store it in the session regardless of whether it's a system API request or not. const sessionCanBeUpdated = (authenticationResult.succeeded() || authenticationResult.redirected()) && - (authenticationResult.shouldUpdateState() || !isSystemAPIRequest); + (authenticationResult.shouldUpdateState() || !isSystemRequest); // If provider owned the session, but failed to authenticate anyway, that likely means that // session is not valid and we should clear it. Also provider can specifically ask to clear diff --git a/x-pack/plugins/security/server/authentication/index.test.ts b/x-pack/plugins/security/server/authentication/index.test.ts index d0de6d571b7a0..3727b1fc13dac 100644 --- a/x-pack/plugins/security/server/authentication/index.test.ts +++ b/x-pack/plugins/security/server/authentication/index.test.ts @@ -32,7 +32,6 @@ import { } from '../../../../../src/core/server'; import { AuthenticatedUser } from '../../common/model'; import { ConfigType, createConfig$ } from '../config'; -import { LegacyAPI } from '../plugin'; import { AuthenticationResult } from './authentication_result'; import { setupAuthentication } from '.'; import { @@ -47,7 +46,6 @@ describe('setupAuthentication()', () => { let mockSetupAuthenticationParams: { config: ConfigType; loggers: LoggerFactory; - getLegacyAPI(): Pick<LegacyAPI, 'isSystemAPIRequest'>; http: jest.Mocked<CoreSetup['http']>; clusterClient: jest.Mocked<IClusterClient>; license: jest.Mocked<SecurityLicense>; @@ -73,7 +71,6 @@ describe('setupAuthentication()', () => { clusterClient: elasticsearchServiceMock.createClusterClient(), license: licenseMock.create(), loggers: loggingServiceMock.create(), - getLegacyAPI: jest.fn(), }; mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index 4b73430ff13c4..467afe0034025 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -14,7 +14,6 @@ import { AuthenticatedUser } from '../../common/model'; import { ConfigType } from '../config'; import { getErrorStatusCode } from '../errors'; import { Authenticator, ProviderSession } from './authenticator'; -import { LegacyAPI } from '../plugin'; import { APIKeys, CreateAPIKeyParams, InvalidateAPIKeyParams } from './api_keys'; import { SecurityLicense } from '../../common/licensing'; @@ -36,7 +35,6 @@ interface SetupAuthenticationParams { config: ConfigType; license: SecurityLicense; loggers: LoggerFactory; - getLegacyAPI(): Pick<LegacyAPI, 'isSystemAPIRequest'>; } export type Authentication = UnwrapPromise<ReturnType<typeof setupAuthentication>>; @@ -47,7 +45,6 @@ export async function setupAuthentication({ config, license, loggers, - getLegacyAPI, }: SetupAuthenticationParams) { const authLogger = loggers.get('authentication'); @@ -83,7 +80,6 @@ export async function setupAuthentication({ clusterClient, basePath: http.basePath, config: { session: config.session, authc: config.authc }, - isSystemAPIRequest: (request: KibanaRequest) => getLegacyAPI().isSystemAPIRequest(request), loggers, sessionStorageFactory: await http.createCookieSessionStorageFactory({ encryptionKey: config.encryptionKey, diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 5764418234739..328f2917fd550 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -9,7 +9,6 @@ import { first } from 'rxjs/operators'; import { ICustomClusterClient, CoreSetup, - KibanaRequest, Logger, PluginInitializerContext, RecursiveReadonly, @@ -40,7 +39,6 @@ export type FeaturesService = Pick<FeaturesSetupContract, 'getFeatures'>; * to function properly. */ export interface LegacyAPI { - isSystemAPIRequest: (request: KibanaRequest) => boolean; auditLogger: { log: (eventType: string, message: string, data?: Record<string, unknown>) => void; }; @@ -133,7 +131,6 @@ export class Plugin { config, license, loggers: this.initializerContext.logger, - getLegacyAPI: this.getLegacyAPI, }); const authz = await setupAuthorization({ diff --git a/x-pack/plugins/siem/server/config.ts b/x-pack/plugins/siem/server/config.ts index 456646cc825f3..224043c0c6fe5 100644 --- a/x-pack/plugins/siem/server/config.ts +++ b/x-pack/plugins/siem/server/config.ts @@ -6,7 +6,7 @@ import { Observable } from 'rxjs'; import { schema, TypeOf } from '@kbn/config-schema'; -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext } from '../../../../src/core/server'; import { SIGNALS_INDEX_KEY, DEFAULT_SIGNALS_INDEX, diff --git a/x-pack/plugins/siem/server/index.ts b/x-pack/plugins/siem/server/index.ts index c675be691b47e..83e2f900a3b90 100644 --- a/x-pack/plugins/siem/server/index.ts +++ b/x-pack/plugins/siem/server/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext } from '../../../../src/core/server'; import { Plugin } from './plugin'; import { configSchema, ConfigType } from './config'; diff --git a/x-pack/plugins/siem/server/plugin.ts b/x-pack/plugins/siem/server/plugin.ts index 866f4d7575e2f..ccc6aef1452b2 100644 --- a/x-pack/plugins/siem/server/plugin.ts +++ b/x-pack/plugins/siem/server/plugin.ts @@ -6,7 +6,7 @@ import { Observable } from 'rxjs'; -import { CoreSetup, PluginInitializerContext, Logger } from 'src/core/server'; +import { CoreSetup, PluginInitializerContext, Logger } from '../../../../src/core/server'; import { createConfig$, ConfigType } from './config'; export class Plugin { diff --git a/x-pack/plugins/spaces/server/lib/utils/url.ts b/x-pack/plugins/spaces/server/lib/utils/url.ts index a5797c0f87868..c91934bb99f1f 100644 --- a/x-pack/plugins/spaces/server/lib/utils/url.ts +++ b/x-pack/plugins/spaces/server/lib/utils/url.ts @@ -8,7 +8,7 @@ // DIRECT COPY FROM `src/core/utils/url`, since it's not possible to import from there, // nor can I re-export from `src/core/server`... -import { ParsedUrlQuery } from 'querystring'; +import { ParsedQuery } from 'query-string'; import { format as formatUrl, parse as parseUrl, UrlObject } from 'url'; export interface URLMeaningfulParts { @@ -19,7 +19,7 @@ export interface URLMeaningfulParts { protocol: string | null; slashes: boolean | null; port: string | null; - query: ParsedUrlQuery | {}; + query: ParsedQuery | {}; } /** diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index dbc6a015f9c97..79b826bc4524f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -78,109 +78,6 @@ "messages": { "common.ui.aggResponse.allDocsTitle": "すべてのドキュメント", "common.ui.aggTypes.rangesFormatMessage": "{gte} {from} と {lt} {to}", - "data.search.aggs.aggGroups.bucketsText": "バケット", - "data.search.aggs.aggGroups.metricsText": "メトリック", - "data.search.aggs.buckets.dateHistogramLabel": "{intervalDescription}ごとの {fieldName}", - "data.search.aggs.buckets.dateHistogramTitle": "日付ヒストグラム", - "data.search.aggs.buckets.dateRangeTitle": "日付範囲", - "data.search.aggs.buckets.filtersTitle": "フィルター", - "data.search.aggs.buckets.filterTitle": "フィルター", - "data.search.aggs.buckets.geohashGridTitle": "ジオハッシュ", - "data.search.aggs.buckets.geotileGridTitle": "ジオタイル", - "data.search.aggs.buckets.histogramTitle": "ヒストグラム", - "data.search.aggs.buckets.intervalOptions.autoDisplayName": "自動", - "data.search.aggs.buckets.intervalOptions.dailyDisplayName": "日ごと", - "data.search.aggs.buckets.intervalOptions.hourlyDisplayName": "1 時間ごと", - "data.search.aggs.buckets.intervalOptions.millisecondDisplayName": "ミリ秒", - "data.search.aggs.buckets.intervalOptions.minuteDisplayName": "分", - "data.search.aggs.buckets.intervalOptions.monthlyDisplayName": "月ごと", - "data.search.aggs.buckets.intervalOptions.secondDisplayName": "秒", - "data.search.aggs.buckets.intervalOptions.weeklyDisplayName": "週ごと", - "data.search.aggs.buckets.intervalOptions.yearlyDisplayName": "1 年ごと", - "data.search.aggs.buckets.ipRangeLabel": "{fieldName} IP 範囲", - "data.search.aggs.buckets.ipRangeTitle": "IPv4 範囲", - "data.search.aggs.aggTypes.rangesFormatMessage": "{gte} {from} と {lt} {to}", - "data.search.aggs.aggTypesLabel": "{fieldName} の範囲", - "data.search.aggs.buckets.rangeTitle": "範囲", - "data.search.aggs.buckets.significantTerms.excludeLabel": "除外", - "data.search.aggs.buckets.significantTerms.includeLabel": "含める", - "data.search.aggs.buckets.significantTermsLabel": "{fieldName} のトップ {size} の珍しいアイテム", - "data.search.aggs.buckets.significantTermsTitle": "重要な用語", - "data.search.aggs.buckets.terms.excludeLabel": "除外", - "data.search.aggs.buckets.terms.includeLabel": "含める", - "data.search.aggs.buckets.terms.missingBucketLabel": "欠測値", - "data.search.aggs.buckets.terms.orderAscendingTitle": "昇順", - "data.search.aggs.buckets.terms.orderDescendingTitle": "降順", - "data.search.aggs.buckets.terms.otherBucketDescription": "このリクエストは、データバケットの基準外のドキュメントの数をカウントします。", - "data.search.aggs.buckets.terms.otherBucketLabel": "その他", - "data.search.aggs.buckets.terms.otherBucketTitle": "他のバケット", - "data.search.aggs.buckets.termsTitle": "用語", - "data.search.aggs.histogram.missingMaxMinValuesWarning": "自動スケールヒストグラムバケットから最高値と最低値を取得できません。これによりビジュアライゼーションのパフォーマンスが低下する可能性があります。", - "data.search.aggs.metrics.averageBucketTitle": "平均バケット", - "data.search.aggs.metrics.averageLabel": "平均 {field}", - "data.search.aggs.metrics.averageTitle": "平均", - "data.search.aggs.metrics.bucketAggTitle": "バケット集約", - "data.search.aggs.metrics.countLabel": "カウント", - "data.search.aggs.metrics.countTitle": "カウント", - "data.search.aggs.metrics.cumulativeSumLabel": "累積合計", - "data.search.aggs.metrics.cumulativeSumTitle": "累積合計", - "data.search.aggs.metrics.derivativeLabel": "派生", - "data.search.aggs.metrics.derivativeTitle": "派生", - "data.search.aggs.metrics.geoBoundsLabel": "境界", - "data.search.aggs.metrics.geoBoundsTitle": "境界", - "data.search.aggs.metrics.geoCentroidLabel": "ジオセントロイド", - "data.search.aggs.metrics.geoCentroidTitle": "ジオセントロイド", - "data.search.aggs.metrics.maxBucketTitle": "最高バケット", - "data.search.aggs.metrics.maxLabel": "最高 {field}", - "data.search.aggs.metrics.maxTitle": "最高", - "data.search.aggs.metrics.medianLabel": "中央 {field}", - "data.search.aggs.metrics.medianTitle": "中央", - "data.search.aggs.metrics.metricAggregationsSubtypeTitle": "メトリック集約", - "data.search.aggs.metrics.metricAggTitle": "メトリック集約", - "data.search.aggs.metrics.minBucketTitle": "最低バケット", - "data.search.aggs.metrics.minLabel": "最低 {field}", - "data.search.aggs.metrics.minTitle": "最低", - "data.search.aggs.metrics.movingAvgLabel": "移動平均", - "data.search.aggs.metrics.movingAvgTitle": "移動平均", - "data.search.aggs.metrics.overallAverageLabel": "全体平均", - "data.search.aggs.metrics.overallMaxLabel": "全体最高", - "data.search.aggs.metrics.overallMinLabel": "全体最低", - "data.search.aggs.metrics.overallSumLabel": "全体合計", - "data.search.aggs.metrics.parentPipelineAggregationsSubtypeTitle": "親パイプライン集約", - "data.search.aggs.metrics.percentileRanks.valuePropsLabel": "「{label}」の {format} のパーセンタイル順位", - "data.search.aggs.metrics.percentileRanksLabel": "{field} のパーセンタイル順位", - "data.search.aggs.metrics.percentileRanksTitle": "パーセンタイル順位", - "data.search.aggs.metrics.percentiles.valuePropsLabel": "{label} の {percentile} パーセンタイル", - "data.search.aggs.metrics.percentilesLabel": "{field} のパーセンタイル", - "data.search.aggs.metrics.percentilesTitle": "パーセンタイル", - "data.search.aggs.metrics.serialDiffLabel": "差分の推移", - "data.search.aggs.metrics.serialDiffTitle": "差分の推移", - "data.search.aggs.metrics.siblingPipelineAggregationsSubtypeTitle": "シブリングパイプラインアグリゲーション", - "data.search.aggs.metrics.standardDeviation.keyDetailsLabel": "{fieldDisplayName} の標準偏差", - "data.search.aggs.metrics.standardDeviation.lowerKeyDetailsTitle": "下の{label}", - "data.search.aggs.metrics.standardDeviation.upperKeyDetailsTitle": "上の{label}", - "data.search.aggs.metrics.standardDeviationLabel": "{field} の標準偏差", - "data.search.aggs.metrics.standardDeviationTitle": "標準偏差", - "data.search.aggs.metrics.sumBucketTitle": "合計バケット", - "data.search.aggs.metrics.sumLabel": "{field} の合計", - "data.search.aggs.metrics.sumTitle": "合計", - "data.search.aggs.metrics.topHit.ascendingLabel": "昇順", - "data.search.aggs.metrics.topHit.averageLabel": "平均", - "data.search.aggs.metrics.topHit.concatenateLabel": "連結", - "data.search.aggs.metrics.topHit.descendingLabel": "降順", - "data.search.aggs.metrics.topHit.firstPrefixLabel": "最初", - "data.search.aggs.metrics.topHit.lastPrefixLabel": "最後", - "data.search.aggs.metrics.topHit.maxLabel": "最高", - "data.search.aggs.metrics.topHit.minLabel": "最低", - "data.search.aggs.metrics.topHit.sumLabel": "合計", - "data.search.aggs.metrics.topHitTitle": "トップヒット", - "data.search.aggs.metrics.uniqueCountLabel": "{field} のユニークカウント", - "data.search.aggs.metrics.uniqueCountTitle": "ユニークカウント", - "data.search.aggs.otherBucket.labelForMissingValuesLabel": "欠測値のラベル", - "data.search.aggs.otherBucket.labelForOtherBucketLabel": "他のバケットのラベル", - "data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage": "保存された {fieldParameter} パラメーターが無効になりました。新しいフィールドを選択してください。", - "data.search.aggs.paramTypes.field.requiredFieldParameterErrorMessage": "{fieldParameter} は必須パラメーターです", - "data.search.aggs.string.customLabel": "カスタムラベル", "common.ui.directives.paginate.size.allDropDownOptionLabel": "すべて", "common.ui.dualRangeControl.mustSetBothErrorMessage": "下と上の値の両方を設定する必要があります", "common.ui.dualRangeControl.outsideOfRangeErrorMessage": "値は {min} と {max} の間でなければなりません", @@ -367,10 +264,223 @@ "common.ui.stateManagement.unableToStoreHistoryInSessionErrorMessage": "セッションがいっぱいで安全に削除できるアイテムが見つからないため、Kibana は履歴アイテムを保存できません。\n\nこれは大抵新規タブに移動することで解決されますが、より大きな問題が原因である可能性もあります。このメッセージが定期的に表示される場合は、{gitHubIssuesUrl} で問題を報告してください。", "common.ui.url.replacementFailedErrorMessage": "置換に失敗、未解決の表現式: {expr}", "common.ui.url.savedObjectIsMissingNotificationMessage": "保存されたオブジェクトがありません", - "data.search.aggs.percentageOfLabel": "{label} のパーセンテージ", "common.ui.vis.defaultFeedbackMessage": "フィードバックがありますか?{link} で問題を報告してください。", "common.ui.vis.kibanaMap.leaflet.fitDataBoundsAriaLabel": "データバウンドを合わせる", "common.ui.vis.kibanaMap.zoomWarning": "ズームレベルが最大に達しました。完全にズームインするには、Elasticsearch と Kibana の {defaultDistribution} にアップグレードしてください。{ems} でより多くのズームレベルが利用できます。または、独自のマップサーバーを構成できます。詳細は { wms } または { configSettings} をご覧ください。", + "data.search.aggs.aggGroups.bucketsText": "バケット", + "data.search.aggs.aggGroups.metricsText": "メトリック", + "data.search.aggs.buckets.dateHistogramLabel": "{intervalDescription}ごとの {fieldName}", + "data.search.aggs.buckets.dateHistogramTitle": "日付ヒストグラム", + "data.search.aggs.buckets.dateRangeTitle": "日付範囲", + "data.search.aggs.buckets.filtersTitle": "フィルター", + "data.search.aggs.buckets.filterTitle": "フィルター", + "data.search.aggs.buckets.geohashGridTitle": "ジオハッシュ", + "data.search.aggs.buckets.geotileGridTitle": "ジオタイル", + "data.search.aggs.buckets.histogramTitle": "ヒストグラム", + "data.search.aggs.buckets.intervalOptions.autoDisplayName": "自動", + "data.search.aggs.buckets.intervalOptions.dailyDisplayName": "日ごと", + "data.search.aggs.buckets.intervalOptions.hourlyDisplayName": "1 時間ごと", + "data.search.aggs.buckets.intervalOptions.millisecondDisplayName": "ミリ秒", + "data.search.aggs.buckets.intervalOptions.minuteDisplayName": "分", + "data.search.aggs.buckets.intervalOptions.monthlyDisplayName": "月ごと", + "data.search.aggs.buckets.intervalOptions.secondDisplayName": "秒", + "data.search.aggs.buckets.intervalOptions.weeklyDisplayName": "週ごと", + "data.search.aggs.buckets.intervalOptions.yearlyDisplayName": "1 年ごと", + "data.search.aggs.buckets.ipRangeLabel": "{fieldName} IP 範囲", + "data.search.aggs.buckets.ipRangeTitle": "IPv4 範囲", + "data.search.aggs.aggTypes.rangesFormatMessage": "{gte} {from} と {lt} {to}", + "data.search.aggs.aggTypesLabel": "{fieldName} の範囲", + "data.search.aggs.buckets.rangeTitle": "範囲", + "data.search.aggs.buckets.significantTerms.excludeLabel": "除外", + "data.search.aggs.buckets.significantTerms.includeLabel": "含める", + "data.search.aggs.buckets.significantTermsLabel": "{fieldName} のトップ {size} の珍しいアイテム", + "data.search.aggs.buckets.significantTermsTitle": "重要な用語", + "data.search.aggs.buckets.terms.excludeLabel": "除外", + "data.search.aggs.buckets.terms.includeLabel": "含める", + "data.search.aggs.buckets.terms.missingBucketLabel": "欠測値", + "data.search.aggs.buckets.terms.orderAscendingTitle": "昇順", + "data.search.aggs.buckets.terms.orderDescendingTitle": "降順", + "data.search.aggs.buckets.terms.otherBucketDescription": "このリクエストは、データバケットの基準外のドキュメントの数をカウントします。", + "data.search.aggs.buckets.terms.otherBucketLabel": "その他", + "data.search.aggs.buckets.terms.otherBucketTitle": "他のバケット", + "data.search.aggs.buckets.termsTitle": "用語", + "data.search.aggs.histogram.missingMaxMinValuesWarning": "自動スケールヒストグラムバケットから最高値と最低値を取得できません。これによりビジュアライゼーションのパフォーマンスが低下する可能性があります。", + "data.search.aggs.metrics.averageBucketTitle": "平均バケット", + "data.search.aggs.metrics.averageLabel": "平均 {field}", + "data.search.aggs.metrics.averageTitle": "平均", + "data.search.aggs.metrics.bucketAggTitle": "バケット集約", + "data.search.aggs.metrics.countLabel": "カウント", + "data.search.aggs.metrics.countTitle": "カウント", + "data.search.aggs.metrics.cumulativeSumLabel": "累積合計", + "data.search.aggs.metrics.cumulativeSumTitle": "累積合計", + "data.search.aggs.metrics.derivativeLabel": "派生", + "data.search.aggs.metrics.derivativeTitle": "派生", + "data.search.aggs.metrics.geoBoundsLabel": "境界", + "data.search.aggs.metrics.geoBoundsTitle": "境界", + "data.search.aggs.metrics.geoCentroidLabel": "ジオセントロイド", + "data.search.aggs.metrics.geoCentroidTitle": "ジオセントロイド", + "data.search.aggs.metrics.maxBucketTitle": "最高バケット", + "data.search.aggs.metrics.maxLabel": "最高 {field}", + "data.search.aggs.metrics.maxTitle": "最高", + "data.search.aggs.metrics.medianLabel": "中央 {field}", + "data.search.aggs.metrics.medianTitle": "中央", + "data.search.aggs.metrics.metricAggregationsSubtypeTitle": "メトリック集約", + "data.search.aggs.metrics.metricAggTitle": "メトリック集約", + "data.search.aggs.metrics.minBucketTitle": "最低バケット", + "data.search.aggs.metrics.minLabel": "最低 {field}", + "data.search.aggs.metrics.minTitle": "最低", + "data.search.aggs.metrics.movingAvgLabel": "移動平均", + "data.search.aggs.metrics.movingAvgTitle": "移動平均", + "data.search.aggs.metrics.overallAverageLabel": "全体平均", + "data.search.aggs.metrics.overallMaxLabel": "全体最高", + "data.search.aggs.metrics.overallMinLabel": "全体最低", + "data.search.aggs.metrics.overallSumLabel": "全体合計", + "data.search.aggs.metrics.parentPipelineAggregationsSubtypeTitle": "親パイプライン集約", + "data.search.aggs.metrics.percentileRanks.valuePropsLabel": "「{label}」の {format} のパーセンタイル順位", + "data.search.aggs.metrics.percentileRanksLabel": "{field} のパーセンタイル順位", + "data.search.aggs.metrics.percentileRanksTitle": "パーセンタイル順位", + "data.search.aggs.metrics.percentiles.valuePropsLabel": "{label} の {percentile} パーセンタイル", + "data.search.aggs.metrics.percentilesLabel": "{field} のパーセンタイル", + "data.search.aggs.metrics.percentilesTitle": "パーセンタイル", + "data.search.aggs.metrics.serialDiffLabel": "差分の推移", + "data.search.aggs.metrics.serialDiffTitle": "差分の推移", + "data.search.aggs.metrics.siblingPipelineAggregationsSubtypeTitle": "シブリングパイプラインアグリゲーション", + "data.search.aggs.metrics.standardDeviation.keyDetailsLabel": "{fieldDisplayName} の標準偏差", + "data.search.aggs.metrics.standardDeviation.lowerKeyDetailsTitle": "下の{label}", + "data.search.aggs.metrics.standardDeviation.upperKeyDetailsTitle": "上の{label}", + "data.search.aggs.metrics.standardDeviationLabel": "{field} の標準偏差", + "data.search.aggs.metrics.standardDeviationTitle": "標準偏差", + "data.search.aggs.metrics.sumBucketTitle": "合計バケット", + "data.search.aggs.metrics.sumLabel": "{field} の合計", + "data.search.aggs.metrics.sumTitle": "合計", + "data.search.aggs.metrics.topHit.ascendingLabel": "昇順", + "data.search.aggs.metrics.topHit.averageLabel": "平均", + "data.search.aggs.metrics.topHit.concatenateLabel": "連結", + "data.search.aggs.metrics.topHit.descendingLabel": "降順", + "data.search.aggs.metrics.topHit.firstPrefixLabel": "最初", + "data.search.aggs.metrics.topHit.lastPrefixLabel": "最後", + "data.search.aggs.metrics.topHit.maxLabel": "最高", + "data.search.aggs.metrics.topHit.minLabel": "最低", + "data.search.aggs.metrics.topHit.sumLabel": "合計", + "data.search.aggs.metrics.topHitTitle": "トップヒット", + "data.search.aggs.metrics.uniqueCountLabel": "{field} のユニークカウント", + "data.search.aggs.metrics.uniqueCountTitle": "ユニークカウント", + "data.search.aggs.otherBucket.labelForMissingValuesLabel": "欠測値のラベル", + "data.search.aggs.otherBucket.labelForOtherBucketLabel": "他のバケットのラベル", + "data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage": "保存された {fieldParameter} パラメーターが無効になりました。新しいフィールドを選択してください。", + "data.search.aggs.paramTypes.field.requiredFieldParameterErrorMessage": "{fieldParameter} は必須パラメーターです", + "data.search.aggs.string.customLabel": "カスタムラベル", + "data.search.aggs.percentageOfLabel": "{label} のパーセンテージ", + "data.filter.applyFilters.popupHeader": "適用するフィルターの選択", + "data.filter.applyFiltersPopup.cancelButtonLabel": "キャンセル", + "data.filter.applyFiltersPopup.saveButtonLabel": "適用", + "data.filter.filterBar.addFilterButtonLabel": "フィルターを追加します", + "data.filter.filterBar.deleteFilterButtonLabel": "削除", + "data.filter.filterBar.disabledFilterPrefix": "無効", + "data.filter.filterBar.disableFilterButtonLabel": "一時的に無効にする", + "data.filter.filterBar.editFilterButtonLabel": "フィルターを編集", + "data.filter.filterBar.enableFilterButtonLabel": "再度有効にする", + "data.filter.filterBar.excludeFilterButtonLabel": "結果を除外", + "data.filter.filterBar.filterItemBadgeAriaLabel": "フィルターアクション", + "data.filter.filterBar.filterItemBadgeIconAriaLabel": "削除", + "data.filter.filterBar.includeFilterButtonLabel": "結果を含める", + "data.filter.filterBar.indexPatternSelectPlaceholder": "インデックスパターンの選択", + "data.filter.filterBar.moreFilterActionsMessage": "フィルター:{innerText}。他のフィルターアクションを使用するには選択してください。", + "data.filter.filterBar.negatedFilterPrefix": "NOT ", + "data.filter.filterBar.pinFilterButtonLabel": "すべてのアプリにピン付け", + "data.filter.filterBar.pinnedFilterPrefix": "ピン付け済み", + "data.filter.filterBar.unpinFilterButtonLabel": "ピンを外す", + "data.filter.filterEditor.cancelButtonLabel": "キャンセル", + "data.filter.filterEditor.createCustomLabelInputLabel": "カスタムラベル", + "data.filter.filterEditor.createCustomLabelSwitchLabel": "カスタムラベルを作成しますか?", + "data.filter.filterEditor.dateFormatHelpLinkLabel": "対応データフォーマット", + "data.filter.filterEditor.doesNotExistOperatorOptionLabel": "存在しません", + "data.filter.filterEditor.editFilterPopupTitle": "フィルターを編集", + "data.filter.filterEditor.editFilterValuesButtonLabel": "フィルター値を編集", + "data.filter.filterEditor.editQueryDslButtonLabel": "クエリ DSL として編集", + "data.filter.filterEditor.existsOperatorOptionLabel": "存在する", + "data.filter.filterEditor.falseOptionLabel": "False", + "data.filter.filterEditor.fieldSelectLabel": "フィールド", + "data.filter.filterEditor.fieldSelectPlaceholder": "フィールドを選択", + "data.filter.filterEditor.indexPatternSelectLabel": "インデックスパターン", + "data.filter.filterEditor.isBetweenOperatorOptionLabel": "is between", + "data.filter.filterEditor.isNotBetweenOperatorOptionLabel": "is not between", + "data.filter.filterEditor.isNotOneOfOperatorOptionLabel": "is not one of", + "data.filter.filterEditor.isNotOperatorOptionLabel": "is not", + "data.filter.filterEditor.isOneOfOperatorOptionLabel": "is one of", + "data.filter.filterEditor.isOperatorOptionLabel": "が", + "data.filter.filterEditor.operatorSelectLabel": "演算子", + "data.filter.filterEditor.operatorSelectPlaceholderSelect": "選択してください", + "data.filter.filterEditor.operatorSelectPlaceholderWaiting": "待機中", + "data.filter.filterEditor.queryDslLabel": "Elasticsearch クエリ DSL", + "data.filter.filterEditor.rangeEndInputPlaceholder": "範囲の終了値", + "data.filter.filterEditor.rangeInputLabel": "範囲", + "data.filter.filterEditor.rangeStartInputPlaceholder": "範囲の開始値", + "data.filter.filterEditor.saveButtonLabel": "保存", + "data.filter.filterEditor.trueOptionLabel": "True", + "data.filter.filterEditor.valueInputLabel": "値", + "data.filter.filterEditor.valueInputPlaceholder": "値を入力", + "data.filter.filterEditor.valueSelectPlaceholder": "値を選択", + "data.filter.filterEditor.valuesSelectLabel": "値", + "data.filter.filterEditor.valuesSelectPlaceholder": "値を選択", + "data.filter.options.changeAllFiltersButtonLabel": "すべてのフィルターの変更", + "data.filter.options.deleteAllFiltersButtonLabel": "すべて削除", + "data.filter.options.disableAllFiltersButtonLabel": "すべて無効にする", + "data.filter.options.enableAllFiltersButtonLabel": "すべて有効にする", + "data.filter.options.invertDisabledFiltersButtonLabel": "有効・無効を反転", + "data.filter.options.invertNegatedFiltersButtonLabel": "含める・除外を反転", + "data.filter.options.pinAllFiltersButtonLabel": "すべてピン付け", + "data.filter.options.unpinAllFiltersButtonLabel": "すべてのピンを外す", + "data.filter.searchBar.changeAllFiltersTitle": "すべてのフィルターの変更", + "data.indexPatterns.unableWriteLabel": "インデックスパターンを書き込めません!このインデックスパターンへの最新の変更を取得するには、ページを更新してください。", + "data.indexPatterns.unknownFieldErrorMessage": "インデックスパターン「{title}」のフィールド「{name}」が不明なフィールドタイプを使用しています。", + "data.indexPatterns.unknownFieldHeader": "不明なフィールドタイプ {type}", + "data.parseEsInterval.invalidEsCalendarIntervalErrorMessage": "無効なカレンダー間隔:{interval}、1よりも大きな値が必要です", + "data.parseEsInterval.invalidEsIntervalFormatErrorMessage": "無効な間隔フォーマット:{interval}", + "data.query.queryBar.comboboxAriaLabel": "{pageType} ページの検索とフィルタリング", + "data.query.queryBar.kqlFullLanguageName": "Kibana クエリ言語", + "data.query.queryBar.kqlLanguageName": "KQL", + "data.query.queryBar.kqlOffLabel": "オフ", + "data.query.queryBar.kqlOnLabel": "オン", + "data.query.queryBar.luceneLanguageName": "Lucene", + "data.query.queryBar.luceneSyntaxWarningMessage": "Lucene クエリ構文を使用しているようですが、Kibana クエリ言語 (KQL) が選択されています。KQL ドキュメント {link} を確認してください。", + "data.query.queryBar.luceneSyntaxWarningOptOutText": "今後表示しない", + "data.query.queryBar.luceneSyntaxWarningTitle": "Lucene 構文警告", + "data.query.queryBar.searchInputAriaLabel": "{pageType} ページの検索とフィルタリングを行うには入力を開始してください", + "data.query.queryBar.searchInputPlaceholder": "検索", + "data.query.queryBar.syntaxOptionsDescription": "{docsLink} (KQL) は、シンプルなクエリ構文とスクリプトフィールドのサポートを提供します。また、KQL はベーシックライセンス以上をご利用の場合、自動入力も提供します。KQL をオフにすると、Kibana は Lucene を使用します。", + "data.query.queryBar.syntaxOptionsDescription.docsLinkText": "こちら", + "data.query.queryBar.syntaxOptionsTitle": "構文オプション", + "data.search.searchBar.savedQueryDescriptionLabelText": "説明", + "data.search.searchBar.savedQueryDescriptionText": "再度使用するクエリテキストとフィルターを保存します。", + "data.search.searchBar.savedQueryForm.titleConflictText": "タイトルが既に保存されているクエリに使用されています", + "data.search.searchBar.savedQueryForm.titleMissingText": "名前が必要です", + "data.search.searchBar.savedQueryForm.whitespaceErrorText": "タイトルの始めと終わりにはスペースを使用できません", + "data.search.searchBar.savedQueryFormCancelButtonText": "キャンセル", + "data.search.searchBar.savedQueryFormSaveButtonText": "保存", + "data.search.searchBar.savedQueryFormTitle": "クエリを保存", + "data.search.searchBar.savedQueryIncludeFiltersLabelText": "フィルターを含める", + "data.search.searchBar.savedQueryIncludeTimeFilterLabelText": "時間フィルターを含める", + "data.search.searchBar.savedQueryNameHelpText": "名前が必要です。タイトルの始めと終わりにはスペースを使用できません。名前は固有でなければなりません。", + "data.search.searchBar.savedQueryNameLabelText": "名前", + "data.search.searchBar.savedQueryNoSavedQueriesText": "保存されたクエリがありません。", + "data.search.searchBar.savedQueryPopoverButtonText": "保存されたクエリを表示", + "data.search.searchBar.savedQueryPopoverClearButtonAriaLabel": "現在保存されているクエリを消去", + "data.search.searchBar.savedQueryPopoverClearButtonText": "消去", + "data.search.searchBar.savedQueryPopoverConfirmDeletionCancelButtonText": "キャンセル", + "data.search.searchBar.savedQueryPopoverConfirmDeletionConfirmButtonText": "削除", + "data.search.searchBar.savedQueryPopoverConfirmDeletionTitle": "「{savedQueryName}」を削除しますか?", + "data.search.searchBar.savedQueryPopoverDeleteButtonAriaLabel": "保存されたクエリ {savedQueryName} を削除", + "data.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel": "新規保存クエリを保存", + "data.search.searchBar.savedQueryPopoverSaveAsNewButtonText": "新規保存", + "data.search.searchBar.savedQueryPopoverSaveButtonAriaLabel": "新規保存クエリを保存", + "data.search.searchBar.savedQueryPopoverSaveButtonText": "現在のクエリを保存", + "data.search.searchBar.savedQueryPopoverSaveChangesButtonAriaLabel": "{title} への変更を保存", + "data.search.searchBar.savedQueryPopoverSaveChangesButtonText": "変更を保存", + "data.search.searchBar.savedQueryPopoverSavedQueryListItemButtonAriaLabel": "保存クエリボタン {savedQueryName}", + "data.search.searchBar.savedQueryPopoverSavedQueryListItemDescriptionAriaLabel": "{savedQueryName} の説明", + "data.search.searchBar.savedQueryPopoverSavedQueryListItemSelectedButtonAriaLabel": "選択されたクエリボタン {savedQueryName} を保存しました。変更を破棄するには押してください。", + "data.search.searchBar.savedQueryPopoverTitleText": "保存されたクエリ", "charts.colormaps.bluesText": "青", "charts.colormaps.greensText": "緑", "charts.colormaps.greenToRedText": "緑から赤", @@ -526,116 +636,6 @@ "dashboardEmbeddableContainer.actions.toggleExpandPanelMenuItem.notExpandedDisplayName": "全画面", "dashboardEmbeddableContainer.dashboardGrid.toast.unableToLoadDashboardDangerMessage": "ダッシュボードが読み込めません。", "dashboardEmbeddableContainer.factory.displayName": "ダッシュボード", - "data.filter.applyFilters.popupHeader": "適用するフィルターの選択", - "data.filter.applyFiltersPopup.cancelButtonLabel": "キャンセル", - "data.filter.applyFiltersPopup.saveButtonLabel": "適用", - "data.filter.filterBar.addFilterButtonLabel": "フィルターを追加します", - "data.filter.filterBar.deleteFilterButtonLabel": "削除", - "data.filter.filterBar.disabledFilterPrefix": "無効", - "data.filter.filterBar.disableFilterButtonLabel": "一時的に無効にする", - "data.filter.filterBar.editFilterButtonLabel": "フィルターを編集", - "data.filter.filterBar.enableFilterButtonLabel": "再度有効にする", - "data.filter.filterBar.excludeFilterButtonLabel": "結果を除外", - "data.filter.filterBar.filterItemBadgeAriaLabel": "フィルターアクション", - "data.filter.filterBar.filterItemBadgeIconAriaLabel": "削除", - "data.filter.filterBar.includeFilterButtonLabel": "結果を含める", - "data.filter.filterBar.indexPatternSelectPlaceholder": "インデックスパターンの選択", - "data.filter.filterBar.moreFilterActionsMessage": "フィルター:{innerText}。他のフィルターアクションを使用するには選択してください。", - "data.filter.filterBar.negatedFilterPrefix": "NOT ", - "data.filter.filterBar.pinFilterButtonLabel": "すべてのアプリにピン付け", - "data.filter.filterBar.pinnedFilterPrefix": "ピン付け済み", - "data.filter.filterBar.unpinFilterButtonLabel": "ピンを外す", - "data.filter.filterEditor.cancelButtonLabel": "キャンセル", - "data.filter.filterEditor.createCustomLabelInputLabel": "カスタムラベル", - "data.filter.filterEditor.createCustomLabelSwitchLabel": "カスタムラベルを作成しますか?", - "data.filter.filterEditor.dateFormatHelpLinkLabel": "対応データフォーマット", - "data.filter.filterEditor.doesNotExistOperatorOptionLabel": "存在しません", - "data.filter.filterEditor.editFilterPopupTitle": "フィルターを編集", - "data.filter.filterEditor.editFilterValuesButtonLabel": "フィルター値を編集", - "data.filter.filterEditor.editQueryDslButtonLabel": "クエリ DSL として編集", - "data.filter.filterEditor.existsOperatorOptionLabel": "存在する", - "data.filter.filterEditor.falseOptionLabel": "False", - "data.filter.filterEditor.fieldSelectLabel": "フィールド", - "data.filter.filterEditor.fieldSelectPlaceholder": "フィールドを選択", - "data.filter.filterEditor.indexPatternSelectLabel": "インデックスパターン", - "data.filter.filterEditor.isBetweenOperatorOptionLabel": "is between", - "data.filter.filterEditor.isNotBetweenOperatorOptionLabel": "is not between", - "data.filter.filterEditor.isNotOneOfOperatorOptionLabel": "is not one of", - "data.filter.filterEditor.isNotOperatorOptionLabel": "is not", - "data.filter.filterEditor.isOneOfOperatorOptionLabel": "is one of", - "data.filter.filterEditor.isOperatorOptionLabel": "が", - "data.filter.filterEditor.operatorSelectLabel": "演算子", - "data.filter.filterEditor.operatorSelectPlaceholderSelect": "選択してください", - "data.filter.filterEditor.operatorSelectPlaceholderWaiting": "待機中", - "data.filter.filterEditor.queryDslLabel": "Elasticsearch クエリ DSL", - "data.filter.filterEditor.rangeEndInputPlaceholder": "範囲の終了値", - "data.filter.filterEditor.rangeInputLabel": "範囲", - "data.filter.filterEditor.rangeStartInputPlaceholder": "範囲の開始値", - "data.filter.filterEditor.saveButtonLabel": "保存", - "data.filter.filterEditor.trueOptionLabel": "True", - "data.filter.filterEditor.valueInputLabel": "値", - "data.filter.filterEditor.valueInputPlaceholder": "値を入力", - "data.filter.filterEditor.valueSelectPlaceholder": "値を選択", - "data.filter.filterEditor.valuesSelectLabel": "値", - "data.filter.filterEditor.valuesSelectPlaceholder": "値を選択", - "data.filter.options.changeAllFiltersButtonLabel": "すべてのフィルターの変更", - "data.filter.options.deleteAllFiltersButtonLabel": "すべて削除", - "data.filter.options.disableAllFiltersButtonLabel": "すべて無効にする", - "data.filter.options.enableAllFiltersButtonLabel": "すべて有効にする", - "data.filter.options.invertDisabledFiltersButtonLabel": "有効・無効を反転", - "data.filter.options.invertNegatedFiltersButtonLabel": "含める・除外を反転", - "data.filter.options.pinAllFiltersButtonLabel": "すべてピン付け", - "data.filter.options.unpinAllFiltersButtonLabel": "すべてのピンを外す", - "data.filter.searchBar.changeAllFiltersTitle": "すべてのフィルターの変更", - "data.indexPatterns.unableWriteLabel": "インデックスパターンを書き込めません!このインデックスパターンへの最新の変更を取得するには、ページを更新してください。", - "data.indexPatterns.unknownFieldErrorMessage": "インデックスパターン「{title}」のフィールド「{name}」が不明なフィールドタイプを使用しています。", - "data.indexPatterns.unknownFieldHeader": "不明なフィールドタイプ {type}", - "data.parseEsInterval.invalidEsCalendarIntervalErrorMessage": "無効なカレンダー間隔:{interval}、1よりも大きな値が必要です", - "data.parseEsInterval.invalidEsIntervalFormatErrorMessage": "無効な間隔フォーマット:{interval}", - "data.query.queryBar.comboboxAriaLabel": "{pageType} ページの検索とフィルタリング", - "data.query.queryBar.kqlFullLanguageName": "Kibana クエリ言語", - "data.query.queryBar.kqlLanguageName": "KQL", - "data.query.queryBar.kqlOffLabel": "オフ", - "data.query.queryBar.kqlOnLabel": "オン", - "data.query.queryBar.luceneLanguageName": "Lucene", - "data.query.queryBar.luceneSyntaxWarningMessage": "Lucene クエリ構文を使用しているようですが、Kibana クエリ言語 (KQL) が選択されています。KQL ドキュメント {link} を確認してください。", - "data.query.queryBar.luceneSyntaxWarningOptOutText": "今後表示しない", - "data.query.queryBar.luceneSyntaxWarningTitle": "Lucene 構文警告", - "data.query.queryBar.searchInputAriaLabel": "{pageType} ページの検索とフィルタリングを行うには入力を開始してください", - "data.query.queryBar.searchInputPlaceholder": "検索", - "data.query.queryBar.syntaxOptionsDescription": "{docsLink} (KQL) は、シンプルなクエリ構文とスクリプトフィールドのサポートを提供します。また、KQL はベーシックライセンス以上をご利用の場合、自動入力も提供します。KQL をオフにすると、Kibana は Lucene を使用します。", - "data.query.queryBar.syntaxOptionsDescription.docsLinkText": "こちら", - "data.query.queryBar.syntaxOptionsTitle": "構文オプション", - "data.search.searchBar.savedQueryDescriptionLabelText": "説明", - "data.search.searchBar.savedQueryDescriptionText": "再度使用するクエリテキストとフィルターを保存します。", - "data.search.searchBar.savedQueryForm.titleConflictText": "タイトルが既に保存されているクエリに使用されています", - "data.search.searchBar.savedQueryForm.titleMissingText": "名前が必要です", - "data.search.searchBar.savedQueryForm.whitespaceErrorText": "タイトルの始めと終わりにはスペースを使用できません", - "data.search.searchBar.savedQueryFormCancelButtonText": "キャンセル", - "data.search.searchBar.savedQueryFormSaveButtonText": "保存", - "data.search.searchBar.savedQueryFormTitle": "クエリを保存", - "data.search.searchBar.savedQueryIncludeFiltersLabelText": "フィルターを含める", - "data.search.searchBar.savedQueryIncludeTimeFilterLabelText": "時間フィルターを含める", - "data.search.searchBar.savedQueryNameHelpText": "名前が必要です。タイトルの始めと終わりにはスペースを使用できません。名前は固有でなければなりません。", - "data.search.searchBar.savedQueryNameLabelText": "名前", - "data.search.searchBar.savedQueryNoSavedQueriesText": "保存されたクエリがありません。", - "data.search.searchBar.savedQueryPopoverButtonText": "保存されたクエリを表示", - "data.search.searchBar.savedQueryPopoverClearButtonAriaLabel": "現在保存されているクエリを消去", - "data.search.searchBar.savedQueryPopoverClearButtonText": "消去", - "data.search.searchBar.savedQueryPopoverConfirmDeletionCancelButtonText": "キャンセル", - "data.search.searchBar.savedQueryPopoverConfirmDeletionConfirmButtonText": "削除", - "data.search.searchBar.savedQueryPopoverConfirmDeletionTitle": "「{savedQueryName}」を削除しますか?", - "data.search.searchBar.savedQueryPopoverDeleteButtonAriaLabel": "保存されたクエリ {savedQueryName} を削除", - "data.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel": "新規保存クエリを保存", - "data.search.searchBar.savedQueryPopoverSaveAsNewButtonText": "新規保存", - "data.search.searchBar.savedQueryPopoverSaveButtonAriaLabel": "新規保存クエリを保存", - "data.search.searchBar.savedQueryPopoverSaveButtonText": "現在のクエリを保存", - "data.search.searchBar.savedQueryPopoverSaveChangesButtonAriaLabel": "{title} への変更を保存", - "data.search.searchBar.savedQueryPopoverSaveChangesButtonText": "変更を保存", - "data.search.searchBar.savedQueryPopoverSavedQueryListItemButtonAriaLabel": "保存クエリボタン {savedQueryName}", - "data.search.searchBar.savedQueryPopoverSavedQueryListItemDescriptionAriaLabel": "{savedQueryName} の説明", - "data.search.searchBar.savedQueryPopoverSavedQueryListItemSelectedButtonAriaLabel": "選択されたクエリボタン {savedQueryName} を保存しました。変更を破棄するには押してください。", - "data.search.searchBar.savedQueryPopoverTitleText": "保存されたクエリ", "embeddableApi.actions.applyFilterActionTitle": "現在のビューにフィルターを適用", "embeddableApi.addPanel.createNewDefaultOption": "新規作成...", "embeddableApi.addPanel.displayName": "パネルの追加", @@ -1286,8 +1286,6 @@ "kbn.home.welcomeDescription": "Elastic Stack への開かれた窓", "kbn.home.welcomeHomePageHeader": "Kibana ホーム", "kbn.home.welcomeTitle": "Kibana へようこそ", - "advancedSettings.badge.readOnly.text": "読み込み専用", - "advancedSettings.badge.readOnly.tooltip": "高度な設定を保存できません", "kbn.management.createIndexPattern.betaLabel": "ベータ", "kbn.management.createIndexPattern.emptyState.checkDataButton": "新規データを確認", "kbn.management.createIndexPattern.emptyStateHeader": "Elasticsearch データが見つかりませんでした", @@ -1435,8 +1433,6 @@ "kbn.management.indexPattern.confirmOverwriteTitle": "{type} を上書きしますか?", "kbn.management.indexPattern.sectionsHeader": "インデックスパターン", "kbn.management.indexPattern.titleExistsLabel": "「{title}」というタイトルのインデックスパターンが既に存在します。", - "management.indexPatternHeader": "インデックスパターン", - "management.indexPatternLabel": "Elasticsearch からのデータの取得に役立つインデックスパターンを管理します。", "kbn.management.indexPatternList.createButton.betaLabel": "ベータ", "kbn.management.indexPatternPrompt.exampleOne": "チャートを作成したりコンテンツを素早くクエリできるように log-west-001 という名前の単一のデータソースをインデックスします。", "kbn.management.indexPatternPrompt.exampleOneTitle": "単一のデータソース", @@ -1555,9 +1551,7 @@ "kbn.management.objects.objectsTable.table.typeFilterName": "タイプ", "kbn.management.objects.objectsTable.unableFindSavedObjectsNotificationMessage": "保存されたオブジェクトが見つかりません", "kbn.management.objects.parsingFieldErrorMessage": "{fieldName} をインデックスパターン {indexName} 用にパース中にエラーが発生しました: {errorMessage}", - "management.objects.savedObjectsDescription": "保存された検索、ビジュアライゼーション、ダッシュボードのインポート、エクスポート、管理を行います", "kbn.management.objects.savedObjectsSectionLabel": "保存されたオブジェクト", - "management.objects.savedObjectsTitle": "保存されたオブジェクト", "kbn.management.objects.view.cancelButtonAriaLabel": "キャンセル", "kbn.management.objects.view.cancelButtonLabel": "キャンセル", "kbn.management.objects.view.deleteItemButtonLabel": "{title} を削除", @@ -1575,50 +1569,7 @@ "kbn.management.objects.view.viewItemTitle": "{title} を表示", "kbn.management.savedObjects.editBreadcrumb": "{savedObjectType} を編集", "kbn.management.savedObjects.indexBreadcrumb": "保存されたオブジェクト", - "advancedSettings.advancedSettingsLabel": "高度な設定", - "advancedSettings.callOutCautionDescription": "これらの設定は非常に上級ユーザー向けなのでご注意ください。ここでの変更は Kibana の重要な部分に不具合を生じさせる可能性があります。これらの設定はドキュメントに記載されていなかったり、サポートされていなかったり、実験的であったりします。フィールドにデフォルトの値が設定されている場合、そのフィールドを未入力のままにするとデフォルトに戻り、他の構成により利用できない可能性があります。カスタム設定を削除すると、Kibana の構成から永久に削除されます。", - "advancedSettings.callOutCautionTitle": "注意:不具合が起こる可能性があります", - "advancedSettings.categoryNames.dashboardLabel": "ダッシュボード", - "advancedSettings.categoryNames.discoverLabel": "ディスカバリ", - "advancedSettings.categoryNames.generalLabel": "一般", - "advancedSettings.categoryNames.notificationsLabel": "通知", - "advancedSettings.categoryNames.reportingLabel": "レポート", - "advancedSettings.categoryNames.searchLabel": "検索", - "advancedSettings.categoryNames.siemLabel": "SIEM", - "advancedSettings.categoryNames.timelionLabel": "Timelion", - "advancedSettings.categoryNames.visualizationsLabel": "ビジュアライゼーション", - "advancedSettings.categorySearchLabel": "カテゴリー", - "advancedSettings.field.cancelEditingButtonAriaLabel": "{ariaName} の編集をキャンセル", - "advancedSettings.field.cancelEditingButtonLabel": "キャンセル", - "advancedSettings.field.changeImageLinkAriaLabel": "{ariaName} を変更", - "advancedSettings.field.changeImageLinkText": "画像を変更", - "advancedSettings.field.codeEditorSyntaxErrorMessage": "無効な JSON 構文", - "advancedSettings.field.customSettingAriaLabel": "カスタム設定", - "advancedSettings.field.customSettingTooltip": "カスタム設定", - "advancedSettings.field.defaultValueText": "デフォルト: {value}", - "advancedSettings.field.defaultValueTypeJsonText": "デフォルト: {value}", - "advancedSettings.field.helpText": "この設定は Kibana サーバーにより上書きされ、変更することはできません。", - "advancedSettings.field.imageChangeErrorMessage": "画像を保存できませんでした", - "advancedSettings.field.imageTooLargeErrorMessage": "画像が大きすぎます。最大サイズは {maxSizeDescription} です", - "advancedSettings.field.offLabel": "オフ", - "advancedSettings.field.onLabel": "オン", - "advancedSettings.field.requiresPageReloadToastButtonLabel": "ページを再読み込み", - "advancedSettings.field.requiresPageReloadToastDescription": "「{settingName}」設定を有効にするには、ページを再読み込みしてください。", - "advancedSettings.field.resetFieldErrorMessage": "{name} をリセットできませんでした", - "advancedSettings.field.resetToDefaultLinkAriaLabel": "{ariaName} をデフォルトにリセット", - "advancedSettings.field.resetToDefaultLinkText": "デフォルトにリセット", - "advancedSettings.field.saveButtonAriaLabel": "{ariaName} を保存", - "advancedSettings.field.saveButtonLabel": "保存", - "advancedSettings.field.saveFieldErrorMessage": "{name} を保存できませんでした", - "advancedSettings.form.clearNoSearchResultText": "(検索結果を消去)", - "advancedSettings.form.clearSearchResultText": "(検索結果を消去)", - "advancedSettings.form.noSearchResultText": "設定が見つかりませんでした {clearSearch}", - "advancedSettings.form.searchResultText": "検索用語により {settingsCount} 件の設定が非表示になっています {clearSearch}", - "advancedSettings.pageTitle": "設定", - "advancedSettings.searchBar.unableToParseQueryErrorMessage": "クエリをパースできません", - "advancedSettings.searchBarAriaLabel": "高度な設定を検索", "kbn.managementTitle": "管理", - "advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "{query} を検索しました。{sectionLenght, plural, one {# セクション} other {# セクション}}に{optionLenght, plural, one {# オプション} other { # オプション}}があります。", "kbn.topNavMenu.openInspectorButtonLabel": "検査", "kbn.topNavMenu.refreshButtonLabel": "更新", "kbn.topNavMenu.saveVisualizationButtonLabel": "保存", @@ -1661,6 +1612,55 @@ "kbn.visualize.wizard.step1Breadcrumb": "作成", "kbn.visualize.wizard.step2Breadcrumb": "作成", "kbn.visualizeTitle": "可視化", + "advancedSettings.badge.readOnly.text": "読み込み専用", + "advancedSettings.badge.readOnly.tooltip": "高度な設定を保存できません", + "advancedSettings.advancedSettingsLabel": "高度な設定", + "advancedSettings.callOutCautionDescription": "これらの設定は非常に上級ユーザー向けなのでご注意ください。ここでの変更は Kibana の重要な部分に不具合を生じさせる可能性があります。これらの設定はドキュメントに記載されていなかったり、サポートされていなかったり、実験的であったりします。フィールドにデフォルトの値が設定されている場合、そのフィールドを未入力のままにするとデフォルトに戻り、他の構成により利用できない可能性があります。カスタム設定を削除すると、Kibana の構成から永久に削除されます。", + "advancedSettings.callOutCautionTitle": "注意:不具合が起こる可能性があります", + "advancedSettings.categoryNames.dashboardLabel": "ダッシュボード", + "advancedSettings.categoryNames.discoverLabel": "ディスカバリ", + "advancedSettings.categoryNames.generalLabel": "一般", + "advancedSettings.categoryNames.notificationsLabel": "通知", + "advancedSettings.categoryNames.reportingLabel": "レポート", + "advancedSettings.categoryNames.searchLabel": "検索", + "advancedSettings.categoryNames.siemLabel": "SIEM", + "advancedSettings.categoryNames.timelionLabel": "Timelion", + "advancedSettings.categoryNames.visualizationsLabel": "ビジュアライゼーション", + "advancedSettings.categorySearchLabel": "カテゴリー", + "advancedSettings.field.cancelEditingButtonAriaLabel": "{ariaName} の編集をキャンセル", + "advancedSettings.field.cancelEditingButtonLabel": "キャンセル", + "advancedSettings.field.changeImageLinkAriaLabel": "{ariaName} を変更", + "advancedSettings.field.changeImageLinkText": "画像を変更", + "advancedSettings.field.codeEditorSyntaxErrorMessage": "無効な JSON 構文", + "advancedSettings.field.customSettingAriaLabel": "カスタム設定", + "advancedSettings.field.customSettingTooltip": "カスタム設定", + "advancedSettings.field.defaultValueText": "デフォルト: {value}", + "advancedSettings.field.defaultValueTypeJsonText": "デフォルト: {value}", + "advancedSettings.field.helpText": "この設定は Kibana サーバーにより上書きされ、変更することはできません。", + "advancedSettings.field.imageChangeErrorMessage": "画像を保存できませんでした", + "advancedSettings.field.imageTooLargeErrorMessage": "画像が大きすぎます。最大サイズは {maxSizeDescription} です", + "advancedSettings.field.offLabel": "オフ", + "advancedSettings.field.onLabel": "オン", + "advancedSettings.field.requiresPageReloadToastButtonLabel": "ページを再読み込み", + "advancedSettings.field.requiresPageReloadToastDescription": "「{settingName}」設定を有効にするには、ページを再読み込みしてください。", + "advancedSettings.field.resetFieldErrorMessage": "{name} をリセットできませんでした", + "advancedSettings.field.resetToDefaultLinkAriaLabel": "{ariaName} をデフォルトにリセット", + "advancedSettings.field.resetToDefaultLinkText": "デフォルトにリセット", + "advancedSettings.field.saveButtonAriaLabel": "{ariaName} を保存", + "advancedSettings.field.saveButtonLabel": "保存", + "advancedSettings.field.saveFieldErrorMessage": "{name} を保存できませんでした", + "advancedSettings.form.clearNoSearchResultText": "(検索結果を消去)", + "advancedSettings.form.clearSearchResultText": "(検索結果を消去)", + "advancedSettings.form.noSearchResultText": "設定が見つかりませんでした {clearSearch}", + "advancedSettings.form.searchResultText": "検索用語により {settingsCount} 件の設定が非表示になっています {clearSearch}", + "advancedSettings.pageTitle": "設定", + "advancedSettings.searchBar.unableToParseQueryErrorMessage": "クエリをパースできません", + "advancedSettings.searchBarAriaLabel": "高度な設定を検索", + "advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "{query} を検索しました。{sectionLenght, plural, one {# セクション} other {# セクション}}に{optionLenght, plural, one {# オプション} other { # オプション}}があります。", + "management.indexPatternHeader": "インデックスパターン", + "management.indexPatternLabel": "Elasticsearch からのデータの取得に役立つインデックスパターンを管理します。", + "management.objects.savedObjectsDescription": "保存された検索、ビジュアライゼーション、ダッシュボードのインポート、エクスポート、管理を行います", + "management.objects.savedObjectsTitle": "保存されたオブジェクト", "kibana_legacy.bigUrlWarningNotificationMessage": "{advancedSettingsLink}で{storeInSessionStorageParam}オプションを有効にするか、オンスクリーンビジュアルを簡素化してください。", "kibana_legacy.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高度な設定", "kibana_legacy.bigUrlWarningNotificationTitle": "URLが大きく、Kibanaの動作が停止する可能性があります", @@ -2492,10 +2492,6 @@ "telemetry.seeExampleOfWhatWeCollectLinkText": "収集されるデータの例を見る", "telemetry.telemetryBannerDescription": "Elastic Stackの改善にご協力ください使用状況データの収集は現在無効です。使用状況データの収集を有効にすると、製品とサービスを管理して改善することができます。詳細については、{privacyStatementLink}をご覧ください。", "telemetry.telemetryConfigDescription": "基本的な機能の利用状況に関する統計情報を提供して、Elastic Stack の改善にご協力ください。このデータは Elastic 社外と共有されません。", - "telemetry.telemetryConfigTitle": "遠隔測定オプトイン", - "telemetry.telemetryErrorNotificationMessageDescription.tryAgainText": "Kibana と Elasticsearch が現在も実行中であることを確認し、再試行してください。", - "telemetry.telemetryErrorNotificationMessageDescription.unableToSaveTelemetryPreferenceText": "遠隔測定設定を保存できません。", - "telemetry.telemetryErrorNotificationMessageTitle": "遠隔測定エラー", "telemetry.telemetryOptedInDisableUsage": "ここで使用状況データを無効にする", "telemetry.telemetryOptedInDismissMessage": "閉じる", "telemetry.telemetryOptedInNoticeDescription": "使用状況データがどのように製品とサービスの管理と改善につながるのかに関する詳細については、{privacyStatementLink}をご覧ください。収集を停止するには、{disableLink}。", @@ -3918,13 +3914,6 @@ "xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigFailedTitle": "構成を削除できませんでした", "xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigSucceededText": "「{serviceName}」の構成が正常に削除されました。エージェントに反映されるまでに少し時間がかかります。", "xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigSucceededTitle": "構成が削除されました", - "xpack.apm.settings.agentConf.flyOut.serviceSection.alreadyConfiguredOption": "既に構成済み", - "xpack.apm.settings.agentConf.flyOut.serviceSection.selectPlaceholder": "選択してください", - "xpack.apm.settings.agentConf.flyOut.serviceSection.serviceEnvironmentSelectHelpText": "構成ごとに 1 つの環境のみがサポートされます。", - "xpack.apm.settings.agentConf.flyOut.serviceSection.serviceEnvironmentSelectLabel": "環境", - "xpack.apm.settings.agentConf.flyOut.serviceSection.serviceNameSelectHelpText": "構成するサービスを選択してください。", - "xpack.apm.settings.agentConf.flyOut.serviceSection.serviceNameSelectLabel": "名前", - "xpack.apm.settings.agentConf.flyOut.serviceSection.title": "サービス", "xpack.apm.settings.agentConf.flyOut.settingsSection.captureBodyInputHelpText": "HTTP リクエストのトランザクションの場合、エージェントはリクエスト本文 (POST 変数など) をキャプチャすることができます。デフォルトは「off」です。", "xpack.apm.settings.agentConf.flyOut.settingsSection.captureBodyInputLabel": "本文をキャプチャ", "xpack.apm.settings.agentConf.flyOut.settingsSection.captureBodyInputPlaceholderText": "オプションを選択", @@ -13187,4 +13176,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。", "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4a2c33eba79da..ce1c713adc4bc 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -78,109 +78,6 @@ "messages": { "common.ui.aggResponse.allDocsTitle": "所有文档", "common.ui.aggTypes.rangesFormatMessage": "{gte} {from} 且 {lt} {to}", - "data.search.aggs.aggGroups.bucketsText": "存储桶", - "data.search.aggs.aggGroups.metricsText": "指标", - "data.search.aggs.buckets.dateHistogramLabel": "{fieldName}/{intervalDescription}", - "data.search.aggs.buckets.dateHistogramTitle": "Date Histogram", - "data.search.aggs.buckets.dateRangeTitle": "日期范围", - "data.search.aggs.buckets.filtersTitle": "筛选", - "data.search.aggs.buckets.filterTitle": "筛选", - "data.search.aggs.buckets.geohashGridTitle": "Geohash", - "data.search.aggs.buckets.geotileGridTitle": "地理磁贴", - "data.search.aggs.buckets.histogramTitle": "Histogram", - "data.search.aggs.buckets.intervalOptions.autoDisplayName": "自动", - "data.search.aggs.buckets.intervalOptions.dailyDisplayName": "每日", - "data.search.aggs.buckets.intervalOptions.hourlyDisplayName": "每小时", - "data.search.aggs.buckets.intervalOptions.millisecondDisplayName": "毫秒", - "data.search.aggs.buckets.intervalOptions.minuteDisplayName": "分钟", - "data.search.aggs.buckets.intervalOptions.monthlyDisplayName": "每月", - "data.search.aggs.buckets.intervalOptions.secondDisplayName": "秒", - "data.search.aggs.buckets.intervalOptions.weeklyDisplayName": "每周", - "data.search.aggs.buckets.intervalOptions.yearlyDisplayName": "每年", - "data.search.aggs.buckets.ipRangeLabel": "{fieldName} IP 范围", - "data.search.aggs.buckets.ipRangeTitle": "IPv4 范围", - "data.search.aggs.aggTypes.rangesFormatMessage": "{gte} {from} 且 {lt} {to}", - "data.search.aggs.aggTypesLabel": "{fieldName} 范围", - "data.search.aggs.buckets.rangeTitle": "范围", - "data.search.aggs.buckets.significantTerms.excludeLabel": "排除", - "data.search.aggs.buckets.significantTerms.includeLabel": "包括", - "data.search.aggs.buckets.significantTermsLabel": "{fieldName} 中排名前 {size} 的罕见词", - "data.search.aggs.buckets.significantTermsTitle": "重要词", - "data.search.aggs.buckets.terms.excludeLabel": "排除", - "data.search.aggs.buckets.terms.includeLabel": "包括", - "data.search.aggs.buckets.terms.missingBucketLabel": "缺失", - "data.search.aggs.buckets.terms.orderAscendingTitle": "升序", - "data.search.aggs.buckets.terms.orderDescendingTitle": "降序", - "data.search.aggs.buckets.terms.otherBucketDescription": "此请求计数不符合数据存储桶条件的文档数目。", - "data.search.aggs.buckets.terms.otherBucketLabel": "其他", - "data.search.aggs.buckets.terms.otherBucketTitle": "其他存储桶", - "data.search.aggs.buckets.termsTitle": "词", - "data.search.aggs.histogram.missingMaxMinValuesWarning": "无法检索最大值和最小值以自动缩放直方图存储桶。这可能会导致可视化性能低下。", - "data.search.aggs.metrics.averageBucketTitle": "平均存储桶", - "data.search.aggs.metrics.averageLabel": "{field}平均值", - "data.search.aggs.metrics.averageTitle": "平均值", - "data.search.aggs.metrics.bucketAggTitle": "存储桶聚合", - "data.search.aggs.metrics.countLabel": "计数", - "data.search.aggs.metrics.countTitle": "计数", - "data.search.aggs.metrics.cumulativeSumLabel": "累计和", - "data.search.aggs.metrics.cumulativeSumTitle": "累计和", - "data.search.aggs.metrics.derivativeLabel": "导数", - "data.search.aggs.metrics.derivativeTitle": "导数", - "data.search.aggs.metrics.geoBoundsLabel": "地理边界", - "data.search.aggs.metrics.geoBoundsTitle": "地理边界", - "data.search.aggs.metrics.geoCentroidLabel": "地理重心", - "data.search.aggs.metrics.geoCentroidTitle": "地理重心", - "data.search.aggs.metrics.maxBucketTitle": "最大存储桶", - "data.search.aggs.metrics.maxLabel": "{field}最大值", - "data.search.aggs.metrics.maxTitle": "最大值", - "data.search.aggs.metrics.medianLabel": "{field}中值", - "data.search.aggs.metrics.medianTitle": "中值", - "data.search.aggs.metrics.metricAggregationsSubtypeTitle": "指标聚合", - "data.search.aggs.metrics.metricAggTitle": "指标聚合", - "data.search.aggs.metrics.minBucketTitle": "最小存储桶", - "data.search.aggs.metrics.minLabel": "{field}最小值", - "data.search.aggs.metrics.minTitle": "最小值", - "data.search.aggs.metrics.movingAvgLabel": "移动平均值", - "data.search.aggs.metrics.movingAvgTitle": "移动平均值", - "data.search.aggs.metrics.overallAverageLabel": "总体平均值", - "data.search.aggs.metrics.overallMaxLabel": "总体最大值", - "data.search.aggs.metrics.overallMinLabel": "总体最大值", - "data.search.aggs.metrics.overallSumLabel": "总和", - "data.search.aggs.metrics.parentPipelineAggregationsSubtypeTitle": "父级管道聚合", - "data.search.aggs.metrics.percentileRanks.valuePropsLabel": "“{label}” 的百分位数排名 {format}", - "data.search.aggs.metrics.percentileRanksLabel": "“{field}” 的百分位数排名", - "data.search.aggs.metrics.percentileRanksTitle": "百分位数排名", - "data.search.aggs.metrics.percentiles.valuePropsLabel": "“{label}” 的 {percentile} 百分位数", - "data.search.aggs.metrics.percentilesLabel": "“{field}” 的百分位数", - "data.search.aggs.metrics.percentilesTitle": "百分位数", - "data.search.aggs.metrics.serialDiffLabel": "序列差异", - "data.search.aggs.metrics.serialDiffTitle": "序列差异", - "data.search.aggs.metrics.siblingPipelineAggregationsSubtypeTitle": "同级管道聚合", - "data.search.aggs.metrics.standardDeviation.keyDetailsLabel": "“{fieldDisplayName}” 的标准偏差", - "data.search.aggs.metrics.standardDeviation.lowerKeyDetailsTitle": "下{label}", - "data.search.aggs.metrics.standardDeviation.upperKeyDetailsTitle": "上{label}", - "data.search.aggs.metrics.standardDeviationLabel": "“{field}” 的标准偏差", - "data.search.aggs.metrics.standardDeviationTitle": "标准偏差", - "data.search.aggs.metrics.sumBucketTitle": "求和存储桶", - "data.search.aggs.metrics.sumLabel": "“{field}” 的和", - "data.search.aggs.metrics.sumTitle": "和", - "data.search.aggs.metrics.topHit.ascendingLabel": "升序", - "data.search.aggs.metrics.topHit.averageLabel": "平均值", - "data.search.aggs.metrics.topHit.concatenateLabel": "连接", - "data.search.aggs.metrics.topHit.descendingLabel": "降序", - "data.search.aggs.metrics.topHit.firstPrefixLabel": "第一", - "data.search.aggs.metrics.topHit.lastPrefixLabel": "最后", - "data.search.aggs.metrics.topHit.maxLabel": "最大值", - "data.search.aggs.metrics.topHit.minLabel": "最小值", - "data.search.aggs.metrics.topHit.sumLabel": "和", - "data.search.aggs.metrics.topHitTitle": "最高命中结果", - "data.search.aggs.metrics.uniqueCountLabel": "“{field}” 的唯一计数", - "data.search.aggs.metrics.uniqueCountTitle": "唯一计数", - "data.search.aggs.otherBucket.labelForMissingValuesLabel": "缺失值的标签", - "data.search.aggs.otherBucket.labelForOtherBucketLabel": "其他存储桶的标签", - "data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage": "已保存的 {fieldParameter} 参数现在无效。请选择新字段。", - "data.search.aggs.paramTypes.field.requiredFieldParameterErrorMessage": "{fieldParameter} 是必需字段", - "data.search.aggs.string.customLabel": "定制标签", "common.ui.directives.paginate.size.allDropDownOptionLabel": "全部", "common.ui.dualRangeControl.mustSetBothErrorMessage": "下限值和上限值都须设置", "common.ui.dualRangeControl.outsideOfRangeErrorMessage": "值必须是在 {min} 到 {max} 的范围内", @@ -367,10 +264,223 @@ "common.ui.stateManagement.unableToStoreHistoryInSessionErrorMessage": "Kibana 无法将历史记录项存储在您的会话中,因为其已满,并且似乎没有任何可安全删除的项。\n\n通常可通过移至新的标签页来解决此问题,但这会导致更大的问题。如果您有规律地看到此消息,请在 {gitHubIssuesUrl} 提交问题。", "common.ui.url.replacementFailedErrorMessage": "替换失败,未解析的表达式:{expr}", "common.ui.url.savedObjectIsMissingNotificationMessage": "已保存对象缺失", - "data.search.aggs.percentageOfLabel": "{label} 的百分比", "common.ui.vis.defaultFeedbackMessage": "想反馈?请在“{link}中创建问题。", "common.ui.vis.kibanaMap.leaflet.fitDataBoundsAriaLabel": "适应数据边界", "common.ui.vis.kibanaMap.zoomWarning": "已达到缩放级别最大数目。要一直放大,请升级到 Elasticsearch 和 Kibana 的 {defaultDistribution}。您可以通过 {ems} 免费使用其他缩放级别。或者,您可以配置自己的地图服务器。请前往 { wms } 或 { configSettings} 以获取详细信息。", + "data.search.aggs.aggGroups.bucketsText": "存储桶", + "data.search.aggs.aggGroups.metricsText": "指标", + "data.search.aggs.buckets.dateHistogramLabel": "{fieldName}/{intervalDescription}", + "data.search.aggs.buckets.dateHistogramTitle": "Date Histogram", + "data.search.aggs.buckets.dateRangeTitle": "日期范围", + "data.search.aggs.buckets.filtersTitle": "筛选", + "data.search.aggs.buckets.filterTitle": "筛选", + "data.search.aggs.buckets.geohashGridTitle": "Geohash", + "data.search.aggs.buckets.geotileGridTitle": "地理磁贴", + "data.search.aggs.buckets.histogramTitle": "Histogram", + "data.search.aggs.buckets.intervalOptions.autoDisplayName": "自动", + "data.search.aggs.buckets.intervalOptions.dailyDisplayName": "每日", + "data.search.aggs.buckets.intervalOptions.hourlyDisplayName": "每小时", + "data.search.aggs.buckets.intervalOptions.millisecondDisplayName": "毫秒", + "data.search.aggs.buckets.intervalOptions.minuteDisplayName": "分钟", + "data.search.aggs.buckets.intervalOptions.monthlyDisplayName": "每月", + "data.search.aggs.buckets.intervalOptions.secondDisplayName": "秒", + "data.search.aggs.buckets.intervalOptions.weeklyDisplayName": "每周", + "data.search.aggs.buckets.intervalOptions.yearlyDisplayName": "每年", + "data.search.aggs.buckets.ipRangeLabel": "{fieldName} IP 范围", + "data.search.aggs.buckets.ipRangeTitle": "IPv4 范围", + "data.search.aggs.aggTypes.rangesFormatMessage": "{gte} {from} 且 {lt} {to}", + "data.search.aggs.aggTypesLabel": "{fieldName} 范围", + "data.search.aggs.buckets.rangeTitle": "范围", + "data.search.aggs.buckets.significantTerms.excludeLabel": "排除", + "data.search.aggs.buckets.significantTerms.includeLabel": "包括", + "data.search.aggs.buckets.significantTermsLabel": "{fieldName} 中排名前 {size} 的罕见词", + "data.search.aggs.buckets.significantTermsTitle": "重要词", + "data.search.aggs.buckets.terms.excludeLabel": "排除", + "data.search.aggs.buckets.terms.includeLabel": "包括", + "data.search.aggs.buckets.terms.missingBucketLabel": "缺失", + "data.search.aggs.buckets.terms.orderAscendingTitle": "升序", + "data.search.aggs.buckets.terms.orderDescendingTitle": "降序", + "data.search.aggs.buckets.terms.otherBucketDescription": "此请求计数不符合数据存储桶条件的文档数目。", + "data.search.aggs.buckets.terms.otherBucketLabel": "其他", + "data.search.aggs.buckets.terms.otherBucketTitle": "其他存储桶", + "data.search.aggs.buckets.termsTitle": "词", + "data.search.aggs.histogram.missingMaxMinValuesWarning": "无法检索最大值和最小值以自动缩放直方图存储桶。这可能会导致可视化性能低下。", + "data.search.aggs.metrics.averageBucketTitle": "平均存储桶", + "data.search.aggs.metrics.averageLabel": "{field}平均值", + "data.search.aggs.metrics.averageTitle": "平均值", + "data.search.aggs.metrics.bucketAggTitle": "存储桶聚合", + "data.search.aggs.metrics.countLabel": "计数", + "data.search.aggs.metrics.countTitle": "计数", + "data.search.aggs.metrics.cumulativeSumLabel": "累计和", + "data.search.aggs.metrics.cumulativeSumTitle": "累计和", + "data.search.aggs.metrics.derivativeLabel": "导数", + "data.search.aggs.metrics.derivativeTitle": "导数", + "data.search.aggs.metrics.geoBoundsLabel": "地理边界", + "data.search.aggs.metrics.geoBoundsTitle": "地理边界", + "data.search.aggs.metrics.geoCentroidLabel": "地理重心", + "data.search.aggs.metrics.geoCentroidTitle": "地理重心", + "data.search.aggs.metrics.maxBucketTitle": "最大存储桶", + "data.search.aggs.metrics.maxLabel": "{field}最大值", + "data.search.aggs.metrics.maxTitle": "最大值", + "data.search.aggs.metrics.medianLabel": "{field}中值", + "data.search.aggs.metrics.medianTitle": "中值", + "data.search.aggs.metrics.metricAggregationsSubtypeTitle": "指标聚合", + "data.search.aggs.metrics.metricAggTitle": "指标聚合", + "data.search.aggs.metrics.minBucketTitle": "最小存储桶", + "data.search.aggs.metrics.minLabel": "{field}最小值", + "data.search.aggs.metrics.minTitle": "最小值", + "data.search.aggs.metrics.movingAvgLabel": "移动平均值", + "data.search.aggs.metrics.movingAvgTitle": "移动平均值", + "data.search.aggs.metrics.overallAverageLabel": "总体平均值", + "data.search.aggs.metrics.overallMaxLabel": "总体最大值", + "data.search.aggs.metrics.overallMinLabel": "总体最大值", + "data.search.aggs.metrics.overallSumLabel": "总和", + "data.search.aggs.metrics.parentPipelineAggregationsSubtypeTitle": "父级管道聚合", + "data.search.aggs.metrics.percentileRanks.valuePropsLabel": "“{label}” 的百分位数排名 {format}", + "data.search.aggs.metrics.percentileRanksLabel": "“{field}” 的百分位数排名", + "data.search.aggs.metrics.percentileRanksTitle": "百分位数排名", + "data.search.aggs.metrics.percentiles.valuePropsLabel": "“{label}” 的 {percentile} 百分位数", + "data.search.aggs.metrics.percentilesLabel": "“{field}” 的百分位数", + "data.search.aggs.metrics.percentilesTitle": "百分位数", + "data.search.aggs.metrics.serialDiffLabel": "序列差异", + "data.search.aggs.metrics.serialDiffTitle": "序列差异", + "data.search.aggs.metrics.siblingPipelineAggregationsSubtypeTitle": "同级管道聚合", + "data.search.aggs.metrics.standardDeviation.keyDetailsLabel": "“{fieldDisplayName}” 的标准偏差", + "data.search.aggs.metrics.standardDeviation.lowerKeyDetailsTitle": "下{label}", + "data.search.aggs.metrics.standardDeviation.upperKeyDetailsTitle": "上{label}", + "data.search.aggs.metrics.standardDeviationLabel": "“{field}” 的标准偏差", + "data.search.aggs.metrics.standardDeviationTitle": "标准偏差", + "data.search.aggs.metrics.sumBucketTitle": "求和存储桶", + "data.search.aggs.metrics.sumLabel": "“{field}” 的和", + "data.search.aggs.metrics.sumTitle": "和", + "data.search.aggs.metrics.topHit.ascendingLabel": "升序", + "data.search.aggs.metrics.topHit.averageLabel": "平均值", + "data.search.aggs.metrics.topHit.concatenateLabel": "连接", + "data.search.aggs.metrics.topHit.descendingLabel": "降序", + "data.search.aggs.metrics.topHit.firstPrefixLabel": "第一", + "data.search.aggs.metrics.topHit.lastPrefixLabel": "最后", + "data.search.aggs.metrics.topHit.maxLabel": "最大值", + "data.search.aggs.metrics.topHit.minLabel": "最小值", + "data.search.aggs.metrics.topHit.sumLabel": "和", + "data.search.aggs.metrics.topHitTitle": "最高命中结果", + "data.search.aggs.metrics.uniqueCountLabel": "“{field}” 的唯一计数", + "data.search.aggs.metrics.uniqueCountTitle": "唯一计数", + "data.search.aggs.otherBucket.labelForMissingValuesLabel": "缺失值的标签", + "data.search.aggs.otherBucket.labelForOtherBucketLabel": "其他存储桶的标签", + "data.search.aggs.paramTypes.field.invalidSavedFieldParameterErrorMessage": "已保存的 {fieldParameter} 参数现在无效。请选择新字段。", + "data.search.aggs.paramTypes.field.requiredFieldParameterErrorMessage": "{fieldParameter} 是必需字段", + "data.search.aggs.string.customLabel": "定制标签", + "data.search.aggs.percentageOfLabel": "{label} 的百分比", + "data.filter.applyFilters.popupHeader": "选择要应用的筛选", + "data.filter.applyFiltersPopup.cancelButtonLabel": "取消", + "data.filter.applyFiltersPopup.saveButtonLabel": "应用", + "data.filter.filterBar.addFilterButtonLabel": "添加筛选", + "data.filter.filterBar.deleteFilterButtonLabel": "删除", + "data.filter.filterBar.disabledFilterPrefix": "已禁用", + "data.filter.filterBar.disableFilterButtonLabel": "暂时禁用", + "data.filter.filterBar.editFilterButtonLabel": "编辑筛选", + "data.filter.filterBar.enableFilterButtonLabel": "重新启用", + "data.filter.filterBar.excludeFilterButtonLabel": "排除结果", + "data.filter.filterBar.filterItemBadgeAriaLabel": "筛选操作", + "data.filter.filterBar.filterItemBadgeIconAriaLabel": "删除", + "data.filter.filterBar.includeFilterButtonLabel": "包括结果", + "data.filter.filterBar.indexPatternSelectPlaceholder": "选择索引模式", + "data.filter.filterBar.moreFilterActionsMessage": "筛选:{innerText}。选择以获取更多筛选操作。", + "data.filter.filterBar.negatedFilterPrefix": "非 ", + "data.filter.filterBar.pinFilterButtonLabel": "在所有应用上固定", + "data.filter.filterBar.pinnedFilterPrefix": "已固定", + "data.filter.filterBar.unpinFilterButtonLabel": "取消固定", + "data.filter.filterEditor.cancelButtonLabel": "取消", + "data.filter.filterEditor.createCustomLabelInputLabel": "定制标签", + "data.filter.filterEditor.createCustomLabelSwitchLabel": "创建定制标签?", + "data.filter.filterEditor.dateFormatHelpLinkLabel": "已接受日期格式", + "data.filter.filterEditor.doesNotExistOperatorOptionLabel": "不存在", + "data.filter.filterEditor.editFilterPopupTitle": "编辑筛选", + "data.filter.filterEditor.editFilterValuesButtonLabel": "编辑筛选值", + "data.filter.filterEditor.editQueryDslButtonLabel": "编辑为查询 DSL", + "data.filter.filterEditor.existsOperatorOptionLabel": "存在", + "data.filter.filterEditor.falseOptionLabel": "false", + "data.filter.filterEditor.fieldSelectLabel": "字段", + "data.filter.filterEditor.fieldSelectPlaceholder": "首先选择字段", + "data.filter.filterEditor.indexPatternSelectLabel": "索引模式", + "data.filter.filterEditor.isBetweenOperatorOptionLabel": "介于", + "data.filter.filterEditor.isNotBetweenOperatorOptionLabel": "不介于", + "data.filter.filterEditor.isNotOneOfOperatorOptionLabel": "不属于", + "data.filter.filterEditor.isNotOperatorOptionLabel": "不是", + "data.filter.filterEditor.isOneOfOperatorOptionLabel": "属于", + "data.filter.filterEditor.isOperatorOptionLabel": "是", + "data.filter.filterEditor.operatorSelectLabel": "运算符", + "data.filter.filterEditor.operatorSelectPlaceholderSelect": "选择", + "data.filter.filterEditor.operatorSelectPlaceholderWaiting": "正在等候", + "data.filter.filterEditor.queryDslLabel": "Elasticsearch 查询 DSL", + "data.filter.filterEditor.rangeEndInputPlaceholder": "范围结束", + "data.filter.filterEditor.rangeInputLabel": "范围", + "data.filter.filterEditor.rangeStartInputPlaceholder": "范围开始", + "data.filter.filterEditor.saveButtonLabel": "保存", + "data.filter.filterEditor.trueOptionLabel": "true", + "data.filter.filterEditor.valueInputLabel": "值", + "data.filter.filterEditor.valueInputPlaceholder": "输入值", + "data.filter.filterEditor.valueSelectPlaceholder": "选择值", + "data.filter.filterEditor.valuesSelectLabel": "值", + "data.filter.filterEditor.valuesSelectPlaceholder": "选择值", + "data.filter.options.changeAllFiltersButtonLabel": "更改所有筛选", + "data.filter.options.deleteAllFiltersButtonLabel": "全部删除", + "data.filter.options.disableAllFiltersButtonLabel": "全部禁用", + "data.filter.options.enableAllFiltersButtonLabel": "全部启用", + "data.filter.options.invertDisabledFiltersButtonLabel": "反向已启用/已禁用", + "data.filter.options.invertNegatedFiltersButtonLabel": "反向包括", + "data.filter.options.pinAllFiltersButtonLabel": "全部固定", + "data.filter.options.unpinAllFiltersButtonLabel": "全部取消固定", + "data.filter.searchBar.changeAllFiltersTitle": "更改所有筛选", + "data.indexPatterns.unableWriteLabel": "无法写入索引模式!请刷新页面以获取此索引模式的最新更改。", + "data.indexPatterns.unknownFieldErrorMessage": "indexPattern “{title}” 中的字段 “{name}” 使用未知字段类型。", + "data.indexPatterns.unknownFieldHeader": "未知字段类型 {type}", + "data.parseEsInterval.invalidEsCalendarIntervalErrorMessage": "无效的日历时间间隔:{interval},值必须为 1", + "data.parseEsInterval.invalidEsIntervalFormatErrorMessage": "时间间隔格式无效:{interval}", + "data.query.queryBar.comboboxAriaLabel": "搜索并筛选 {pageType} 页面", + "data.query.queryBar.kqlFullLanguageName": "Kibana 查询语言", + "data.query.queryBar.kqlLanguageName": "KQL", + "data.query.queryBar.kqlOffLabel": "关闭", + "data.query.queryBar.kqlOnLabel": "开启", + "data.query.queryBar.luceneLanguageName": "Lucene", + "data.query.queryBar.luceneSyntaxWarningMessage": "尽管您选择了 Kibana 查询语言 (KQL),但似乎您正在尝试使用 Lucene 查询语法。请查看 KQL 文档 {link}。", + "data.query.queryBar.luceneSyntaxWarningOptOutText": "不再显示", + "data.query.queryBar.luceneSyntaxWarningTitle": "Lucene 语法警告", + "data.query.queryBar.searchInputAriaLabel": "开始键入内容,以搜索并筛选 {pageType} 页面", + "data.query.queryBar.searchInputPlaceholder": "搜索", + "data.query.queryBar.syntaxOptionsDescription": "{docsLink} (KQL) 提供简化查询语法并支持脚本字段。如果您具有基本许可或更高级别的许可,KQL 还提供自动填充功能。如果关闭 KQL,Kibana 将使用 Lucene。", + "data.query.queryBar.syntaxOptionsDescription.docsLinkText": "此处", + "data.query.queryBar.syntaxOptionsTitle": "语法选项", + "data.search.searchBar.savedQueryDescriptionLabelText": "描述", + "data.search.searchBar.savedQueryDescriptionText": "保存想要再次使用的查询文本和筛选。", + "data.search.searchBar.savedQueryForm.titleConflictText": "标题与现有已保存查询有冲突", + "data.search.searchBar.savedQueryForm.titleMissingText": "“名称”必填", + "data.search.searchBar.savedQueryForm.whitespaceErrorText": "标题不能包含前导或尾随空格", + "data.search.searchBar.savedQueryFormCancelButtonText": "取消", + "data.search.searchBar.savedQueryFormSaveButtonText": "保存", + "data.search.searchBar.savedQueryFormTitle": "保存查询", + "data.search.searchBar.savedQueryIncludeFiltersLabelText": "包括筛选", + "data.search.searchBar.savedQueryIncludeTimeFilterLabelText": "包括时间筛选", + "data.search.searchBar.savedQueryNameHelpText": "“名称”必填。标题不能包含前导或尾随空格。名称必须唯一。", + "data.search.searchBar.savedQueryNameLabelText": "名称", + "data.search.searchBar.savedQueryNoSavedQueriesText": "没有已保存查询。", + "data.search.searchBar.savedQueryPopoverButtonText": "查看已保存查询", + "data.search.searchBar.savedQueryPopoverClearButtonAriaLabel": "清除当前已保存查询", + "data.search.searchBar.savedQueryPopoverClearButtonText": "清除", + "data.search.searchBar.savedQueryPopoverConfirmDeletionCancelButtonText": "取消", + "data.search.searchBar.savedQueryPopoverConfirmDeletionConfirmButtonText": "删除", + "data.search.searchBar.savedQueryPopoverConfirmDeletionTitle": "删除“{savedQueryName}”?", + "data.search.searchBar.savedQueryPopoverDeleteButtonAriaLabel": "删除已保存查询 {savedQueryName}", + "data.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel": "另存为新的已保存查询", + "data.search.searchBar.savedQueryPopoverSaveAsNewButtonText": "另存为新的", + "data.search.searchBar.savedQueryPopoverSaveButtonAriaLabel": "保存新的已保存查询", + "data.search.searchBar.savedQueryPopoverSaveButtonText": "保存当前查询", + "data.search.searchBar.savedQueryPopoverSaveChangesButtonAriaLabel": "将更改保存到 {title}", + "data.search.searchBar.savedQueryPopoverSaveChangesButtonText": "保存更改", + "data.search.searchBar.savedQueryPopoverSavedQueryListItemButtonAriaLabel": "已保存查询按钮 {savedQueryName}", + "data.search.searchBar.savedQueryPopoverSavedQueryListItemDescriptionAriaLabel": "{savedQueryName} 描述", + "data.search.searchBar.savedQueryPopoverSavedQueryListItemSelectedButtonAriaLabel": "已保存查询按钮已选择 {savedQueryName}。按下可清除任何更改。", + "data.search.searchBar.savedQueryPopoverTitleText": "已保存查询", "charts.colormaps.bluesText": "蓝色", "charts.colormaps.greensText": "绿色", "charts.colormaps.greenToRedText": "绿到红", @@ -526,116 +636,6 @@ "dashboardEmbeddableContainer.actions.toggleExpandPanelMenuItem.notExpandedDisplayName": "全屏", "dashboardEmbeddableContainer.dashboardGrid.toast.unableToLoadDashboardDangerMessage": "无法加载仪表板。", "dashboardEmbeddableContainer.factory.displayName": "仪表板", - "data.filter.applyFilters.popupHeader": "选择要应用的筛选", - "data.filter.applyFiltersPopup.cancelButtonLabel": "取消", - "data.filter.applyFiltersPopup.saveButtonLabel": "应用", - "data.filter.filterBar.addFilterButtonLabel": "添加筛选", - "data.filter.filterBar.deleteFilterButtonLabel": "删除", - "data.filter.filterBar.disabledFilterPrefix": "已禁用", - "data.filter.filterBar.disableFilterButtonLabel": "暂时禁用", - "data.filter.filterBar.editFilterButtonLabel": "编辑筛选", - "data.filter.filterBar.enableFilterButtonLabel": "重新启用", - "data.filter.filterBar.excludeFilterButtonLabel": "排除结果", - "data.filter.filterBar.filterItemBadgeAriaLabel": "筛选操作", - "data.filter.filterBar.filterItemBadgeIconAriaLabel": "删除", - "data.filter.filterBar.includeFilterButtonLabel": "包括结果", - "data.filter.filterBar.indexPatternSelectPlaceholder": "选择索引模式", - "data.filter.filterBar.moreFilterActionsMessage": "筛选:{innerText}。选择以获取更多筛选操作。", - "data.filter.filterBar.negatedFilterPrefix": "非 ", - "data.filter.filterBar.pinFilterButtonLabel": "在所有应用上固定", - "data.filter.filterBar.pinnedFilterPrefix": "已固定", - "data.filter.filterBar.unpinFilterButtonLabel": "取消固定", - "data.filter.filterEditor.cancelButtonLabel": "取消", - "data.filter.filterEditor.createCustomLabelInputLabel": "定制标签", - "data.filter.filterEditor.createCustomLabelSwitchLabel": "创建定制标签?", - "data.filter.filterEditor.dateFormatHelpLinkLabel": "已接受日期格式", - "data.filter.filterEditor.doesNotExistOperatorOptionLabel": "不存在", - "data.filter.filterEditor.editFilterPopupTitle": "编辑筛选", - "data.filter.filterEditor.editFilterValuesButtonLabel": "编辑筛选值", - "data.filter.filterEditor.editQueryDslButtonLabel": "编辑为查询 DSL", - "data.filter.filterEditor.existsOperatorOptionLabel": "存在", - "data.filter.filterEditor.falseOptionLabel": "false", - "data.filter.filterEditor.fieldSelectLabel": "字段", - "data.filter.filterEditor.fieldSelectPlaceholder": "首先选择字段", - "data.filter.filterEditor.indexPatternSelectLabel": "索引模式", - "data.filter.filterEditor.isBetweenOperatorOptionLabel": "介于", - "data.filter.filterEditor.isNotBetweenOperatorOptionLabel": "不介于", - "data.filter.filterEditor.isNotOneOfOperatorOptionLabel": "不属于", - "data.filter.filterEditor.isNotOperatorOptionLabel": "不是", - "data.filter.filterEditor.isOneOfOperatorOptionLabel": "属于", - "data.filter.filterEditor.isOperatorOptionLabel": "是", - "data.filter.filterEditor.operatorSelectLabel": "运算符", - "data.filter.filterEditor.operatorSelectPlaceholderSelect": "选择", - "data.filter.filterEditor.operatorSelectPlaceholderWaiting": "正在等候", - "data.filter.filterEditor.queryDslLabel": "Elasticsearch 查询 DSL", - "data.filter.filterEditor.rangeEndInputPlaceholder": "范围结束", - "data.filter.filterEditor.rangeInputLabel": "范围", - "data.filter.filterEditor.rangeStartInputPlaceholder": "范围开始", - "data.filter.filterEditor.saveButtonLabel": "保存", - "data.filter.filterEditor.trueOptionLabel": "true", - "data.filter.filterEditor.valueInputLabel": "值", - "data.filter.filterEditor.valueInputPlaceholder": "输入值", - "data.filter.filterEditor.valueSelectPlaceholder": "选择值", - "data.filter.filterEditor.valuesSelectLabel": "值", - "data.filter.filterEditor.valuesSelectPlaceholder": "选择值", - "data.filter.options.changeAllFiltersButtonLabel": "更改所有筛选", - "data.filter.options.deleteAllFiltersButtonLabel": "全部删除", - "data.filter.options.disableAllFiltersButtonLabel": "全部禁用", - "data.filter.options.enableAllFiltersButtonLabel": "全部启用", - "data.filter.options.invertDisabledFiltersButtonLabel": "反向已启用/已禁用", - "data.filter.options.invertNegatedFiltersButtonLabel": "反向包括", - "data.filter.options.pinAllFiltersButtonLabel": "全部固定", - "data.filter.options.unpinAllFiltersButtonLabel": "全部取消固定", - "data.filter.searchBar.changeAllFiltersTitle": "更改所有筛选", - "data.indexPatterns.unableWriteLabel": "无法写入索引模式!请刷新页面以获取此索引模式的最新更改。", - "data.indexPatterns.unknownFieldErrorMessage": "indexPattern “{title}” 中的字段 “{name}” 使用未知字段类型。", - "data.indexPatterns.unknownFieldHeader": "未知字段类型 {type}", - "data.parseEsInterval.invalidEsCalendarIntervalErrorMessage": "无效的日历时间间隔:{interval},值必须为 1", - "data.parseEsInterval.invalidEsIntervalFormatErrorMessage": "时间间隔格式无效:{interval}", - "data.query.queryBar.comboboxAriaLabel": "搜索并筛选 {pageType} 页面", - "data.query.queryBar.kqlFullLanguageName": "Kibana 查询语言", - "data.query.queryBar.kqlLanguageName": "KQL", - "data.query.queryBar.kqlOffLabel": "关闭", - "data.query.queryBar.kqlOnLabel": "开启", - "data.query.queryBar.luceneLanguageName": "Lucene", - "data.query.queryBar.luceneSyntaxWarningMessage": "尽管您选择了 Kibana 查询语言 (KQL),但似乎您正在尝试使用 Lucene 查询语法。请查看 KQL 文档 {link}。", - "data.query.queryBar.luceneSyntaxWarningOptOutText": "不再显示", - "data.query.queryBar.luceneSyntaxWarningTitle": "Lucene 语法警告", - "data.query.queryBar.searchInputAriaLabel": "开始键入内容,以搜索并筛选 {pageType} 页面", - "data.query.queryBar.searchInputPlaceholder": "搜索", - "data.query.queryBar.syntaxOptionsDescription": "{docsLink} (KQL) 提供简化查询语法并支持脚本字段。如果您具有基本许可或更高级别的许可,KQL 还提供自动填充功能。如果关闭 KQL,Kibana 将使用 Lucene。", - "data.query.queryBar.syntaxOptionsDescription.docsLinkText": "此处", - "data.query.queryBar.syntaxOptionsTitle": "语法选项", - "data.search.searchBar.savedQueryDescriptionLabelText": "描述", - "data.search.searchBar.savedQueryDescriptionText": "保存想要再次使用的查询文本和筛选。", - "data.search.searchBar.savedQueryForm.titleConflictText": "标题与现有已保存查询有冲突", - "data.search.searchBar.savedQueryForm.titleMissingText": "“名称”必填", - "data.search.searchBar.savedQueryForm.whitespaceErrorText": "标题不能包含前导或尾随空格", - "data.search.searchBar.savedQueryFormCancelButtonText": "取消", - "data.search.searchBar.savedQueryFormSaveButtonText": "保存", - "data.search.searchBar.savedQueryFormTitle": "保存查询", - "data.search.searchBar.savedQueryIncludeFiltersLabelText": "包括筛选", - "data.search.searchBar.savedQueryIncludeTimeFilterLabelText": "包括时间筛选", - "data.search.searchBar.savedQueryNameHelpText": "“名称”必填。标题不能包含前导或尾随空格。名称必须唯一。", - "data.search.searchBar.savedQueryNameLabelText": "名称", - "data.search.searchBar.savedQueryNoSavedQueriesText": "没有已保存查询。", - "data.search.searchBar.savedQueryPopoverButtonText": "查看已保存查询", - "data.search.searchBar.savedQueryPopoverClearButtonAriaLabel": "清除当前已保存查询", - "data.search.searchBar.savedQueryPopoverClearButtonText": "清除", - "data.search.searchBar.savedQueryPopoverConfirmDeletionCancelButtonText": "取消", - "data.search.searchBar.savedQueryPopoverConfirmDeletionConfirmButtonText": "删除", - "data.search.searchBar.savedQueryPopoverConfirmDeletionTitle": "删除“{savedQueryName}”?", - "data.search.searchBar.savedQueryPopoverDeleteButtonAriaLabel": "删除已保存查询 {savedQueryName}", - "data.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel": "另存为新的已保存查询", - "data.search.searchBar.savedQueryPopoverSaveAsNewButtonText": "另存为新的", - "data.search.searchBar.savedQueryPopoverSaveButtonAriaLabel": "保存新的已保存查询", - "data.search.searchBar.savedQueryPopoverSaveButtonText": "保存当前查询", - "data.search.searchBar.savedQueryPopoverSaveChangesButtonAriaLabel": "将更改保存到 {title}", - "data.search.searchBar.savedQueryPopoverSaveChangesButtonText": "保存更改", - "data.search.searchBar.savedQueryPopoverSavedQueryListItemButtonAriaLabel": "已保存查询按钮 {savedQueryName}", - "data.search.searchBar.savedQueryPopoverSavedQueryListItemDescriptionAriaLabel": "{savedQueryName} 描述", - "data.search.searchBar.savedQueryPopoverSavedQueryListItemSelectedButtonAriaLabel": "已保存查询按钮已选择 {savedQueryName}。按下可清除任何更改。", - "data.search.searchBar.savedQueryPopoverTitleText": "已保存查询", "embeddableApi.actions.applyFilterActionTitle": "将筛选应用于当前视图", "embeddableApi.addPanel.createNewDefaultOption": "创建新的......", "embeddableApi.addPanel.displayName": "添加面板", @@ -1286,8 +1286,6 @@ "kbn.home.welcomeDescription": "您了解 Elastic Stack 的窗口", "kbn.home.welcomeHomePageHeader": "Kibana 主页", "kbn.home.welcomeTitle": "欢迎使用 Kibana", - "advancedSettings.badge.readOnly.text": "只读", - "advancedSettings.badge.readOnly.tooltip": "无法保存高级设置", "kbn.management.createIndexPattern.betaLabel": "公测版", "kbn.management.createIndexPattern.emptyState.checkDataButton": "检查新数据", "kbn.management.createIndexPattern.emptyStateHeader": "找不到任何 Elasticsearch 数据", @@ -1435,8 +1433,6 @@ "kbn.management.indexPattern.confirmOverwriteTitle": "覆盖“{type}”?", "kbn.management.indexPattern.sectionsHeader": "索引模式", "kbn.management.indexPattern.titleExistsLabel": "具有标题 “{title}” 的索引模式已存在。", - "management.indexPatternHeader": "索引模式", - "management.indexPatternLabel": "管理帮助从 Elasticsearch 检索数据的索引模式。", "kbn.management.indexPatternList.createButton.betaLabel": "公测版", "kbn.management.indexPatternPrompt.exampleOne": "索引单个称作 log-west-001 的数据源,以便可以快速地构建图表或查询其内容。", "kbn.management.indexPatternPrompt.exampleOneTitle": "单数据源", @@ -1555,9 +1551,7 @@ "kbn.management.objects.objectsTable.table.typeFilterName": "类型", "kbn.management.objects.objectsTable.unableFindSavedObjectsNotificationMessage": "找不到已保存对象", "kbn.management.objects.parsingFieldErrorMessage": "为索引模式 “{indexName}” 解析 “{fieldName}” 时发生错误:{errorMessage}", - "management.objects.savedObjectsDescription": "导入、导出和管理您的已保存搜索、可视化和仪表板。", "kbn.management.objects.savedObjectsSectionLabel": "已保存对象", - "management.objects.savedObjectsTitle": "已保存对象", "kbn.management.objects.view.cancelButtonAriaLabel": "取消", "kbn.management.objects.view.cancelButtonLabel": "取消", "kbn.management.objects.view.deleteItemButtonLabel": "删除“{title}”", @@ -1575,50 +1569,7 @@ "kbn.management.objects.view.viewItemTitle": "查看“{title}”", "kbn.management.savedObjects.editBreadcrumb": "编辑 {savedObjectType}", "kbn.management.savedObjects.indexBreadcrumb": "已保存对象", - "advancedSettings.advancedSettingsLabel": "高级设置", - "advancedSettings.callOutCautionDescription": "此处请谨慎操作,这些设置仅供高级用户使用。您在这里所做的更改可能使 Kibana 的大部分功能出现问题。这些设置有一部分可能未在文档中说明、不受支持或是实验性设置。如果字段有默认值,将字段留空会将其设置为默认值,其他配置指令可能不接受其默认值。删除定制设置会将其从 Kibana 的配置中永久删除。", - "advancedSettings.callOutCautionTitle": "注意:在这里您可能会使问题出现", - "advancedSettings.categoryNames.dashboardLabel": "仪表板", - "advancedSettings.categoryNames.discoverLabel": "Discover", - "advancedSettings.categoryNames.generalLabel": "常规", - "advancedSettings.categoryNames.notificationsLabel": "通知", - "advancedSettings.categoryNames.reportingLabel": "报告", - "advancedSettings.categoryNames.searchLabel": "搜索", - "advancedSettings.categoryNames.siemLabel": "SIEM", - "advancedSettings.categoryNames.timelionLabel": "Timelion", - "advancedSettings.categoryNames.visualizationsLabel": "可视化", - "advancedSettings.categorySearchLabel": "类别", - "advancedSettings.field.cancelEditingButtonAriaLabel": "取消编辑 {ariaName}", - "advancedSettings.field.cancelEditingButtonLabel": "取消", - "advancedSettings.field.changeImageLinkAriaLabel": "更改 {ariaName}", - "advancedSettings.field.changeImageLinkText": "更改图片", - "advancedSettings.field.codeEditorSyntaxErrorMessage": "JSON 语法无效", - "advancedSettings.field.customSettingAriaLabel": "定制设置", - "advancedSettings.field.customSettingTooltip": "定制设置", - "advancedSettings.field.defaultValueText": "默认值:{value}", - "advancedSettings.field.defaultValueTypeJsonText": "默认值:{value}", - "advancedSettings.field.helpText": "此设置将由 Kibana 覆盖,无法更改。", - "advancedSettings.field.imageChangeErrorMessage": "图片无法保存", - "advancedSettings.field.imageTooLargeErrorMessage": "图像过大,最大大小为 {maxSizeDescription}", - "advancedSettings.field.offLabel": "关闭", - "advancedSettings.field.onLabel": "开启", - "advancedSettings.field.requiresPageReloadToastButtonLabel": "重新加载页面", - "advancedSettings.field.requiresPageReloadToastDescription": "请重新加载页面,以使“{settingName}”设置生效。", - "advancedSettings.field.resetFieldErrorMessage": "无法重置 {name}", - "advancedSettings.field.resetToDefaultLinkAriaLabel": "将 {ariaName} 重置为默认值", - "advancedSettings.field.resetToDefaultLinkText": "重置为默认值", - "advancedSettings.field.saveButtonAriaLabel": "保存 {ariaName}", - "advancedSettings.field.saveButtonLabel": "保存", - "advancedSettings.field.saveFieldErrorMessage": "无法保存 {name}", - "advancedSettings.form.clearNoSearchResultText": "(清除搜索)", - "advancedSettings.form.clearSearchResultText": "(清除搜索)", - "advancedSettings.form.noSearchResultText": "未找到设置{clearSearch}", - "advancedSettings.form.searchResultText": "搜索词隐藏了 {settingsCount} 个设置{clearSearch}", - "advancedSettings.pageTitle": "设置", - "advancedSettings.searchBar.unableToParseQueryErrorMessage": "无法解析查询", - "advancedSettings.searchBarAriaLabel": "搜索高级设置", "kbn.managementTitle": "管理", - "advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "您已搜索 {query}。{sectionLenght, plural, one {# 个部分} other {# 个部分}}中有 {optionLenght, plural, one {# 个选项} other {# 个选项}}", "kbn.topNavMenu.openInspectorButtonLabel": "检查", "kbn.topNavMenu.refreshButtonLabel": "刷新", "kbn.topNavMenu.saveVisualizationButtonLabel": "保存", @@ -1661,6 +1612,55 @@ "kbn.visualize.wizard.step1Breadcrumb": "创建", "kbn.visualize.wizard.step2Breadcrumb": "创建", "kbn.visualizeTitle": "可视化", + "advancedSettings.badge.readOnly.text": "只读", + "advancedSettings.badge.readOnly.tooltip": "无法保存高级设置", + "advancedSettings.advancedSettingsLabel": "高级设置", + "advancedSettings.callOutCautionDescription": "此处请谨慎操作,这些设置仅供高级用户使用。您在这里所做的更改可能使 Kibana 的大部分功能出现问题。这些设置有一部分可能未在文档中说明、不受支持或是实验性设置。如果字段有默认值,将字段留空会将其设置为默认值,其他配置指令可能不接受其默认值。删除定制设置会将其从 Kibana 的配置中永久删除。", + "advancedSettings.callOutCautionTitle": "注意:在这里您可能会使问题出现", + "advancedSettings.categoryNames.dashboardLabel": "仪表板", + "advancedSettings.categoryNames.discoverLabel": "Discover", + "advancedSettings.categoryNames.generalLabel": "常规", + "advancedSettings.categoryNames.notificationsLabel": "通知", + "advancedSettings.categoryNames.reportingLabel": "报告", + "advancedSettings.categoryNames.searchLabel": "搜索", + "advancedSettings.categoryNames.siemLabel": "SIEM", + "advancedSettings.categoryNames.timelionLabel": "Timelion", + "advancedSettings.categoryNames.visualizationsLabel": "可视化", + "advancedSettings.categorySearchLabel": "类别", + "advancedSettings.field.cancelEditingButtonAriaLabel": "取消编辑 {ariaName}", + "advancedSettings.field.cancelEditingButtonLabel": "取消", + "advancedSettings.field.changeImageLinkAriaLabel": "更改 {ariaName}", + "advancedSettings.field.changeImageLinkText": "更改图片", + "advancedSettings.field.codeEditorSyntaxErrorMessage": "JSON 语法无效", + "advancedSettings.field.customSettingAriaLabel": "定制设置", + "advancedSettings.field.customSettingTooltip": "定制设置", + "advancedSettings.field.defaultValueText": "默认值:{value}", + "advancedSettings.field.defaultValueTypeJsonText": "默认值:{value}", + "advancedSettings.field.helpText": "此设置将由 Kibana 覆盖,无法更改。", + "advancedSettings.field.imageChangeErrorMessage": "图片无法保存", + "advancedSettings.field.imageTooLargeErrorMessage": "图像过大,最大大小为 {maxSizeDescription}", + "advancedSettings.field.offLabel": "关闭", + "advancedSettings.field.onLabel": "开启", + "advancedSettings.field.requiresPageReloadToastButtonLabel": "重新加载页面", + "advancedSettings.field.requiresPageReloadToastDescription": "请重新加载页面,以使“{settingName}”设置生效。", + "advancedSettings.field.resetFieldErrorMessage": "无法重置 {name}", + "advancedSettings.field.resetToDefaultLinkAriaLabel": "将 {ariaName} 重置为默认值", + "advancedSettings.field.resetToDefaultLinkText": "重置为默认值", + "advancedSettings.field.saveButtonAriaLabel": "保存 {ariaName}", + "advancedSettings.field.saveButtonLabel": "保存", + "advancedSettings.field.saveFieldErrorMessage": "无法保存 {name}", + "advancedSettings.form.clearNoSearchResultText": "(清除搜索)", + "advancedSettings.form.clearSearchResultText": "(清除搜索)", + "advancedSettings.form.noSearchResultText": "未找到设置{clearSearch}", + "advancedSettings.form.searchResultText": "搜索词隐藏了 {settingsCount} 个设置{clearSearch}", + "advancedSettings.pageTitle": "设置", + "advancedSettings.searchBar.unableToParseQueryErrorMessage": "无法解析查询", + "advancedSettings.searchBarAriaLabel": "搜索高级设置", + "advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "您已搜索 {query}。{sectionLenght, plural, one {# 个部分} other {# 个部分}}中有 {optionLenght, plural, one {# 个选项} other {# 个选项}}", + "management.indexPatternHeader": "索引模式", + "management.indexPatternLabel": "管理帮助从 Elasticsearch 检索数据的索引模式。", + "management.objects.savedObjectsDescription": "导入、导出和管理您的已保存搜索、可视化和仪表板。", + "management.objects.savedObjectsTitle": "已保存对象", "kibana_legacy.bigUrlWarningNotificationMessage": "在{advancedSettingsLink}中启用“{storeInSessionStorageParam}”选项或简化屏幕视觉效果。", "kibana_legacy.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高级设置", "kibana_legacy.bigUrlWarningNotificationTitle": "URL 过长,Kibana 可能无法工作", @@ -2492,10 +2492,6 @@ "telemetry.seeExampleOfWhatWeCollectLinkText": "查看我们收集的内容示例", "telemetry.telemetryBannerDescription": "想帮助我们改进 Elastic Stack?数据使用情况收集当前已禁用。启用数据使用情况收集可帮助我们管理并改善产品和服务。有关详情,请参阅我们的{privacyStatementLink}。", "telemetry.telemetryConfigDescription": "通过提供基本功能的使用情况统计信息,来帮助我们改进 Elastic Stack。我们不会在 Elastic 之外共享此数据。", - "telemetry.telemetryConfigTitle": "遥测选择加入", - "telemetry.telemetryErrorNotificationMessageDescription.tryAgainText": "确认 Kibana 和 Elasticsearch 仍在运行,然后重试。", - "telemetry.telemetryErrorNotificationMessageDescription.unableToSaveTelemetryPreferenceText": "无法保存遥测首选项。", - "telemetry.telemetryErrorNotificationMessageTitle": "遥测错误", "telemetry.telemetryOptedInDisableUsage": "请在此禁用使用情况数据", "telemetry.telemetryOptedInDismissMessage": "关闭", "telemetry.telemetryOptedInNoticeDescription": "要了解使用情况数据如何帮助我们管理和改善产品和服务,请参阅我们的{privacyStatementLink}。要停止收集,{disableLink}。", @@ -3918,13 +3914,6 @@ "xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigFailedTitle": "配置无法删除", "xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigSucceededText": "您已成功为“{serviceName}”删除配置。将需要一些时间才能传播到代理。", "xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigSucceededTitle": "配置已删除", - "xpack.apm.settings.agentConf.flyOut.serviceSection.alreadyConfiguredOption": "已配置", - "xpack.apm.settings.agentConf.flyOut.serviceSection.selectPlaceholder": "选择", - "xpack.apm.settings.agentConf.flyOut.serviceSection.serviceEnvironmentSelectHelpText": "每个配置仅支持单个环境。", - "xpack.apm.settings.agentConf.flyOut.serviceSection.serviceEnvironmentSelectLabel": "环境", - "xpack.apm.settings.agentConf.flyOut.serviceSection.serviceNameSelectHelpText": "选择要配置的服务。", - "xpack.apm.settings.agentConf.flyOut.serviceSection.serviceNameSelectLabel": "名称", - "xpack.apm.settings.agentConf.flyOut.serviceSection.title": "服务", "xpack.apm.settings.agentConf.flyOut.settingsSection.captureBodyInputHelpText": "有关属于 HTTP 请求的事务,代理可以选择性地捕获请求正文(例如 POST 变量)。默认为“off”。", "xpack.apm.settings.agentConf.flyOut.settingsSection.captureBodyInputLabel": "捕获正文", "xpack.apm.settings.agentConf.flyOut.settingsSection.captureBodyInputPlaceholderText": "选择选项", @@ -13186,4 +13175,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。", "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx index 1708b2f0ae016..f7d2b8f60157f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx @@ -10,6 +10,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexItem, EuiFlexGroup, + EuiFormLabel, EuiExpression, EuiPopover, EuiPopoverTitle, @@ -327,7 +328,15 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<IndexThr <EuiSpacer /> </Fragment> ) : null} - <EuiFlexGroup gutterSize="s" wrap> + <EuiSpacer size="l" /> + <EuiFormLabel> + <FormattedMessage + defaultMessage="Select Index to query:" + id="xpack.triggersActionsUI.sections.alertAdd.selectIndex" + /> + </EuiFormLabel> + <EuiSpacer size="m" /> + <EuiFlexGroup wrap> <EuiFlexItem grow={false}> <EuiPopover id="indexPopover" @@ -340,7 +349,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<IndexThr defaultMessage: 'index', } )} - value={index || firstFieldOption.text} + value={index ? index.join(' ') : firstFieldOption.text} isActive={indexPopoverOpen} onClick={() => { setIndexPopoverOpen(true); @@ -370,6 +379,8 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<IndexThr </div> </EuiPopover> </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup> <EuiFlexItem grow={false}> <WhenExpression aggType={aggType ?? DEFAULT_VALUES.AGGREGATION_TYPE} @@ -391,6 +402,8 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<IndexThr /> </EuiFlexItem> ) : null} + </EuiFlexGroup> + <EuiFlexGroup> <EuiFlexItem grow={false}> <GroupByExpression groupBy={groupBy || DEFAULT_VALUES.GROUP_BY} @@ -407,6 +420,16 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<IndexThr } /> </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer size="xl" /> + <EuiFormLabel> + <FormattedMessage + defaultMessage="Define the alert condition:" + id="xpack.triggersActionsUI.sections.alertAdd.conditionPrompt" + /> + </EuiFormLabel> + <EuiSpacer size="m" /> + <EuiFlexGroup> <EuiFlexItem grow={false}> <ThresholdExpression thresholdComparator={thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx index ce7ef0dd39bac..4d97a59e36320 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx @@ -214,7 +214,6 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({ > {error} </EuiCallOut> - <EuiSpacer size="l" /> </Fragment> ); } @@ -248,7 +247,7 @@ export const ThresholdVisualization: React.FunctionComponent<Props> = ({ <div data-test-subj="alertVisualizationChart"> <EuiSpacer size="l" /> {alertVisualizationDataKeys.length ? ( - <Chart size={['100%', 300]} renderer="canvas"> + <Chart size={['100%', 200]} renderer="canvas"> <Settings theme={[customTheme(), chartsTheme]} xDomain={domain} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts index 9c6f4daccc705..93a46862f4cd2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api.test.ts @@ -39,7 +39,7 @@ describe('loadAlertTypes', () => { id: 'test', name: 'Test', actionVariables: ['var1'], - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], }, ]; http.get.mockResolvedValueOnce(resolvedValue); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/_index.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/_index.scss index f8fa882cd617d..b5755bc35b1c1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/_index.scss +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/_index.scss @@ -1,3 +1,7 @@ .actConnectorModal { z-index: 9000; } + +.euiComboBoxOptionsList { + z-index: 10000; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.tsx index 90b84e11fccd2..95c049f32436a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_form.tsx @@ -19,7 +19,6 @@ import { EuiFormRow, EuiComboBox, EuiKeyPadMenuItem, - EuiLink, EuiFieldNumber, EuiSelect, EuiIconTip, @@ -27,6 +26,7 @@ import { EuiButtonIcon, EuiEmptyPrompt, EuiButtonEmpty, + EuiHorizontalRule, } from '@elastic/eui'; import { loadAlertTypes } from '../../lib/alert_api'; import { loadActionTypes, loadAllActions } from '../../lib/action_connector_api'; @@ -40,6 +40,7 @@ import { ActionTypeIndex, ActionConnector, AlertTypeIndex, + ActionGroup, } from '../../../types'; import { SectionLoading } from '../../components/section_loading'; import { ConnectorAddModal } from '../action_connector_form/connector_add_modal'; @@ -118,7 +119,7 @@ export const AlertForm = ({ const [alertThrottleUnit, setAlertThrottleUnit] = useState<string>('m'); const [isAddActionPanelOpen, setIsAddActionPanelOpen] = useState<boolean>(true); const [connectors, setConnectors] = useState<ActionConnector[]>([]); - const [defaultActionGroup, setDefaultActionGroup] = useState<string | undefined>(undefined); + const [defaultActionGroup, setDefaultActionGroup] = useState<ActionGroup | undefined>(undefined); const [activeActionItem, setActiveActionItem] = useState<ActiveActionConnectorState | undefined>( undefined ); @@ -158,7 +159,11 @@ export const AlertForm = ({ // temp hack of API result alertTypes.push({ id: 'threshold', - actionGroups: ['Alert', 'Warning', 'If unacknowledged'], + actionGroups: [ + { id: 'alert', name: 'Alert' }, + { id: 'warning', name: 'Warning' }, + { id: 'ifUnacknowledged', name: 'If unacknowledged' }, + ], name: 'threshold', actionVariables: ['ctx.metadata.name', 'ctx.metadata.test'], }); @@ -261,7 +266,7 @@ export const AlertForm = ({ alert.actions.push({ id: '', actionTypeId: actionTypeModel.id, - group: defaultActionGroup ?? 'Alert', + group: defaultActionGroup?.id ?? 'Alert', params: {}, }); setActionProperty('id', freeConnectors[0].id, alert.actions.length - 1); @@ -273,7 +278,7 @@ export const AlertForm = ({ alert.actions.push({ id: '', actionTypeId: actionTypeModel.id, - group: defaultActionGroup ?? 'Alert', + group: defaultActionGroup?.id ?? 'Alert', params: {}, }); setActionProperty('id', alert.actions.length, alert.actions.length - 1); @@ -351,7 +356,7 @@ export const AlertForm = ({ id, })); const actionTypeRegisterd = actionTypeRegistry.get(actionConnector.actionTypeId); - if (actionTypeRegisterd === null || actionItem.group !== defaultActionGroup) return null; + if (actionTypeRegisterd === null || actionItem.group !== defaultActionGroup?.id) return null; const ParamsFieldsComponent = actionTypeRegisterd.actionParamsFields; const actionParamsErrors: { errors: IErrorObject } = Object.keys(actionsErrors).length > 0 ? actionsErrors[actionItem.id] : { errors: {} }; @@ -474,7 +479,7 @@ export const AlertForm = ({ ? actionTypesIndex[actionItem.actionTypeId].name : actionItem.actionTypeId; const actionTypeRegisterd = actionTypeRegistry.get(actionItem.actionTypeId); - if (actionTypeRegisterd === null || actionItem.group !== defaultActionGroup) return null; + if (actionTypeRegisterd === null || actionItem.group !== defaultActionGroup?.id) return null; return ( <EuiAccordion initialIsOpen={true} @@ -590,12 +595,13 @@ export const AlertForm = ({ const alertTypeDetails = ( <Fragment> + <EuiHorizontalRule /> <EuiFlexGroup alignItems="center" gutterSize="s"> <EuiFlexItem> <EuiTitle size="s" data-test-subj="selectedAlertTypeTitle"> <h5 id="selectedAlertTypeTitle"> <FormattedMessage - defaultMessage="Trigger: {alertType}" + defaultMessage="{alertType}" id="xpack.triggersActionsUI.sections.alertForm.selectedAlertTypeTitle" values={{ alertType: alertTypeModel ? alertTypeModel.name : '' }} /> @@ -604,17 +610,20 @@ export const AlertForm = ({ </EuiFlexItem> {canChangeTrigger ? ( <EuiFlexItem grow={false}> - <EuiLink + <EuiButtonIcon + iconType="cross" + color="danger" + aria-label={i18n.translate( + 'xpack.triggersActionsUI.sections.alertForm.changeAlertTypeAriaLabel', + { + defaultMessage: 'Delete', + } + )} onClick={() => { setAlertProperty('alertTypeId', null); setAlertTypeModel(null); }} - > - <FormattedMessage - defaultMessage="Change" - id="xpack.triggersActionsUI.sections.alertForm.changeAlertTypeLink" - /> - </EuiLink> + /> </EuiFlexItem> ) : null} </EuiFlexGroup> @@ -631,7 +640,7 @@ export const AlertForm = ({ {selectedGroupActions} {isAddActionPanelOpen ? ( <Fragment> - <EuiTitle size="s"> + <EuiTitle size="xs"> <h5 id="alertActionTypeTitle"> <FormattedMessage defaultMessage="Actions: Select an action type" @@ -834,6 +843,7 @@ export const AlertForm = ({ <Fragment>{alertTypeDetails}</Fragment> ) : ( <Fragment> + <EuiHorizontalRule /> <EuiTitle size="s"> <h5 id="alertTypeTitle"> <FormattedMessage diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index e8221e546cea0..c67954bdc44fd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -44,7 +44,7 @@ describe('alert_details', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -64,7 +64,7 @@ describe('alert_details', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -91,7 +91,7 @@ describe('alert_details', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -140,7 +140,7 @@ describe('alert_details', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; const actionTypes: ActionType[] = [ @@ -190,7 +190,7 @@ describe('alert_details', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -214,7 +214,7 @@ describe('alert_details', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -238,7 +238,7 @@ describe('alert_details', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -267,7 +267,7 @@ describe('enable button', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -292,7 +292,7 @@ describe('enable button', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -317,7 +317,7 @@ describe('enable button', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -351,7 +351,7 @@ describe('enable button', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -388,7 +388,7 @@ describe('mute button', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -414,7 +414,7 @@ describe('mute button', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -440,7 +440,7 @@ describe('mute button', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -475,7 +475,7 @@ describe('mute button', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; @@ -510,7 +510,7 @@ describe('mute button', () => { const alertType = { id: '.noop', name: 'No Op', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], actionVariables: [], }; diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 30718f702c9cb..86853e88a81cd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -70,10 +70,14 @@ export interface ActionConnectorTableItem extends ActionConnector { actionType: ActionType['name']; } +export interface ActionGroup { + id: string; + name: string; +} export interface AlertType { id: string; name: string; - actionGroups: string[]; + actionGroups: ActionGroup[]; actionVariables: string[]; } diff --git a/x-pack/legacy/plugins/watcher/public/index.scss b/x-pack/plugins/watcher/public/index.scss similarity index 80% rename from x-pack/legacy/plugins/watcher/public/index.scss rename to x-pack/plugins/watcher/public/index.scss index 33ebf21326c7b..101db14aee9e6 100644 --- a/x-pack/legacy/plugins/watcher/public/index.scss +++ b/x-pack/plugins/watcher/public/index.scss @@ -1,6 +1,3 @@ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; - // Watcher plugin styles // Prefix all styles with "watcher" to avoid conflicts. diff --git a/x-pack/plugins/watcher/public/index.ts b/x-pack/plugins/watcher/public/index.ts index ff635579316e5..783668285e74a 100644 --- a/x-pack/plugins/watcher/public/index.ts +++ b/x-pack/plugins/watcher/public/index.ts @@ -3,6 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import './index.scss'; import { WatcherUIPlugin } from './plugin'; export const plugin = () => new WatcherUIPlugin(); diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts index f7f3d0fa91fff..0cc45a624bc1a 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts @@ -202,7 +202,10 @@ export default function(kibana: any) { const alwaysFiringAlertType: AlertType = { id: 'test.always-firing', name: 'Test: Always Firing', - actionGroups: ['default', 'other'], + actionGroups: [ + { id: 'default', name: 'Default' }, + { id: 'other', name: 'Other' }, + ], async executor(alertExecutorOptions: AlertExecutorOptions) { const { services, @@ -253,7 +256,10 @@ export default function(kibana: any) { const cumulativeFiringAlertType: AlertType = { id: 'test.cumulative-firing', name: 'Test: Cumulative Firing', - actionGroups: ['default', 'other'], + actionGroups: [ + { id: 'default', name: 'Default' }, + { id: 'other', name: 'Other' }, + ], async executor(alertExecutorOptions: AlertExecutorOptions) { const { services, state } = alertExecutorOptions; const group = 'default'; @@ -383,7 +389,7 @@ export default function(kibana: any) { const noopAlertType: AlertType = { id: 'test.noop', name: 'Test: Noop', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], async executor({ services, params, state }: AlertExecutorOptions) {}, }; server.plugins.alerting.setup.registerType(alwaysFiringAlertType); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts index 21f61eb713753..517a60f77849e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/list_alert_types.ts @@ -40,7 +40,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { (alertType: any) => alertType.id === 'test.noop' ); expect(fixtureAlertType).to.eql({ - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], id: 'test.noop', name: 'Test: Noop', }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts index efa9dbf507199..55570744f6af9 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/list_alert_types.ts @@ -20,7 +20,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { expect(response.statusCode).to.eql(200); const fixtureAlertType = response.body.find((alertType: any) => alertType.id === 'test.noop'); expect(fixtureAlertType).to.eql({ - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], id: 'test.noop', name: 'Test: Noop', }); diff --git a/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js b/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js index be6139ed7a0a7..e1a435e000fae 100644 --- a/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js +++ b/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js @@ -5,8 +5,7 @@ */ import expect from '@kbn/expect'; -import querystring from 'querystring'; - +import { stringify } from 'query-string'; import { registerHelpers } from './rollup.test_helpers'; import { INDEX_TO_ROLLUP_MAPPINGS, INDEX_PATTERNS_EXTENSION_BASE_PATH } from './constants'; import { getRandomString } from './lib'; @@ -39,7 +38,7 @@ export default function({ getService }) { it('"params" is required', async () => { params = { pattern: 'foo' }; - uri = `${BASE_URI}?${querystring.stringify(params)}`; + uri = `${BASE_URI}?${stringify(params, { sort: false })}`; ({ body } = await supertest.get(uri).expect(400)); expect(body.message).to.contain( '[request query.params]: expected value of type [string]' @@ -48,14 +47,14 @@ export default function({ getService }) { it('"params" must be a valid JSON string', async () => { params = { pattern: 'foo', params: 'foobarbaz' }; - uri = `${BASE_URI}?${querystring.stringify(params)}`; + uri = `${BASE_URI}?${stringify(params, { sort: false })}`; ({ body } = await supertest.get(uri).expect(400)); expect(body.message).to.contain('[request query.params]: expected JSON string'); }); it('"params" requires a "rollup_index" property', async () => { params = { pattern: 'foo', params: JSON.stringify({}) }; - uri = `${BASE_URI}?${querystring.stringify(params)}`; + uri = `${BASE_URI}?${stringify(params, { sort: false })}`; ({ body } = await supertest.get(uri).expect(400)); expect(body.message).to.contain('[request query.params]: "rollup_index" is required'); }); @@ -65,7 +64,7 @@ export default function({ getService }) { pattern: 'foo', params: JSON.stringify({ rollup_index: 'my_index', someProp: 'bar' }), }; - uri = `${BASE_URI}?${querystring.stringify(params)}`; + uri = `${BASE_URI}?${stringify(params, { sort: false })}`; ({ body } = await supertest.get(uri).expect(400)); expect(body.message).to.contain('[request query.params]: someProp is not allowed'); }); @@ -76,7 +75,7 @@ export default function({ getService }) { params: JSON.stringify({ rollup_index: 'bar' }), meta_fields: 'stringValue', }; - uri = `${BASE_URI}?${querystring.stringify(params)}`; + uri = `${BASE_URI}?${stringify(params, { sort: false })}`; ({ body } = await supertest.get(uri).expect(400)); expect(body.message).to.contain( '[request query.meta_fields]: could not parse array value from [stringValue]' @@ -84,10 +83,13 @@ export default function({ getService }) { }); it('should return 404 the rollup index to query does not exist', async () => { - uri = `${BASE_URI}?${querystring.stringify({ - pattern: 'foo', - params: JSON.stringify({ rollup_index: 'bar' }), - })}`; + uri = `${BASE_URI}?${stringify( + { + pattern: 'foo', + params: JSON.stringify({ rollup_index: 'bar' }), + }, + { sort: false } + )}`; ({ body } = await supertest.get(uri).expect(404)); expect(body.message).to.contain('[index_not_found_exception] no such index [bar]'); }); @@ -105,7 +107,7 @@ export default function({ getService }) { pattern: indexName, params: JSON.stringify({ rollup_index: rollupIndex }), }; - const uri = `${BASE_URI}?${querystring.stringify(params)}`; + const uri = `${BASE_URI}?${stringify(params, { sort: false })}`; const { body } = await supertest.get(uri).expect(200); // Verify that the fields for wildcard correspond to our declared mappings diff --git a/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts b/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts new file mode 100644 index 0000000000000..89dbd73f3fb64 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts @@ -0,0 +1,305 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +const start = 1554463535770; +const end = 1574316073914; +const analyzer = { + tokenizer: 'ml_classic', + filter: [ + { + type: 'stop', + stopwords: [ + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + 'Sunday', + 'Mon', + 'Tue', + 'Wed', + 'Thu', + 'Fri', + 'Sat', + 'Sun', + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + 'GMT', + 'UTC', + ], + }, + ], +}; +const defaultRequestBody = { + indexPatternTitle: 'categorization_functional_test', + query: { bool: { must: [{ match_all: {} }] } }, + size: 5, + timeField: '@timestamp', + start, + end, + analyzer, +}; + +const testDataList = [ + { + title: 'valid with good number of tokens', + requestBody: { + ...defaultRequestBody, + field: 'field1', + }, + expected: { + responseCode: 200, + overallValidStatus: 'valid', + sampleSize: 1000, + exampleLength: 5, + validationChecks: [ + { + id: 0, + valid: 'valid', + message: '1000 field values analyzed, 95% contain 3 or more tokens.', + }, + ], + }, + }, + { + title: 'invalid, too many tokens.', + requestBody: { + ...defaultRequestBody, + field: 'field2', + }, + expected: { + responseCode: 200, + overallValidStatus: 'invalid', + sampleSize: 500, + exampleLength: 5, + validationChecks: [ + { + id: 1, + valid: 'partially_valid', + message: 'The median length for the field values analyzed is over 400 characters.', + }, + { + id: 4, + valid: 'invalid', + message: + 'Tokenization of field value examples has failed due to more than 10000 tokens being found in a sample of 50 values.', + }, + ], + }, + }, + { + title: 'partially valid, more than 75% are null', + requestBody: { + ...defaultRequestBody, + field: 'field3', + }, + expected: { + responseCode: 200, + overallValidStatus: 'partially_valid', + sampleSize: 250, + exampleLength: 5, + validationChecks: [ + { + id: 0, + valid: 'valid', + message: '250 field values analyzed, 95% contain 3 or more tokens.', + }, + { + id: 2, + valid: 'partially_valid', + message: 'More than 75% of field values are null.', + }, + ], + }, + }, + { + title: 'partially valid, median length is over 400 characters', + requestBody: { + ...defaultRequestBody, + field: 'field4', + }, + expected: { + responseCode: 200, + overallValidStatus: 'partially_valid', + sampleSize: 500, + exampleLength: 5, + validationChecks: [ + { + id: 0, + valid: 'valid', + message: '500 field values analyzed, 100% contain 3 or more tokens.', + }, + { + id: 1, + valid: 'partially_valid', + message: 'The median length for the field values analyzed is over 400 characters.', + }, + ], + }, + }, + { + title: 'invalid, no values in any doc', + requestBody: { + ...defaultRequestBody, + field: 'field5', + }, + expected: { + responseCode: 200, + overallValidStatus: 'invalid', + sampleSize: 0, + exampleLength: 0, + validationChecks: [ + { + id: 3, + valid: 'invalid', + message: + 'No examples for this field could be found. Please ensure the selected date range contains data.', + }, + ], + }, + }, + { + title: 'invalid, mostly made up of stop words, so no matched tokens', + requestBody: { + ...defaultRequestBody, + field: 'field6', + }, + expected: { + responseCode: 200, + overallValidStatus: 'invalid', + sampleSize: 1000, + exampleLength: 5, + validationChecks: [ + { + id: 0, + valid: 'invalid', + message: '1000 field values analyzed, 0% contain 3 or more tokens.', + }, + ], + }, + }, + { + title: 'valid, mostly made up of stop words, but analyser has no stop words. so it is ok.', + requestBody: { + ...defaultRequestBody, + field: 'field6', + analyzer: { + tokenizer: 'ml_classic', + }, + }, + expected: { + responseCode: 200, + overallValidStatus: 'valid', + sampleSize: 1000, + exampleLength: 5, + validationChecks: [ + { + id: 0, + valid: 'valid', + message: '1000 field values analyzed, 100% contain 3 or more tokens.', + }, + ], + }, + }, + { + title: 'partially valid, half the docs are stop words.', + requestBody: { + ...defaultRequestBody, + field: 'field7', + }, + expected: { + responseCode: 200, + overallValidStatus: 'partially_valid', + sampleSize: 1000, + exampleLength: 5, + validationChecks: [ + { + id: 0, + valid: 'partially_valid', + message: '1000 field values analyzed, 50% contain 3 or more tokens.', + }, + ], + }, + }, + { + title: "endpoint error, index doesn't exist", + requestBody: { + ...defaultRequestBody, + indexPatternTitle: 'does_not_exist', + field: 'field1', + }, + expected: { + responseCode: 404, + overallValidStatus: undefined, + sampleSize: undefined, + validationChecks: undefined, + }, + }, +]; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + + describe('Categorization example endpoint - ', function() { + before(async () => { + await esArchiver.load('ml/categorization'); + }); + + after(async () => { + await esArchiver.unload('ml/categorization'); + }); + + for (const testData of testDataList) { + it(testData.title, async () => { + const { body } = await supertest + .post('/api/ml/jobs/categorization_field_examples') + .set(COMMON_HEADERS) + .send(testData.requestBody) + .expect(testData.expected.responseCode); + + expect(body.overallValidStatus).to.eql(testData.expected.overallValidStatus); + expect(body.sampleSize).to.eql(testData.expected.sampleSize); + expect(body.validationChecks).to.eql(testData.expected.validationChecks); + if (body.statusCode === 200) { + expect(body.examples.length).to.eql(testData.expected.exampleLength); + } + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index 9fff4ca8436b0..1df5dfe2941ce 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -12,5 +12,6 @@ export default function({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./bucket_span_estimator')); loadTestFile(require.resolve('./calculate_model_memory_limit')); + loadTestFile(require.resolve('./categorization_field_examples')); }); } diff --git a/x-pack/test/api_integration/apis/security/basic_login.js b/x-pack/test/api_integration/apis/security/basic_login.js index d4b41603944f6..9b6f49a9a916b 100644 --- a/x-pack/test/api_integration/apis/security/basic_login.js +++ b/x-pack/test/api_integration/apis/security/basic_login.js @@ -201,7 +201,7 @@ export default function({ getService }) { const systemAPIResponse = await supertest .get('/internal/security/me') .set('kbn-xsrf', 'xxx') - .set('kbn-system-api', 'true') + .set('kbn-system-request', 'true') .set('Cookie', sessionCookie.cookieString()) .expect(200); diff --git a/x-pack/test/api_integration/apis/security/session.ts b/x-pack/test/api_integration/apis/security/session.ts index 5d0935bb1ae2d..d819dd38dddb1 100644 --- a/x-pack/test/api_integration/apis/security/session.ts +++ b/x-pack/test/api_integration/apis/security/session.ts @@ -28,7 +28,7 @@ export default function({ getService }: FtrProviderContext) { supertest .get('/internal/security/session') .set('kbn-xsrf', 'xxx') - .set('kbn-system-api', 'true') + .set('kbn-system-request', 'true') .set('Cookie', sessionCookie.cookieString()) .send() .expect(200); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts index dfa297c85dfb8..be008a34343c4 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts @@ -80,7 +80,7 @@ export default ({ getService }: FtrProviderContext): void => { }); // TODO: This is a valid issue and will be fixed in an upcoming PR and then enabled once that PR is merged - it.skip('should return a 200 ok but have a 409 conflict if we attempt to create the same rule_id twice', async () => { + it('should return a 200 ok but have a 409 conflict if we attempt to create the same rule_id twice', async () => { const { body } = await supertest .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`) .set('kbn-xsrf', 'true') @@ -89,9 +89,11 @@ export default ({ getService }: FtrProviderContext): void => { expect(body).to.eql([ { - error: 'Conflict', - message: 'rule_id: "rule-1" already exists', - statusCode: 409, + error: { + message: 'rule_id: "rule-1" already exists', + status_code: 409, + }, + rule_id: 'rule-1', }, ]); }); diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts index 6efaae70e089b..8f902471cf6cd 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts @@ -178,7 +178,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { expect(navLinks).to.eql(['Discover', 'Stack Management']); }); - it(`does not allow navigation to advanced settings; redirects to management home`, async () => { + // https://github.com/elastic/kibana/issues/57377 + it.skip(`does not allow navigation to advanced settings; redirects to management home`, async () => { await PageObjects.common.navigateToActualUrl('kibana', 'management/kibana/settings', { ensureCurrentUrl: false, shouldLoginIfPrompted: false, diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts index c780a8efae304..aceebf7219c3f 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts @@ -13,7 +13,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); - describe('spaces feature controls', () => { + // FLAKY: https://github.com/elastic/kibana/issues/57377 + describe.skip('spaces feature controls', () => { before(async () => { await esArchiver.loadIfNeeded('logstash_functional'); }); @@ -57,7 +58,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); }); - describe('space with Advanced Settings disabled', () => { + // https://github.com/elastic/kibana/issues/57413 + describe.skip('space with Advanced Settings disabled', () => { before(async () => { // we need to load the following in every situation as deleting // a space deletes all of the associated saved objects @@ -80,9 +82,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { ensureCurrentUrl: false, shouldLoginIfPrompted: false, }); - await testSubjects.existOrFail('managementHome', { - timeout: 10000, - }); + await testSubjects.existOrFail('managementHome'); }); }); }); diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts index 6a6e2f23785e3..ad09bc5c89143 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts @@ -21,7 +21,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const queryBar = getService('queryBar'); const savedQueryManagementComponent = getService('savedQueryManagementComponent'); - describe('dashboard security', () => { + // FLAKY: https://github.com/elastic/kibana/issues/44631 + describe.skip('dashboard security', () => { before(async () => { await esArchiver.load('dashboard/feature_controls/security'); await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts index 1796858165a2b..8669577f202a7 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts @@ -27,7 +27,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); } - describe('security', () => { + // FLAKY: https://github.com/elastic/kibana/issues/45348 + describe.skip('security', () => { before(async () => { await esArchiver.load('discover/feature_controls/security'); await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts b/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts index d8eb969b99b3b..bda336e73c4f8 100644 --- a/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts +++ b/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts @@ -47,7 +47,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { ensureCurrentUrl: false, shouldLoginIfPrompted: false, }); - await testSubjects.existOrFail('endpointManagement'); + await testSubjects.existOrFail('managementViewTitle'); }); }); diff --git a/x-pack/test/functional/apps/endpoint/index.ts b/x-pack/test/functional/apps/endpoint/index.ts index e44a4cb846f2c..5fdf54b98cda6 100644 --- a/x-pack/test/functional/apps/endpoint/index.ts +++ b/x-pack/test/functional/apps/endpoint/index.ts @@ -11,5 +11,6 @@ export default function({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./landing_page')); + loadTestFile(require.resolve('./management')); }); } diff --git a/x-pack/test/functional/apps/endpoint/management.ts b/x-pack/test/functional/apps/endpoint/management.ts new file mode 100644 index 0000000000000..bac87f34ceb82 --- /dev/null +++ b/x-pack/test/functional/apps/endpoint/management.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const pageObjects = getPageObjects(['common', 'endpoint']); + const esArchiver = getService('esArchiver'); + const testSubjects = getService('testSubjects'); + + describe('Endpoint Management List', function() { + this.tags('ciGroup7'); + before(async () => { + await esArchiver.load('endpoint/endpoints/api_feature'); + await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/management'); + }); + + it('finds title', async () => { + const title = await testSubjects.getVisibleText('managementViewTitle'); + expect(title).to.equal('Hosts'); + }); + + it('displays table data', async () => { + const data = await pageObjects.endpoint.getManagementTableData(); + [ + 'Hostnamecadmann-4.example.com', + 'PolicyPolicy Name', + 'Policy StatusPolicy Status', + 'Alerts0', + 'Operating Systemwindows 10.0', + 'IP Address10.192.213.130, 10.70.28.129', + 'Sensor Versionversion', + 'Last Activexxxx', + ].forEach((cellValue, index) => { + expect(data[1][index]).to.equal(cellValue); + }); + }); + + after(async () => { + await esArchiver.unload('endpoint/endpoints/api_feature'); + }); + }); +}; diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts index 75020d6eab7e4..b303ad23f8977 100644 --- a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts @@ -52,7 +52,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); }); - describe('space with Index Patterns disabled', () => { + // https://github.com/elastic/kibana/issues/57601 + describe.skip('space with Index Patterns disabled', () => { before(async () => { // we need to load the following in every situation as deleting // a space deletes all of the associated saved objects diff --git a/x-pack/test/functional/apps/infra/link_to.ts b/x-pack/test/functional/apps/infra/link_to.ts index 639b65ec5eca8..738dc7efd8fd9 100644 --- a/x-pack/test/functional/apps/infra/link_to.ts +++ b/x-pack/test/functional/apps/infra/link_to.ts @@ -22,7 +22,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { state: undefined, }; const expectedSearchString = - "logFilter=(expression:'trace.id:433b4651687e18be2c6c8e3b11f53d09',kind:kuery)&logPosition=(position:(tiebreaker:0,time:1565707203194),streamLive:!f)&sourceId=default"; + "sourceId=default&logPosition=(position:(tiebreaker:0,time:1565707203194),streamLive:!f)&logFilter=(expression:'trace.id:433b4651687e18be2c6c8e3b11f53d09',kind:kuery)"; const expectedRedirectPath = '/logs/stream?'; await pageObjects.common.navigateToActualUrl( diff --git a/x-pack/test/functional/apps/machine_learning/data_visualizer/index_data_visualizer.ts b/x-pack/test/functional/apps/machine_learning/data_visualizer/index_data_visualizer.ts index cd2b2fca426f9..cb1b652073469 100644 --- a/x-pack/test/functional/apps/machine_learning/data_visualizer/index_data_visualizer.ts +++ b/x-pack/test/functional/apps/machine_learning/data_visualizer/index_data_visualizer.ts @@ -8,6 +8,24 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; import { ML_JOB_FIELD_TYPES } from '../../../../../legacy/plugins/ml/common/constants/field_types'; import { FieldVisConfig } from '../../../../../legacy/plugins/ml/public/application/datavisualizer/index_based/common'; +interface TestData { + suiteTitle: string; + sourceIndexOrSavedSearch: string; + advancedJobWizardDatafeedQuery: string; + metricFieldsFilter: string; + nonMetricFieldsFilter: string; + nonMetricFieldsTypeFilter: string; + expected: { + totalDocCount: number; + fieldsPanelCount: number; + metricCards?: FieldVisConfig[]; + nonMetricCards?: FieldVisConfig[]; + nonMetricFieldsTypeFilterCardCount: number; + metricFieldsFilterCardCount: number; + nonMetricFieldsFilterCardCount: number; + }; +} + function getFieldTypes(cards: FieldVisConfig[]) { const fieldTypes: ML_JOB_FIELD_TYPES[] = []; cards.forEach(card => { @@ -25,154 +43,337 @@ export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - const testDataList = [ - { - suiteTitle: 'with full farequote index', - sourceIndexOrSavedSearch: 'farequote', - metricFieldsFilter: 'document', - nonMetricFieldsFilter: 'airline', - nonMetricFieldsTypeFilter: 'keyword', - expected: { - totalDocCount: 86274, - fieldsPanelCount: 2, // Metrics panel and Fields panel - metricCards: [ - { - type: ML_JOB_FIELD_TYPES.NUMBER, // document count card - existsInDocs: true, - aggregatable: true, - loading: false, - }, - { - fieldName: 'responsetime', - type: ML_JOB_FIELD_TYPES.NUMBER, - existsInDocs: true, - aggregatable: true, - loading: false, - }, - ], - nonMetricCards: [ - { - fieldName: '@timestamp', - type: ML_JOB_FIELD_TYPES.DATE, - existsInDocs: true, - aggregatable: true, - loading: false, - }, - { - fieldName: '@version', - type: ML_JOB_FIELD_TYPES.TEXT, - existsInDocs: true, - aggregatable: false, - loading: false, - }, - { - fieldName: '@version.keyword', - type: ML_JOB_FIELD_TYPES.KEYWORD, - existsInDocs: true, - aggregatable: true, - loading: false, - }, - { - fieldName: 'airline', - type: ML_JOB_FIELD_TYPES.KEYWORD, - existsInDocs: true, - aggregatable: true, - loading: false, - }, - { - fieldName: 'type', - type: ML_JOB_FIELD_TYPES.TEXT, - existsInDocs: true, - aggregatable: false, - loading: false, - }, - { - fieldName: 'type.keyword', - type: ML_JOB_FIELD_TYPES.KEYWORD, - existsInDocs: true, - aggregatable: true, - loading: false, - }, - ], - nonMetricFieldsTypeFilterCardCount: 3, - metricFieldsFilterCardCount: 1, - nonMetricFieldsFilterCardCount: 1, - }, + const farequoteIndexPatternTestData: TestData = { + suiteTitle: 'index pattern', + sourceIndexOrSavedSearch: 'farequote', + advancedJobWizardDatafeedQuery: `{ + "bool": { + "must": [ + { + "match_all": {} + } + ] + } +}`, + metricFieldsFilter: 'document', + nonMetricFieldsFilter: 'airline', + nonMetricFieldsTypeFilter: 'keyword', + expected: { + totalDocCount: 86274, + fieldsPanelCount: 2, // Metrics panel and Fields panel + metricCards: [ + { + type: ML_JOB_FIELD_TYPES.NUMBER, // document count card + existsInDocs: true, + aggregatable: true, + loading: false, + }, + { + fieldName: 'responsetime', + type: ML_JOB_FIELD_TYPES.NUMBER, + existsInDocs: true, + aggregatable: true, + loading: false, + }, + ], + nonMetricCards: [ + { + fieldName: '@timestamp', + type: ML_JOB_FIELD_TYPES.DATE, + existsInDocs: true, + aggregatable: true, + loading: false, + }, + { + fieldName: '@version', + type: ML_JOB_FIELD_TYPES.TEXT, + existsInDocs: true, + aggregatable: false, + loading: false, + }, + { + fieldName: '@version.keyword', + type: ML_JOB_FIELD_TYPES.KEYWORD, + existsInDocs: true, + aggregatable: true, + loading: false, + }, + { + fieldName: 'airline', + type: ML_JOB_FIELD_TYPES.KEYWORD, + existsInDocs: true, + aggregatable: true, + loading: false, + }, + { + fieldName: 'type', + type: ML_JOB_FIELD_TYPES.TEXT, + existsInDocs: true, + aggregatable: false, + loading: false, + }, + { + fieldName: 'type.keyword', + type: ML_JOB_FIELD_TYPES.KEYWORD, + existsInDocs: true, + aggregatable: true, + loading: false, + }, + ], + nonMetricFieldsTypeFilterCardCount: 3, + metricFieldsFilterCardCount: 1, + nonMetricFieldsFilterCardCount: 1, + }, + }; + + const farequoteKQLSearchTestData: TestData = { + suiteTitle: 'KQL saved search', + sourceIndexOrSavedSearch: 'farequote_kuery', + advancedJobWizardDatafeedQuery: `{ + "bool": { + "must": [ + { + "match_all": {} + } + ] + } +}`, // Note query is not currently passed to the wizard + metricFieldsFilter: 'responsetime', + nonMetricFieldsFilter: 'airline', + nonMetricFieldsTypeFilter: 'keyword', + expected: { + totalDocCount: 34415, + fieldsPanelCount: 2, // Metrics panel and Fields panel + metricCards: [ + { + type: ML_JOB_FIELD_TYPES.NUMBER, // document count card + existsInDocs: true, + aggregatable: true, + loading: false, + }, + { + fieldName: 'responsetime', + type: ML_JOB_FIELD_TYPES.NUMBER, + existsInDocs: true, + aggregatable: true, + loading: false, + }, + ], + nonMetricCards: [ + { + fieldName: '@timestamp', + type: ML_JOB_FIELD_TYPES.DATE, + existsInDocs: true, + aggregatable: true, + loading: false, + }, + { + fieldName: '@version', + type: ML_JOB_FIELD_TYPES.TEXT, + existsInDocs: true, + aggregatable: false, + loading: false, + }, + { + fieldName: '@version.keyword', + type: ML_JOB_FIELD_TYPES.KEYWORD, + existsInDocs: true, + aggregatable: true, + loading: false, + }, + { + fieldName: 'airline', + type: ML_JOB_FIELD_TYPES.KEYWORD, + existsInDocs: true, + aggregatable: true, + loading: false, + }, + { + fieldName: 'type', + type: ML_JOB_FIELD_TYPES.TEXT, + existsInDocs: true, + aggregatable: false, + loading: false, + }, + { + fieldName: 'type.keyword', + type: ML_JOB_FIELD_TYPES.KEYWORD, + existsInDocs: true, + aggregatable: true, + loading: false, + }, + ], + nonMetricFieldsTypeFilterCardCount: 3, + metricFieldsFilterCardCount: 2, + nonMetricFieldsFilterCardCount: 1, }, - { - suiteTitle: 'with lucene query on farequote index', - sourceIndexOrSavedSearch: 'farequote_lucene', - metricFieldsFilter: 'responsetime', - nonMetricFieldsFilter: 'version', - nonMetricFieldsTypeFilter: 'keyword', - expected: { - totalDocCount: 34416, - fieldsPanelCount: 2, // Metrics panel and Fields panel - metricCards: [ - { - type: ML_JOB_FIELD_TYPES.NUMBER, // document count card - existsInDocs: true, - aggregatable: true, - loading: false, - }, - { - fieldName: 'responsetime', - type: ML_JOB_FIELD_TYPES.NUMBER, - existsInDocs: true, - aggregatable: true, - loading: false, - }, - ], - nonMetricCards: [ - { - fieldName: '@timestamp', - type: ML_JOB_FIELD_TYPES.DATE, - existsInDocs: true, - aggregatable: true, - loading: false, - }, - { - fieldName: '@version', - type: ML_JOB_FIELD_TYPES.TEXT, - existsInDocs: true, - aggregatable: false, - loading: false, - }, - { - fieldName: '@version.keyword', - type: ML_JOB_FIELD_TYPES.KEYWORD, - existsInDocs: true, - aggregatable: true, - loading: false, - }, - { - fieldName: 'airline', - type: ML_JOB_FIELD_TYPES.KEYWORD, - existsInDocs: true, - aggregatable: true, - loading: false, - }, - { - fieldName: 'type', - type: ML_JOB_FIELD_TYPES.TEXT, - existsInDocs: true, - aggregatable: false, - loading: false, - }, - { - fieldName: 'type.keyword', - type: ML_JOB_FIELD_TYPES.KEYWORD, - existsInDocs: true, - aggregatable: true, - loading: false, - }, - ], - nonMetricFieldsTypeFilterCardCount: 3, - metricFieldsFilterCardCount: 2, - nonMetricFieldsFilterCardCount: 1, - }, + }; + + const farequoteLuceneSearchTestData: TestData = { + suiteTitle: 'lucene saved search', + sourceIndexOrSavedSearch: 'farequote_lucene', + advancedJobWizardDatafeedQuery: `{ + "bool": { + "must": [ + { + "match_all": {} + } + ] + } +}`, // Note query is not currently passed to the wizard + metricFieldsFilter: 'responsetime', + nonMetricFieldsFilter: 'version', + nonMetricFieldsTypeFilter: 'keyword', + expected: { + totalDocCount: 34416, + fieldsPanelCount: 2, // Metrics panel and Fields panel + metricCards: [ + { + type: ML_JOB_FIELD_TYPES.NUMBER, // document count card + existsInDocs: true, + aggregatable: true, + loading: false, + }, + { + fieldName: 'responsetime', + type: ML_JOB_FIELD_TYPES.NUMBER, + existsInDocs: true, + aggregatable: true, + loading: false, + }, + ], + nonMetricCards: [ + { + fieldName: '@timestamp', + type: ML_JOB_FIELD_TYPES.DATE, + existsInDocs: true, + aggregatable: true, + loading: false, + }, + { + fieldName: '@version', + type: ML_JOB_FIELD_TYPES.TEXT, + existsInDocs: true, + aggregatable: false, + loading: false, + }, + { + fieldName: '@version.keyword', + type: ML_JOB_FIELD_TYPES.KEYWORD, + existsInDocs: true, + aggregatable: true, + loading: false, + }, + { + fieldName: 'airline', + type: ML_JOB_FIELD_TYPES.KEYWORD, + existsInDocs: true, + aggregatable: true, + loading: false, + }, + { + fieldName: 'type', + type: ML_JOB_FIELD_TYPES.TEXT, + existsInDocs: true, + aggregatable: false, + loading: false, + }, + { + fieldName: 'type.keyword', + type: ML_JOB_FIELD_TYPES.KEYWORD, + existsInDocs: true, + aggregatable: true, + loading: false, + }, + ], + nonMetricFieldsTypeFilterCardCount: 3, + metricFieldsFilterCardCount: 2, + nonMetricFieldsFilterCardCount: 1, }, - ]; + }; + + function runTests(testData: TestData) { + it(`${testData.suiteTitle} loads the saved search selection page`, async () => { + await ml.dataVisualizer.navigateToIndexPatternSelection(); + }); + + it(`${testData.suiteTitle} loads the index data visualizer page`, async () => { + await ml.jobSourceSelection.selectSourceForIndexBasedDataVisualizer( + testData.sourceIndexOrSavedSearch + ); + }); + + it(`${testData.suiteTitle} displays the time range step`, async () => { + await ml.dataVisualizerIndexBased.assertTimeRangeSelectorSectionExists(); + }); + + it(`${testData.suiteTitle} loads data for full time range`, async () => { + await ml.dataVisualizerIndexBased.clickUseFullDataButton(testData.expected.totalDocCount); + }); + + it(`${testData.suiteTitle} displays the panels of fields`, async () => { + await ml.dataVisualizerIndexBased.assertFieldsPanelsExist(testData.expected.fieldsPanelCount); + }); + + if (testData.expected.metricCards !== undefined && testData.expected.metricCards.length > 0) { + it(`${testData.suiteTitle} displays the Metrics panel`, async () => { + await ml.dataVisualizerIndexBased.assertFieldsPanelForTypesExist([ + ML_JOB_FIELD_TYPES.NUMBER, + ]); // document_count not exposed as a type in the panel + }); + + it(`${testData.suiteTitle} displays the expected metric field cards`, async () => { + for (const fieldCard of testData.expected.metricCards as FieldVisConfig[]) { + await ml.dataVisualizerIndexBased.assertCardExists(fieldCard.type, fieldCard.fieldName); + } + }); + + it(`${testData.suiteTitle} filters metric fields cards with search`, async () => { + await ml.dataVisualizerIndexBased.filterFieldsPanelWithSearchString( + ['number'], + testData.metricFieldsFilter, + testData.expected.metricFieldsFilterCardCount + ); + }); + } + + if ( + testData.expected.nonMetricCards !== undefined && + testData.expected.nonMetricCards.length > 0 + ) { + it(`${testData.suiteTitle} displays the non-metric Fields panel`, async () => { + await ml.dataVisualizerIndexBased.assertFieldsPanelForTypesExist( + getFieldTypes(testData.expected.nonMetricCards as FieldVisConfig[]) + ); + }); + + it(`${testData.suiteTitle} displays the expected non-metric field cards`, async () => { + for (const fieldCard of testData.expected.nonMetricCards!) { + await ml.dataVisualizerIndexBased.assertCardExists(fieldCard.type, fieldCard.fieldName); + } + }); + + it(`${testData.suiteTitle} sets the non metric field types input`, async () => { + const fieldTypes: ML_JOB_FIELD_TYPES[] = getFieldTypes( + testData.expected.nonMetricCards as FieldVisConfig[] + ); + await ml.dataVisualizerIndexBased.assertFieldsPanelTypeInputExists(fieldTypes); + await ml.dataVisualizerIndexBased.setFieldsPanelTypeInputValue( + fieldTypes, + testData.nonMetricFieldsTypeFilter, + testData.expected.nonMetricFieldsTypeFilterCardCount + ); + }); + + it(`${testData.suiteTitle} filters non-metric fields cards with search`, async () => { + await ml.dataVisualizerIndexBased.filterFieldsPanelWithSearchString( + getFieldTypes(testData.expected.nonMetricCards as FieldVisConfig[]), + testData.nonMetricFieldsFilter, + testData.expected.nonMetricFieldsFilterCardCount + ); + }); + } + } describe('index based', function() { this.tags(['smoke', 'mlqa']); @@ -187,101 +388,45 @@ export default function({ getService }: FtrProviderContext) { // TODO - add tests for // - validating metrics displayed inside the cards // - selecting a document sample size - // - clicking on the link to the Advanced job wizard - // - a test suite using a KQL based saved search - for (const testData of testDataList) { - describe(`${testData.suiteTitle}`, function() { - it('loads the data visualizer selector page', async () => { - await ml.navigation.navigateToMl(); - await ml.navigation.navigateToDataVisualizer(); - }); - - it('loads the saved search selection page', async () => { - await ml.dataVisualizer.navigateToIndexPatternSelection(); - }); - it('loads the index data visualizer page', async () => { - await ml.jobSourceSelection.selectSourceForIndexBasedDataVisualizer( - testData.sourceIndexOrSavedSearch - ); - }); - - it('displays the time range step', async () => { - await ml.dataVisualizerIndexBased.assertTimeRangeSelectorSectionExists(); - }); - - it('loads data for full time range', async () => { - await ml.dataVisualizerIndexBased.clickUseFullDataButton(testData.expected.totalDocCount); - }); - - it('displays the panels of fields', async () => { - await ml.dataVisualizerIndexBased.assertFieldsPanelsExist( - testData.expected.fieldsPanelCount - ); - }); - - if (testData.expected.metricCards && testData.expected.metricCards.length > 0) { - it('displays the Metrics panel', async () => { - await ml.dataVisualizerIndexBased.assertFieldsPanelForTypesExist([ - ML_JOB_FIELD_TYPES.NUMBER, - ]); // document_count not exposed as a type in the panel - }); + describe('with farequote', function() { + // Run tests on full farequote index. + it(`${farequoteIndexPatternTestData.suiteTitle} loads the data visualizer selector page`, async () => { + // Start navigation from the base of the ML app. + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToDataVisualizer(); + }); - it('displays the expected metric field cards', async () => { - for (const fieldCard of testData.expected.metricCards) { - await ml.dataVisualizerIndexBased.assertCardExists( - fieldCard.type, - fieldCard.fieldName - ); - } - }); + runTests(farequoteIndexPatternTestData); - it('filters metric fields cards with search', async () => { - await ml.dataVisualizerIndexBased.filterFieldsPanelWithSearchString( - ['number'], - testData.metricFieldsFilter, - testData.expected.metricFieldsFilterCardCount - ); - }); - } + // Run tests on farequote KQL saved search. + it(`${farequoteKQLSearchTestData.suiteTitle} loads the data visualizer selector page`, async () => { + // Only navigate back to the data visualizer selector page before running next tests, + // to ensure the time picker isn't set back to the default (last 15 minutes). + await ml.navigation.navigateToDataVisualizer(); + }); - if (testData.expected.nonMetricCards && testData.expected.nonMetricCards.length > 0) { - it('displays the non-metric Fields panel', async () => { - await ml.dataVisualizerIndexBased.assertFieldsPanelForTypesExist( - getFieldTypes(testData.expected.nonMetricCards) - ); - }); + runTests(farequoteKQLSearchTestData); - it('displays the expected non-metric field cards', async () => { - for (const fieldCard of testData.expected.nonMetricCards) { - await ml.dataVisualizerIndexBased.assertCardExists( - fieldCard.type, - fieldCard.fieldName - ); - } - }); + // Run tests on farequote lucene saved search. + it(`${farequoteLuceneSearchTestData.suiteTitle} loads the data visualizer selector page`, async () => { + // Only navigate back to the data visualizer selector page before running next tests, + // to ensure the time picker isn't set back to the default (last 15 minutes). + await ml.navigation.navigateToDataVisualizer(); + }); - it('sets the non metric field types input', async () => { - const fieldTypes: ML_JOB_FIELD_TYPES[] = getFieldTypes( - testData.expected.nonMetricCards - ); - await ml.dataVisualizerIndexBased.assertFieldsPanelTypeInputExists(fieldTypes); - await ml.dataVisualizerIndexBased.setFieldsPanelTypeInputValue( - fieldTypes, - testData.nonMetricFieldsTypeFilter, - testData.expected.nonMetricFieldsTypeFilterCardCount - ); - }); + runTests(farequoteLuceneSearchTestData); - it('filters non-metric fields cards with search', async () => { - await ml.dataVisualizerIndexBased.filterFieldsPanelWithSearchString( - getFieldTypes(testData.expected.nonMetricCards), - testData.nonMetricFieldsFilter, - testData.expected.nonMetricFieldsFilterCardCount - ); - }); - } + // Test the Create advanced job button. + // Note the search is not currently passed to the wizard, just the index. + it(`${farequoteLuceneSearchTestData.suiteTitle} opens the advanced job wizard`, async () => { + await ml.dataVisualizerIndexBased.clickCreateAdvancedJobButton(); + await ml.jobTypeSelection.assertAdvancedJobWizardOpen(); + await ml.jobWizardAdvanced.assertDatafeedQueryEditorExists(); + await ml.jobWizardAdvanced.assertDatafeedQueryEditorValue( + farequoteLuceneSearchTestData.advancedJobWizardDatafeedQuery + ); }); - } + }); }); } diff --git a/x-pack/test/functional/apps/maps/documents_source/top_hits.js b/x-pack/test/functional/apps/maps/documents_source/top_hits.js index 65b008e3f07c9..59a4bea27ebab 100644 --- a/x-pack/test/functional/apps/maps/documents_source/top_hits.js +++ b/x-pack/test/functional/apps/maps/documents_source/top_hits.js @@ -13,60 +13,73 @@ export default function({ getPageObjects, getService }) { const inspector = getService('inspector'); const find = getService('find'); - describe('top hits', () => { - before(async () => { - await PageObjects.maps.loadSavedMap('document example top hits'); - }); - - it('should not fetch any search hits', async () => { - await inspector.open(); - await inspector.openInspectorRequestsView(); - const requestStats = await inspector.getTableData(); - const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits'); - expect(hits).to.equal('0'); // aggregation requests do not return any documents - }); - - it('should display top hits per entity', async () => { - const mapboxStyle = await PageObjects.maps.getMapboxStyle(); - expect(mapboxStyle.sources[VECTOR_SOURCE_ID].data.features.length).to.equal(10); - }); - - describe('configuration', () => { + describe('geo top hits', () => { + describe('split on string field', () => { before(async () => { - await PageObjects.maps.openLayerPanel('logstash'); - // Can not use testSubjects because data-test-subj is placed range input and number input - const sizeInput = await find.byCssSelector( - `input[data-test-subj="layerPanelTopHitsSize"][type='number']` - ); - await sizeInput.click(); - await sizeInput.clearValue(); - await sizeInput.type('3'); - await PageObjects.maps.waitForLayersToLoad(); + await PageObjects.maps.loadSavedMap('document example top hits'); }); - after(async () => { - await PageObjects.maps.closeLayerPanel(); + it('should not fetch any search hits', async () => { + await inspector.open(); + await inspector.openInspectorRequestsView(); + const requestStats = await inspector.getTableData(); + const hits = PageObjects.maps.getInspectorStatRowHit(requestStats, 'Hits'); + expect(hits).to.equal('0'); // aggregation requests do not return any documents }); - it('should update top hits when configation changes', async () => { + it('should display top hits per entity', async () => { const mapboxStyle = await PageObjects.maps.getMapboxStyle(); - expect(mapboxStyle.sources[VECTOR_SOURCE_ID].data.features.length).to.equal(15); + expect(mapboxStyle.sources[VECTOR_SOURCE_ID].data.features.length).to.equal(10); }); - }); - describe('query', () => { - before(async () => { - await PageObjects.maps.setAndSubmitQuery('machine.os.raw : "win 8"'); + describe('configuration', () => { + before(async () => { + await PageObjects.maps.openLayerPanel('logstash'); + // Can not use testSubjects because data-test-subj is placed range input and number input + const sizeInput = await find.byCssSelector( + `input[data-test-subj="layerPanelTopHitsSize"][type='number']` + ); + await sizeInput.click(); + await sizeInput.clearValue(); + await sizeInput.type('3'); + await PageObjects.maps.waitForLayersToLoad(); + }); + + after(async () => { + await PageObjects.maps.closeLayerPanel(); + }); + + it('should update top hits when configation changes', async () => { + const mapboxStyle = await PageObjects.maps.getMapboxStyle(); + expect(mapboxStyle.sources[VECTOR_SOURCE_ID].data.features.length).to.equal(15); + }); + }); + + describe('query', () => { + before(async () => { + await PageObjects.maps.setAndSubmitQuery('machine.os.raw : "win 8"'); + }); + + after(async () => { + await PageObjects.maps.setAndSubmitQuery(''); + }); + + it('should apply query to top hits request', async () => { + await PageObjects.maps.setAndSubmitQuery('machine.os.raw : "win 8"'); + const mapboxStyle = await PageObjects.maps.getMapboxStyle(); + expect(mapboxStyle.sources[VECTOR_SOURCE_ID].data.features.length).to.equal(2); + }); }); + }); - after(async () => { - await PageObjects.maps.setAndSubmitQuery(''); + describe('split on scripted field', () => { + before(async () => { + await PageObjects.maps.loadSavedMap('document example top hits split with scripted field'); }); - it('should apply query to top hits request', async () => { - await PageObjects.maps.setAndSubmitQuery('machine.os.raw : "win 8"'); + it('should display top hits per entity', async () => { const mapboxStyle = await PageObjects.maps.getMapboxStyle(); - expect(mapboxStyle.sources[VECTOR_SOURCE_ID].data.features.length).to.equal(2); + expect(mapboxStyle.sources[VECTOR_SOURCE_ID].data.features.length).to.equal(24); }); }); }); diff --git a/x-pack/test/functional/apps/rollup_job/tsvb.js b/x-pack/test/functional/apps/rollup_job/tsvb.js index f3782c4c91644..b697e751ef550 100644 --- a/x-pack/test/functional/apps/rollup_job/tsvb.js +++ b/x-pack/test/functional/apps/rollup_job/tsvb.js @@ -21,7 +21,8 @@ export default function({ getService, getPageObjects }) { 'timePicker', ]); - describe('tsvb integration', function() { + // https://github.com/elastic/kibana/issues/56816 + describe.skip('tsvb integration', function() { //Since rollups can only be created once with the same name (even if you delete it), //we add the Date.now() to avoid name collision if you run the tests locally back to back. const rollupJobName = `tsvb-test-rollup-job-${Date.now()}`; diff --git a/x-pack/test/functional/apps/transform/creation_saved_search.ts b/x-pack/test/functional/apps/transform/creation_saved_search.ts index 4528a2c84d9de..333a53a98c82b 100644 --- a/x-pack/test/functional/apps/transform/creation_saved_search.ts +++ b/x-pack/test/functional/apps/transform/creation_saved_search.ts @@ -17,7 +17,8 @@ export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const transform = getService('transform'); - describe('creation_saved_search', function() { + // flaky test, see #55179 + describe.skip('creation_saved_search', function() { this.tags(['smoke']); before(async () => { await esArchiver.load('ml/farequote'); diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts index bdcdc4b7cd3ec..6db8ad28deccb 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts @@ -24,7 +24,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const queryBar = getService('queryBar'); const savedQueryManagementComponent = getService('savedQueryManagementComponent'); - describe('feature controls security', () => { + // FLAKY: https://github.com/elastic/kibana/issues/50018 + describe.skip('feature controls security', () => { before(async () => { await esArchiver.load('visualize/default'); await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/x-pack/test/functional/config.ie.js b/x-pack/test/functional/config.ie.js index 081bab4b80457..bac4547b4aa5c 100644 --- a/x-pack/test/functional/config.ie.js +++ b/x-pack/test/functional/config.ie.js @@ -58,14 +58,17 @@ export default async function({ readConfigFile }) { defaults: { 'accessibility:disableAnimations': true, 'dateFormat:tz': 'UTC', - 'telemetry:optIn': false, 'state:storeInSessionStorage': true, }, }, kbnTestServer: { ...defaultConfig.get('kbnTestServer'), - serverArgs: [...defaultConfig.get('kbnTestServer.serverArgs'), '--csp.strict=false'], + serverArgs: [ + ...defaultConfig.get('kbnTestServer.serverArgs'), + '--csp.strict=false', + '--telemetry.optIn=false', + ], }, }; } diff --git a/x-pack/test/functional/es_archives/maps/kibana/data.json b/x-pack/test/functional/es_archives/maps/kibana/data.json index e24734841bf55..e50ec593cc990 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/data.json +++ b/x-pack/test/functional/es_archives/maps/kibana/data.json @@ -278,6 +278,62 @@ } } +{ + "type": "doc", + "value": { + "id": "map:4ea1e4f0-4dba-11ea-b554-4ba0def79f86", + "index": ".kibana", + "source": { + "map": { + "title" : "document example top hits split with scripted field", + "description" : "", + "mapStateJSON" : "{\"zoom\":4.1,\"center\":{\"lon\":-100.61091,\"lat\":33.23887},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-24T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]}", + "layerListJSON" : "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"VECTOR_TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"useTopHits\":true,\"topHitsSplitField\":\"hour_of_day\",\"topHitsSize\":1,\"sortField\":\"@timestamp\",\"sortOrder\":\"desc\",\"applyGlobalQuery\":true,\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"airfield\"}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", + "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}", + "bounds" : { + "type" : "Polygon", + "coordinates" : [ + [ + [ + -141.61334, + 47.30762 + ], + [ + -141.61334, + 16.49119 + ], + [ + -59.60848, + 16.49119 + ], + [ + -59.60848, + 47.30762 + ], + [ + -141.61334, + 47.30762 + ] + ] + ] + } + }, + "type" : "map", + "references" : [ + { + "name" : "layer_1_source_index_pattern", + "type" : "index-pattern", + "id" : "c698b940-e149-11e8-a35a-370a8516603a" + } + ], + "migrationVersion" : { + "map" : "7.7.0" + }, + "updated_at" : "2020-02-12T17:08:36.671Z" + } + } +} + { "type": "doc", "value": { diff --git a/x-pack/test/functional/es_archives/ml/categorization/data.json.gz b/x-pack/test/functional/es_archives/ml/categorization/data.json.gz new file mode 100644 index 0000000000000..a66b68d815943 Binary files /dev/null and b/x-pack/test/functional/es_archives/ml/categorization/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/ml/categorization/mappings.json b/x-pack/test/functional/es_archives/ml/categorization/mappings.json new file mode 100644 index 0000000000000..5c97427b6fb0a --- /dev/null +++ b/x-pack/test/functional/es_archives/ml/categorization/mappings.json @@ -0,0 +1,873 @@ +{ + "type": "index", + "value": { + "aliases": {}, + "index": "categorization_functional_test", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "field1": { + "type": "text" + }, + "field2": { + "type": "text" + }, + "field3": { + "type": "text" + }, + "field4": { + "type": "text" + }, + "field5": { + "type": "text" + }, + "field6": { + "type": "text" + }, + "field7": { + "type": "text" + } + } + }, + "settings": { + "index": { + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + ".kibana": {} + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "c0c235fba02ebd2a2412bcda79009b58", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "e588043a01d3d43477e7cad7efa0f5d8", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-services-telemetry": "07ee1939fa4302c62ddc052ec03fed90", + "config": "87aca8fdb053154f11383fce3dbf3edf", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "21c3ea0763beb1ecb0162529706b88c5", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "23d7aa4a720d4938ccde3983f87bd58d", + "maps-telemetry": "268da3a48066123fc5baf35abaa55014", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "181661168bbadd1eff5902361e2a0d5c", + "server": "ec97f1c5da1a19609a60874e5af1100c", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "358ffaa88ba34a97d55af0933a117de4", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "a53a20fe086b72c9a86da3cc12dad8a6", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "52d7a13ad68a150c4525b292d23e12cc" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-services-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "dotnet": { + "null_value": 0, + "type": "long" + }, + "go": { + "null_value": 0, + "type": "long" + }, + "java": { + "null_value": 0, + "type": "long" + }, + "js-base": { + "null_value": 0, + "type": "long" + }, + "nodejs": { + "null_value": 0, + "type": "long" + }, + "python": { + "null_value": 0, + "type": "long" + }, + "ruby": { + "null_value": 0, + "type": "long" + }, + "rum-js": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "defaultIndex": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "expression": { + "index": false, + "type": "keyword" + }, + "state": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "bounds": { + "type": "geo_shape" + }, + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "properties": { + "attributesPerMap": { + "properties": { + "dataSourcesCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + }, + "emsVectorLayersCount": { + "dynamic": "true", + "type": "object" + }, + "layerTypesCount": { + "dynamic": "true", + "type": "object" + }, + "layersCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + } + } + }, + "indexPatternsWithGeoFieldCount": { + "type": "long" + }, + "mapsTotalCount": { + "type": "long" + }, + "settings": { + "properties": { + "showMapVisualizationTypes": { + "type": "boolean" + } + } + }, + "timeCaptured": { + "type": "date" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "ignore_above": 256, + "type": "keyword" + }, + "sendUsageFrom": { + "ignore_above": 256, + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "dynamic": "true", + "properties": { + "indexName": { + "type": "keyword" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchRefName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/page_objects/endpoint_page.ts b/x-pack/test/functional/page_objects/endpoint_page.ts index f02a899f6d37d..a306a855a83eb 100644 --- a/x-pack/test/functional/page_objects/endpoint_page.ts +++ b/x-pack/test/functional/page_objects/endpoint_page.ts @@ -8,10 +8,15 @@ import { FtrProviderContext } from '../ftr_provider_context'; export function EndpointPageProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); + const table = getService('table'); return { async welcomeEndpointTitle() { return await testSubjects.getVisibleText('welcomeTitle'); }, + + async getManagementTableData() { + return await table.getDataFromTestSubj('managementListTable'); + }, }; } diff --git a/x-pack/test/functional/services/machine_learning/data_visualizer_index_based.ts b/x-pack/test/functional/services/machine_learning/data_visualizer_index_based.ts index 32d9bc94de50d..f43ccbb2be91f 100644 --- a/x-pack/test/functional/services/machine_learning/data_visualizer_index_based.ts +++ b/x-pack/test/functional/services/machine_learning/data_visualizer_index_based.ts @@ -103,5 +103,9 @@ export function MachineLearningDataVisualizerIndexBasedProvider({ await this.assertFieldsPanelTypeInputValue(filterFieldType); await this.assertFieldsPanelCardCount(panelFieldTypes, expectedCardCount); }, + + async clickCreateAdvancedJobButton() { + await testSubjects.clickWhenNotDisabled('mlDataVisualizerCreateAdvancedJobCard'); + }, }; } diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index 938b98591b6a2..6d83e0bbf1df7 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import uuid from 'uuid'; -import { omit } from 'lodash'; +import { omit, mapValues } from 'lodash'; import moment from 'moment'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -204,64 +204,66 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - it('renders the active alert instances', async () => { + it.skip('renders the active alert instances', async () => { const testBeganAt = moment().utc(); // Verify content await testSubjects.existOrFail('alertInstancesList'); - const { - alertInstances: { - ['us-central']: { - meta: { - lastScheduledActions: { date }, - }, - }, - }, - } = await alerting.alerts.getAlertState(alert.id); + const { alertInstances } = await alerting.alerts.getAlertState(alert.id); - const dateOnAllInstances = moment(date) - .utc() - .format('D MMM YYYY @ HH:mm:ss'); + const dateOnAllInstances = mapValues( + alertInstances, + ({ + meta: { + lastScheduledActions: { date }, + }, + }) => moment(date).utc() + ); const instancesList = await pageObjects.alertDetailsUI.getAlertInstancesList(); expect(instancesList.map(instance => omit(instance, 'duration'))).to.eql([ { instance: 'us-central', status: 'Active', - start: dateOnAllInstances, + start: dateOnAllInstances['us-central'].format('D MMM YYYY @ HH:mm:ss'), }, { instance: 'us-east', status: 'Active', - start: dateOnAllInstances, + start: dateOnAllInstances['us-east'].format('D MMM YYYY @ HH:mm:ss'), }, { instance: 'us-west', status: 'Active', - start: dateOnAllInstances, + start: dateOnAllInstances['us-west'].format('D MMM YYYY @ HH:mm:ss'), }, ]); - const durationFromInstanceTillPageLoad = moment.duration( - testBeganAt.diff(moment(date).utc()) + const durationFromInstanceTillPageLoad = mapValues(dateOnAllInstances, date => + moment.duration(testBeganAt.diff(moment(date).utc())) ); instancesList - .map(alertInstance => alertInstance.duration.split(':').map(part => parseInt(part, 10))) - .map(([hours, minutes, seconds]) => - moment.duration({ + .map(alertInstance => ({ + id: alertInstance.instance, + duration: alertInstance.duration.split(':').map(part => parseInt(part, 10)), + })) + .map(({ id, duration: [hours, minutes, seconds] }) => ({ + id, + duration: moment.duration({ hours, minutes, seconds, - }) - ) - .forEach(alertInstanceDuration => { - // make sure the duration is within a 2 second range - expect(alertInstanceDuration.as('milliseconds')).to.greaterThan( - durationFromInstanceTillPageLoad.subtract(1000 * 2).as('milliseconds') + }), + })) + .forEach(({ id, duration }) => { + // make sure the duration is within a 10 second range which is + // good enough as the alert interval is 1m, so we know it is a fresh value + expect(duration.as('milliseconds')).to.greaterThan( + durationFromInstanceTillPageLoad[id].subtract(1000 * 10).as('milliseconds') ); - expect(alertInstanceDuration.as('milliseconds')).to.lessThan( - durationFromInstanceTillPageLoad.add(1000 * 2).as('milliseconds') + expect(duration.as('milliseconds')).to.lessThan( + durationFromInstanceTillPageLoad[id].add(1000 * 10).as('milliseconds') ); }); }); diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts index 15d1baadf7806..d7551345203b4 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts @@ -22,7 +22,7 @@ function createNoopAlertType(setupContract: any) { const noopAlertType: AlertType = { id: 'test.noop', name: 'Test: Noop', - actionGroups: ['default'], + actionGroups: [{ id: 'default', name: 'Default' }], async executor() {}, }; setupContract.registerType(noopAlertType); @@ -33,7 +33,10 @@ function createAlwaysFiringAlertType(setupContract: any) { const alwaysFiringAlertType: any = { id: 'test.always-firing', name: 'Always Firing', - actionGroups: ['default', 'other'], + actionGroups: [ + { id: 'default', name: 'Default' }, + { id: 'other', name: 'Other' }, + ], async executor(alertExecutorOptions: any) { const { services, state, params } = alertExecutorOptions; diff --git a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts index 570d7026cf99e..55853f8b0fbde 100644 --- a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts +++ b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts @@ -199,7 +199,7 @@ export default function({ getService }: FtrProviderContext) { const systemAPIResponse = await supertest .get('/internal/security/me') .set('kbn-xsrf', 'xxx') - .set('kbn-system-api', 'true') + .set('kbn-system-request', 'true') .set('Cookie', sessionCookie.cookieString()) .expect(200); diff --git a/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.js b/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.js index 094537fd61436..abb65e46263ab 100644 --- a/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.js +++ b/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.js @@ -285,7 +285,7 @@ export default function({ getService }) { const systemAPIResponse = await supertest .get('/internal/security/me') .set('kbn-xsrf', 'xxx') - .set('kbn-system-api', 'true') + .set('kbn-system-request', 'true') .set('Cookie', sessionCookie.cookieString()) .expect(200); diff --git a/x-pack/test/pki_api_integration/apis/security/pki_auth.ts b/x-pack/test/pki_api_integration/apis/security/pki_auth.ts index 1ae7488fcf379..6cb92585de36e 100644 --- a/x-pack/test/pki_api_integration/apis/security/pki_auth.ts +++ b/x-pack/test/pki_api_integration/apis/security/pki_auth.ts @@ -242,7 +242,7 @@ export default function({ getService }: FtrProviderContext) { .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .set('kbn-xsrf', 'xxx') - .set('kbn-system-api', 'true') + .set('kbn-system-request', 'true') .set('Cookie', sessionCookie.cookieString()) .expect(200); diff --git a/x-pack/test/reporting/functional/reporting.js b/x-pack/test/reporting/functional/reporting.js index 0e1078a2a4c8b..012f0922c28cf 100644 --- a/x-pack/test/reporting/functional/reporting.js +++ b/x-pack/test/reporting/functional/reporting.js @@ -94,8 +94,8 @@ export default function({ getService, getPageObjects }) { // Generating and then comparing reports can take longer than the default 60s timeout because the comparePngs // function is taking about 15 seconds per comparison in jenkins. this.timeout(300000); - - await PageObjects.dashboard.switchToEditMode(); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.gotoDashboardEditMode('My PDF Dashboard'); await PageObjects.reporting.setTimepickerInDataRange(); const visualizations = PageObjects.dashboard.getTestVisualizationNames(); @@ -135,7 +135,8 @@ export default function({ getService, getPageObjects }) { it('matches baseline report', async function() { this.timeout(300000); - await PageObjects.dashboard.switchToEditMode(); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.gotoDashboardEditMode('My PNG Dash'); await PageObjects.reporting.setTimepickerInDataRange(); const visualizations = PageObjects.dashboard.getTestVisualizationNames(); diff --git a/x-pack/test/saml_api_integration/apis/security/saml_login.ts b/x-pack/test/saml_api_integration/apis/security/saml_login.ts index 6ede8aadeb5a7..b8296aa703607 100644 --- a/x-pack/test/saml_api_integration/apis/security/saml_login.ts +++ b/x-pack/test/saml_api_integration/apis/security/saml_login.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import querystring from 'querystring'; +import { stringify } from 'query-string'; import url from 'url'; import { delay } from 'bluebird'; import expect from '@kbn/expect'; @@ -330,7 +330,7 @@ export default function({ getService }: FtrProviderContext) { const systemAPIResponse = await supertest .get('/internal/security/me') .set('kbn-xsrf', 'xxx') - .set('kbn-system-api', 'true') + .set('kbn-system-request', 'true') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -443,7 +443,7 @@ export default function({ getService }: FtrProviderContext) { it('should invalidate access token on IdP initiated logout', async () => { const logoutRequest = await createLogoutRequest({ sessionIndex: idpSessionIndex }); const logoutResponse = await supertest - .get(`/api/security/logout?${querystring.stringify(logoutRequest)}`) + .get(`/api/security/logout?${stringify(logoutRequest, { sort: false })}`) .set('Cookie', sessionCookie.cookieString()) .expect(302); @@ -479,7 +479,7 @@ export default function({ getService }: FtrProviderContext) { it('should invalidate access token on IdP initiated logout even if there is no Kibana session', async () => { const logoutRequest = await createLogoutRequest({ sessionIndex: idpSessionIndex }); const logoutResponse = await supertest - .get(`/api/security/logout?${querystring.stringify(logoutRequest)}`) + .get(`/api/security/logout?${stringify(logoutRequest, { sort: false })}`) .expect(302); expect(logoutResponse.headers['set-cookie']).to.be(undefined); diff --git a/x-pack/test/saml_api_integration/fixtures/saml_tools.ts b/x-pack/test/saml_api_integration/fixtures/saml_tools.ts index b7b94b8eeb17a..bbe0df7ff3a2c 100644 --- a/x-pack/test/saml_api_integration/fixtures/saml_tools.ts +++ b/x-pack/test/saml_api_integration/fixtures/saml_tools.ts @@ -6,7 +6,7 @@ import crypto from 'crypto'; import fs from 'fs'; -import querystring from 'querystring'; +import { stringify } from 'query-string'; import url from 'url'; import zlib from 'zlib'; import { promisify } from 'util'; @@ -140,7 +140,7 @@ export async function getLogoutRequest({ }; const signer = crypto.createSign('RSA-SHA256'); - signer.update(querystring.stringify(queryStringParameters)); + signer.update(stringify(queryStringParameters, { sort: false })); queryStringParameters.Signature = signer.sign(signingKey.toString(), 'base64'); return queryStringParameters; diff --git a/yarn.lock b/yarn.lock index 491e8ab8cf95d..0a55e3d7c7850 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2399,6 +2399,16 @@ "@types/istanbul-reports" "^1.1.1" "@types/yargs" "^13.0.0" +"@jest/types@^25.1.0": + version "25.1.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.1.0.tgz#b26831916f0d7c381e11dbb5e103a72aed1b4395" + integrity sha512-VpOtt7tCrgvamWZh1reVsGADujKigBUFTi19mlRjqEGsE8qH4r3s+skY33dNdXOwyZIvuftZ5tqdF1IgsMejMA== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^1.1.1" + "@types/yargs" "^15.0.0" + chalk "^3.0.0" + "@jimp/bmp@^0.8.4": version "0.8.4" resolved "https://registry.yarnpkg.com/@jimp/bmp/-/bmp-0.8.4.tgz#3246e0c6b073b3e2d9b61075ac0146d9124c9277" @@ -4094,6 +4104,11 @@ resolved "https://registry.yarnpkg.com/@types/angular/-/angular-1.6.56.tgz#20124077bd44061e018c7283c0bb83f4b00322dd" integrity sha512-HxtqilvklZ7i6XOaiP7uIJIrFXEVEhfbSY45nfv2DeBRngncI58Y4ZOUMiUkcT8sqgLL1ablmbfylChUg7A3GA== +"@types/anymatch@*": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a" + integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA== + "@types/archiver@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/archiver/-/archiver-3.0.0.tgz#c0a53e0ed3b7aef626ce683d081d7821d8c638b4" @@ -4206,6 +4221,11 @@ resolved "https://registry.yarnpkg.com/@types/boom/-/boom-7.2.1.tgz#a21e21ba08cc49d17b26baef98e1a77ee4d6cdb0" integrity sha512-kOiap+kSa4DPoookJXQGQyKy1rjZ55tgfKAh9F0m1NUdukkcwVzpSnXPMH42a5L+U++ugdQlh/xFJu/WAdr1aw== +"@types/browserslist-useragent@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/browserslist-useragent/-/browserslist-useragent-3.0.0.tgz#d425c9818182ce71ce53866798cee9c7d41d6e53" + integrity sha512-ZBvKzg3yyWNYEkwxAzdmUzp27sFvw+1m080/+2lwrt+eltNefn1f4fnpMyrjOla31p8zLleCYqQXw+3EETfn0w== + "@types/caseless@*": version "0.12.2" resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8" @@ -4265,6 +4285,11 @@ resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.0.tgz#926f76f7e66f49cc59ad880bb15b030abbf0b66d" integrity sha512-gZ/Rb+MFXF0pXSEQxdRoPMm5jeO3TycjOdvbpbcpHX/B+n9AqaHFe5q6Ga9CsZ7ir/UgIWPfrBzUzn3F19VH/w== +"@types/color-name@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" + integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== + "@types/color@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/color/-/color-3.0.0.tgz#40f8a6bf2fd86e969876b339a837d8ff1b0a6e30" @@ -4473,6 +4498,13 @@ "@types/glob" "*" fast-glob "^2.0.2" +"@types/graceful-fs@*": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.3.tgz#039af35fe26bec35003e8d86d2ee9c586354348f" + integrity sha512-AiHRaEB50LQg0pZmm659vNBb9f4SJ0qrAnteuzhSeAUcJKxoYgEnprg/83kppCnc2zvtCKbdZry1a5pVY3lOTQ== + dependencies: + "@types/node" "*" + "@types/graphql@^0.13.2": version "0.13.4" resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.13.4.tgz#55ae9c29f0fd6b85ee536f5c72b4769d5c5e06b1" @@ -4681,6 +4713,14 @@ "@types/node" "*" rxjs "^6.5.1" +"@types/loader-utils@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@types/loader-utils/-/loader-utils-1.1.3.tgz#82b9163f2ead596c68a8c03e450fbd6e089df401" + integrity sha512-euKGFr2oCB3ASBwG39CYJMR3N9T0nanVqXdiH7Zu/Nqddt6SmFRxytq/i2w9LQYNQekEtGBz+pE3qG6fQTNvRg== + dependencies: + "@types/node" "*" + "@types/webpack" "*" + "@types/lodash.clonedeep@^4.5.4": version "4.5.4" resolved "https://registry.yarnpkg.com/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.4.tgz#2515c5f08bc95afebfb597711871b0497f5d7da7" @@ -5071,13 +5111,6 @@ dependencies: "@types/normalize-package-data" "*" -"@types/recompose@^0.30.5": - version "0.30.5" - resolved "https://registry.yarnpkg.com/@types/recompose/-/recompose-0.30.5.tgz#09890e3c504546b38193479e610e427ac0888393" - integrity sha512-PEQvFmudB9n0+ZvD8l7lh0olGAWmVAuVwCM4eotzWouH8/Kcr8/EcZyLhYILqoTlqzi6ey/3kbKQzJ/h3KkyXw== - dependencies: - "@types/react" "*" - "@types/recompose@^0.30.6": version "0.30.6" resolved "https://registry.yarnpkg.com/@types/recompose/-/recompose-0.30.6.tgz#f6ffae2008b84df916ed6633751f9287f344ea3e" @@ -5139,6 +5172,11 @@ resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== +"@types/source-list-map@*": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" + integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== + "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" @@ -5204,6 +5242,11 @@ dependencies: "@types/superagent" "*" +"@types/tapable@*": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.4.tgz#b4ffc7dc97b498c969b360a41eee247f82616370" + integrity sha512-78AdXtlhpCHT0K3EytMpn4JNxaf5tbqbLcbIRoQIHzpTIyjpxLQKRoxU55ujBXAtg3Nl2h/XWvfDa9dsMOd0pQ== + "@types/tar-fs@^1.16.1": version "1.16.1" resolved "https://registry.yarnpkg.com/@types/tar-fs/-/tar-fs-1.16.1.tgz#6e3fba276c173e365ae91e55f7b797a0e64298e5" @@ -5259,6 +5302,13 @@ resolved "https://registry.yarnpkg.com/@types/type-detect/-/type-detect-4.0.1.tgz#3b0f5ac82ea630090cbf57c57a1bf5a63a29b9b6" integrity sha512-0+S1S9Iq0oJ9w9IaBC5W/z1WsPNDUIAJG+THGmqR4vUAxUPCzIY+dApTvyGsaBUWjafTDL0Dg8Z9+iRuk3/BQA== +"@types/uglify-js@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082" + integrity sha512-SudIN9TRJ+v8g5pTG8RRCqfqTMNqgWCKKd3vtynhGzkIIjxaicNAMuY5TRadJ6tzDu3Dotf3ngaMILtmOdmWEQ== + dependencies: + source-map "^0.6.1" + "@types/undertaker-registry@*": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/undertaker-registry/-/undertaker-registry-1.0.1.tgz#4306d4a03d7acedb974b66530832b90729e1d1da" @@ -5321,11 +5371,41 @@ dependencies: "@types/node" "*" +"@types/watchpack@^1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@types/watchpack/-/watchpack-1.1.5.tgz#e5622eb2a49e2239d94d8882275fbc7893147e97" + integrity sha512-9clzOLesGBv5/60QQ3UvpOPsRSNu4ybw4jUBq1aofGdA2NtS5dL2D/m6WAXycxdg+rcGOHTN2rgpTMAdJ4jMWg== + dependencies: + "@types/graceful-fs" "*" + "@types/node" "*" + chokidar "^2.1.2" + "@types/webpack-env@^1.13.7": version "1.14.1" resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.14.1.tgz#0d8a53f308f017c53a5ddc3d07f4d6fa76b790d7" integrity sha512-0Ki9jAAhKDSuLDXOIMADg54Hu60SuBTEsWaJGGy5cV+SSUQ63J2a+RrYYGrErzz39fXzTibhKrAQJAb8M7PNcA== +"@types/webpack-sources@*": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.5.tgz#be47c10f783d3d6efe1471ff7f042611bd464a92" + integrity sha512-zfvjpp7jiafSmrzJ2/i3LqOyTYTuJ7u1KOXlKgDlvsj9Rr0x7ZiYu5lZbXwobL7lmsRNtPXlBfmaUD8eU2Hu8w== + dependencies: + "@types/node" "*" + "@types/source-list-map" "*" + source-map "^0.6.1" + +"@types/webpack@*", "@types/webpack@^4.4.31", "@types/webpack@^4.41.3": + version "4.41.3" + resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.3.tgz#30c2251db1d69a45bbffd79c0577dd9baf50e7ba" + integrity sha512-dH+BZ6pHBZFrXpnif0YU/PbmUq3lQrvRPnqkxsciSIzvG/DE+Vm/Wrjn56T7V3+B5ryQa5fw0oGnHL8tk4ll6w== + dependencies: + "@types/anymatch" "*" + "@types/node" "*" + "@types/tapable" "*" + "@types/uglify-js" "*" + "@types/webpack-sources" "*" + source-map "^0.6.0" + "@types/wrap-ansi@^2.0.15": version "2.0.15" resolved "https://registry.yarnpkg.com/@types/wrap-ansi/-/wrap-ansi-2.0.15.tgz#87affc11a46864cb6853b642e89363633d544aa7" @@ -5373,6 +5453,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/yargs@^15.0.0": + version "15.0.3" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.3.tgz#41453a0bc7ab393e995d1f5451455638edbd2baf" + integrity sha512-XCMQRK6kfpNBixHLyHUsGmXrpEmFFxzMrcnSXFMziHd8CoNJo8l16FkHyQq4x+xbM7E2XL83/O78OD8u+iZTdQ== + dependencies: + "@types/yargs-parser" "*" + "@types/zen-observable@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" @@ -5693,11 +5780,6 @@ accepts@~1.3.7: mime-types "~2.1.24" negotiator "0.6.2" -acorn-dynamic-import@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948" - integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw== - acorn-globals@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-3.1.0.tgz#fd8270f71fbb4996b004fa880ee5d46573a731bf" @@ -5772,11 +5854,6 @@ acorn@^6.0.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f" integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA== -acorn@^6.0.5: - version "6.4.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.0.tgz#b659d2ffbafa24baf5db1cdbb2c94a983ecd2784" - integrity sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw== - acorn@^6.2.1: version "6.3.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e" @@ -6195,6 +6272,14 @@ ansi-styles@^3.2.0: dependencies: color-convert "^1.9.0" +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" + integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== + dependencies: + "@types/color-name" "^1.1.1" + color-convert "^2.0.1" + ansi-styles@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178" @@ -6968,18 +7053,18 @@ autobind-decorator@^1.3.4: resolved "https://registry.yarnpkg.com/autobind-decorator/-/autobind-decorator-1.4.3.tgz#4c96ffa77b10622ede24f110f5dbbf56691417d1" integrity sha1-TJb/p3sQYi7eJPEQ9du/VmkUF9E= -autoprefixer@9.6.1, autoprefixer@^9.4.9: - version "9.6.1" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.6.1.tgz#51967a02d2d2300bb01866c1611ec8348d355a47" - integrity sha512-aVo5WxR3VyvyJxcJC3h4FKfwCQvQWb1tSI5VHNibddCVWrcD1NvlxEweg3TSgiPztMnWfjpy2FURKA2kvDE+Tw== +autoprefixer@^9.4.9, autoprefixer@^9.7.4: + version "9.7.4" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.7.4.tgz#f8bf3e06707d047f0641d87aee8cfb174b2a5378" + integrity sha512-g0Ya30YrMBAEZk60lp+qfX5YQllG+S5W3GYCFvyHTvhOki0AEQJLPEcIuGRsqVwLi8FvXPVtwTGhfr38hVpm0g== dependencies: - browserslist "^4.6.3" - caniuse-lite "^1.0.30000980" + browserslist "^4.8.3" + caniuse-lite "^1.0.30001020" chalk "^2.4.2" normalize-range "^0.1.2" num2fraction "^1.2.2" - postcss "^7.0.17" - postcss-value-parser "^4.0.0" + postcss "^7.0.26" + postcss-value-parser "^4.0.2" await-event@^2.1.0: version "2.1.0" @@ -7701,11 +7786,6 @@ big-time@2.x.x: resolved "https://registry.yarnpkg.com/big-time/-/big-time-2.0.1.tgz#68c7df8dc30f97e953f25a67a76ac9713c16c9de" integrity sha1-aMffjcMPl+lT8lpnp2rJcTwWyd4= -big.js@^3.1.3: - version "3.2.0" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" - integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q== - big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -8160,6 +8240,15 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" +browserslist-useragent@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/browserslist-useragent/-/browserslist-useragent-3.0.2.tgz#f0e209b2742baa5de0e451b52e678e8b4402617c" + integrity sha512-/UPzK9xZnk5mwwWx4wcuBKAKx/mD3MNY8sUuZ2NPqnr4RVFWZogX+8mOP0cQEYo8j78sHk0hiDNaVXZ1U3hM9A== + dependencies: + browserslist "^4.6.6" + semver "^6.3.0" + useragent "^2.3.0" + browserslist@4.6.6: version "4.6.6" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.6.6.tgz#6e4bf467cde520bc9dbdf3747dafa03531cec453" @@ -8169,14 +8258,14 @@ browserslist@4.6.6: electron-to-chromium "^1.3.191" node-releases "^1.1.25" -browserslist@^4.6.0, browserslist@^4.6.3: - version "4.6.4" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.6.4.tgz#fd0638b3f8867fec2c604ed0ed9300379f8ec7c2" - integrity sha512-ErJT8qGfRt/VWHSr1HeqZzz50DvxHtr1fVL1m5wf20aGrG8e1ce8fpZ2EjZEfs09DDZYSvtRaDlMpWslBf8Low== +browserslist@^4.6.0, browserslist@^4.6.6, browserslist@^4.8.3: + version "4.8.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.8.5.tgz#691af4e327ac877b25e7a3f7ee869c4ef36cdea3" + integrity sha512-4LMHuicxkabIB+n9874jZX/az1IaZ5a+EUuvD7KFOu9x/Bd5YHyO0DIz2ls/Kl8g0ItS4X/ilEgf4T1Br0lgSg== dependencies: - caniuse-lite "^1.0.30000981" - electron-to-chromium "^1.3.188" - node-releases "^1.1.25" + caniuse-lite "^1.0.30001022" + electron-to-chromium "^1.3.338" + node-releases "^1.1.46" bser@^2.0.0: version "2.0.0" @@ -8565,11 +8654,6 @@ camelcase@^5.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== -camelcase@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.2.0.tgz#e7522abda5ed94cc0489e1b8466610e88404cf45" - integrity sha512-IXFsBS2pC+X0j0N/GE7Dm7j3bsEBp+oTpb7F50dwEVX7rf3IgwO9XatnegTsDtniKCUtEJH4fSU6Asw7uoVLfQ== - camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" @@ -8585,10 +8669,10 @@ can-use-dom@^0.1.0: resolved "https://registry.yarnpkg.com/can-use-dom/-/can-use-dom-0.1.0.tgz#22cc4a34a0abc43950f42c6411024a3f6366b45a" integrity sha1-IsxKNKCrxDlQ9CxkEQJKP2NmtFo= -caniuse-lite@^1.0.30000980, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30000984: - version "1.0.30001016" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001016.tgz#16ea48d7d6e8caf3cad3295c2d746fe38c4e7f66" - integrity sha512-yYQ2QfotceRiH4U+h1Us86WJXtVHDmy3nEKIdYPsZCYnOV5/tMgGbmoIlrMzmh2VXlproqYtVaKeGDBkMZifFA== +caniuse-lite@^1.0.30000984, caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001022: + version "1.0.30001022" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001022.tgz#9eeffe580c3a8f110b7b1742dcf06a395885e4c6" + integrity sha512-FjwPPtt/I07KyLPkBQ0g7/XuZg6oUkYBVnPHNj3VHJbOjmmJ/GdSo/GUY6MwINEQvjhP6WZVbX8Tvms8xh0D5A== capture-exit@^2.0.0: version "2.0.0" @@ -8728,6 +8812,14 @@ chalk@^2.3.0: escape-string-regexp "^1.0.5" supports-color "^5.2.0" +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chalk@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f" @@ -8948,7 +9040,7 @@ chroma-js@^2.0.4: dependencies: cross-env "^6.0.3" -chrome-trace-event@^1.0.0, chrome-trace-event@^1.0.2: +chrome-trace-event@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ== @@ -9044,6 +9136,14 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== +clean-webpack-plugin@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/clean-webpack-plugin/-/clean-webpack-plugin-3.0.0.tgz#a99d8ec34c1c628a4541567aa7b457446460c62b" + integrity sha512-MciirUH5r+cYLGCOL5JX/ZLzOZbVr1ot3Fw+KcvbhUb6PM+yycqd9ZhIlcigQ5gl+XhppNmw3bEFuaaMNyLj3A== + dependencies: + "@types/webpack" "^4.4.31" + del "^4.1.1" + cli-boxes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" @@ -9349,12 +9449,19 @@ color-convert@^1.9.1: dependencies: color-name "1.1.3" +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@^1.0.0, color-name@^1.1.1: +color-name@^1.0.0, color-name@^1.1.1, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== @@ -9985,7 +10092,7 @@ cosmiconfig@^5.2.0: js-yaml "^3.13.1" parse-json "^4.0.0" -cp-file@^6.1.0, cp-file@^6.2.0: +cp-file@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/cp-file/-/cp-file-6.2.0.tgz#40d5ea4a1def2a9acdd07ba5c0b0246ef73dc10d" integrity sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA== @@ -9996,15 +10103,28 @@ cp-file@^6.1.0, cp-file@^6.2.0: pify "^4.0.1" safe-buffer "^5.0.1" -cpy@^7.3.0: - version "7.3.0" - resolved "https://registry.yarnpkg.com/cpy/-/cpy-7.3.0.tgz#62f2847986b4ff9d029710568a49e9a9ab5a210e" - integrity sha512-auvDu6h/J+cO1uqV40ymL/VoPM0+qPpNGaNttTzkYVXO/+GeynuyAK/MwFcWgU/P82ezcZw7RaN34CIIWajKLA== +cp-file@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cp-file/-/cp-file-7.0.0.tgz#b9454cfd07fe3b974ab9ea0e5f29655791a9b8cd" + integrity sha512-0Cbj7gyvFVApzpK/uhCtQ/9kE9UnYpxMzaq5nQQC/Dh4iaj5fxp7iEFIullrYwzj8nf0qnsI1Qsx34hAeAebvw== dependencies: - arrify "^1.0.1" - cp-file "^6.1.0" + graceful-fs "^4.1.2" + make-dir "^3.0.0" + nested-error-stacks "^2.0.0" + p-event "^4.1.0" + +cpy@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/cpy/-/cpy-8.0.0.tgz#8195db0db19a9ea6aa4f229784cbf3e3f53c3158" + integrity sha512-iTjLUqtVr45e17GFAyxA0lqFinbGMblMCTtAqrPzT/IETNtDuyyhDDk8weEZ08MiCc6EcuyNq2KtGH5J2BIAoQ== + dependencies: + arrify "^2.0.1" + cp-file "^7.0.0" globby "^9.2.0" + is-glob "^4.0.1" + junk "^3.1.0" nested-error-stacks "^2.1.0" + p-all "^2.1.0" crc32-stream@^3.0.1: version "3.0.1" @@ -10282,40 +10402,23 @@ css-in-js-utils@^2.0.0: hyphenate-style-name "^1.0.2" isobject "^3.0.1" -css-loader@2.1.1, css-loader@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-2.1.1.tgz#d8254f72e412bb2238bb44dd674ffbef497333ea" - integrity sha512-OcKJU/lt232vl1P9EEDamhoO9iKY3tIjY5GU+XDLblAykTdgs6Ux9P1hTHve8nFKy5KPpOXOsVI/hIwi3841+w== - dependencies: - camelcase "^5.2.0" - icss-utils "^4.1.0" - loader-utils "^1.2.3" - normalize-path "^3.0.0" - postcss "^7.0.14" - postcss-modules-extract-imports "^2.0.0" - postcss-modules-local-by-default "^2.0.6" - postcss-modules-scope "^2.1.0" - postcss-modules-values "^2.0.0" - postcss-value-parser "^3.3.0" - schema-utils "^1.0.0" - -css-loader@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.2.0.tgz#bb570d89c194f763627fcf1f80059c6832d009b2" - integrity sha512-QTF3Ud5H7DaZotgdcJjGMvyDj5F3Pn1j/sC6VBEOVp94cbwqyIBdcs/quzj4MC1BKQSrTpQznegH/5giYbhnCQ== +css-loader@^3.0.0, css-loader@^3.4.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.4.2.tgz#d3fdb3358b43f233b78501c5ed7b1c6da6133202" + integrity sha512-jYq4zdZT0oS0Iykt+fqnzVLRIeiPWhka+7BqPn+oSIpWJAHak5tmB/WZrJ2a21JhCeFyNnnlroSl8c+MtVndzA== dependencies: camelcase "^5.3.1" cssesc "^3.0.0" icss-utils "^4.1.1" loader-utils "^1.2.3" normalize-path "^3.0.0" - postcss "^7.0.17" + postcss "^7.0.23" postcss-modules-extract-imports "^2.0.0" postcss-modules-local-by-default "^3.0.2" - postcss-modules-scope "^2.1.0" + postcss-modules-scope "^2.1.1" postcss-modules-values "^3.0.0" - postcss-value-parser "^4.0.0" - schema-utils "^2.0.0" + postcss-value-parser "^4.0.2" + schema-utils "^2.6.0" css-select-base-adapter@^0.1.1: version "0.1.1" @@ -11448,6 +11551,11 @@ diff-sequences@^24.9.0: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== +diff-sequences@^25.1.0: + version "25.1.0" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.1.0.tgz#fd29a46f1c913fd66c22645dc75bffbe43051f32" + integrity sha512-nFIfVk5B/NStCsJ+zaPO4vYuLjlzQ6uFvPxzYyHlejNZ/UGa7G/n7peOXVrVNvRuyfstt+mZQYGpjxg9Z6N8Kw== + diff@3.5.0, diff@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -11918,15 +12026,10 @@ elasticsearch@^16.4.0, elasticsearch@^16.5.0: chalk "^1.0.0" lodash "^4.17.10" -electron-to-chromium@^1.3.188: - version "1.3.190" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.190.tgz#5bf599519983bfffd9d4387817039a3ed7ca085f" - integrity sha512-cs9WnTnGBGnYYVFMCtLmr9jXNTOkdp95RLz5VhwzDn7dErg1Lnt9o4d01gEH69XlmRKWUr91Yu1hA+Hi8qW0PA== - -electron-to-chromium@^1.3.191: - version "1.3.246" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.246.tgz#38c30a380398b293f39a19d4346f18e2cb376b72" - integrity sha512-CzR7VM16UmZQVgd5I5qu/rx0e67l6FF17rpJD2kRFX9n1ygHFIS+TV9DO55MSZKBGVuQ0Ph1JLLTFEReCKU6nQ== +electron-to-chromium@^1.3.191, electron-to-chromium@^1.3.338: + version "1.3.340" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.340.tgz#5d4fe78e984d4211194cf5a52e08069543da146f" + integrity sha512-hRFBAglhcj5iVYH+o8QU0+XId1WGoc0VGowJB1cuJAt3exHGrivZvWeAO5BRgBZqwZtwxjm8a5MQeGoT/Su3ww== elegant-spinner@^1.0.1: version "1.0.1" @@ -12641,7 +12744,7 @@ eslint-rule-composer@^0.3.0: resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9" integrity sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg== -eslint-scope@^4.0.0, eslint-scope@^4.0.3: +eslint-scope@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== @@ -13730,16 +13833,7 @@ finalhandler@~1.1.2: statuses "~1.5.0" unpipe "~1.0.0" -find-cache-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.0.0.tgz#4c1faed59f45184530fb9d7fa123a4d04a98472d" - integrity sha512-LDUY6V1Xs5eFskUVYtIwatojt6+9xC9Chnlk/jYOOvn3FAFfSaWddxahDGyNHh0b2dMXa6YW2m0tk8TdVaXHlA== - dependencies: - commondir "^1.0.1" - make-dir "^1.0.0" - pkg-dir "^3.0.0" - -find-cache-dir@^2.1.0: +find-cache-dir@^2.0.0, find-cache-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== @@ -16349,11 +16443,6 @@ iconv-lite@^0.5.0: dependencies: safer-buffer ">= 2.1.2 < 3" -icss-replace-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" - integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0= - icss-utils@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.0.0.tgz#d52cf4bcdcfa1c45c2dbefb4ffdf6b00ef608098" @@ -16361,13 +16450,6 @@ icss-utils@^4.0.0: dependencies: postcss "^7.0.5" -icss-utils@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.0.tgz#339dbbffb9f8729a243b701e1c29d4cc58c52f0e" - integrity sha512-3DEun4VOeMvSczifM3F2cKQrDQ5Pj6WKhkOq6HD4QTnDUAq8MQRxy5TX6Sy1iY6WPBe4gQ3p5vTECjbIkglkkQ== - dependencies: - postcss "^7.0.14" - icss-utils@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" @@ -16813,16 +16895,11 @@ internal-ip@^4.3.0: default-gateway "^4.2.0" ipaddr.js "^1.9.0" -interpret@1.2.0, interpret@^1.1.0, interpret@^1.2.0: +interpret@1.2.0, interpret@^1.0.0, interpret@^1.1.0, interpret@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== -interpret@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" - integrity sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ= - intl-format-cache@^2.0.5, intl-format-cache@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/intl-format-cache/-/intl-format-cache-2.1.0.tgz#04a369fecbfad6da6005bae1f14333332dcf9316" @@ -17384,9 +17461,9 @@ is-path-inside@^2.1.0: path-is-inside "^1.0.2" is-path-inside@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.1.tgz#7417049ed551d053ab82bba3fdd6baa6b3a81e89" - integrity sha512-CKstxrctq1kUesU6WhtZDbYKzzYBuRH0UYInAVrkc/EYdB9ltbfE0gOoayG9nhohG6447sOOVGhHqsdmBvkbNg== + version "3.0.2" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" + integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: version "1.1.0" @@ -17664,7 +17741,7 @@ isstream@0.1.x, isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= -istanbul-instrumenter-loader@3.0.1: +istanbul-instrumenter-loader@3.0.1, istanbul-instrumenter-loader@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/istanbul-instrumenter-loader/-/istanbul-instrumenter-loader-3.0.1.tgz#9957bd59252b373fae5c52b7b5188e6fde2a0949" integrity sha512-a5SPObZgS0jB/ixaKSMdn6n/gXSrK2S6q/UfRJBT3e6gQmVjwZROTODQsYW5ZNwOu78hG62Y3fWlebaVOL0C+w== @@ -17900,6 +17977,16 @@ jest-diff@^24.0.0: jest-get-type "^24.0.0" pretty-format "^24.0.0" +jest-diff@^25.1.0: + version "25.1.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.1.0.tgz#58b827e63edea1bc80c1de952b80cec9ac50e1ad" + integrity sha512-nepXgajT+h017APJTreSieh4zCqnSHEJ1iT8HDlewu630lSJ4Kjjr9KNzm+kzGwwcpsDE6Snx1GJGzzsefaEHw== + dependencies: + chalk "^3.0.0" + diff-sequences "^25.1.0" + jest-get-type "^25.1.0" + pretty-format "^25.1.0" + jest-docblock@^24.3.0: version "24.3.0" resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.3.0.tgz#b9c32dac70f72e4464520d2ba4aec02ab14db5dd" @@ -17951,6 +18038,11 @@ jest-get-type@^24.9.0: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e" integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q== +jest-get-type@^25.1.0: + version "25.1.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-25.1.0.tgz#1cfe5fc34f148dc3a8a3b7275f6b9ce9e2e8a876" + integrity sha512-yWkBnT+5tMr8ANB6V+OjmrIJufHtCAqI5ic2H40v+tRqxDmE0PGnIiTyvRWFOMtmVHYpwRqyazDbTnhpjsGvLw== + jest-haste-map@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.9.0.tgz#b38a5d64274934e21fa417ae9a9fbeb77ceaac7d" @@ -18487,11 +18579,6 @@ json3@^3.3.2: resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" integrity sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE= -json5@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" - integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= - json5@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" @@ -18663,6 +18750,11 @@ jszip@^3.1.5: readable-stream "~2.3.6" set-immediate-shim "~1.0.1" +junk@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1" + integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ== + just-curry-it@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/just-curry-it/-/just-curry-it-3.1.0.tgz#ab59daed308a58b847ada166edd0a2d40766fbc5" @@ -19329,12 +19421,12 @@ load-source-map@^1.0.0: semver "^5.3.0" source-map "^0.5.6" -loader-runner@^2.3.0, loader-runner@^2.3.1, loader-runner@^2.4.0: +loader-runner@^2.3.1, loader-runner@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== -loader-utils@1.2.3, loader-utils@^1.0.4, loader-utils@^1.2.3: +loader-utils@1.2.3, loader-utils@^1.0.0, loader-utils@^1.0.2, loader-utils@^1.0.4, loader-utils@^1.1.0, loader-utils@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== @@ -19343,15 +19435,6 @@ loader-utils@1.2.3, loader-utils@^1.0.4, loader-utils@^1.2.3: emojis-list "^2.0.0" json5 "^1.0.1" -loader-utils@^1.0.0, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" - integrity sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0= - dependencies: - big.js "^3.1.3" - emojis-list "^2.0.0" - json5 "^0.5.0" - locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -20239,10 +20322,10 @@ memoizerific@^1.11.3: memory-fs@^0.2.0: version "0.2.0" - resolved "https://registry.npmjs.org/memory-fs/-/memory-fs-0.2.0.tgz#f2bb25368bc121e391c2520de92969caee0a0290" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.2.0.tgz#f2bb25368bc121e391c2520de92969caee0a0290" integrity sha1-8rslNovBIeORwlIN6Slpyu4KApA= -memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1: +memory-fs@^0.4.0, memory-fs@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= @@ -20346,7 +20429,7 @@ microevent.ts@~0.1.1: resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0" integrity sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g== -micromatch@3.1.10, micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8: +micromatch@3.1.10, micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== @@ -20983,16 +21066,11 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nan@^2.13.2: +nan@^2.13.2, nan@^2.9.2: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== -nan@^2.9.2: - version "2.10.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" - integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA== - nano-css@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/nano-css/-/nano-css-5.2.1.tgz#73b8470fa40b028a134d3393ae36bbb34b9fa332" @@ -21323,7 +21401,7 @@ node-jose@1.1.0: util "^0.11.0" vm-browserify "0.0.4" -node-libs-browser@^2.0.0, node-libs-browser@^2.2.1: +node-libs-browser@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== @@ -21384,14 +21462,14 @@ node-pre-gyp@^0.10.0: semver "^5.3.0" tar "^4" -node-releases@^1.1.25: - version "1.1.25" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.25.tgz#0c2d7dbc7fed30fbe02a9ee3007b8c90bf0133d3" - integrity sha512-fI5BXuk83lKEoZDdH3gRhtsNgh05/wZacuXkgbiYkceE7+QIMXOg98n9ZV7mz27B+kFHnqHcUpscZZlGRSmTpQ== +node-releases@^1.1.25, node-releases@^1.1.46: + version "1.1.47" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.47.tgz#c59ef739a1fd7ecbd9f0b7cf5b7871e8a8b591e4" + integrity sha512-k4xjVPx5FpwBUj0Gw7uvFOTF4Ep8Hok1I6qjwL3pLfwe7Y0REQSAqOwwv9TWBCUtMHxcXfY4PgRLRozcChvTcA== dependencies: - semver "^5.3.0" + semver "^6.3.0" -node-sass@^4.13.1: +node-sass@^4.13.0, node-sass@^4.13.1: version "4.13.1" resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.13.1.tgz#9db5689696bb2eec2c32b98bfea4c7a2e992d0a3" integrity sha512-TTWFx+ZhyDx1Biiez2nB0L3YrCZ/8oHagaDalbuBSlqXgUPsdkUSzJsVxeDO9LtPB49+Fh3WQl3slABo6AotNw== @@ -22188,6 +22266,13 @@ output-file-sync@^2.0.0: is-plain-obj "^1.1.0" mkdirp "^0.5.1" +p-all@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-all/-/p-all-2.1.0.tgz#91419be56b7dee8fe4c5db875d55e0da084244a0" + integrity sha512-HbZxz5FONzz/z2gJfk6bFca0BCiSRF8jU3yCsWOen/vR6lZjfPOu/e7L3uFzTW1i0H8TlC3vqQstEJPQL4/uLA== + dependencies: + p-map "^2.0.0" + p-any@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-any/-/p-any-1.1.0.tgz#1d03835c7eed1e34b8e539c47b7b60d0d015d4e1" @@ -22217,6 +22302,13 @@ p-each-series@^1.0.0: dependencies: p-reduce "^1.0.0" +p-event@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-event/-/p-event-4.1.0.tgz#e92bb866d7e8e5b732293b1c8269d38e9982bf8e" + integrity sha512-4vAd06GCsgflX4wHN1JqrMzBh/8QZ4j+rzp0cd2scXRwuBEv+QR3wrVA5aLhWDLw4y2WgDKvzWF3CCLmVM1UgA== + dependencies: + p-timeout "^2.0.1" + p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" @@ -23086,7 +23178,7 @@ postcss-load-config@^2.0.0: cosmiconfig "^4.0.0" import-cwd "^2.0.0" -postcss-loader@3.0.0, postcss-loader@^3.0.0: +postcss-loader@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-3.0.0.tgz#6b97943e47c72d845fa9e03f273773d4e8dd6c2d" integrity sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA== @@ -23103,15 +23195,6 @@ postcss-modules-extract-imports@^2.0.0: dependencies: postcss "^7.0.5" -postcss-modules-local-by-default@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-2.0.6.tgz#dd9953f6dd476b5fd1ef2d8830c8929760b56e63" - integrity sha512-oLUV5YNkeIBa0yQl7EYnxMgy4N6noxmiwZStaEJUSe2xPMcdNc8WmBQuQCx18H5psYbVxz8zoHk0RAAYZXP9gA== - dependencies: - postcss "^7.0.6" - postcss-selector-parser "^6.0.0" - postcss-value-parser "^3.3.1" - postcss-modules-local-by-default@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz#e8a6561be914aaf3c052876377524ca90dbb7915" @@ -23122,22 +23205,14 @@ postcss-modules-local-by-default@^3.0.2: postcss-selector-parser "^6.0.2" postcss-value-parser "^4.0.0" -postcss-modules-scope@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.1.0.tgz#ad3f5bf7856114f6fcab901b0502e2a2bc39d4eb" - integrity sha512-91Rjps0JnmtUB0cujlc8KIKCsJXWjzuxGeT/+Q2i2HXKZ7nBUeF9YQTZZTNvHVoNYj1AthsjnGLtqDUE0Op79A== +postcss-modules-scope@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.1.1.tgz#33d4fc946602eb5e9355c4165d68a10727689dba" + integrity sha512-OXRUPecnHCg8b9xWvldG/jUpRIGPNRka0r4D4j0ESUU2/5IOnpsjfPPmDprM3Ih8CgZ8FXjWqaniK5v4rWt3oQ== dependencies: postcss "^7.0.6" postcss-selector-parser "^6.0.0" -postcss-modules-values@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-2.0.0.tgz#479b46dc0c5ca3dc7fa5270851836b9ec7152f64" - integrity sha512-Ki7JZa7ff1N3EIMlPnGTZfUMe69FFwiQPnVSXC9mnn3jozCRBYIxiZd44yJOV2AmabOo4qFf8s0dC/+lweG7+w== - dependencies: - icss-replace-symbols "^1.1.0" - postcss "^7.0.6" - postcss-modules-values@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10" @@ -23173,7 +23248,7 @@ postcss-url@^8.0.0: postcss "^7.0.2" xxhashjs "^0.2.1" -postcss-value-parser@^3.3.0, postcss-value-parser@^3.3.1: +postcss-value-parser@^3.3.0: version "3.3.1" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== @@ -23197,19 +23272,10 @@ postcss-values-parser@^1.5.0: indexes-of "^1.0.1" uniq "^1.0.1" -postcss@^7.0.0, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.5, postcss@^7.0.6: - version "7.0.17" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.17.tgz#4da1bdff5322d4a0acaab4d87f3e782436bad31f" - integrity sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ== - dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.1.0" - -postcss@^7.0.16: - version "7.0.21" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.21.tgz#06bb07824c19c2021c5d056d5b10c35b989f7e17" - integrity sha512-uIFtJElxJo29QC753JzhidoAhvp/e/Exezkdhfmt8AymWT6/5B7W1WmponYWkHk2eg6sONyTch0A3nkMPun3SQ== +postcss@^7.0.0, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.2, postcss@^7.0.23, postcss@^7.0.26, postcss@^7.0.5, postcss@^7.0.6: + version "7.0.26" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.26.tgz#5ed615cfcab35ba9bbb82414a4fa88ea10429587" + integrity sha512-IY4oRjpXWYshuTDFxMVkJDtWIk2LhsTlu8bZnbEJA4+bYT16Lvpo8Qv6EvDumhYRgzjZl489pmsY3qVgJQ08nA== dependencies: chalk "^2.4.2" source-map "^0.6.1" @@ -23304,6 +23370,16 @@ pretty-format@^24.3.0, pretty-format@^24.9.0: ansi-styles "^3.2.0" react-is "^16.8.4" +pretty-format@^25.1.0: + version "25.1.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.1.0.tgz#ed869bdaec1356fc5ae45de045e2c8ec7b07b0c8" + integrity sha512-46zLRSGLd02Rp+Lhad9zzuNZ+swunitn8zIpfD2B4OPCRLXbM87RJT2aBLBWYOznNUML/2l/ReMyWNC80PJBUQ== + dependencies: + "@jest/types" "^25.1.0" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^16.12.0" + pretty-hrtime@^1.0.0, pretty-hrtime@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" @@ -23769,6 +23845,15 @@ qs@~6.4.0: resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" integrity sha1-E+JtKK1rD/qpExLNO/cI7TUecjM= +query-string@6.10.1: + version "6.10.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.10.1.tgz#30b3505f6fca741d5ae541964d1b3ae9dc2a0de8" + integrity sha512-SHTUV6gDlgMXg/AQUuLpTiBtW/etZ9JT6k6RCtCyqADquApLX0Aq5oK/s5UeTUAWBG50IExjIr587GqfXRfM4A== + dependencies: + decode-uri-component "^0.2.0" + split-on-first "^1.0.0" + strict-uri-encode "^2.0.0" + query-string@^4.1.0, query-string@^4.2.2: version "4.3.4" resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" @@ -23786,11 +23871,6 @@ query-string@^5.0.1: object-assign "^4.1.0" strict-uri-encode "^1.0.0" -querystring-browser@1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/querystring-browser/-/querystring-browser-1.0.4.tgz#f2e35881840a819bc7b1bf597faf0979e6622dc6" - integrity sha1-8uNYgYQKgZvHsb9Zf68JeeZiLcY= - querystring-es3@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -24347,7 +24427,7 @@ react-is@^16.10.2, react-is@^16.9.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.11.0.tgz#b85dfecd48ad1ce469ff558a882ca8e8313928fa" integrity sha512-gbBVYR2p8mnriqAwWx9LbuUrShnAuSCNnuPGyc7GJrMVQtPDAh8iLpv7FRuMPFb56KkaVZIYSz1PrjI9q0QPCw== -react-is@^16.3.1: +react-is@^16.12.0, react-is@^16.3.1: version "16.12.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q== @@ -25197,6 +25277,11 @@ redux-actions@2.6.5: reduce-reducers "^0.4.3" to-camel-case "^1.0.0" +redux-devtools-extension@^2.13.8: + version "2.13.8" + resolved "https://registry.yarnpkg.com/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz#37b982688626e5e4993ff87220c9bbb7cd2d96e1" + integrity sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg== + redux-observable@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/redux-observable/-/redux-observable-1.0.0.tgz#780ff2455493eedcef806616fe286b454fd15d91" @@ -26363,15 +26448,15 @@ sass-lint@^1.12.1: path-is-absolute "^1.0.0" util "^0.10.3" -sass-loader@^7.3.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.3.1.tgz#a5bf68a04bcea1c13ff842d747150f7ab7d0d23f" - integrity sha512-tuU7+zm0pTCynKYHpdqaPpe+MMTQ76I9TPZ7i4/5dZsigE350shQWe5EZNl5dBidM49TPET75tNqRbcsUZWeNA== +sass-loader@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-8.0.2.tgz#debecd8c3ce243c76454f2e8290482150380090d" + integrity sha512-7o4dbSK8/Ol2KflEmSco4jTjQoV988bM82P9CZdmo9hR3RLnvNc0ufMNdMrB0caq38JQ/FgF4/7RcbcfKzxoFQ== dependencies: clone-deep "^4.0.1" - loader-utils "^1.0.1" - neo-async "^2.5.0" - pify "^4.0.1" + loader-utils "^1.2.3" + neo-async "^2.6.1" + schema-utils "^2.6.1" semver "^6.3.0" sass-lookup@^3.0.0: @@ -26451,7 +26536,7 @@ schema-utils@^2.0.0, schema-utils@^2.0.1: ajv "^6.1.0" ajv-keywords "^3.1.0" -schema-utils@^2.4.1, schema-utils@^2.6.4: +schema-utils@^2.4.1, schema-utils@^2.5.0, schema-utils@^2.6.0, schema-utils@^2.6.1, schema-utils@^2.6.4: version "2.6.4" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.6.4.tgz#a27efbf6e4e78689d91872ee3ccfa57d7bdd0f53" integrity sha512-VNjcaUxVnEeun6B2fiiUDjXXBtD4ZSH7pdbfIu1pOFwgptDPLMo/z9jr4sUfsjFVPqDCEin/F7IYlq7/E6yDbQ== @@ -27387,6 +27472,11 @@ spdy@^4.0.1: select-hose "^2.0.0" spdy-transport "^3.0.0" +split-on-first@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" + integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== + split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" @@ -27700,6 +27790,11 @@ strict-uri-encode@^1.0.0: resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= +strict-uri-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" + integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY= + string-length@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/string-length/-/string-length-1.0.1.tgz#56970fb1c38558e9e70b728bf3de269ac45adfac" @@ -28012,7 +28107,7 @@ style-it@^2.1.3: dependencies: react-lib-adler32 "^1.0.3" -style-loader@0.23.1, style-loader@^0.23.1: +style-loader@^0.23.1: version "0.23.1" resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.23.1.tgz#cb9154606f3e771ab6c4ab637026a1049174d925" integrity sha512-XK+uv9kWwhZMZ1y7mysB+zoihsEj4wneFWAS5qoiLwzW0WzSqMrrsIy+a3zkQJq0ipFtBpX5W3MqyRIBF/WFGg== @@ -28020,6 +28115,14 @@ style-loader@0.23.1, style-loader@^0.23.1: loader-utils "^1.1.0" schema-utils "^1.0.0" +style-loader@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.1.3.tgz#9e826e69c683c4d9bf9db924f85e9abb30d5e200" + integrity sha512-rlkH7X/22yuwFYK357fMN/BxYOorfnfq0eD7+vqlemSK4wEcejFF1dg4zxP0euBW8NrYx2WZzZ8PPFevr7D+Kw== + dependencies: + loader-utils "^1.2.3" + schema-utils "^2.6.4" + styled-components@^3: version "3.4.10" resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-3.4.10.tgz#9a654c50ea2b516c36ade57ddcfa296bf85c96e1" @@ -28195,6 +28298,13 @@ supports-color@^7.0.0: dependencies: has-flag "^4.0.0" +supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + supports-hyperlinks@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-1.0.1.tgz#71daedf36cc1060ac5100c351bb3da48c29c0ef7" @@ -28510,36 +28620,50 @@ term-size@^1.2.0: dependencies: execa "^0.7.0" -terser-webpack-plugin@^1.1.0: - version "1.4.3" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz#5ecaf2dbdc5fb99745fd06791f46fc9ddb1c9a7c" - integrity sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA== +terser-webpack-plugin@^1.2.4: + version "1.4.1" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz#61b18e40eaee5be97e771cdbb10ed1280888c2b4" + integrity sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg== dependencies: cacache "^12.0.2" find-cache-dir "^2.1.0" is-wsl "^1.1.0" schema-utils "^1.0.0" - serialize-javascript "^2.1.2" + serialize-javascript "^1.7.0" source-map "^0.6.1" terser "^4.1.2" webpack-sources "^1.4.0" worker-farm "^1.7.0" -terser-webpack-plugin@^1.2.4, terser-webpack-plugin@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz#61b18e40eaee5be97e771cdbb10ed1280888c2b4" - integrity sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg== +terser-webpack-plugin@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz#5ecaf2dbdc5fb99745fd06791f46fc9ddb1c9a7c" + integrity sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA== dependencies: cacache "^12.0.2" find-cache-dir "^2.1.0" is-wsl "^1.1.0" schema-utils "^1.0.0" - serialize-javascript "^1.7.0" + serialize-javascript "^2.1.2" source-map "^0.6.1" terser "^4.1.2" webpack-sources "^1.4.0" worker-farm "^1.7.0" +terser-webpack-plugin@^2.1.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-2.3.2.tgz#6d3d1b0590c8f729bfbaeb7fb2528b8b62db4c74" + integrity sha512-SmvB/6gtEPv+CJ88MH5zDOsZdKXPS/Uzv2//e90+wM1IHFUhsguPKEILgzqrM1nQ4acRXN/SV4Obr55SXC+0oA== + dependencies: + cacache "^13.0.1" + find-cache-dir "^3.2.0" + jest-worker "^24.9.0" + schema-utils "^2.6.1" + serialize-javascript "^2.1.2" + source-map "^0.6.1" + terser "^4.4.3" + webpack-sources "^1.4.3" + terser-webpack-plugin@^2.3.4: version "2.3.4" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-2.3.4.tgz#ac045703bd8da0936ce910d8fb6350d0e1dee5fe" @@ -30183,6 +30307,15 @@ url-loader@2.2.0, url-loader@^2.0.1: mime "^2.4.4" schema-utils "^2.4.1" +url-loader@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-2.3.0.tgz#e0e2ef658f003efb8ca41b0f3ffbf76bab88658b" + integrity sha512-goSdg8VY+7nPZKUEChZSEtW5gjbS66USIGCeSJ1OVOJ7Yfuh/36YxCwMi5HVEJh6mqUYOoy3NJ0vlOMrWsSHog== + dependencies: + loader-utils "^1.2.3" + mime "^2.4.4" + schema-utils "^2.5.0" + url-parse-lax@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" @@ -30249,7 +30382,7 @@ user-home@^2.0.0: dependencies: os-homedir "^1.0.0" -useragent@2.3.0: +useragent@2.3.0, useragent@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.3.0.tgz#217f943ad540cb2128658ab23fc960f6a88c9972" integrity sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw== @@ -31092,7 +31225,7 @@ warning@^4.0.2, warning@^4.0.3: dependencies: loose-envify "^1.0.0" -watchpack@^1.5.0, watchpack@^1.6.0: +watchpack@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" integrity sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA== @@ -31132,10 +31265,10 @@ webidl-conversions@^4.0.1, webidl-conversions@^4.0.2: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== -webpack-cli@^3.3.9: - version "3.3.9" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.9.tgz#79c27e71f94b7fe324d594ab64a8e396b9daa91a" - integrity sha512-xwnSxWl8nZtBl/AFJCOn9pG7s5CYUYdZxmmukv+fAHLcBIHM36dImfpQg3WfShZXeArkWlf6QRw24Klcsv8a5A== +webpack-cli@^3.3.10: + version "3.3.10" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.10.tgz#17b279267e9b4fb549023fae170da8e6e766da13" + integrity sha512-u1dgND9+MXaEt74sJR4PR7qkPxXUSQ0RXYq8x1L6Jg1MYVEmGPrH6Ah6C4arD4r0J1P5HKjRqpab36k0eIzPqg== dependencies: chalk "2.4.2" cross-spawn "6.0.5" @@ -31237,7 +31370,7 @@ webpack-log@^2.0.0: ansi-colors "^3.0.0" uuid "^3.3.2" -webpack-merge@4.2.2: +webpack-merge@4.2.2, webpack-merge@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.2.tgz#a27c52ea783d1398afd2087f547d7b9d2f43634d" integrity sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g== @@ -31252,7 +31385,7 @@ webpack-sources@^1.1.0: source-list-map "^2.0.0" source-map "~0.6.1" -webpack-sources@^1.3.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: +webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== @@ -31260,40 +31393,10 @@ webpack-sources@^1.3.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack- source-list-map "^2.0.0" source-map "~0.6.1" -webpack@4.34.0: - version "4.34.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.34.0.tgz#a4c30129482f7b4ece4c0842002dedf2b56fab58" - integrity sha512-ry2IQy1wJjOefLe1uJLzn5tG/DdIKzQqNlIAd2L84kcaADqNvQDTBlo8UcCNyDaT5FiaB+16jhAkb63YeG3H8Q== - dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/helper-module-context" "1.8.5" - "@webassemblyjs/wasm-edit" "1.8.5" - "@webassemblyjs/wasm-parser" "1.8.5" - acorn "^6.0.5" - acorn-dynamic-import "^4.0.0" - ajv "^6.1.0" - ajv-keywords "^3.1.0" - chrome-trace-event "^1.0.0" - enhanced-resolve "^4.1.0" - eslint-scope "^4.0.0" - json-parse-better-errors "^1.0.2" - loader-runner "^2.3.0" - loader-utils "^1.1.0" - memory-fs "~0.4.1" - micromatch "^3.1.8" - mkdirp "~0.5.0" - neo-async "^2.5.0" - node-libs-browser "^2.0.0" - schema-utils "^1.0.0" - tapable "^1.1.0" - terser-webpack-plugin "^1.1.0" - watchpack "^1.5.0" - webpack-sources "^1.3.0" - -webpack@4.41.0, webpack@^4.33.0, webpack@^4.38.0, webpack@^4.41.0: - version "4.41.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.41.0.tgz#db6a254bde671769f7c14e90a1a55e73602fc70b" - integrity sha512-yNV98U4r7wX1VJAj5kyMsu36T8RPPQntcb5fJLOsMz/pt/WrKC0Vp1bAlqPLkA1LegSwQwf6P+kAbyhRKVQ72g== +webpack@^4.33.0, webpack@^4.38.0, webpack@^4.41.5: + version "4.41.5" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.41.5.tgz#3210f1886bce5310e62bb97204d18c263341b77c" + integrity sha512-wp0Co4vpyumnp3KlkmpM5LWuzvZYayDwM2n17EHFr4qxBBbRokC7DJawPJC7TfSFZ9HZ6GsdH40EBj4UV0nmpw== dependencies: "@webassemblyjs/ast" "1.8.5" "@webassemblyjs/helper-module-context" "1.8.5" @@ -31315,7 +31418,7 @@ webpack@4.41.0, webpack@^4.33.0, webpack@^4.38.0, webpack@^4.41.0: node-libs-browser "^2.2.1" schema-utils "^1.0.0" tapable "^1.1.3" - terser-webpack-plugin "^1.4.1" + terser-webpack-plugin "^1.4.3" watchpack "^1.6.0" webpack-sources "^1.4.1"