diff --git a/.browserslistrc b/.browserslistrc index a788f9544ab8a2..89114f393c4624 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 abfe5e0a6cc270..9b00135df5bac7 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 7901bd331edff5..bf1e341c796fa0 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 c171b842254eeb..6874d02304e490 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 4e6f3141a12e7b..1b4350d5b91e90 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 69be6db72cff2c..33c1d535d7df32 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/accessibility.asciidoc b/docs/accessibility.asciidoc new file mode 100644 index 00000000000000..4869d35dab1568 --- /dev/null +++ b/docs/accessibility.asciidoc @@ -0,0 +1,65 @@ +[chapter] +[[accessibility]] += Accessibility Statement for Kibana +++++ +Accessibility +++++ + +Elastic is committed to ensuring digital accessibility for people with disabilities. We are continually improving the user experience, and strive toward ensuring our tools are usable by everyone. + +[float] +[[accessibility-measures]] +== Measures to support accessibility +Elastic takes the following measures to ensure accessibility of Kibana: + +* Maintains and incorporates an https://elastic.github.io/eui/[accessible component library]. +* Provides continual accessibility training for our staff. +* Employs a third-party audit. + +[float] +[[accessibility-conformance-status]] +== Conformance status +Kibana aims to meet https://www.w3.org/WAI/WCAG21/quickref/?currentsidebar=%23col_customize&levels=aaa&technologies=server%2Csmil%2Cflash%2Csl[WCAG 2.1 level AA] compliance. Currently, we can only claim to partially conform, meaning we do not fully meet all of the success criteria. However, we do try to take a broader view of accessibility, and go above and beyond the legal and regulatory standards to provide a good experience for all of our users. + +[float] +[[accessibility-feedback]] +== Feedback +We welcome your feedback on the accessibility of Kibana. Please let us know if you encounter accessibility barriers on Kibana by either emailing us at accessibility@elastic.co or opening https://github.com/elastic/kibana/issues/new?labels=Project%3AAccessibility&template=Accessibility.md&title=%28Accessibility%29[an issue on GitHub]. + +[float] +[[accessibility-specs]] +== Technical specifications +Accessibility of Kibana relies on the following technologies to work with your web browser and any assistive technologies or plugins installed on your computer: + +* HTML +* CSS +* JavaScript +* WAI-ARIA + +[float] +[[accessibility-limitations-and-alternatives]] +== Limitations and alternatives +Despite our best efforts to ensure accessibility of Kibana, there are some limitations. Please https://github.com/elastic/kibana/issues/new?labels=Project%3AAccessibility&template=Accessibility.md&title=%28Accessibility%29[open an issue on GitHub] if you observe an issue not in this list. + +Known limitations are in the following areas: + +* *Charts*: We have a clear plan for the first steps of making charts accessible. We’ve opened this https://github.com/elastic/elastic-charts/issues/300[Charts accessibility ticket on GitHub] for tracking our progress. +* *Maps*: Maps might pose difficulties to users with vision disabilities. We welcome your input on making our maps accessible. Go to the https://github.com/elastic/kibana/issues/57271[Maps accessibility ticket on GitHub] to join the discussion and view our plans. +* *Tables*: Although generally accessible and marked-up as standard HTML tables with column headers, tables rarely make use of row headers and have poor captions. You will see incremental improvements as various applications adopt a new accessible component. +* *Color contrast*: Modern Kibana interfaces generally do not have color contrast issues. However, older code might fall below the recommended contrast levels. As we continue to update our code, this issue will phase out naturally. + +To see individual tickets, view our https://github.com/elastic/kibana/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3AProject%3AAccessibility[GitHub issues with label "`Project:Accessibility`"]. + +[float] +[[accessibility-approach]] +== Assessment approach +Elastic assesses the accessibility of Kibana with the following approaches: + +* *Self-evaluation*: Our employees are familiar with accessibility standards and review new designs and implemented features to confirm that they are accessible. +* *External evaluation*: We engage external contractors to help us conduct an independent assessment and generate a formal VPAT. Please email accessibility@elastic.co if you’d like a copy. +* *Automated evaluation*: We are starting to run https://www.deque.com/axe/[axe] on every page. See our current progress in the https://github.com/elastic/kibana/issues/51456[automated testing GitHub issue]. + +Manual testing largely focuses on screen reader support and is done on: + +* VoiceOver on MacOS with Safari, Chrome and Edge +* NVDA on Windows with Chrome and Firefox diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc index c4611f3b41e552..c6174e1786c78f 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/developer/plugin/development-plugin-resources.asciidoc b/docs/developer/plugin/development-plugin-resources.asciidoc index 71c442aaf52e87..a2fd0e23d0be4a 100644 --- a/docs/developer/plugin/development-plugin-resources.asciidoc +++ b/docs/developer/plugin/development-plugin-resources.asciidoc @@ -66,3 +66,8 @@ To enable TypeScript support, create a `tsconfig.json` file at the root of your TypeScript code is automatically converted into JavaScript during development, but not in the distributable version of Kibana. If you use the {repo}blob/{branch}/packages/kbn-plugin-helpers[@kbn/plugin-helpers] to build your plugin, then your `.ts` and `.tsx` files will be permanently transpiled before your plugin is archived. If you have your own build process, make sure to run the TypeScript compiler on your source files and ship the compilation output so that your plugin will work with the distributable version of Kibana. + +==== {kib} platform migration guide + +{repo}blob/{branch}/src/core/MIGRATION.md#migrating-legacy-plugins-to-the-new-platform[This guide] +provides an action plan for moving a legacy plugin to the new platform. 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 00000000000000..d3ceeabcd81f41 --- /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 7eadd4d4e9d440..1ae368a11674f7 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 3ad7e3b1656d80..d5a0bef9470f78 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 6109671bb1aa66..49287cc6e261e0 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 5ea6b3ad0ca88a..00000000000000 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 00000000000000..d121ce561e341b 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 00000000000000..79daa1298883dd 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 00000000000000..71dd9b76b8cafd 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 00000000000000..f2094c3cbb3f1b 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 00000000000000..eca9735b495d0f 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 00000000000000..d4a0829fef64e7 Binary files /dev/null and b/docs/images/markdown_example_4.png differ diff --git a/docs/index.asciidoc b/docs/index.asciidoc index 491a9629e983e6..5474772ab7da81 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -22,6 +22,8 @@ include::{asciidoc-dir}/../../shared/attributes.asciidoc[] include::user/index.asciidoc[] +include::accessibility.asciidoc[] + include::limitations.asciidoc[] include::release-notes/highlights.asciidoc[] diff --git a/docs/infrastructure/images/infra-sysmon.png b/docs/infrastructure/images/infra-sysmon.png index 5b82d8c9b4e194..dd653bb046f451 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 9ad862ec6515d8..6001f18d283fe5 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 d9640e0d9f5da1..f96c30f0e18488 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 7ccf8891678afe..6d56491f7d4850 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 818cc766bf6a93..30a716641cc5d9 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 442460c67017cb..00000000000000 --- 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 214f33eef5c42b..00000000000000 --- 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 ac9ff7c9a5235e..74ce8d682e1cc8 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 f8a3acc9883e02..efa693a2ac5292 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 863e488e6c6c08..ffba6f972aeb77 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 2c4a65590aa1b4..176c71466aa385 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 5feb3d96089746..8e94c31c6862aa 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 f191f7d746cf8c..d84a9260521c7e 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/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 9caa3900fccfdb..ec626677d09025 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -8,6 +8,7 @@ for displayed decimal values. . Go to *Management > {kib} > Advanced Settings*. . Scroll or search for the setting you want to modify. . Enter a new value for the setting. +. Click *Save changes*. [float] @@ -34,7 +35,7 @@ removes it from {kib} permanently. [float] [[kibana-general-settings]] -=== General settings +==== General [horizontal] `csv:quoteValues`:: Set this property to `true` to quote exported values. @@ -109,7 +110,7 @@ cluster alert notifications from Monitoring. [float] [[kibana-accessibility-settings]] -=== Accessibility settings +==== Accessibility [horizontal] `accessibility:disableAnimations`:: Turns off all unnecessary animations in the @@ -117,14 +118,14 @@ cluster alert notifications from Monitoring. [float] [[kibana-dashboard-settings]] -=== Dashboard settings +==== Dashboard [horizontal] `xpackDashboardMode:roles`:: The roles that belong to <>. [float] [[kibana-discover-settings]] -=== Discover settings +==== Discover [horizontal] `context:defaultSize`:: The number of surrounding entries to display in the context view. The default value is 5. @@ -150,7 +151,7 @@ working on big documents. [float] [[kibana-notification-settings]] -=== Notifications settings +==== Notifications [horizontal] `notifications:banner`:: A custom banner intended for temporary notices to all users. @@ -169,7 +170,7 @@ displays. The default value is 10000. Set this field to `Infinity` to disable wa [float] [[kibana-reporting-settings]] -=== Reporting settings +==== Reporting [horizontal] `xpackReporting:customPdfLogo`:: A custom image to use in the footer of the PDF. @@ -177,7 +178,7 @@ displays. The default value is 10000. Set this field to `Infinity` to disable wa [float] [[kibana-rollups-settings]] -=== Rollup settings +==== Rollup [horizontal] `rollups:enableIndexPatterns`:: Enables the creation of index patterns that @@ -187,7 +188,7 @@ Refresh the page to apply the changes. [float] [[kibana-search-settings]] -=== Search settings +==== Search [horizontal] `courier:batchSearches`:: **Deprecated in 7.6. Starting in 8.0, this setting will be optimized internally.** @@ -215,21 +216,21 @@ might increase the search time. This setting is off by default. Users must opt-i [float] [[kibana-siem-settings]] -=== SIEM settings +==== SIEM [horizontal] `siem:defaultAnomalyScore`:: The threshold above which Machine Learning job anomalies are displayed in the SIEM app. `siem:defaultIndex`:: A comma-delimited list of Elasticsearch indices from which the SIEM app collects events. -`siem:enableNewsFeed`:: Enables the security news feed on the SIEM *Overview* +`siem:enableNewsFeed`:: Enables the security news feed on the SIEM *Overview* page. -`siem:newsFeedUrl`:: The URL from which the security news feed content is +`siem:newsFeedUrl`:: The URL from which the security news feed content is retrieved. `siem:refreshIntervalDefaults`:: The default refresh interval for the SIEM time filter, in milliseconds. `siem:timeDefaults`:: The default period of time in the SIEM time filter. [float] [[kibana-timelion-settings]] -=== Timelion settings +==== Timelion [horizontal] `timelion:default_columns`:: The default number of columns to use on a Timelion sheet. @@ -252,7 +253,7 @@ this is the number of buckets to try to represent. [float] [[kibana-visualization-settings]] -=== Visualization settings +==== Visualization [horizontal] `visualization:colorMapping`:: Maps values to specified colors in visualizations. @@ -273,7 +274,7 @@ If disabled, only visualizations that are considered production-ready are availa [float] [[kibana-telemetry-settings]] -=== Usage data settings +==== Usage data Helps improve the Elastic Stack by providing usage statistics for basic features. This data will not be shared outside of Elastic. diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 4eddb1779a26ab..c1f06aff722b52 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 5692fe6d1ae01d..1bcbd51a9629ae 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 a197998ecdc9de..d6e39d35b7b23f 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 2cb8aa7cb3c1fb..ba291e3cc6859a 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 c3762c2eabd28b..3156e87e763b23 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "@elastic/charts": "^17.0.2", "@elastic/datemath": "5.0.2", "@elastic/ems-client": "7.6.0", - "@elastic/eui": "18.3.0", + "@elastic/eui": "19.0.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "2.3.5", @@ -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 c7809f28fec7bd..ee06e2588b0222 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 e6a8bd81b602ea..d76a3e9714838d 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-config-schema/src/types/duration_type.test.ts b/packages/kbn-config-schema/src/types/duration_type.test.ts index 09e92ce727f2a2..57e917dc99b2b3 100644 --- a/packages/kbn-config-schema/src/types/duration_type.test.ts +++ b/packages/kbn-config-schema/src/types/duration_type.test.ts @@ -101,7 +101,7 @@ describe('#defaultValue', () => { source: duration({ defaultValue: 600 }), target: duration({ defaultValue: siblingRef('source') }), fromContext: duration({ defaultValue: contextRef('val') }), - }).validate(undefined, { val: momentDuration(700, 'ms') }) + }).validate({}, { val: momentDuration(700, 'ms') }) ).toMatchInlineSnapshot(` Object { "fromContext": "PT0.7S", @@ -115,7 +115,7 @@ Object { source: duration({ defaultValue: '1h' }), target: duration({ defaultValue: siblingRef('source') }), fromContext: duration({ defaultValue: contextRef('val') }), - }).validate(undefined, { val: momentDuration(2, 'hour') }) + }).validate({}, { val: momentDuration(2, 'hour') }) ).toMatchInlineSnapshot(` Object { "fromContext": "PT2H", @@ -129,7 +129,7 @@ Object { source: duration({ defaultValue: momentDuration(1, 'hour') }), target: duration({ defaultValue: siblingRef('source') }), fromContext: duration({ defaultValue: contextRef('val') }), - }).validate(undefined, { val: momentDuration(2, 'hour') }) + }).validate({}, { val: momentDuration(2, 'hour') }) ).toMatchInlineSnapshot(` Object { "fromContext": "PT2H", diff --git a/packages/kbn-config-schema/src/types/maybe_type.test.ts b/packages/kbn-config-schema/src/types/maybe_type.test.ts index ecc1d218e186d4..c35fa18593520a 100644 --- a/packages/kbn-config-schema/src/types/maybe_type.test.ts +++ b/packages/kbn-config-schema/src/types/maybe_type.test.ts @@ -60,3 +60,41 @@ test('includes namespace in failure', () => { const type = schema.maybe(schema.string()); expect(() => type.validate(null, {}, 'foo-namespace')).toThrowErrorMatchingSnapshot(); }); + +describe('maybe + object', () => { + test('returns undefined if undefined object', () => { + const type = schema.maybe(schema.object({})); + expect(type.validate(undefined)).toEqual(undefined); + }); + + test('returns undefined if undefined object with no defaults', () => { + const type = schema.maybe( + schema.object({ + type: schema.string(), + id: schema.string(), + }) + ); + + expect(type.validate(undefined)).toEqual(undefined); + }); + + test('returns empty object if maybe keys', () => { + const type = schema.object({ + name: schema.maybe(schema.string()), + }); + expect(type.validate({})).toEqual({}); + }); + + test('returns empty object if maybe nested object', () => { + const type = schema.object({ + name: schema.maybe( + schema.object({ + type: schema.string(), + id: schema.string(), + }) + ), + }); + + expect(type.validate({})).toEqual({}); + }); +}); diff --git a/packages/kbn-config-schema/src/types/maybe_type.ts b/packages/kbn-config-schema/src/types/maybe_type.ts index 06a93691102036..415f6315c57231 100644 --- a/packages/kbn-config-schema/src/types/maybe_type.ts +++ b/packages/kbn-config-schema/src/types/maybe_type.ts @@ -25,7 +25,7 @@ export class MaybeType extends Type { type .getSchema() .optional() - .default() + .default(() => undefined, 'undefined') ); } } diff --git a/packages/kbn-config-schema/src/types/object_type.test.ts b/packages/kbn-config-schema/src/types/object_type.test.ts index 5786984cf7ebdc..64739d7a4c4daa 100644 --- a/packages/kbn-config-schema/src/types/object_type.test.ts +++ b/packages/kbn-config-schema/src/types/object_type.test.ts @@ -30,6 +30,11 @@ test('returns value by default', () => { expect(type.validate(value)).toEqual({ name: 'test' }); }); +test('returns empty object if undefined', () => { + const type = schema.object({}); + expect(type.validate(undefined)).toEqual({}); +}); + test('properly parse the value if input is a string', () => { const type = schema.object({ name: schema.string(), @@ -112,14 +117,26 @@ test('undefined object within object', () => { }), }); + expect(type.validate(undefined)).toEqual({ + foo: { + bar: 'hello world', + }, + }); + expect(type.validate({})).toEqual({ foo: { bar: 'hello world', }, }); + + expect(type.validate({ foo: {} })).toEqual({ + foo: { + bar: 'hello world', + }, + }); }); -test('object within object with required', () => { +test('object within object with key without defaultValue', () => { const type = schema.object({ foo: schema.object({ bar: schema.string(), @@ -127,6 +144,9 @@ test('object within object with required', () => { }); const value = { foo: {} }; + expect(() => type.validate(undefined)).toThrowErrorMatchingInlineSnapshot( + `"[foo.bar]: expected value of type [string] but got [undefined]"` + ); expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot( `"[foo.bar]: expected value of type [string] but got [undefined]"` ); diff --git a/packages/kbn-config-schema/src/types/object_type.ts b/packages/kbn-config-schema/src/types/object_type.ts index d2e6c708c263ca..4f3d68a6bac97d 100644 --- a/packages/kbn-config-schema/src/types/object_type.ts +++ b/packages/kbn-config-schema/src/types/object_type.ts @@ -33,23 +33,23 @@ export type ObjectResultType

= Readonly<{ [K in keyof P]: TypeO export type ObjectTypeOptions

= TypeOptions< { [K in keyof P]: TypeOf } > & { + /** Should uknown keys not be defined in the schema be allowed. Defaults to `false` */ allowUnknowns?: boolean; }; export class ObjectType

extends Type> { private props: Record; - constructor(props: P, options: ObjectTypeOptions

= {}) { + constructor(props: P, { allowUnknowns = false, ...typeOptions }: ObjectTypeOptions

= {}) { const schemaKeys = {} as Record; for (const [key, value] of Object.entries(props)) { schemaKeys[key] = value.getSchema(); } - const { allowUnknowns, ...typeOptions } = options; const schema = internals .object() .keys(schemaKeys) - .optional() .default() + .optional() .unknown(Boolean(allowUnknowns)); super(schema, typeOptions); diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index 714ed56ac47033..305e29a0e41df6 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 661ed7329347fe..9edc63dd7d8423 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 1f5afac26d561d..f8009a255f0107 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 65b625de9f3083..b8c12433a0ebbd 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 e02c38494991a9..da0b799b338edc 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 9fae27011767e1..332f7e8a20cc2e 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 4faa1bc8e542f5..d2f0b0c3582845 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 00000000000000..c7f50c6af8dfde --- /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 00000000000000..ff657603f4c8d2 --- /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 ffb0e88c60a0da..aa55df9215c2f9 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 00000000000000..e8bb31f1e365da --- /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 00000000000000..20c8046daa65e5 --- /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 8b02f8ce4c5b0a..66fa55479f3b9e 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 45aa86a6ef64a7..091fae72ad635f 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 00000000000000..6e4e9c70a115c0 --- /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 00000000000000..12e580bbb76b37 --- /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 00000000000000..870e5a80452808 --- /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 00000000000000..256856181ccd84 --- /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 00000000000000..3064d6814e2b1b --- /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 00000000000000..9d3871df247397 --- /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 00000000000000..04a8c7e5b1eec2 --- /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 00000000000000..b9e044523a6a51 --- /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 00000000000000..12e580bbb76b37 --- /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 00000000000000..870e5a80452808 --- /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 00000000000000..dcb4dcd35698d0 --- /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 00000000000000..9d45217486ee89 --- /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 00000000000000..740f018d192981 --- /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 00000000000000..ec78a1bdf020ec --- /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 00000000000000..f1bc0965a46cc7 --- /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 00000000000000..f6118739045ba0 --- /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 00000000000000..1dbc7f1d1b6b0c --- /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 00000000000000..5f2e9d518bfa69 --- /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 00000000000000..60982abff2d870 --- /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 00000000000000..c1585f79ede6e9 --- /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 00000000000000..ea0560f1321535 --- /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 00000000000000..72be71e6bf7ec7 --- /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 00000000000000..1114f65bacb192 --- /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/packages/kbn-optimizer/src/common/ts_helpers.ts b/packages/kbn-optimizer/src/common/ts_helpers.ts new file mode 100644 index 00000000000000..8c0b857d212ac6 --- /dev/null +++ b/packages/kbn-optimizer/src/common/ts_helpers.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. + */ + +/** + * 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 00000000000000..c999260872d0f8 --- /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 00000000000000..d3c03f483d7e83 --- /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 00000000000000..48777f1d54aafd --- /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 00000000000000..706f79978beee4 --- /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 00000000000000..dda818875db23a --- /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 00000000000000..1bfd8d3fd073a2 --- /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 00000000000000..c02a857883a988 --- /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 00000000000000..1ee4e47bfd9ee0 --- /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 00000000000000..dd4d5c294dfc85 --- /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 00000000000000..001783b167c7a5 --- /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 00000000000000..55e8e1d3fd0842 --- /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 00000000000000..44234acd897dc5 --- /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 00000000000000..3529ffa587f16c --- /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 00000000000000..9d95d883d605c9 --- /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 00000000000000..7cd7bf15317e0e --- /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 00000000000000..04a6dfb3e36259 --- /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 00000000000000..0c03b029c0dc46 --- /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 a63a93f3a70d56..e1ecd3f1078ad5 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 2aa53db11c1d9d..9ac156cb5b8de2 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/packages/kbn-optimizer/src/optimizer/index.ts b/packages/kbn-optimizer/src/optimizer/index.ts new file mode 100644 index 00000000000000..b7f14cf3c517f2 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/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 * 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 00000000000000..e047b6d1e44cf4 --- /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 00000000000000..2165878e92ff4d --- /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 00000000000000..bfc853e5a6b750 --- /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 00000000000000..d67b9574167530 --- /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 00000000000000..a258e1010fce38 --- /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 00000000000000..c1e6572bd7e758 --- /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 00000000000000..e91b0d25fd72b9 --- /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 00000000000000..9149c483786fc5 --- /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 00000000000000..343f391921383d --- /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 00000000000000..e6cce8d306e35d --- /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 00000000000000..571bae86dee371 --- /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 00000000000000..7dcce8a0fae8d8 --- /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 00000000000000..cbec4c3f44c7d8 --- /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 4af545e982eaa0..6d6686a5bde1b9 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 00000000000000..1e87b8a5a7f7b5 --- /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 00000000000000..a11c85c64198e3 --- /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 00000000000000..e2994f4d024149 --- /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 00000000000000..3f82ebc9cdbae3 --- /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 129125c4583d52..51a404379fedb0 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 f3e401bedcef33..314bcf31e6d055 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; @@ -88442,7 +88563,7 @@ module.exports = function kindOf(val) { }; function ctorName(val) { - return val.constructor ? val.constructor.name : null; + return typeof val.constructor === 'function' ? val.constructor.name : null; } function isArray(val) { @@ -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; @@ -88653,7 +88774,7 @@ module.exports = function kindOf(val) { }; function ctorName(val) { - return val.constructor ? val.constructor.name : null; + return typeof val.constructor === 'function' ? val.constructor.name : null; } function isArray(val) { @@ -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; @@ -88844,7 +88965,7 @@ module.exports = function kindOf(val) { }; function ctorName(val) { - return val.constructor ? val.constructor.name : null; + return typeof val.constructor === 'function' ? val.constructor.name : null; } function isArray(val) { @@ -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; @@ -101921,7 +102042,7 @@ module.exports = function kindOf(val) { }; function ctorName(val) { - return val.constructor ? val.constructor.name : null; + return typeof val.constructor === 'function' ? val.constructor.name : null; } function isArray(val) { @@ -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; @@ -104780,7 +104901,7 @@ module.exports = function kindOf(val) { }; function ctorName(val) { - return val.constructor ? val.constructor.name : null; + return typeof val.constructor === 'function' ? val.constructor.name : null; } function isArray(val) { @@ -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('../../kibana/'); +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 f57365905292b8..444d46307b0593 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/__fixtures__/external_packages/with_kibana_link_deps/package.json b/packages/kbn-pm/src/production/__fixtures__/external_packages/with_kibana_link_deps/package.json index 6466095ed5df18..c0ed787bcb0e8d 100644 --- a/packages/kbn-pm/src/production/__fixtures__/external_packages/with_kibana_link_deps/package.json +++ b/packages/kbn-pm/src/production/__fixtures__/external_packages/with_kibana_link_deps/package.json @@ -2,6 +2,6 @@ "name": "quux", "version": "1.0.0", "dependencies": { - "@kbn/foo": "link:../../kibana/packages/foo" + "@kbn/foo": "link:../../packages/foo" } } diff --git a/packages/kbn-pm/src/production/prepare_project_dependencies.ts b/packages/kbn-pm/src/production/prepare_project_dependencies.ts index e02a2515cf4a4c..9817770166480d 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('../../kibana/'); +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 6948ae81806eb1..73deadba0a6198 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 d50f6a15c2e0b8..8645923a13d30d 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 4bb4c660a01aba..fc245ca3fe9217 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 fc9d159ea9b95d..4b4db9d7f37f37 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -11,13 +11,13 @@ "devDependencies": { "@elastic/charts": "^17.0.2", "abort-controller": "^3.0.0", - "@elastic/eui": "18.3.0", + "@elastic/eui": "19.0.0", "@kbn/dev-utils": "1.0.0", "@kbn/i18n": "1.0.0", "@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 1fbe83476d4a8b..642c4a98b57998 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 00000000000000..4d6963144d085f --- /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 bd37e854e1691c..707778861fb59f 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 3fa4bdcbc5fa5e..2f308915fb332b 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 00000000000000..af73059c0758e6 --- /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 00000000000000..7752d4a45ab655 --- /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 4f9337681e0835..e775f71442a771 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 fb87f1a87654c3..c73d3edbf7df70 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 06ee87e3198fd1..6f083bb2a1fa2a 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 9cf5691b88399e..be3fc319389d71 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 f8699364fa9e28..fa0edd8faadd7e 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 aac54a4a14680b..9abc2bb77d7d12 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 dee47315fc3222..d2a827d381be54 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 8757f73a1206ca..5487ca53170dd7 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -55,7 +55,7 @@ let service: ApplicationService; describe('#setup()', () => { beforeEach(() => { - const http = httpServiceMock.createSetupContract({ basePath: '/test' }); + const http = httpServiceMock.createSetupContract({ basePath: '/base-path' }); setupDeps = { http, context: contextServiceMock.createSetupContract(), @@ -167,7 +167,7 @@ describe('#setup()', () => { const { register } = service.setup(setupDeps); expect(() => - register(Symbol(), createApp({ id: 'app2', appRoute: '/test/app2' })) + register(Symbol(), createApp({ id: 'app2', appRoute: '/base-path/app2' })) ).toThrowErrorMatchingInlineSnapshot( `"Cannot register an application route that includes HTTP base path"` ); @@ -430,7 +430,7 @@ describe('#start()', () => { beforeEach(() => { MockHistory.push.mockReset(); - const http = httpServiceMock.createSetupContract({ basePath: '/test' }); + const http = httpServiceMock.createSetupContract({ basePath: '/base-path' }); setupDeps = { http, context: contextServiceMock.createSetupContract(), @@ -561,7 +561,7 @@ describe('#start()', () => { const { getUrlForApp } = await service.start(startDeps); - expect(getUrlForApp('app1')).toBe('/app/app1'); + expect(getUrlForApp('app1')).toBe('/base-path/app/app1'); }); it('creates URL for registered appId', async () => { @@ -573,20 +573,29 @@ describe('#start()', () => { const { getUrlForApp } = await service.start(startDeps); - expect(getUrlForApp('app1')).toBe('/app/app1'); - expect(getUrlForApp('legacyApp1')).toBe('/app/legacyApp1'); - expect(getUrlForApp('app2')).toBe('/custom/path'); + expect(getUrlForApp('app1')).toBe('/base-path/app/app1'); + expect(getUrlForApp('legacyApp1')).toBe('/base-path/app/legacyApp1'); + expect(getUrlForApp('app2')).toBe('/base-path/custom/path'); }); 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'); + 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'); + 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', { path: 'deep/link' })).toBe('/app/app1/deep/link'); - expect(getUrlForApp('app1', { path: '/deep//link/' })).toBe('/app/app1/deep/link'); - expect(getUrlForApp('app1', { path: '//deep/link//' })).toBe('/app/app1/deep/link'); - expect(getUrlForApp('app1', { path: 'deep/link///' })).toBe('/app/app1/deep/link'); + 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' + ); }); }); @@ -659,7 +668,7 @@ describe('#start()', () => { const { navigateToApp } = await service.start(startDeps); await navigateToApp('myTestApp'); - expect(setupDeps.redirectTo).toHaveBeenCalledWith('/test/app/myTestApp'); + expect(setupDeps.redirectTo).toHaveBeenCalledWith('/base-path/app/myTestApp'); }); it('updates currentApp$ after mounting', async () => { diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 511f348e118230..77f06e316c0aa8 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 } = {}) => - 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 17fdfc627187e9..977bb7a52da22d 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 a99b7607d71491..efd9fdd0536746 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 1043b50dff9584..b433acdb6dbb97 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 2156df57989742..74aed4049613e4 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 c38b9da4429438..5909572c7e545e 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 e4788e686dd45f..1b7e25f5855661 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 48100cba4f26e0..19cfadf70be1b8 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 e24be35331f394..e5cbffc3e2d943 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 776ed7d7c55705..63aba0dde2af81 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 aa7ca4fee675ed..f0289cc2b83550 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 3aa7f9e2aa8ad2..4fa51dcd5a0825 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 db363fcd4d7512..05a8f40a09a88f 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 276e3955a4678b..e418726465efa2 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 230a229b36888f..81d756f47d7609 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 e8e20580a36db3..46436461505c06 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 00000000000000..c1b7164908ed68 --- /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 1c6ab91a392799..44f02f0c90d4e5 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 00000000000000..dcb19020f769e8 --- /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 00000000000000..067fb204ca7f36 --- /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 d51058ca561c6b..0c1a7730f92a77 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 65fe64b0458018..3fbec7a45148da 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/__snapshots__/logging_service.test.ts.snap b/src/core/server/logging/__snapshots__/logging_service.test.ts.snap index 54c170f523299d..2add00457b2ed5 100644 --- a/src/core/server/logging/__snapshots__/logging_service.test.ts.snap +++ b/src/core/server/logging/__snapshots__/logging_service.test.ts.snap @@ -1,20 +1,20 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`appends records via multiple appenders.: console logs 1`] = `"[2012-02-01T00:00:00.000Z][INFO ][some-context] You know, just for your info."`; +exports[`appends records via multiple appenders.: console logs 1`] = `"[2012-01-31T23:33:22.011Z][INFO ][some-context] You know, just for your info."`; exports[`appends records via multiple appenders.: file logs 1`] = ` -"[2012-02-01T00:00:00.000Z][WARN ][tests] Config is not ready! +"[2012-01-31T23:33:22.011Z][WARN ][tests] Config is not ready! " `; exports[`appends records via multiple appenders.: file logs 2`] = ` -"[2012-02-01T00:00:00.000Z][ERROR][tests.child] Too bad that config is not ready :/ +"[2012-01-31T23:33:22.011Z][ERROR][tests.child] Too bad that config is not ready :/ " `; exports[`asLoggerFactory() only allows to create new loggers. 1`] = ` Object { - "@timestamp": "2012-02-01T00:00:00.000Z", + "@timestamp": "2012-01-31T18:33:22.011-05:00", "context": "test.context", "level": "TRACE", "message": "buffered trace message", @@ -24,7 +24,7 @@ Object { exports[`asLoggerFactory() only allows to create new loggers. 2`] = ` Object { - "@timestamp": "2012-02-01T00:00:00.000Z", + "@timestamp": "2012-01-31T13:33:22.011-05:00", "context": "test.context", "level": "INFO", "message": "buffered info message", @@ -37,7 +37,7 @@ Object { exports[`asLoggerFactory() only allows to create new loggers. 3`] = ` Object { - "@timestamp": "2012-02-01T00:00:00.000Z", + "@timestamp": "2012-01-31T08:33:22.011-05:00", "context": "test.context", "level": "FATAL", "message": "buffered fatal message", @@ -47,7 +47,7 @@ Object { exports[`flushes memory buffer logger and switches to real logger once config is provided: buffered messages 1`] = ` Object { - "@timestamp": "2012-02-01T00:00:00.000Z", + "@timestamp": "2012-02-01T09:33:22.011-05:00", "context": "test.context", "level": "INFO", "message": "buffered info message", @@ -60,7 +60,7 @@ Object { exports[`flushes memory buffer logger and switches to real logger once config is provided: new messages 1`] = ` Object { - "@timestamp": "2012-02-01T00:00:00.000Z", + "@timestamp": "2012-01-31T23:33:22.011-05:00", "context": "test.context", "level": "INFO", "message": "some new info message", @@ -71,7 +71,7 @@ Object { exports[`uses \`root\` logger if context is not specified. 1`] = ` Array [ Array [ - "[2012-02-01T00:00:00.000Z][INFO ][root] This message goes to a root context.", + "[2012-01-31T23:33:22.011Z][INFO ][root] This message goes to a root context.", ], ] `; @@ -86,7 +86,7 @@ Object { "message": "trace message", "meta": undefined, "pid": Any, - "timestamp": 2012-02-01T00:00:00.000Z, + "timestamp": 2012-02-01T14:33:22.011Z, } `; @@ -102,6 +102,6 @@ Object { "some": "value", }, "pid": Any, - "timestamp": 2012-02-01T00:00:00.000Z, + "timestamp": 2012-02-01T14:33:22.011Z, } `; diff --git a/src/core/server/logging/integration_tests/logging.test.ts b/src/core/server/logging/integration_tests/logging.test.ts index 7142f91300f124..b88f5ba2c2b60b 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 21cf4302c49dc3..14c071b40ad7af 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,13 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`\`format()\` correctly formats record. 1`] = `"{\\"@timestamp\\":\\"2012-02-01T00:00:00.000Z\\",\\"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. 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-01T00:00:00.000Z\\",\\"context\\":\\"context-2\\",\\"level\\":\\"ERROR\\",\\"message\\":\\"message-2\\",\\"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}"`; -exports[`\`format()\` correctly formats record. 3`] = `"{\\"@timestamp\\":\\"2012-02-01T00:00:00.000Z\\",\\"context\\":\\"context-3\\",\\"level\\":\\"WARN\\",\\"message\\":\\"message-3\\",\\"pid\\":5355}"`; +exports[`\`format()\` correctly formats record. 3`] = `"{\\"@timestamp\\":\\"2012-02-01T09:30:22.011-05:00\\",\\"context\\":\\"context-3\\",\\"level\\":\\"WARN\\",\\"message\\":\\"message-3\\",\\"pid\\":5355}"`; -exports[`\`format()\` correctly formats record. 4`] = `"{\\"@timestamp\\":\\"2012-02-01T00:00:00.000Z\\",\\"context\\":\\"context-4\\",\\"level\\":\\"DEBUG\\",\\"message\\":\\"message-4\\",\\"pid\\":5355}"`; +exports[`\`format()\` correctly formats record. 4`] = `"{\\"@timestamp\\":\\"2012-02-01T09:30:22.011-05:00\\",\\"context\\":\\"context-4\\",\\"level\\":\\"DEBUG\\",\\"message\\":\\"message-4\\",\\"pid\\":5355}"`; -exports[`\`format()\` correctly formats record. 5`] = `"{\\"@timestamp\\":\\"2012-02-01T00:00:00.000Z\\",\\"context\\":\\"context-5\\",\\"level\\":\\"INFO\\",\\"message\\":\\"message-5\\",\\"pid\\":5355}"`; +exports[`\`format()\` correctly formats record. 5`] = `"{\\"@timestamp\\":\\"2012-02-01T09:30:22.011-05:00\\",\\"context\\":\\"context-5\\",\\"level\\":\\"INFO\\",\\"message\\":\\"message-5\\",\\"pid\\":5355}"`; -exports[`\`format()\` correctly formats record. 6`] = `"{\\"@timestamp\\":\\"2012-02-01T00:00:00.000Z\\",\\"context\\":\\"context-6\\",\\"level\\":\\"TRACE\\",\\"message\\":\\"message-6\\",\\"pid\\":5355}"`; +exports[`\`format()\` correctly formats record. 6`] = `"{\\"@timestamp\\":\\"2012-02-01T09:30:22.011-05:00\\",\\"context\\":\\"context-6\\",\\"level\\":\\"TRACE\\",\\"message\\":\\"message-6\\",\\"pid\\":5355}"`; diff --git a/src/core/server/logging/layouts/__snapshots__/pattern_layout.test.ts.snap b/src/core/server/logging/layouts/__snapshots__/pattern_layout.test.ts.snap index 9ff4f7445d0433..1bf13204873a65 100644 --- a/src/core/server/logging/layouts/__snapshots__/pattern_layout.test.ts.snap +++ b/src/core/server/logging/layouts/__snapshots__/pattern_layout.test.ts.snap @@ -12,29 +12,29 @@ exports[`\`format()\` correctly formats record with custom pattern. 5`] = `"mock exports[`\`format()\` correctly formats record with custom pattern. 6`] = `"mock-message-6-context-6-message-6"`; -exports[`\`format()\` correctly formats record with full pattern. 1`] = `"[2012-02-01T00:00:00.000Z][FATAL][context-1] Some error stack"`; +exports[`\`format()\` correctly formats record with full pattern. 1`] = `"[2012-02-01T14:30:22.011Z][FATAL][context-1] Some error stack"`; -exports[`\`format()\` correctly formats record with full pattern. 2`] = `"[2012-02-01T00:00:00.000Z][ERROR][context-2] message-2"`; +exports[`\`format()\` correctly formats record with full pattern. 2`] = `"[2012-02-01T14:30:22.011Z][ERROR][context-2] message-2"`; -exports[`\`format()\` correctly formats record with full pattern. 3`] = `"[2012-02-01T00:00:00.000Z][WARN ][context-3] message-3"`; +exports[`\`format()\` correctly formats record with full pattern. 3`] = `"[2012-02-01T14:30:22.011Z][WARN ][context-3] message-3"`; -exports[`\`format()\` correctly formats record with full pattern. 4`] = `"[2012-02-01T00:00:00.000Z][DEBUG][context-4] message-4"`; +exports[`\`format()\` correctly formats record with full pattern. 4`] = `"[2012-02-01T14:30:22.011Z][DEBUG][context-4] message-4"`; -exports[`\`format()\` correctly formats record with full pattern. 5`] = `"[2012-02-01T00:00:00.000Z][INFO ][context-5] message-5"`; +exports[`\`format()\` correctly formats record with full pattern. 5`] = `"[2012-02-01T14:30:22.011Z][INFO ][context-5] message-5"`; -exports[`\`format()\` correctly formats record with full pattern. 6`] = `"[2012-02-01T00:00:00.000Z][TRACE][context-6] message-6"`; +exports[`\`format()\` correctly formats record with full pattern. 6`] = `"[2012-02-01T14:30:22.011Z][TRACE][context-6] message-6"`; -exports[`\`format()\` correctly formats record with highlighting. 1`] = `"[2012-02-01T00:00:00.000Z][FATAL][context-1] Some error stack"`; +exports[`\`format()\` correctly formats record with highlighting. 1`] = `"[2012-02-01T14:30:22.011Z][FATAL][context-1] Some error stack"`; -exports[`\`format()\` correctly formats record with highlighting. 2`] = `"[2012-02-01T00:00:00.000Z][ERROR][context-2] message-2"`; +exports[`\`format()\` correctly formats record with highlighting. 2`] = `"[2012-02-01T14:30:22.011Z][ERROR][context-2] message-2"`; -exports[`\`format()\` correctly formats record with highlighting. 3`] = `"[2012-02-01T00:00:00.000Z][WARN ][context-3] message-3"`; +exports[`\`format()\` correctly formats record with highlighting. 3`] = `"[2012-02-01T14:30:22.011Z][WARN ][context-3] message-3"`; -exports[`\`format()\` correctly formats record with highlighting. 4`] = `"[2012-02-01T00:00:00.000Z][DEBUG][context-4] message-4"`; +exports[`\`format()\` correctly formats record with highlighting. 4`] = `"[2012-02-01T14:30:22.011Z][DEBUG][context-4] message-4"`; -exports[`\`format()\` correctly formats record with highlighting. 5`] = `"[2012-02-01T00:00:00.000Z][INFO ][context-5] message-5"`; +exports[`\`format()\` correctly formats record with highlighting. 5`] = `"[2012-02-01T14:30:22.011Z][INFO ][context-5] message-5"`; -exports[`\`format()\` correctly formats record with highlighting. 6`] = `"[2012-02-01T00:00:00.000Z][TRACE][context-6] message-6"`; +exports[`\`format()\` correctly formats record with highlighting. 6`] = `"[2012-02-01T14:30:22.011Z][TRACE][context-6] message-6"`; exports[`allows specifying the PID in custom pattern 1`] = `"5355-context-1-Some error stack"`; diff --git a/src/core/server/logging/layouts/conversions/date.ts b/src/core/server/logging/layouts/conversions/date.ts new file mode 100644 index 00000000000000..d3ed54fb982402 --- /dev/null +++ b/src/core/server/logging/layouts/conversions/date.ts @@ -0,0 +1,91 @@ +/* + * 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-timezone'; +import { last } from 'lodash'; + +import { Conversion } from './type'; +import { LogRecord } from '../../log_record'; + +const dateRegExp = /%date({(?[^}]+)})?({(?[^}]+)})?/g; + +const formats = { + ISO8601: 'ISO8601', + ISO8601_TZ: 'ISO8601_TZ', + ABSOLUTE: 'ABSOLUTE', + UNIX: 'UNIX', + UNIX_MILLIS: 'UNIX_MILLIS', +}; + +function formatDate(date: Date, dateFormat: string = formats.ISO8601, timezone?: string): string { + const momentDate = moment(date); + if (timezone) { + momentDate.tz(timezone); + } + switch (dateFormat) { + case formats.ISO8601: + return momentDate.toISOString(); + case formats.ISO8601_TZ: + return momentDate.format('YYYY-MM-DDTHH:mm:ss.SSSZ'); + case formats.ABSOLUTE: + return momentDate.format('HH:mm:ss.SSS'); + case formats.UNIX: + return momentDate.format('X'); + case formats.UNIX_MILLIS: + return momentDate.format('x'); + default: + throw new Error(`Unknown format: ${dateFormat}`); + } +} + +function validateDateFormat(input: string) { + if (!Reflect.has(formats, input)) { + throw new Error( + `Date format expected one of ${Reflect.ownKeys(formats).join(', ')}, but given: ${input}` + ); + } +} + +function validateTimezone(timezone: string) { + if (moment.tz.zone(timezone)) return; + throw new Error(`Unknown timezone: ${timezone}`); +} + +function validate(rawString: string) { + for (const matched of rawString.matchAll(dateRegExp)) { + const { format, timezone } = matched.groups!; + + if (format) { + validateDateFormat(format); + } + if (timezone) { + validateTimezone(timezone); + } + } +} + +export const DateConversion: Conversion = { + pattern: dateRegExp, + convert(record: LogRecord, highlight: boolean, ...matched: any[]) { + const groups: Record = last(matched); + const { format, timezone } = groups; + + return formatDate(record.timestamp, format, timezone); + }, + validate, +}; 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 00000000000000..23e6aded6c6f7b --- /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/legacy/core_plugins/telemetry/common/get_xpack_config_with_deprecated.ts b/src/core/server/logging/layouts/conversions/level.ts similarity index 53% rename from src/legacy/core_plugins/telemetry/common/get_xpack_config_with_deprecated.ts rename to src/core/server/logging/layouts/conversions/level.ts index 3f7a8d3410993b..58b271140eff5b 100644 --- a/src/legacy/core_plugins/telemetry/common/get_xpack_config_with_deprecated.ts +++ b/src/core/server/logging/layouts/conversions/level.ts @@ -17,25 +17,28 @@ * under the License. */ -import { KibanaConfig } from 'src/legacy/server/kbn_server'; +import chalk from 'chalk'; -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 - } +import { Conversion } from './type'; +import { LogLevel } from '../../log_level'; +import { LogRecord } from '../../log_record'; - return config.get(configPath); -} +const LEVEL_COLORS = new Map([ + [LogLevel.Fatal, chalk.red], + [LogLevel.Error, chalk.red], + [LogLevel.Warn, chalk.yellow], + [LogLevel.Debug, chalk.green], + [LogLevel.Trace, chalk.blue], +]); + +export const LevelConversion: Conversion = { + 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)!; + message = color(message); + } + return message; + }, +}; diff --git a/src/core/server/logging/layouts/conversions/logger.ts b/src/core/server/logging/layouts/conversions/logger.ts new file mode 100644 index 00000000000000..debb1737ab95a3 --- /dev/null +++ b/src/core/server/logging/layouts/conversions/logger.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import chalk from 'chalk'; + +import { Conversion } from './type'; +import { LogRecord } from '../../log_record'; + +export const LoggerConversion: Conversion = { + pattern: /%logger/g, + convert(record: LogRecord, highlight: boolean) { + let message = record.context; + if (highlight) { + message = chalk.magenta(message); + } + return message; + }, +}; diff --git a/src/core/server/logging/layouts/conversions/message.ts b/src/core/server/logging/layouts/conversions/message.ts new file mode 100644 index 00000000000000..f8c5e68ada4fbf --- /dev/null +++ b/src/core/server/logging/layouts/conversions/message.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Conversion } from './type'; +import { LogRecord } from '../../log_record'; + +export const MessageConversion: Conversion = { + 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/legacy/core_plugins/telemetry/public/components/index.ts b/src/core/server/logging/layouts/conversions/meta.ts similarity index 76% rename from src/legacy/core_plugins/telemetry/public/components/index.ts rename to src/core/server/logging/layouts/conversions/meta.ts index 1fc55eadd1e103..ee8c207389fbe0 100644 --- a/src/legacy/core_plugins/telemetry/public/components/index.ts +++ b/src/core/server/logging/layouts/conversions/meta.ts @@ -16,9 +16,12 @@ * specific language governing permissions and limitations * under the License. */ +import { Conversion } from './type'; +import { LogRecord } from '../../log_record'; -// @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 const MetaConversion: Conversion = { + 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 new file mode 100644 index 00000000000000..37d34a4f1cf8b7 --- /dev/null +++ b/src/core/server/logging/layouts/conversions/pid.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. + */ + +import { Conversion } from './type'; +import { LogRecord } from '../../log_record'; + +export const PidConversion: Conversion = { + pattern: /%pid/g, + convert(record: LogRecord) { + return String(record.pid); + }, +}; diff --git a/src/legacy/core_plugins/inspector_views/index.js b/src/core/server/logging/layouts/conversions/type.ts similarity index 80% rename from src/legacy/core_plugins/inspector_views/index.js rename to src/core/server/logging/layouts/conversions/type.ts index a37b6bb3db426f..a57a1f954e53ae 100644 --- a/src/legacy/core_plugins/inspector_views/index.js +++ b/src/core/server/logging/layouts/conversions/type.ts @@ -16,13 +16,10 @@ * specific language governing permissions and limitations * under the License. */ +import { LogRecord } from 'kibana/server'; -import { resolve } from 'path'; - -export default function(kibana) { - return new kibana.Plugin({ - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }, - }); +export interface Conversion { + pattern: RegExp; + 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 2e4c5af80dd2e3..77e2876c143da0 100644 --- a/src/core/server/logging/layouts/json_layout.test.ts +++ b/src/core/server/logging/layouts/json_layout.test.ts @@ -21,6 +21,7 @@ import { LogLevel } from '../log_level'; import { LogRecord } from '../log_record'; import { JsonLayout } from './json_layout'; +const timestamp = new Date(Date.UTC(2012, 1, 1, 14, 30, 22, 11)); const records: LogRecord[] = [ { context: 'context-1', @@ -31,42 +32,42 @@ const records: LogRecord[] = [ }, level: LogLevel.Fatal, message: 'message-1', - timestamp: new Date(Date.UTC(2012, 1, 1)), + timestamp, pid: 5355, }, { context: 'context-2', level: LogLevel.Error, message: 'message-2', - timestamp: new Date(Date.UTC(2012, 1, 1)), + timestamp, pid: 5355, }, { context: 'context-3', level: LogLevel.Warn, message: 'message-3', - timestamp: new Date(Date.UTC(2012, 1, 1)), + timestamp, pid: 5355, }, { context: 'context-4', level: LogLevel.Debug, message: 'message-4', - timestamp: new Date(Date.UTC(2012, 1, 1)), + timestamp, pid: 5355, }, { context: 'context-5', level: LogLevel.Info, message: 'message-5', - timestamp: new Date(Date.UTC(2012, 1, 1)), + timestamp, pid: 5355, }, { context: 'context-6', level: LogLevel.Trace, message: 'message-6', - timestamp: new Date(Date.UTC(2012, 1, 1)), + timestamp, pid: 5355, }, ]; @@ -84,3 +85,73 @@ test('`format()` correctly formats record.', () => { expect(layout.format(record)).toMatchSnapshot(); } }); + +test('`format()` correctly formats record with meta-data', () => { + const layout = new JsonLayout(); + + expect( + 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( + 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/json_layout.ts b/src/core/server/logging/layouts/json_layout.ts index 8e90b2f7eb7828..ad8c33d7cb0231 100644 --- a/src/core/server/logging/layouts/json_layout.ts +++ b/src/core/server/logging/layouts/json_layout.ts @@ -17,6 +17,7 @@ * under the License. */ +import moment from 'moment-timezone'; import { schema, TypeOf } from '@kbn/config-schema'; import { LogRecord } from '../log_record'; @@ -52,7 +53,7 @@ export class JsonLayout implements Layout { public format(record: LogRecord): string { return JSON.stringify({ - '@timestamp': record.timestamp.toISOString(), + '@timestamp': moment(record.timestamp).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), context: record.context, error: JsonLayout.errorToSerializableObject(record.error), level: record.level.id.toUpperCase(), diff --git a/src/core/server/logging/layouts/layouts.test.ts b/src/core/server/logging/layouts/layouts.test.ts index aa1c54c846bc66..b1fb836f40d5d0 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 23656c5d20510d..cce55b147e0ed3 100644 --- a/src/core/server/logging/layouts/pattern_layout.test.ts +++ b/src/core/server/logging/layouts/pattern_layout.test.ts @@ -20,8 +20,9 @@ import { stripAnsiSnapshotSerializer } from '../../../test_helpers/strip_ansi_snapshot_serializer'; import { LogLevel } from '../log_level'; import { LogRecord } from '../log_record'; -import { PatternLayout } from './pattern_layout'; +import { PatternLayout, patternSchema } from './pattern_layout'; +const timestamp = new Date(Date.UTC(2012, 1, 1, 14, 30, 22, 11)); const records: LogRecord[] = [ { context: 'context-1', @@ -32,42 +33,42 @@ const records: LogRecord[] = [ }, level: LogLevel.Fatal, message: 'message-1', - timestamp: new Date(Date.UTC(2012, 1, 1)), + timestamp, pid: 5355, }, { context: 'context-2', level: LogLevel.Error, message: 'message-2', - timestamp: new Date(Date.UTC(2012, 1, 1)), + timestamp, pid: 5355, }, { context: 'context-3', level: LogLevel.Warn, message: 'message-3', - timestamp: new Date(Date.UTC(2012, 1, 1)), + timestamp, pid: 5355, }, { context: 'context-4', level: LogLevel.Debug, message: 'message-4', - timestamp: new Date(Date.UTC(2012, 1, 1)), + timestamp, pid: 5355, }, { context: 'context-5', level: LogLevel.Info, message: 'message-5', - timestamp: new Date(Date.UTC(2012, 1, 1)), + timestamp, pid: 5355, }, { context: 'context-6', level: LogLevel.Trace, message: 'message-6', - timestamp: new Date(Date.UTC(2012, 1, 1)), + timestamp, pid: 5355, }, ]; @@ -87,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' }; @@ -111,13 +112,52 @@ 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(); } }); +test('`format()` correctly formats record with meta data.', () => { + const layout = new PatternLayout(); + + expect( + layout.format({ + context: 'context-meta', + level: LogLevel.Debug, + message: 'message-meta', + timestamp, + pid: 5355, + meta: { + from: 'v7', + to: 'v8', + }, + }) + ).toBe('[2012-02-01T14:30:22.011Z][DEBUG][context-meta]{"from":"v7","to":"v8"} message-meta'); + + expect( + layout.format({ + context: 'context-meta', + level: LogLevel.Debug, + message: 'message-meta', + timestamp, + pid: 5355, + meta: {}, + }) + ).toBe('[2012-02-01T14:30:22.011Z][DEBUG][context-meta]{} message-meta'); + + expect( + layout.format({ + context: 'context-meta', + level: LogLevel.Debug, + message: 'message-meta', + timestamp, + pid: 5355, + }) + ).toBe('[2012-02-01T14:30:22.011Z][DEBUG][context-meta] message-meta'); +}); + test('`format()` correctly formats record with highlighting.', () => { const layout = new PatternLayout(undefined, true); @@ -127,9 +167,161 @@ 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(); } }); + +test('`format()` allows specifying pattern with meta.', () => { + const layout = new PatternLayout('%logger-%meta-%message'); + const record = { + context: 'context', + level: LogLevel.Debug, + message: 'message', + timestamp, + pid: 5355, + meta: { + from: 'v7', + to: 'v8', + }, + }; + expect(layout.format(record)).toBe('context-{"from":"v7","to":"v8"}-message'); +}); + +describe('format', () => { + describe('timestamp', () => { + const record = { + context: 'context', + level: LogLevel.Debug, + message: 'message', + timestamp, + pid: 5355, + }; + it('uses ISO8601 as default', () => { + const layout = new PatternLayout(); + + expect(layout.format(record)).toBe('[2012-02-01T14:30:22.011Z][DEBUG][context] message'); + }); + + describe('supports specifying a predefined format', () => { + it('ISO8601', () => { + 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('[%date{ISO8601_TZ}][%logger]'); + + expect(layout.format(record)).toBe('[2012-02-01T09:30:22.011-05:00][context]'); + }); + + it('ABSOLUTE', () => { + const layout = new PatternLayout('[%date{ABSOLUTE}][%logger]'); + + expect(layout.format(record)).toBe('[09:30:22.011][context]'); + }); + + it('UNIX', () => { + const layout = new PatternLayout('[%date{UNIX}][%logger]'); + + expect(layout.format(record)).toBe('[1328106622][context]'); + }); + + it('UNIX_MILLIS', () => { + const layout = new PatternLayout('[%date{UNIX_MILLIS}][%logger]'); + + expect(layout.format(record)).toBe('[1328106622011][context]'); + }); + }); + + describe('supports specifying a predefined format and timezone', () => { + it('ISO8601', () => { + 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('[%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('[%date{ABSOLUTE}{America/Los_Angeles}][%logger]'); + + expect(layout.format(record)).toBe('[06:30:22.011][context]'); + }); + + it('UNIX', () => { + const layout = new PatternLayout('[%date{UNIX}{America/Los_Angeles}][%logger]'); + + expect(layout.format(record)).toBe('[1328106622][context]'); + }); + + it('UNIX_MILLIS', () => { + 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( + '[%date{ABSOLUTE}{America/Los_Angeles}][%logger][%date{UNIX}]' + ); + + expect(layout.format(record)).toBe('[06:30:22.011][context][1328106622]'); + }); + }); +}); + +describe('schema', () => { + describe('pattern', () => { + 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 %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 %date with predefined date format', () => { + expect(patternSchema.validate('%date{ISO8601}')).toBe('%date{ISO8601}'); + }); + + 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 %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 %date with predefined date format and invalid timezone', () => { + expect(() => + patternSchema.validate('%date{ISO8601_TZ}{Europe/Kibana}') + ).toThrowErrorMatchingInlineSnapshot(`"Unknown timezone: Europe/Kibana"`); + }); + + it('validates several %date in pattern', () => { + expect(() => + 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 64424c02268ff5..9490db149cc0f2 100644 --- a/src/core/server/logging/layouts/pattern_layout.ts +++ b/src/core/server/logging/layouts/pattern_layout.ts @@ -18,55 +18,45 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import chalk from 'chalk'; -import { LogLevel } from '../log_level'; import { LogRecord } from '../log_record'; import { Layout } from './layouts'; - -/** - * A set of static constants describing supported parameters in the log message pattern. - */ -const Parameters = Object.freeze({ - Context: '{context}', - Level: '{level}', - Message: '{message}', - Timestamp: '{timestamp}', - Pid: '{pid}', -}); - -/** - * Regular expression used to parse log message pattern and fill in placeholders - * with the actual data. - */ -const PATTERN_REGEX = new RegExp( - `${Parameters.Timestamp}|${Parameters.Level}|${Parameters.Context}|${Parameters.Message}|${Parameters.Pid}`, - 'gi' -); - -/** - * Mapping between `LogLevel` and color that is used to highlight `level` part of - * the log message. - */ -const LEVEL_COLORS = new Map([ - [LogLevel.Fatal, chalk.red], - [LogLevel.Error, chalk.red], - [LogLevel.Warn, chalk.yellow], - [LogLevel.Debug, chalk.green], - [LogLevel.Trace, chalk.blue], -]); +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 = `[${Parameters.Timestamp}][${Parameters.Level}][${Parameters.Context}] ${Parameters.Message}`; +const DEFAULT_PATTERN = `[%date][%level][%logger]%meta %message`; + +export const patternSchema = schema.string({ + validate: string => { + DateConversion.validate!(string); + }, +}); const patternLayoutSchema = schema.object({ highlight: schema.maybe(schema.boolean()), kind: schema.literal('pattern'), - pattern: schema.maybe(schema.string()), + pattern: schema.maybe(patternSchema), }); +const conversions: Conversion[] = [ + LoggerConversion, + MessageConversion, + LevelConversion, + MetaConversion, + PidConversion, + DateConversion, +]; + /** @internal */ export type PatternLayoutConfigType = TypeOf; @@ -77,19 +67,6 @@ export type PatternLayoutConfigType = TypeOf; */ export class PatternLayout implements Layout { public static configSchema = patternLayoutSchema; - - private static highlightRecord(record: LogRecord, formattedRecord: Map) { - if (LEVEL_COLORS.has(record.level)) { - const color = LEVEL_COLORS.get(record.level)!; - formattedRecord.set(Parameters.Level, color(formattedRecord.get(Parameters.Level)!)); - } - - formattedRecord.set( - Parameters.Context, - chalk.magenta(formattedRecord.get(Parameters.Context)!) - ); - } - constructor(private readonly pattern = DEFAULT_PATTERN, private readonly highlight = false) {} /** @@ -97,20 +74,14 @@ export class PatternLayout implements Layout { * @param record Instance of `LogRecord` to format into string. */ public format(record: LogRecord): string { - // Error stack is much more useful than just the message. - const message = (record.error && record.error.stack) || record.message; - const formattedRecord = new Map([ - [Parameters.Timestamp, record.timestamp.toISOString()], - [Parameters.Level, record.level.id.toUpperCase().padEnd(5)], - [Parameters.Context, record.context], - [Parameters.Message, message], - [Parameters.Pid, String(record.pid)], - ]); - - if (this.highlight) { - PatternLayout.highlightRecord(record, formattedRecord); + let recordString = this.pattern; + for (const conversion of conversions) { + recordString = recordString.replace( + conversion.pattern, + conversion.convert.bind(null, record, this.highlight) + ); } - return this.pattern.replace(PATTERN_REGEX, match => formattedRecord.get(match)!); + return recordString; } } diff --git a/src/core/server/logging/logging_config.test.ts b/src/core/server/logging/logging_config.test.ts index b3631abb9ff002..75f571d34c25c5 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 f1fbf787737b4a..8f80be7d79cb12 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/logging/logging_service.test.ts b/src/core/server/logging/logging_service.test.ts index 51697fd15bebea..1e6c253c56c7b1 100644 --- a/src/core/server/logging/logging_service.test.ts +++ b/src/core/server/logging/logging_service.test.ts @@ -29,7 +29,7 @@ jest.mock('../../../legacy/server/logging/rotate', () => ({ setupLoggingRotate: jest.fn().mockImplementation(() => Promise.resolve({})), })); -const timestamp = new Date(Date.UTC(2012, 1, 1)); +const timestamp = new Date(Date.UTC(2012, 1, 1, 14, 33, 22, 11)); let mockConsoleLog: jest.SpyInstance; import { createWriteStream } from 'fs'; diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index 6768e85c8db17e..df618b2c0a706f 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 5a50cf8ea8ba2c..427cc19a8614fd 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 { // timezone in all tests. const moment = jest.requireActual('moment-timezone'); moment.tz.guess = () => 'America/New_York'; + moment.tz.setDefault('America/New_York'); return moment; }); diff --git a/src/legacy/core_plugins/console_legacy/index.ts b/src/legacy/core_plugins/console_legacy/index.ts index 65547e1ee54067..af080fd5ace9cc 100644 --- a/src/legacy/core_plugins/console_legacy/index.ts +++ b/src/legacy/core_plugins/console_legacy/index.ts @@ -19,7 +19,6 @@ import { first } from 'rxjs/operators'; import { head } from 'lodash'; -import { resolve } from 'path'; import url from 'url'; // TODO: Remove this hack once we can get the ES config we need for Console proxy a better way. @@ -40,7 +39,6 @@ export default function(kibana: any) { }, uiExports: { - styleSheetPaths: resolve(__dirname, 'public/styles/index.scss'), injectDefaultVars: () => ({ 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 714203de203851..4a609225e6d7f1 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 548e85746f866d..00000000000000 --- 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 c91500cd545d4b..428f0c305a375e 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/actions/select_range_action.ts b/src/legacy/core_plugins/data/public/actions/select_range_action.ts index 7e1135ca96f9e2..8d0b74be505352 100644 --- a/src/legacy/core_plugins/data/public/actions/select_range_action.ts +++ b/src/legacy/core_plugins/data/public/actions/select_range_action.ts @@ -26,12 +26,10 @@ import { // @ts-ignore import { onBrushEvent } from './filters/brush_event'; import { - esFilters, + Filter, FilterManager, TimefilterContract, - changeTimeFilter, - extractTimeFilter, - mapAndFlattenFilters, + esFilters, } from '../../../../../plugins/data/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getIndexPatterns } from '../../../../../plugins/data/public/services'; @@ -45,7 +43,7 @@ interface ActionContext { async function isCompatible(context: ActionContext) { try { - const filters: esFilters.Filter[] = (await onBrushEvent(context.data, getIndexPatterns)) || []; + const filters: Filter[] = (await onBrushEvent(context.data, getIndexPatterns)) || []; return filters.length > 0; } catch { return false; @@ -70,18 +68,18 @@ export function selectRangeAction( throw new IncompatibleActionError(); } - const filters: esFilters.Filter[] = (await onBrushEvent(data, getIndexPatterns)) || []; + const filters: Filter[] = (await onBrushEvent(data, getIndexPatterns)) || []; - const selectedFilters: esFilters.Filter[] = mapAndFlattenFilters(filters); + const selectedFilters: Filter[] = esFilters.mapAndFlattenFilters(filters); if (timeFieldName) { - const { timeRangeFilter, restOfFilters } = extractTimeFilter( + const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( timeFieldName, selectedFilters ); filterManager.addFilters(restOfFilters); if (timeRangeFilter) { - changeTimeFilter(timeFilter, timeRangeFilter); + esFilters.changeTimeFilter(timeFilter, timeRangeFilter); } } else { filterManager.addFilters(selectedFilters); diff --git a/src/legacy/core_plugins/data/public/actions/value_click_action.ts b/src/legacy/core_plugins/data/public/actions/value_click_action.ts index 1e474b8f9355cf..260b401e6d6581 100644 --- a/src/legacy/core_plugins/data/public/actions/value_click_action.ts +++ b/src/legacy/core_plugins/data/public/actions/value_click_action.ts @@ -31,12 +31,10 @@ import { applyFiltersPopover } from '../../../../../plugins/data/public/ui/apply // @ts-ignore import { createFiltersFromEvent } from './filters/create_filters_from_event'; import { - esFilters, + Filter, FilterManager, TimefilterContract, - changeTimeFilter, - extractTimeFilter, - mapAndFlattenFilters, + esFilters, } from '../../../../../plugins/data/public'; export const VALUE_CLICK_ACTION = 'VALUE_CLICK_ACTION'; @@ -48,7 +46,7 @@ interface ActionContext { async function isCompatible(context: ActionContext) { try { - const filters: esFilters.Filter[] = (await createFiltersFromEvent(context.data)) || []; + const filters: Filter[] = (await createFiltersFromEvent(context.data)) || []; return filters.length > 0; } catch { return false; @@ -73,9 +71,9 @@ export function valueClickAction( throw new IncompatibleActionError(); } - const filters: esFilters.Filter[] = (await createFiltersFromEvent(data)) || []; + const filters: Filter[] = (await createFiltersFromEvent(data)) || []; - let selectedFilters: esFilters.Filter[] = mapAndFlattenFilters(filters); + let selectedFilters: Filter[] = esFilters.mapAndFlattenFilters(filters); if (selectedFilters.length > 1) { const indexPatterns = await Promise.all( @@ -84,7 +82,7 @@ export function valueClickAction( }) ); - const filterSelectionPromise: Promise = new Promise(resolve => { + const filterSelectionPromise: Promise = new Promise(resolve => { const overlay = getOverlays().openModal( toMountPoint( applyFiltersPopover( @@ -94,7 +92,7 @@ export function valueClickAction( overlay.close(); resolve([]); }, - (filterSelection: esFilters.Filter[]) => { + (filterSelection: Filter[]) => { overlay.close(); resolve(filterSelection); } @@ -110,13 +108,13 @@ export function valueClickAction( } if (timeFieldName) { - const { timeRangeFilter, restOfFilters } = extractTimeFilter( + const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( timeFieldName, selectedFilters ); filterManager.addFilters(restOfFilters); if (timeRangeFilter) { - changeTimeFilter(timeFilter, timeRangeFilter); + esFilters.changeTimeFilter(timeFilter, timeRangeFilter); } } else { filterManager.addFilters(selectedFilters); diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts index c9c72a7be9a147..e095493c94c58c 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts @@ -20,10 +20,9 @@ import _ from 'lodash'; import { Subscription } from 'rxjs'; import { State } from 'ui/state_management/state'; -import { FilterManager, esFilters } from '../../../../../../plugins/data/public'; -import { compareFilters, COMPARE_ALL_OPTIONS } from '../../../../../../plugins/data/public'; +import { FilterManager, esFilters, Filter } from '../../../../../../plugins/data/public'; -type GetAppStateFunc = () => { filters?: esFilters.Filter[]; save?: () => void } | undefined | null; +type GetAppStateFunc = () => { filters?: Filter[]; save?: () => void } | undefined | null; /** * FilterStateManager is responsible for watching for filter changes @@ -68,15 +67,15 @@ export class FilterStateManager { const globalFilters = this.globalState.filters || []; const appFilters = (appState && appState.filters) || []; - const globalFilterChanged = !compareFilters( + const globalFilterChanged = !esFilters.compareFilters( this.filterManager.getGlobalFilters(), globalFilters, - COMPARE_ALL_OPTIONS + esFilters.COMPARE_ALL_OPTIONS ); - const appFilterChanged = !compareFilters( + const appFilterChanged = !esFilters.compareFilters( this.filterManager.getAppFilters(), appFilters, - COMPARE_ALL_OPTIONS + esFilters.COMPARE_ALL_OPTIONS ); const filterStateChanged = globalFilterChanged || appFilterChanged; diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/get_stub_filter.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/get_stub_filter.ts index 5238efe5efa59c..74eaad34fe160e 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/get_stub_filter.ts +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/get_stub_filter.ts @@ -17,15 +17,15 @@ * under the License. */ -import { esFilters } from '../../../../../../../plugins/data/public'; +import { Filter } from '../../../../../../../plugins/data/public'; export function getFilter( - store: esFilters.FilterStateStore, + store: any, // I don't want to export only for this, as it should move to data plugin disabled: boolean, negated: boolean, queryKey: string, queryValue: any -): esFilters.Filter { +): Filter { return { $state: { store, diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/stub_state.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/stub_state.ts index f0a4bdef0229d0..272c8a4e199134 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/stub_state.ts +++ b/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/stub_state.ts @@ -20,10 +20,10 @@ import sinon from 'sinon'; import { State } from 'ui/state_management/state'; -import { esFilters } from '../../../../../../../plugins/data/public'; +import { Filter } from '../../../../../../../plugins/data/public'; export class StubState implements State { - filters: esFilters.Filter[]; + filters: Filter[]; save: sinon.SinonSpy; constructor() { 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 22877e217279f9..00000000000000 --- 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/create_filter/date_histogram.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts index e212132257ef68..0d3f58c50a42e7 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts @@ -23,14 +23,14 @@ import { intervalOptions } from '../_interval_options'; import { AggConfigs } from '../../agg_configs'; import { IBucketDateHistogramAggConfig } from '../date_histogram'; import { BUCKET_TYPES } from '../bucket_agg_types'; -import { esFilters } from '../../../../../../../../plugins/data/public'; +import { RangeFilter } from '../../../../../../../../plugins/data/public'; jest.mock('ui/new_platform'); describe('AggConfig Filters', () => { describe('date_histogram', () => { let agg: IBucketDateHistogramAggConfig; - let filter: esFilters.RangeFilter; + let filter: RangeFilter; let bucketStart: any; let field: any; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_range.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_range.ts index f7f2cfdb7bb61b..7af8ebc3236a77 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_range.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/date_range.ts @@ -20,10 +20,10 @@ import moment from 'moment'; import { IBucketAggConfig } from '../_bucket_agg_type'; import { DateRangeKey } from '../date_range'; -import { esFilters } from '../../../../../../../../plugins/data/public'; +import { esFilters, RangeFilterParams } from '../../../../../../../../plugins/data/public'; export const createFilterDateRange = (agg: IBucketAggConfig, { from, to }: DateRangeKey) => { - const filter: esFilters.RangeFilterParams = {}; + const filter: RangeFilterParams = {}; if (from) filter.gte = moment(from).toISOString(); if (to) filter.lt = moment(to).toISOString(); if (to && from) filter.format = 'strict_date_optional_time'; diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/histogram.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/histogram.ts index 820f3de5ae9f0d..badd6dba6ea8a9 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/histogram.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/histogram.ts @@ -18,11 +18,11 @@ */ import { IBucketAggConfig } from '../_bucket_agg_type'; -import { esFilters } from '../../../../../../../../plugins/data/public'; +import { esFilters, RangeFilterParams } from '../../../../../../../../plugins/data/public'; export const createFilterHistogram = (aggConfig: IBucketAggConfig, key: string) => { const value = parseInt(key, 10); - const params: esFilters.RangeFilterParams = { gte: value, lt: value + aggConfig.params.interval }; + const params: RangeFilterParams = { gte: value, lt: value + aggConfig.params.interval }; return esFilters.buildRangeFilter( aggConfig.params.field, diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/ip_range.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/ip_range.ts index d78f4579cd713d..36be4143838240 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/ip_range.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/ip_range.ts @@ -20,10 +20,10 @@ import { CidrMask } from '../lib/cidr_mask'; import { IBucketAggConfig } from '../_bucket_agg_type'; import { IpRangeKey } from '../ip_range'; -import { esFilters } from '../../../../../../../../plugins/data/public'; +import { esFilters, RangeFilterParams } from '../../../../../../../../plugins/data/public'; export const createFilterIpRange = (aggConfig: IBucketAggConfig, key: IpRangeKey) => { - let range: esFilters.RangeFilterParams; + let range: RangeFilterParams; if (key.type === 'mask') { range = new CidrMask(key.mask).getRange(); diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts index d5fd1337f2cb20..7c6e769437ca1d 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts @@ -21,7 +21,7 @@ import { createFilterTerms } from './terms'; import { AggConfigs } from '../../agg_configs'; import { BUCKET_TYPES } from '../bucket_agg_types'; import { IBucketAggConfig } from '../_bucket_agg_type'; -import { esFilters } from '../../../../../../../../plugins/data/public'; +import { Filter, ExistsFilter } from '../../../../../../../../plugins/data/public'; jest.mock('ui/new_platform'); @@ -54,7 +54,7 @@ describe('AggConfig Filters', () => { aggConfigs.aggs[0] as IBucketAggConfig, 'apache', {} - ) as esFilters.Filter; + ) as Filter; expect(filter).toHaveProperty('query'); expect(filter.query).toHaveProperty('match_phrase'); @@ -73,7 +73,7 @@ describe('AggConfig Filters', () => { aggConfigs.aggs[0] as IBucketAggConfig, '', {} - ) as esFilters.Filter; + ) as Filter; expect(filterFalse).toHaveProperty('query'); expect(filterFalse.query).toHaveProperty('match_phrase'); @@ -84,7 +84,7 @@ describe('AggConfig Filters', () => { aggConfigs.aggs[0] as IBucketAggConfig, '1', {} - ) as esFilters.Filter; + ) as Filter; expect(filterTrue).toHaveProperty('query'); expect(filterTrue.query).toHaveProperty('match_phrase'); @@ -100,7 +100,7 @@ describe('AggConfig Filters', () => { aggConfigs.aggs[0] as IBucketAggConfig, '__missing__', {} - ) as esFilters.ExistsFilter; + ) as ExistsFilter; expect(filter).toHaveProperty('exists'); expect(filter.exists).toHaveProperty('field', 'field'); @@ -116,7 +116,7 @@ describe('AggConfig Filters', () => { const [filter] = createFilterTerms(aggConfigs.aggs[0] as IBucketAggConfig, '__other__', { terms: ['apache'], - }) as esFilters.Filter[]; + }) as Filter[]; expect(filter).toHaveProperty('query'); expect(filter.query).toHaveProperty('bool'); diff --git a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/terms.ts b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/terms.ts index e0d1f91c1e16a4..4152258ffa0ee0 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/terms.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/buckets/create_filter/terms.ts @@ -18,7 +18,7 @@ */ import { IBucketAggConfig } from '../_bucket_agg_type'; -import { esFilters } from '../../../../../../../../plugins/data/public'; +import { esFilters, Filter } from '../../../../../../../../plugins/data/public'; export const createFilterTerms = (aggConfig: IBucketAggConfig, key: string, params: any) => { const field = aggConfig.params.field; @@ -30,7 +30,7 @@ export const createFilterTerms = (aggConfig: IBucketAggConfig, key: string, para const phraseFilter = esFilters.buildPhrasesFilter(field, terms, indexPattern); phraseFilter.meta.negate = true; - const filters: esFilters.Filter[] = [phraseFilter]; + const filters: Filter[] = [phraseFilter]; if (terms.some((term: string) => term === '__missing__')) { filters.push(esFilters.buildExistsFilter(field, indexPattern)); 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 5ff68c5426e341..f0ad5954764867 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 afd4e18dd266ca..8732f926b0fb2e 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 639b6d1fbb03e3..00000000000000 --- 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 0d1b2472bb8e21..e24aca08271c72 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 3956bda1812ad1..e7c98e575fdb4f 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/data/public/search/aggs/param_types/field.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/field.ts index 9a204bb151e2dc..40c30f6210a833 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/field.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/field.ts @@ -25,7 +25,11 @@ import { SavedObjectNotFound } from '../../../../../../../plugins/kibana_utils/p import { BaseParamType } from './base'; import { propFilter } from '../filter'; import { IMetricAggConfig } from '../metrics/metric_agg_type'; -import { Field, isNestedField, KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; +import { + IndexPatternField, + indexPatterns, + KBN_FIELD_TYPES, +} from '../../../../../../../plugins/data/public'; const filterByType = propFilter('type'); @@ -72,7 +76,7 @@ export class FieldParamType extends BaseParamType { }; } - this.serialize = (field: Field) => { + this.serialize = (field: IndexPatternField) => { return field.name; }; @@ -112,11 +116,11 @@ export class FieldParamType extends BaseParamType { */ getAvailableFields = (aggConfig: IAggConfig) => { const fields = aggConfig.getIndexPattern().fields; - const filteredFields = fields.filter((field: Field) => { + const filteredFields = fields.filter((field: IndexPatternField) => { const { onlyAggregatable, scriptable, filterFieldTypes } = this; if ( - (onlyAggregatable && (!field.aggregatable || isNestedField(field))) || + (onlyAggregatable && (!field.aggregatable || indexPatterns.isNestedField(field))) || (!scriptable && field.scripted) ) { return false; diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/filter/field_filters.test.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/filter/field_filters.test.ts index fb53e72b85c60c..bc36bb46d3d166 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/filter/field_filters.test.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/filter/field_filters.test.ts @@ -20,7 +20,7 @@ import { IndexedArray } from 'ui/indexed_array'; import { AggTypeFieldFilters } from './field_filters'; import { AggConfig } from '../../agg_config'; -import { Field } from '../../../../../../../../plugins/data/public'; +import { IndexPatternField } from '../../../../../../../../plugins/data/public'; describe('AggTypeFieldFilters', () => { let registry: AggTypeFieldFilters; @@ -31,13 +31,13 @@ describe('AggTypeFieldFilters', () => { }); it('should filter nothing without registered filters', async () => { - const fields = [{ name: 'foo' }, { name: 'bar' }] as IndexedArray; + const fields = [{ name: 'foo' }, { name: 'bar' }] as IndexedArray; const filtered = registry.filter(fields, aggConfig); expect(filtered).toEqual(fields); }); it('should pass all fields to the registered filter', async () => { - const fields = [{ name: 'foo' }, { name: 'bar' }] as IndexedArray; + const fields = [{ name: 'foo' }, { name: 'bar' }] as IndexedArray; const filter = jest.fn(); registry.addFilter(filter); registry.filter(fields, aggConfig); @@ -46,7 +46,7 @@ describe('AggTypeFieldFilters', () => { }); it('should allow registered filters to filter out fields', async () => { - const fields = [{ name: 'foo' }, { name: 'bar' }] as IndexedArray; + const fields = [{ name: 'foo' }, { name: 'bar' }] as IndexedArray; let filtered = registry.filter(fields, aggConfig); expect(filtered).toEqual(fields); diff --git a/src/legacy/core_plugins/data/public/search/aggs/param_types/filter/field_filters.ts b/src/legacy/core_plugins/data/public/search/aggs/param_types/filter/field_filters.ts index 7d44bedafa7e1a..7d1348ab5423be 100644 --- a/src/legacy/core_plugins/data/public/search/aggs/param_types/filter/field_filters.ts +++ b/src/legacy/core_plugins/data/public/search/aggs/param_types/filter/field_filters.ts @@ -16,10 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { Field } from 'src/plugins/data/public'; +import { IndexPatternField } from 'src/plugins/data/public'; import { AggConfig } from '../../agg_config'; -type AggTypeFieldFilter = (field: Field, aggConfig: AggConfig) => boolean; +type AggTypeFieldFilter = (field: IndexPatternField, aggConfig: AggConfig) => boolean; /** * A registry to store {@link AggTypeFieldFilter} which are used to filter down @@ -45,7 +45,7 @@ class AggTypeFieldFilters { * @param aggConfig The aggConfig for which the returning list will be used. * @return A filtered list of the passed fields. */ - public filter(fields: Field[], aggConfig: AggConfig) { + public filter(fields: IndexPatternField[], aggConfig: AggConfig) { const allFilters = Array.from(this.filters); const allowedAggTypeFields = fields.filter(field => { const isAggTypeFieldAllowed = allFilters.every(filter => filter(field, aggConfig)); diff --git a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts index 6a0748a33e7248..9aee7124c95211 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts @@ -32,7 +32,7 @@ import { SearchSource, Query, TimeRange, - esFilters, + Filter, getTime, FilterManager, } from '../../../../../../plugins/data/public'; @@ -53,7 +53,7 @@ export interface RequestHandlerParams { aggs: IAggConfigs; timeRange?: TimeRange; query?: Query; - filters?: esFilters.Filter[]; + filters?: Filter[]; forceFetch: boolean; filterManager: FilterManager; uiState?: PersistedState; diff --git a/src/legacy/core_plugins/embeddable_api/index.ts b/src/legacy/core_plugins/embeddable_api/index.ts index 465e13df10bbc9..52206e3d0f105f 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 3f1977b909c31c..00000000000000 --- 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/control/control.ts b/src/legacy/core_plugins/input_control_vis/public/control/control.ts index 9dc03ecc234522..6fddef777f73ee 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/control.ts +++ b/src/legacy/core_plugins/input_control_vis/public/control/control.ts @@ -22,7 +22,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { esFilters } from '../../../../../plugins/data/public'; +import { Filter } from '../../../../../plugins/data/public'; import { SearchSource as SearchSourceClass } from '../legacy_imports'; import { ControlParams, ControlParamsOptions, CONTROL_TYPES } from '../editor_utils'; import { RangeFilterManager } from './filter_manager/range_filter_manager'; @@ -46,7 +46,7 @@ export function noIndexPatternMsg(indexPatternId: string) { } export abstract class Control { - private kbnFilter: esFilters.Filter | null = null; + private kbnFilter: Filter | null = null; enable: boolean = false; disabledReason: string = ''; diff --git a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts b/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts index c8fa5af5e052bb..f7927962307574 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts +++ b/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts @@ -17,7 +17,7 @@ * under the License. */ -import { esFilters, IndexPattern, TimefilterSetup } from '../../../../../plugins/data/public'; +import { PhraseFilter, IndexPattern, TimefilterSetup } from '../../../../../plugins/data/public'; import { SearchSource as SearchSourceClass, SearchSourceFields } from '../legacy_imports'; export function createSearchSource( @@ -26,7 +26,7 @@ export function createSearchSource( indexPattern: IndexPattern, aggs: any, useTimeFilter: boolean, - filters: esFilters.PhraseFilter[] = [], + filters: PhraseFilter[] = [], timefilter: TimefilterSetup['timefilter'] ) { const searchSource = initialState ? new SearchSource(initialState) : new SearchSource(); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts index fd2cbae121b7e1..39c9d843e6bce9 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts +++ b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts @@ -22,7 +22,7 @@ import expect from '@kbn/expect'; import { FilterManager } from './filter_manager'; import { coreMock } from '../../../../../../core/public/mocks'; import { - esFilters, + Filter, IndexPattern, FilterManager as QueryFilterManager, } from '../../../../../../plugins/data/public'; @@ -31,7 +31,7 @@ const setupMock = coreMock.createSetup(); class FilterManagerTest extends FilterManager { createFilter() { - return {} as esFilters.Filter; + return {} as Filter; } getValueFromFilterBar() { @@ -44,7 +44,7 @@ describe('FilterManager', function() { describe('findFilters', function() { const indexPatternMock = {} as IndexPattern; - let kbnFilters: esFilters.Filter[]; + let kbnFilters: Filter[]; const queryFilterMock = new QueryFilterManager(setupMock.uiSettings); queryFilterMock.getAppFilters = () => kbnFilters; queryFilterMock.getGlobalFilters = () => []; @@ -56,7 +56,7 @@ describe('FilterManager', function() { }); test('should not find filters that are not controlled by any visualization', function() { - kbnFilters.push({} as esFilters.Filter); + kbnFilters.push({} as Filter); const foundFilters = filterManager.findFilters(); expect(foundFilters.length).to.be(0); }); @@ -66,7 +66,7 @@ describe('FilterManager', function() { meta: { controlledBy: 'anotherControl', }, - } as esFilters.Filter); + } as Filter); const foundFilters = filterManager.findFilters(); expect(foundFilters.length).to.be(0); }); @@ -76,7 +76,7 @@ describe('FilterManager', function() { meta: { controlledBy: controlId, }, - } as esFilters.Filter); + } as Filter); const foundFilters = filterManager.findFilters(); expect(foundFilters.length).to.be(1); }); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.ts b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.ts index d80a74ed46eae9..90b88a56950e2a 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.ts +++ b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.ts @@ -22,7 +22,7 @@ import _ from 'lodash'; import { FilterManager as QueryFilterManager, IndexPattern, - esFilters, + Filter, } from '../../../../../../plugins/data/public'; export abstract class FilterManager { @@ -41,7 +41,7 @@ export abstract class FilterManager { * single phrase: match query * multiple phrases: bool query with should containing list of match_phrase queries */ - abstract createFilter(phrases: any): esFilters.Filter; + abstract createFilter(phrases: any): Filter; abstract getValueFromFilterBar(): any; @@ -53,7 +53,7 @@ export abstract class FilterManager { return this.indexPattern.fields.getByName(this.fieldName); } - findFilters(): esFilters.Filter[] { + findFilters(): Filter[] { const kbnFilters = _.flatten([ this.queryFilter.getAppFilters(), this.queryFilter.getGlobalFilters(), diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts index dc577ca7168d1d..5be5d0157541e6 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts +++ b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts @@ -20,7 +20,7 @@ import expect from '@kbn/expect'; import { - esFilters, + Filter, IndexPattern, FilterManager as QueryFilterManager, } from '../../../../../../plugins/data/public'; @@ -88,7 +88,7 @@ describe('PhraseFilterManager', function() { describe('getValueFromFilterBar', function() { class MockFindFiltersPhraseFilterManager extends PhraseFilterManager { - mockFilters: esFilters.Filter[]; + mockFilters: Filter[]; constructor( id: string, @@ -104,7 +104,7 @@ describe('PhraseFilterManager', function() { return this.mockFilters; } - setMockFilters(mockFilters: esFilters.Filter[]) { + setMockFilters(mockFilters: Filter[]) { this.mockFilters = mockFilters; } } @@ -133,7 +133,7 @@ describe('PhraseFilterManager', function() { }, }, }, - ] as esFilters.Filter[]); + ] as Filter[]); expect(filterManager.getValueFromFilterBar()).to.eql(['ios']); }); @@ -159,7 +159,7 @@ describe('PhraseFilterManager', function() { }, }, }, - ] as esFilters.Filter[]); + ] as Filter[]); expect(filterManager.getValueFromFilterBar()).to.eql(['ios', 'win xp']); }); @@ -183,7 +183,7 @@ describe('PhraseFilterManager', function() { }, }, }, - ] as esFilters.Filter[]); + ] as Filter[]); expect(filterManager.getValueFromFilterBar()).to.eql(['ios', 'win xp']); }); @@ -199,7 +199,7 @@ describe('PhraseFilterManager', function() { }, }, }, - ] as esFilters.Filter[]); + ] as Filter[]); expect(filterManager.getValueFromFilterBar()).to.eql(undefined); }); }); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts index b0b46be86f1e8f..6f4a95b4919079 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts +++ b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts @@ -21,6 +21,7 @@ import _ from 'lodash'; import { FilterManager } from './filter_manager'; import { + PhraseFilter, esFilters, IndexPattern, FilterManager as QueryFilterManager, @@ -36,8 +37,8 @@ export class PhraseFilterManager extends FilterManager { super(controlId, fieldName, indexPattern, queryFilter); } - createFilter(phrases: any): esFilters.PhraseFilter { - let newFilter: esFilters.PhraseFilter; + createFilter(phrases: any): PhraseFilter { + let newFilter: PhraseFilter; const value = this.indexPattern.fields.getByName(this.fieldName); if (!value) { @@ -79,13 +80,13 @@ export class PhraseFilterManager extends FilterManager { /** * Extract filtering value from kibana filters * - * @param {esFilters.PhraseFilter} kbnFilter + * @param {PhraseFilter} kbnFilter * @return {Array.} array of values pulled from filter */ - private getValueFromFilter(kbnFilter: esFilters.PhraseFilter): any { + private getValueFromFilter(kbnFilter: PhraseFilter): any { // bool filter - multiple phrase filters if (_.has(kbnFilter, 'query.bool.should')) { - return _.get(kbnFilter, 'query.bool.should') + return _.get(kbnFilter, 'query.bool.should') .map(kbnQueryFilter => { return this.getValueFromFilter(kbnQueryFilter); }) diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts index f4993a60c5b39f..c776042ea4ba6b 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts +++ b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts @@ -21,7 +21,8 @@ import expect from '@kbn/expect'; import { RangeFilterManager } from './range_filter_manager'; import { - esFilters, + RangeFilter, + RangeFilterMeta, IndexPattern, FilterManager as QueryFilterManager, } from '../../../../../../plugins/data/public'; @@ -69,7 +70,7 @@ describe('RangeFilterManager', function() { describe('getValueFromFilterBar', function() { class MockFindFiltersRangeFilterManager extends RangeFilterManager { - mockFilters: esFilters.RangeFilter[]; + mockFilters: RangeFilter[]; constructor( id: string, @@ -85,7 +86,7 @@ describe('RangeFilterManager', function() { return this.mockFilters; } - setMockFilters(mockFilters: esFilters.RangeFilter[]) { + setMockFilters(mockFilters: RangeFilter[]) { this.mockFilters = mockFilters; } } @@ -111,9 +112,9 @@ describe('RangeFilterManager', function() { lt: 3, }, }, - meta: {} as esFilters.RangeFilterMeta, + meta: {} as RangeFilterMeta, }, - ] as esFilters.RangeFilter[]); + ] as RangeFilter[]); const value = filterManager.getValueFromFilterBar(); expect(value).to.be.a('object'); expect(value).to.have.property('min'); @@ -131,9 +132,9 @@ describe('RangeFilterManager', function() { lte: 3, }, }, - meta: {} as esFilters.RangeFilterMeta, + meta: {} as RangeFilterMeta, }, - ] as esFilters.RangeFilter[]); + ] as RangeFilter[]); expect(filterManager.getValueFromFilterBar()).to.eql(undefined); }); }); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts index 0a6819bd68e6f9..7a6719e85961b7 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts +++ b/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts @@ -20,7 +20,12 @@ import _ from 'lodash'; import { FilterManager } from './filter_manager'; -import { esFilters, IFieldType } from '../../../../../../plugins/data/public'; +import { + esFilters, + RangeFilter, + RangeFilterParams, + IFieldType, +} from '../../../../../../plugins/data/public'; interface SliderValue { min?: string | number; @@ -36,7 +41,7 @@ function toRange(sliderValue: SliderValue) { } // Convert ES range filter into slider value -function fromRange(range: esFilters.RangeFilterParams): SliderValue { +function fromRange(range: RangeFilterParams): SliderValue { const sliderValue: SliderValue = {}; if (_.has(range, 'gte')) { sliderValue.min = _.get(range, 'gte'); @@ -60,7 +65,7 @@ export class RangeFilterManager extends FilterManager { * @param {object} react-input-range value - POJO with `min` and `max` properties * @return {object} range filter */ - createFilter(value: SliderValue): esFilters.RangeFilter { + createFilter(value: SliderValue): RangeFilter { const newFilter = esFilters.buildRangeFilter( // TODO: Fix type to be required this.indexPattern.fields.getByName(this.fieldName) as IFieldType, @@ -78,7 +83,7 @@ export class RangeFilterManager extends FilterManager { return; } - let range: esFilters.RangeFilterParams; + let range: RangeFilterParams; if (_.has(kbnFilters[0], 'script')) { range = _.get(kbnFilters[0], 'script.script.params'); } else { diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/get_deps_mock.tsx b/src/legacy/core_plugins/input_control_vis/public/test_utils/get_deps_mock.tsx index 7b10eea3dde444..78a4ef3a5597ac 100644 --- a/src/legacy/core_plugins/input_control_vis/public/test_utils/get_deps_mock.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/test_utils/get_deps_mock.tsx @@ -18,13 +18,12 @@ */ import React from 'react'; -import { FieldList } from 'src/plugins/data/public'; import { InputControlVisDependencies } from '../plugin'; -const fields: FieldList = [] as any; +const fields = [] as any; fields.push({ name: 'myField' } as any); fields.getByName = (name: any) => { - return fields.find(({ name: n }) => n === name); + return fields.find(({ name: n }: { name: string }) => n === name); }; export const getDepsMock = (): InputControlVisDependencies => 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 9cdf777992ec55..624d000dd8d7ac 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 @@ -30,7 +30,7 @@ import { ControlParams } from './editor_utils'; import { RangeControl } from './control/range_control_factory'; import { ListControl } from './control/list_control_factory'; import { InputControlVisDependencies } from './plugin'; -import { FilterManager, esFilters } from '../../../../plugins/data/public'; +import { FilterManager, Filter } from '../../../../plugins/data/public'; import { VisParams, Vis } from '../../visualizations/public'; export const createInputControlVisController = (deps: InputControlVisDependencies) => { @@ -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() { @@ -155,7 +153,7 @@ export const createInputControlVisController = (deps: InputControlVisDependencie const newFilters = stagedControls .map(control => control.getKbnFilter()) - .filter((filter): filter is esFilters.Filter => { + .filter((filter): filter is Filter => { return filter !== null; }); 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 74c61c2bcfd2a5..00000000000000 --- 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 d6a076c540f88f..00000000000000 --- 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 db6f17a2960a97..9427a2f8a2d0f3 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 360f35020764d7..00000000000000 --- 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 395e0da218307e..ea81193c1dd0ab 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 d0157882689d39..5b9fb8c0b6360b 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 9c13337a71126e..cedb6fbc9b5efa 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/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts index 57edf5e838170d..d5198dc557f04f 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts @@ -24,9 +24,6 @@ * directly where they are needed. */ -import chrome from 'ui/chrome'; - -export const legacyChrome = chrome; export { SavedObjectSaveOpts } from 'ui/saved_objects/types'; export { npSetup, npStart } from 'ui/new_platform'; export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; @@ -37,8 +34,6 @@ export { createTopNavDirective, createTopNavHelper } from 'ui/kbn_top_nav/kbn_to export { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url/index'; export { IInjector } from 'ui/chrome'; export { SavedObjectLoader } from 'ui/saved_objects'; -export { VISUALIZE_EMBEDDABLE_TYPE } from '../../../visualizations/public/embeddable'; -export { registerTimefilterWithGlobalStateFactory } from 'ui/timefilter/setup_router'; export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; export { configureAppAngularModule, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.test.ts index ae3edae3b85d61..621983b1ca8a51 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.test.ts @@ -18,9 +18,9 @@ */ import { moveFiltersToQuery, Pre600FilterQuery } from './move_filters_to_query'; -import { esFilters } from '../../../../../../plugins/data/public'; +import { esFilters, Filter } from '../../../../../../plugins/data/public'; -const filter: esFilters.Filter = { +const filter: Filter = { meta: { disabled: false, negate: false, alias: '' }, query: {}, $state: { store: esFilters.FilterStateStore.APP_STATE }, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.ts b/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.ts index e82fc58670e392..7207f601a225ed 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.ts @@ -17,7 +17,7 @@ * under the License. */ -import { esFilters, Query } from '../../../../../../plugins/data/public'; +import { Filter, Query } from '../../../../../../plugins/data/public'; export interface Pre600FilterQuery { // pre 6.0.0 global query:queryString:options were stored per dashboard and would @@ -29,18 +29,18 @@ export interface Pre600FilterQuery { export interface SearchSourcePre600 { // I encountered at least one export from 7.0.0-alpha that was missing the filter property in here. // The maps data in esarchives actually has it, but I don't know how/when they created it. - filter?: Array; + filter?: Array; } export interface SearchSource730 { - filter: esFilters.Filter[]; + filter: Filter[]; query: Query; highlightAll?: boolean; version?: boolean; } -function isQueryFilter(filter: esFilters.Filter | { query: unknown }): filter is Pre600FilterQuery { - return filter.query && !(filter as esFilters.Filter).meta; +function isQueryFilter(filter: Filter | { query: unknown }): filter is Pre600FilterQuery { + return filter.query && !(filter as Filter).meta; } export function moveFiltersToQuery( 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 e608eb7b7f48c0..cc104c1a931d00 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 ad69ef322a9099..c0a0693431295b 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 @@ -30,7 +30,7 @@ import { IIndexPattern, TimeRange, Query, - esFilters, + Filter, SavedQuery, } from '../../../../../../plugins/data/public'; @@ -44,7 +44,7 @@ export interface DashboardAppScope extends ng.IScope { screenTitle: string; model: { query: Query; - filters: esFilters.Filter[]; + filters: Filter[]; timeRestore: boolean; title: string; description: string; @@ -69,9 +69,9 @@ export interface DashboardAppScope extends ng.IScope { isPaused: boolean; refreshInterval: any; }) => void; - onFiltersUpdated: (filters: esFilters.Filter[]) => void; + onFiltersUpdated: (filters: Filter[]) => void; onCancelApplyFilters: () => void; - onApplyFilters: (filters: esFilters.Filter[]) => void; + onApplyFilters: (filters: Filter[]) => void; onQuerySaved: (savedQuery: SavedQuery) => void; onSavedQueryUpdated: (savedQuery: SavedQuery) => void; onClearSavedQuery: () => void; @@ -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 0b55adc1d52be7..465203be0d34c1 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 @@ -30,8 +30,7 @@ import { DashboardEmptyScreen, DashboardEmptyScreenProps } from './dashboard_emp import { migrateLegacyQuery, SavedObjectSaveOpts, subscribeWithScope } from '../legacy_imports'; import { - COMPARE_ALL_OPTIONS, - compareFilters, + esFilters, IndexPattern, IndexPatternsContract, Query, @@ -97,6 +96,7 @@ export class DashboardAppController { }; constructor({ + pluginInitializerContext, $scope, $route, $routeParams, @@ -104,10 +104,10 @@ export class DashboardAppController { localStorage, indexPatterns, savedQueryService, - embeddables, + embeddable, share, dashboardCapabilities, - npDataStart: { query: queryService }, + data: { query: queryService }, core: { notifications, overlays, @@ -142,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, }); @@ -187,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); }); @@ -285,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 @@ -319,10 +319,10 @@ export class DashboardAppController { // appState.save which will cause refreshDashboardContainer to be called. if ( - !compareFilters( + !esFilters.compareFilters( container.getInput().filters, queryFilter.getFilters(), - COMPARE_ALL_OPTIONS + esFilters.COMPARE_ALL_OPTIONS ) ) { // Add filters modifies the object passed to it, hence the clone deep. @@ -422,7 +422,11 @@ export class DashboardAppController { // Filters shouldn't be compared using regular isEqual if ( - !compareFilters(containerInput.filters, appStateDashboardInput.filters, COMPARE_ALL_OPTIONS) + !esFilters.compareFilters( + containerInput.filters, + appStateDashboardInput.filters, + esFilters.COMPARE_ALL_OPTIONS + ) ) { differences.filters = appStateDashboardInput.filters; } @@ -815,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), @@ -826,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/dashboard_state_manager.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts index 987afd65bb67bf..fa5354a17b6d93 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_state_manager.ts @@ -27,7 +27,7 @@ import { DashboardContainer } from 'src/legacy/core_plugins/dashboard_embeddable import { ViewMode } from '../../../../../../plugins/embeddable/public'; import { migrateLegacyQuery } from '../legacy_imports'; import { - esFilters, + Filter, Query, TimefilterContract as Timefilter, } from '../../../../../../plugins/data/public'; @@ -62,7 +62,7 @@ export class DashboardStateManager { public lastSavedDashboardFilters: { timeTo?: string | Moment; timeFrom?: string | Moment; - filterBars: esFilters.Filter[]; + filterBars: Filter[]; query: Query; }; private stateDefaults: DashboardAppStateDefaults; @@ -251,7 +251,7 @@ export class DashboardStateManager { this.stateContainer.transitions.set('fullScreenMode', fullScreenMode); } - public setFilters(filters: esFilters.Filter[]) { + public setFilters(filters: Filter[]) { this.stateContainer.transitions.set('filters', filters); } @@ -367,7 +367,7 @@ export class DashboardStateManager { return this.savedDashboard.timeRestore; } - public getLastSavedFilterBars(): esFilters.Filter[] { + public getLastSavedFilterBars(): Filter[] { return this.lastSavedDashboardFilters.filterBars; } @@ -546,7 +546,7 @@ export class DashboardStateManager { * Applies the current filter state to the dashboard. * @param filter An array of filter bar filters. */ - public applyFilters(query: Query, filters: esFilters.Filter[]) { + public applyFilters(query: Query, filters: Filter[]) { this.savedDashboard.searchSource.setField('query', query); this.savedDashboard.searchSource.setField('filter', filters); this.stateContainer.transitions.set('query', query); 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 b0f70b7a0c68f3..ce9cc85be57b27 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/np_ready/lib/filter_utils.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/filter_utils.ts index 6fbc04969b1c8c..f7b45b0371378d 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/filter_utils.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/filter_utils.ts @@ -19,7 +19,7 @@ import _ from 'lodash'; import moment, { Moment } from 'moment'; -import { esFilters } from '../../../../../../../plugins/data/public'; +import { Filter } from '../../../../../../../plugins/data/public'; /** * @typedef {Object} QueryFilter @@ -65,9 +65,9 @@ export class FilterUtils { * @param filters {Array.} * @returns {Array.} */ - public static cleanFiltersForComparison(filters: esFilters.Filter[]) { + public static cleanFiltersForComparison(filters: Filter[]) { return _.map(filters, filter => { - const f: Partial = _.omit(filter, ['$$hashKey', '$state']); + const f: Partial = _.omit(filter, ['$$hashKey', '$state']); if (f.meta) { // f.meta.value is the value displayed in the filter bar. // It may also be loaded differently and shouldn't be used in this comparison. diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/types.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/types.ts index 146affda282008..0f3a7e322ebf3a 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/types.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/types.ts @@ -26,7 +26,7 @@ import { RawSavedDashboardPanel640To720, RawSavedDashboardPanel730ToLatest, } from '../migrations/types'; -import { Query, esFilters } from '../../../../../../plugins/data/public'; +import { Query, Filter } from '../../../../../../plugins/data/public'; export type NavAction = (anchorElement?: any) => void; @@ -103,7 +103,7 @@ export interface DashboardAppState { useMargins: boolean; }; query: Query | string; - filters: esFilters.Filter[]; + filters: Filter[]; viewMode: ViewMode; savedQuery?: string; } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts index 09ae49f2305fda..7d330676e79edc 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/dashboard/saved_dashboard/saved_dashboard.ts b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.ts index 08a6f067d20260..5babaf8061de9a 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.ts @@ -21,7 +21,7 @@ import { createSavedObjectClass } from 'ui/saved_objects/saved_object'; import { extractReferences, injectReferences } from './saved_dashboard_references'; import { - esFilters, + Filter, ISearchSource, Query, RefreshInterval, @@ -42,7 +42,7 @@ export interface SavedObjectDashboard extends SavedObject { refreshInterval?: RefreshInterval; searchSource: ISearchSource; getQuery(): Query; - getFilters(): esFilters.Filter[]; + getFilters(): Filter[]; } // Used only by the savedDashboards service, usually no reason to change this diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_add_filter.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_add_filter.js index f2acbf363d825d..87eb283639c789 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_add_filter.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_add_filter.js @@ -21,7 +21,6 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import { createStateStub } from './_utils'; import { getQueryParameterActions } from '../../np_ready/angular/context/query_parameters/actions'; -import { createIndexPatternsStub } from '../../np_ready/angular/context/api/__tests__/_stubs'; import { pluginInstance } from 'plugins/kibana/discover/legacy'; import { npStart } from 'ui/new_platform'; @@ -29,11 +28,6 @@ describe('context app', function() { beforeEach(() => pluginInstance.initializeInnerAngular()); beforeEach(() => pluginInstance.initializeServices()); beforeEach(ngMock.module('app/discover')); - beforeEach( - ngMock.module(function createServiceStubs($provide) { - $provide.value('indexPatterns', createIndexPatternsStub()); - }) - ); describe('action addFilter', function() { let addFilter; diff --git a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts index eb6d7e6467f2f6..373395c86636c7 100644 --- a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts +++ b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts @@ -39,7 +39,7 @@ import { StateManagementConfigProvider } from 'ui/state_management/config_provid import { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url'; // @ts-ignore import { createTopNavDirective, createTopNavHelper } from 'ui/kbn_top_nav/kbn_top_nav'; -import { IndexPatterns, DataPublicPluginStart } from '../../../../../plugins/data/public'; +import { DataPublicPluginStart } from '../../../../../plugins/data/public'; import { Storage } from '../../../../../plugins/kibana_utils/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; import { createDocTableDirective } from './np_ready/angular/doc_table/doc_table'; @@ -125,7 +125,6 @@ export function initializeInnerAngularModule( createLocalAppStateModule(); createLocalStorageModule(); createElasticSearchModule(data); - createIndexPatternsModule(); createPagerFactoryModule(); createDocTableModule(); initialized = true; @@ -164,7 +163,6 @@ export function initializeInnerAngularModule( 'discoverGlobalState', 'discoverAppState', 'discoverLocalStorageProvider', - 'discoverIndexPatterns', 'discoverEs', 'discoverDocTable', 'discoverPagerFactory', @@ -299,10 +297,6 @@ function createElasticSearchModule(data: DataPublicPluginStart) { }); } -function createIndexPatternsModule() { - angular.module('discoverIndexPatterns', []).value('indexPatterns', IndexPatterns); -} - function createPagerFactoryModule() { angular.module('discoverPagerFactory', []).factory('pagerFactory', createPagerFactory); } diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/anchor.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/anchor.js index debcccebbd11c0..63834fb750e214 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/anchor.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/anchor.js @@ -33,12 +33,6 @@ describe('context app', function() { let fetchAnchor; let searchSourceStub; - beforeEach( - ngMock.module(function createServiceStubs($provide) { - $provide.value('indexPatterns', createIndexPatternsStub()); - }) - ); - beforeEach( ngMock.inject(function createPrivateStubs() { searchSourceStub = createSearchSourceStub([{ _id: 'hit1' }]); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/predecessors.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/predecessors.js index c24b6ac6307ffd..02d998e8f4529f 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/predecessors.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/predecessors.js @@ -41,12 +41,6 @@ describe('context app', function() { let fetchPredecessors; let searchSourceStub; - beforeEach( - ngMock.module(function createServiceStubs($provide) { - $provide.value('indexPatterns', createIndexPatternsStub()); - }) - ); - beforeEach( ngMock.inject(function createPrivateStubs() { searchSourceStub = createContextSearchSourceStub([], '@timestamp', MS_PER_DAY * 8); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts index 6054b9f8d03c5f..a9c6918adbfdea 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts @@ -24,7 +24,7 @@ import { fetchHitsInInterval } from './utils/fetch_hits_in_interval'; import { generateIntervals } from './utils/generate_intervals'; import { getEsQuerySearchAfter } from './utils/get_es_query_search_after'; import { getEsQuerySort } from './utils/get_es_query_sort'; -import { esFilters, IndexPatternsContract } from '../../../../../../../../../plugins/data/public'; +import { Filter, IndexPatternsContract } from '../../../../../../../../../plugins/data/public'; export type SurrDocType = 'successors' | 'predecessors'; export interface EsHitRecord { @@ -65,7 +65,7 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract) { tieBreakerField: string, sortDir: SortDirection, size: number, - filters: esFilters.Filter[] + filters: Filter[] ) { if (typeof anchor !== 'object' || anchor === null) { return []; @@ -110,7 +110,7 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract) { return documents; } - async function createSearchSource(indexPattern: IndexPattern, filters: esFilters.Filter[]) { + async function createSearchSource(indexPattern: IndexPattern, filters: Filter[]) { return new SearchSource() .setParent(undefined) .setField('index', indexPattern) diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query_parameters/actions.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query_parameters/actions.js index c5f1836bcc0e1c..5be1179a9ae09c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query_parameters/actions.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query_parameters/actions.js @@ -19,7 +19,7 @@ import _ from 'lodash'; import { getServices } from '../../../../kibana_services'; -import { generateFilters } from '../../../../../../../../../plugins/data/public'; +import { esFilters } from '../../../../../../../../../plugins/data/public'; import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE, QUERY_PARAMETER_KEYS } from './constants'; @@ -49,7 +49,13 @@ export function getQueryParameterActions() { const addFilter = state => async (field, values, operation) => { const indexPatternId = state.queryParameters.indexPatternId; - const newFilters = generateFilters(filterManager, field, values, operation, indexPatternId); + const newFilters = esFilters.generateFilters( + filterManager, + field, + values, + operation, + indexPatternId + ); filterManager.addFilters(newFilters); const indexPattern = await getServices().indexPatterns.get(indexPatternId); indexPattern.popularizeField(field.name, 1); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/__snapshots__/no_results.test.js.snap b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/__snapshots__/no_results.test.js.snap index 98cb3ccf6dd91e..4126bd9d27ffd5 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/__snapshots__/no_results.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/__snapshots__/no_results.test.js.snap @@ -77,12 +77,8 @@ Array [
- - + + 200 @@ -101,12 +97,8 @@ Array [
- - + + status:200 @@ -125,12 +117,8 @@ Array [
- - + + status:[400 TO 499] @@ -149,12 +137,8 @@ Array [
- - + + status:[400 TO 499] AND extension:PHP @@ -173,12 +157,8 @@ Array [
- - + + status:[400 TO 499] AND (extension:php OR extension:html) @@ -291,15 +271,9 @@ Array [
-
-
-              
+          
+
+              
                 {"reason":"Awful error"}
               
             
@@ -320,15 +294,9 @@ Array [
-
-
-              
+          
+
+              
                 {"reason":"Bad error"}
               
             
diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/histogram.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/histogram.tsx index 77bbab97d95c7c..8db3c77ba0f472 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/histogram.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/histogram.tsx @@ -41,7 +41,7 @@ import { } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; -import { EuiChartThemeType } from '@elastic/eui/src/themes/charts/themes'; +import { EuiChartThemeType } from '@elastic/eui/dist/eui_charts_theme'; import { Subscription } from 'rxjs'; import { getServices, timezoneProvider } from '../../../kibana_services'; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/no_results.test.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/no_results.test.js index 7de792c6129931..98a4a926a282e9 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/no_results.test.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/no_results.test.js @@ -36,6 +36,31 @@ jest.mock('../../../kibana_services', () => { }; }); +// Mocking to prevent errors with React portal. +// Temporary until https://github.com/elastic/kibana/pull/55877 provides other alternatives. +jest.mock('@elastic/eui/lib/components/code/code_block', () => { + const React = require.requireActual('react'); + return { + EuiCodeBlock: ({ children }) => ( +
+
+          {children}
+        
+
+ ), + }; +}); +jest.mock('@elastic/eui/lib/components/code/code', () => { + const React = require.requireActual('react'); + return { + EuiCode: ({ children }) => ( + + {children} + + ), + }; +}); + beforeEach(() => { jest.clearAllMocks(); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js index 69f69d449354c6..39a9ca6641fd16 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js @@ -75,7 +75,7 @@ const { import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../helpers/breadcrumbs'; import { - generateFilters, + esFilters, indexPatterns as indexPatternsUtils, } from '../../../../../../../plugins/data/public'; import { getIndexPatternId } from '../helpers/get_index_pattern_id'; @@ -901,7 +901,7 @@ function discoverController( // TODO: On array fields, negating does not negate the combination, rather all terms $scope.filterQuery = function(field, values, operation) { $scope.indexPattern.popularizeField(field, 1); - const newFilters = generateFilters( + const newFilters = esFilters.generateFilters( filterManager, field, values, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.js index 47b3ec6b07e8ee..a175a1aebebdfa 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/field_chooser/field_chooser.js @@ -23,8 +23,8 @@ import { fieldCalculator } from './lib/field_calculator'; import './discover_field'; import './discover_field_search_directive'; import './discover_index_pattern_directive'; -import { FieldList } from '../../../../../../../../plugins/data/public'; import fieldChooserTemplate from './field_chooser.html'; +import { IndexPatternFieldList } from '../../../../../../../../plugins/data/public'; export function createFieldChooserDirective($location, config, $route) { return { @@ -281,7 +281,7 @@ export function createFieldChooserDirective($location, config, $route) { }); }); - const fields = new FieldList(indexPattern, fieldSpecs); + const fields = new IndexPatternFieldList(indexPattern, fieldSpecs); if (prevFields) { fields.forEach(function(field) { diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable.ts index 3f877520b5bf90..2bb76386bb7ba9 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable.ts @@ -24,10 +24,9 @@ import { ExecuteTriggerActions } from 'src/plugins/ui_actions/public'; import { RequestAdapter, Adapters } from '../../../../../../../plugins/inspector/public'; import { esFilters, + Filter, TimeRange, FilterManager, - onlyDisabledFiltersChanged, - generateFilters, getTime, Query, IFieldType, @@ -97,7 +96,7 @@ export class SearchEmbeddable extends Embeddable private abortController?: AbortController; private prevTimeRange?: TimeRange; - private prevFilters?: esFilters.Filter[]; + private prevFilters?: Filter[]; private prevQuery?: Query; constructor( @@ -236,7 +235,13 @@ export class SearchEmbeddable extends Embeddable }; searchScope.filter = async (field, value, operator) => { - let filters = generateFilters(this.filterManager, field, value, operator, indexPattern.id!); + let filters = esFilters.generateFilters( + this.filterManager, + field, + value, + operator, + indexPattern.id! + ); filters = filters.map(filter => ({ ...filter, $state: { store: esFilters.FilterStateStore.APP_STATE }, @@ -316,7 +321,7 @@ export class SearchEmbeddable extends Embeddable private pushContainerStateParamsToScope(searchScope: SearchScope) { const isFetchRequired = - !onlyDisabledFiltersChanged(this.input.filters, this.prevFilters) || + !esFilters.onlyDisabledFiltersChanged(this.input.filters, this.prevFilters) || !_.isEqual(this.prevQuery, this.input.query) || !_.isEqual(this.prevTimeRange, this.input.timeRange) || !_.isEqual(searchScope.sort, this.input.sort || this.savedSearch.sort); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/types.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/types.ts index 3d6acb0963bed6..e7aa390cda858f 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/types.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/types.ts @@ -20,17 +20,12 @@ import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from 'src/plugins/embeddable/public'; import { SavedSearch } from '../types'; import { SortOrder } from '../angular/doc_table/components/table_header/helpers'; -import { - esFilters, - IIndexPattern, - TimeRange, - Query, -} from '../../../../../../../plugins/data/public'; +import { Filter, IIndexPattern, TimeRange, Query } from '../../../../../../../plugins/data/public'; export interface SearchInput extends EmbeddableInput { timeRange: TimeRange; query?: Query; - filters?: esFilters.Filter[]; + filters?: Filter[]; hidePanelTitles?: boolean; columns?: string[]; sort?: SortOrder[]; diff --git a/src/legacy/core_plugins/kibana/public/home/index.ts b/src/legacy/core_plugins/kibana/public/home/index.ts index c4e58e1a5e1ae7..74b6da33c65422 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 66c4d995e2566a..6cb1531be6b5b2 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 4563b633c3dfc3..9d27362e627394 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 e08d802406fff9..661d1d33a5283d 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 6f76ceecbba13a..df7cc7bcbaed06 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 0c09c6c3c74fcb..617a1810028fcd 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 f6c91b412381cd..d7531864582a38 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 e6f5c07c94f9fe..cb43c18a8e78b6 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 daf996444eb3c2..c7e623657bf71f 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 55c469fa58fc61..d9da47a2b43da5 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 6983aabc4c7b1e..7906caeda1b384 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 e530906d5698e7..75e7cc2e453be8 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/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js index b74ceda5a6df81..0dcf778a5a6629 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js @@ -17,7 +17,7 @@ * under the License. */ -import { Field } from '../../../../../../../../../plugins/data/public'; +import { IndexPatternField } from '../../../../../../../../../plugins/data/public'; import { RegistryFieldFormatEditorsProvider } from 'ui/registry/field_format_editors'; import { docTitle } from 'ui/doc_title'; import { KbnUrlProvider } from 'ui/url'; @@ -39,7 +39,7 @@ const renderFieldEditor = ( $scope, indexPattern, field, - { Field, getConfig, $http, fieldFormatEditors, redirectAway } + { getConfig, $http, fieldFormatEditors, redirectAway } ) => { $scope.$$postDigest(() => { const node = document.getElementById(REACT_FIELD_EDITOR_ID); @@ -53,7 +53,7 @@ const renderFieldEditor = ( indexPattern={indexPattern} field={field} helpers={{ - Field, + Field: IndexPatternField, getConfig, $http, fieldFormatEditors, @@ -135,7 +135,7 @@ uiRoutes return; } } else if (this.mode === 'create') { - this.field = new Field(this.indexPattern, { + this.field = new IndexPatternField(this.indexPattern, { scripted: true, type: 'number', }); @@ -158,7 +158,6 @@ uiRoutes docTitle.change([fieldName, this.indexPattern.title]); renderFieldEditor($scope, this.indexPattern, this.field, { - Field, getConfig, $http, fieldFormatEditors, diff --git a/src/legacy/core_plugins/kibana/public/visualize/index.ts b/src/legacy/core_plugins/kibana/public/visualize/index.ts index 83b820a8e31340..c3ae39d9fde255 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 428e6cb2257104..6082fb8428ac3f 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 2d615e3132b01e..bc2d700f6c6a10 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/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts index ff7d167ccaacda..ac9fc227406ffd 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts @@ -55,7 +55,6 @@ export { wrapInI18nContext } from 'ui/i18n'; export { DashboardConstants } from '../dashboard/np_ready/dashboard_constants'; export { VisSavedObject } from '../../../visualizations/public/embeddable/visualize_embeddable'; export { VISUALIZE_EMBEDDABLE_TYPE } from '../../../visualizations/public/embeddable'; -export { VisType } from '../../../visualizations/public'; export { configureAppAngularModule, ensureDefaultIndexPattern, 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 44e7e9c2a74132..3d5fd6605f56b1 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 46ae45c3a5fa2b..27fb9b63843c4a 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 18a60f7c3c10bc..502bd6e56fb1f9 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 b2386f83b252c6..8032152f88173a 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 d3a8602226b574..524bc4b3196b7c 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 @@ -17,7 +17,7 @@ * under the License. */ -import { TimeRange, Query, esFilters, DataPublicPluginStart } from 'src/plugins/data/public'; +import { TimeRange, Query, Filter, DataPublicPluginStart } from 'src/plugins/data/public'; import { IEmbeddableStart } from 'src/plugins/embeddable/public'; import { LegacyCoreStart } from 'kibana/public'; import { VisSavedObject, AppState, PersistedState } from '../legacy_imports'; @@ -26,8 +26,8 @@ export interface EditorRenderProps { appState: AppState; core: LegacyCoreStart; data: DataPublicPluginStart; - embeddables: IEmbeddableStart; - filters: esFilters.Filter[]; + embeddable: IEmbeddableStart; + filters: Filter[]; uiState: PersistedState; timeRange: TimeRange; query?: Query; diff --git a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts index ce93fe7c2d5783..16715677d1e207 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 32d5f040760c66..00000000000000 --- 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 8fddb8e6aeced3..00000000000000 --- 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 8f2221eb4d4c74..00000000000000 --- 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 cf2c9c883871bf..52981c04ad34af 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/index.ts b/src/legacy/core_plugins/telemetry/index.ts index 2a81e3fa05c6c9..ec70380d83a0a2 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 079a43e77616d2..00000000000000 --- 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 fe0c2c3449af15..00000000000000 --- 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 ad9ee0998e3bbc..00000000000000 --- 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 74f1de4934a78b..00000000000000 --- 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 ede81f638a3fcd..00000000000000 --- 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 8fa777ead3e4b6..00000000000000 --- 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 45a0653cd7a540..00000000000000 --- 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 1930d65d5c09b9..00000000000000 --- 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 44971e24667944..00000000000000 --- 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 0caabe826ae573..00000000000000 --- 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 c03fdb85c4d1cd..00000000000000 --- 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 8f05675565a5ee..00000000000000 --- 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 c4c5c3e9e0aa28..00000000000000 --- 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 70b50308666202..00000000000000 --- 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 9578d462bc85ca..00000000000000 --- 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 45539c4eea46c9..00000000000000 --- 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 494ed24bcc1cbd..00000000000000 --- 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 4543266be46dfa..00000000000000 --- 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 af908bea7f4b10..00000000000000 --- 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 7032775e391bb1..c8ae410e0aa57a 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 99090cb2fb7ef2..6919b6959aa8c6 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 9edd8457f2b89e..6e16328c4abd89 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 00000000000000..b28a01bffa44dc --- /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 00000000000000..77eae0d80db61a --- /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 6c62d03adf25ce..85d7d80234ffc2 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 f5cb6fdf93002d..2f54d23590c334 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 772edaa4ff4f5e..910def8a0c78e8 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/components/agg_param_props.ts b/src/legacy/core_plugins/vis_default_editor/public/components/agg_param_props.ts index c858fb62045ca5..843cfddc070109 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_param_props.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg_param_props.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Field } from 'src/plugins/data/public'; +import { IndexPatternField } from 'src/plugins/data/public'; import { VisState } from 'src/legacy/core_plugins/visualizations/public'; import { IAggConfig, AggParam } from '../legacy_imports'; import { ComboBoxGroupedOptions } from '../utils'; @@ -33,7 +33,7 @@ export interface AggParamCommonProps { disabled?: boolean; editorConfig: EditorConfig; formIsTouched: boolean; - indexedFields?: ComboBoxGroupedOptions; + indexedFields?: ComboBoxGroupedOptions; showValidation: boolean; state: VisState; value?: T; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.ts b/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.ts index 124c41a50c0df3..0c0726ec67d509 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg_params_helper.ts @@ -19,7 +19,7 @@ import { get, isEmpty } from 'lodash'; -import { IndexPattern, Field } from 'src/plugins/data/public'; +import { IndexPattern, IndexPatternField } from 'src/plugins/data/public'; import { VisState } from 'src/legacy/core_plugins/visualizations/public'; import { groupAndSortBy, ComboBoxGroupedOptions } from '../utils'; import { AggTypeState, AggParamsState } from './agg_params_state'; @@ -45,7 +45,7 @@ interface ParamInstanceBase { export interface ParamInstance extends ParamInstanceBase { aggParam: AggParam; - indexedFields: ComboBoxGroupedOptions; + indexedFields: ComboBoxGroupedOptions; paramEditor: React.ComponentType>; value: unknown; } @@ -65,15 +65,17 @@ function getAggParamsToRender({ agg, editorConfig, metricAggs, state }: ParamIns // build collection of agg params components paramsToRender.forEach((param: AggParam, index: number) => { - let indexedFields: ComboBoxGroupedOptions = []; - let fields: Field[]; + let indexedFields: ComboBoxGroupedOptions = []; + let fields: IndexPatternField[]; if (agg.schema.hideCustomLabel && param.name === 'customLabel') { return; } // if field param exists, compute allowed fields if (param.type === 'field') { - const availableFields: Field[] = (param as IFieldParamType).getAvailableFields(agg); + const availableFields: IndexPatternField[] = (param as IFieldParamType).getAvailableFields( + agg + ); fields = aggTypeFieldFilters.filter(availableFields, agg); indexedFields = groupAndSortBy(fields, 'type', 'name'); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/agg_select.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/agg_select.tsx index 0ec19bfa1b843d..a2cec61b122efa 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/agg_select.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/agg_select.tsx @@ -24,7 +24,8 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { IndexPattern } from 'src/plugins/data/public'; -import { IAggType, documentationLinks } from '../legacy_imports'; +import { useKibana } from '../../../../../plugins/kibana_react/public'; +import { IAggType } from '../legacy_imports'; import { ComboBoxGroupedOptions } from '../utils'; import { AGG_TYPE_ACTION_KEYS, AggTypeAction } from './agg_params_state'; @@ -51,6 +52,7 @@ function DefaultEditorAggSelect({ isSubAggregation, onChangeAggType, }: DefaultEditorAggSelectProps) { + const { services } = useKibana(); const selectedOptions: ComboBoxGroupedOptions = value ? [{ label: value.title, target: value }] : []; @@ -69,7 +71,7 @@ function DefaultEditorAggSelect({ let aggHelpLink: string | undefined; if (has(value, 'name')) { - aggHelpLink = get(documentationLinks, ['aggs', value.name]); + aggHelpLink = services.docLinks.links.aggs[value.name]; } const helpLink = value && aggHelpLink && ( diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/date_ranges.test.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/date_ranges.test.tsx index 92212c3ad1a5c2..6b1a4dca7b84f5 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/date_ranges.test.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/date_ranges.test.tsx @@ -20,10 +20,8 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { DateRangesParamEditor } from './date_ranges'; - -jest.mock('../../legacy_imports', () => ({ - getDocLink: jest.fn(), -})); +import { KibanaContextProvider } from '../../../../../../plugins/kibana_react/public'; +import { docLinksServiceMock } from '../../../../../../core/public/mocks'; describe('DateRangesParamEditor component', () => { let setValue: jest.Mock; @@ -50,14 +48,25 @@ describe('DateRangesParamEditor component', () => { }; }); + function DateRangesWrapped(props: any) { + const services = { + docLinks: docLinksServiceMock.createStartContract(), + }; + return ( + + + + ); + } + it('should add default range if there is an empty ininitial value', () => { - mountWithIntl(); + mountWithIntl(); expect(setValue).toHaveBeenCalledWith([{}]); }); it('should validate range values with date math', function() { - const component = mountWithIntl(); + const component = mountWithIntl(); // should allow empty values expect(setValidity).toHaveBeenNthCalledWith(1, true); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/date_ranges.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/date_ranges.tsx index adeadc6e385359..ca4a9315d6bfb1 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/date_ranges.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/date_ranges.tsx @@ -37,8 +37,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { isEqual, omit } from 'lodash'; +import { useKibana } from '../../../../../../plugins/kibana_react/public'; import { AggParamEditorProps } from '../agg_param_props'; -import { getDocLink } from '../../legacy_imports'; const FROM_PLACEHOLDER = '\u2212\u221E'; const TO_PLACEHOLDER = '+\u221E'; @@ -66,6 +66,7 @@ function DateRangesParamEditor({ setValue, setValidity, }: AggParamEditorProps) { + const { services } = useKibana(); const [ranges, setRanges] = useState(() => value.map(range => ({ ...range, id: generateId() }))); const hasInvalidRange = value.some( ({ from, to }) => (!from && !to) || !validateDateMath(from) || !validateDateMath(to) @@ -115,7 +116,7 @@ function DateRangesParamEditor({ <> - + { let setTouched: jest.Mock; let onChange: jest.Mock; let defaultProps: FieldParamEditorProps; - let indexedFields: ComboBoxGroupedOptions; - let field: Field; + let indexedFields: ComboBoxGroupedOptions; + let field: IndexPatternField; let option: { label: string; - target: Field; + target: IndexPatternField; }; beforeEach(() => { @@ -54,7 +54,7 @@ describe('FieldParamEditor component', () => { setTouched = jest.fn(); onChange = jest.fn(); - field = { displayName: 'bytes' } as Field; + field = { displayName: 'bytes' } as IndexPatternField; option = { label: 'bytes', target: field }; indexedFields = [ { diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx index f374353afabeca..8bf7bc384b07a0 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/field.tsx @@ -23,7 +23,7 @@ import React, { useEffect } from 'react'; import { EuiComboBox, EuiComboBoxOptionProps, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Field } from 'src/plugins/data/public'; +import { IndexPatternField } from 'src/plugins/data/public'; import { AggParam, IAggConfig, IFieldParamType } from '../../legacy_imports'; import { formatListAsProse, parseCommaSeparatedList, useValidation } from './utils'; import { AggParamEditorProps } from '../agg_param_props'; @@ -33,7 +33,7 @@ const label = i18n.translate('visDefaultEditor.controls.field.fieldLabel', { defaultMessage: 'Field', }); -export interface FieldParamEditorProps extends AggParamEditorProps { +export interface FieldParamEditorProps extends AggParamEditorProps { customError?: string; customLabel?: string; } @@ -50,12 +50,12 @@ function FieldParamEditor({ setValidity, setValue, }: FieldParamEditorProps) { - const selectedOptions: ComboBoxGroupedOptions = value + const selectedOptions: ComboBoxGroupedOptions = value ? [{ label: value.displayName || value.name, target: value }] : []; const onChange = (options: EuiComboBoxOptionProps[]) => { - const selectedOption: Field = get(options, '0.target'); + const selectedOption: IndexPatternField = get(options, '0.target'); if (!(aggParam.required && !selectedOption)) { setValue(selectedOption); } diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/top_field.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/top_field.tsx index 6811f6b4c2034b..f625fe3c75c8a5 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/top_field.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/top_field.tsx @@ -20,12 +20,12 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { Field } from 'src/plugins/data/public'; +import { IndexPatternField } from 'src/plugins/data/public'; import { FieldParamEditor } from './field'; import { getCompatibleAggs } from './top_aggregate'; import { AggParamEditorProps } from '../agg_param_props'; -function TopFieldParamEditor(props: AggParamEditorProps) { +function TopFieldParamEditor(props: AggParamEditorProps) { const compatibleAggs = getCompatibleAggs(props.agg); let customError; diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/controls/top_sort_field.tsx b/src/legacy/core_plugins/vis_default_editor/public/components/controls/top_sort_field.tsx index 6ca030d05b604b..1e5a4e187f19a9 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/controls/top_sort_field.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/components/controls/top_sort_field.tsx @@ -20,11 +20,11 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { Field } from 'src/plugins/data/public'; +import { IndexPatternField } from 'src/plugins/data/public'; import { FieldParamEditor } from './field'; import { AggParamEditorProps } from '../agg_param_props'; -function TopSortFieldParamEditor(props: AggParamEditorProps) { +function TopSortFieldParamEditor(props: AggParamEditorProps) { const customLabel = i18n.translate('visDefaultEditor.controls.sortOnLabel', { defaultMessage: 'Sort on', }); diff --git a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/reducers.ts b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/reducers.ts index 851263f0ed7020..6591aa5fb53d5f 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/reducers.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/components/sidebar/state/reducers.ts @@ -20,7 +20,7 @@ import { cloneDeep } from 'lodash'; import { Vis, VisState } from 'src/legacy/core_plugins/visualizations/public'; -import { AggConfigs, IAggConfig, AggGroupNames, move } from '../../../legacy_imports'; +import { AggConfigs, IAggConfig, AggGroupNames } from '../../../legacy_imports'; import { EditorStateActionTypes } from './constants'; import { getEnabledMetricAggsCount } from '../../agg_group_helper'; import { EditorAction } from './actions'; @@ -136,7 +136,8 @@ function editorStateReducer(state: VisState, action: EditorAction): VisState { case EditorStateActionTypes.REORDER_AGGS: { const { sourceAgg, destinationAgg } = action.payload; const destinationIndex = state.aggs.aggs.indexOf(destinationAgg); - const newAggs = move([...state.aggs.aggs], sourceAgg, destinationIndex); + const newAggs = [...state.aggs.aggs]; + newAggs.splice(destinationIndex, 0, newAggs.splice(state.aggs.aggs.indexOf(sourceAgg), 1)[0]); return { ...state, 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 48a1a6f9d21216..32ea71c0bc0052 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_default_editor/public/legacy_imports.ts b/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts index b7fd6b1e9ebb60..5e547eed1c9573 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts +++ b/src/legacy/core_plugins/vis_default_editor/public/legacy_imports.ts @@ -49,7 +49,4 @@ export { AggParamOption } from 'ui/agg_types'; export { CidrMask } from 'ui/agg_types'; export { PersistedState } from 'ui/persisted_state'; -export { getDocLink } from 'ui/documentation_links'; -export { documentationLinks } from 'ui/documentation_links/documentation_links'; -export { move } from 'ui/utils/collection'; export * from 'ui/vis/lib'; 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 72838d2d97421c..5729618b6ae072 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 365566503e25bd..b97c7ccbac0f7c 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/get_inner_angular.ts b/src/legacy/core_plugins/vis_type_table/public/get_inner_angular.ts index 18d8e7bc9d8bb9..6fb5658d8e8151 100644 --- a/src/legacy/core_plugins/vis_type_table/public/get_inner_angular.ts +++ b/src/legacy/core_plugins/vis_type_table/public/get_inner_angular.ts @@ -30,7 +30,6 @@ import { PaginateControlsDirectiveProvider, watchMultiDecorator, KbnAccessibleClickProvider, - StateManagementConfigProvider, configureAppAngularModule, } from './legacy_imports'; import { initAngularBootstrap } from '../../../../plugins/kibana_legacy/public'; @@ -72,22 +71,19 @@ function createLocalPrivateModule() { } function createLocalConfigModule(uiSettings: IUiSettingsClient) { - angular - .module('tableVisConfig', ['tableVisPrivate']) - .provider('stateManagementConfig', StateManagementConfigProvider) - .provider('config', function() { - return { - $get: () => ({ - get: (value: string) => { - return uiSettings ? uiSettings.get(value) : undefined; - }, - // set method is used in agg_table mocha test - set: (key: string, value: string) => { - return uiSettings ? uiSettings.set(key, value) : undefined; - }, - }), - }; - }); + angular.module('tableVisConfig', []).provider('config', function() { + return { + $get: () => ({ + get: (value: string) => { + return uiSettings ? uiSettings.get(value) : undefined; + }, + // set method is used in agg_table mocha test + set: (key: string, value: string) => { + return uiSettings ? uiSettings.set(key, value) : undefined; + }, + }), + }; + }); } function createLocalI18nModule() { diff --git a/src/legacy/core_plugins/vis_type_table/public/legacy_imports.ts b/src/legacy/core_plugins/vis_type_table/public/legacy_imports.ts index ed0a09e139b09f..cb44814897bcfc 100644 --- a/src/legacy/core_plugins/vis_type_table/public/legacy_imports.ts +++ b/src/legacy/core_plugins/vis_type_table/public/legacy_imports.ts @@ -24,8 +24,6 @@ export { IAggConfig, AggGroupNames, Schemas } from 'ui/agg_types'; export { PaginateDirectiveProvider } from 'ui/directives/paginate'; // @ts-ignore export { PaginateControlsDirectiveProvider } from 'ui/directives/paginate'; -// @ts-ignore -export { StateManagementConfigProvider } from 'ui/state_management/config_provider'; export { tabifyGetColumns } from 'ui/agg_response/tabify/_get_columns'; // @ts-ignore export { tabifyAggResponse } from 'ui/agg_response/tabify'; 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 a792fc98842f10..2d27a99bdd8af9 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_timelion/public/components/timelion_expression_input_helpers.test.ts b/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.test.ts index cf40d2f791fc28..2f99256e2a1920 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.test.ts +++ b/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.test.ts @@ -20,12 +20,12 @@ import { SUGGESTION_TYPE, suggest } from './timelion_expression_input_helpers'; import { getArgValueSuggestions } from '../helpers/arg_value_suggestions'; import { setIndexPatterns, setSavedObjectsClient } from '../helpers/plugin_services'; -import { IndexPatterns } from 'src/plugins/data/public'; +import { IndexPatternsContract } from 'src/plugins/data/public'; import { SavedObjectsClient } from 'kibana/public'; import { ITimelionFunction } from '../../../../../plugins/timelion/common/types'; describe('Timelion expression suggestions', () => { - setIndexPatterns({} as IndexPatterns); + setIndexPatterns({} as IndexPatternsContract); setSavedObjectsClient({} as SavedObjectsClient); const argValueSuggestions = getArgValueSuggestions(); diff --git a/src/legacy/core_plugins/vis_type_timelion/public/helpers/arg_value_suggestions.ts b/src/legacy/core_plugins/vis_type_timelion/public/helpers/arg_value_suggestions.ts index 56562121397ce9..95e01f9c8db5b3 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/helpers/arg_value_suggestions.ts +++ b/src/legacy/core_plugins/vis_type_timelion/public/helpers/arg_value_suggestions.ts @@ -20,7 +20,7 @@ import { get } from 'lodash'; import { getIndexPatterns, getSavedObjectsClient } from './plugin_services'; import { TimelionFunctionArgs } from '../../../../../plugins/timelion/common/types'; -import { isNestedField } from '../../../../../plugins/data/public'; +import { indexPatterns as indexPatternsUtils } from '../../../../../plugins/data/public'; export interface Location { min: number; @@ -122,7 +122,7 @@ export function getArgValueSuggestions() { field.aggregatable && 'number' === field.type && containsFieldName(valueSplit[1], field) && - !isNestedField(field) + !indexPatternsUtils.isNestedField(field) ); }) .map(field => { @@ -141,7 +141,7 @@ export function getArgValueSuggestions() { field.aggregatable && ['number', 'boolean', 'date', 'ip', 'string'].includes(field.type) && containsFieldName(partial, field) && - !isNestedField(field) + !indexPatternsUtils.isNestedField(field) ); }) .map(field => { @@ -157,7 +157,9 @@ export function getArgValueSuggestions() { return indexPattern.fields .filter(field => { return ( - 'date' === field.type && containsFieldName(partial, field) && !isNestedField(field) + 'date' === field.type && + containsFieldName(partial, field) && + !indexPatternsUtils.isNestedField(field) ); }) .map(field => { diff --git a/src/legacy/core_plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts b/src/legacy/core_plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts index 6ce2538567e5b7..603c911438f2a2 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts +++ b/src/legacy/core_plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { KIBANA_CONTEXT_NAME } from 'src/plugins/expressions/public'; import { VisParams } from 'src/legacy/core_plugins/visualizations/public'; -import { TimeRange, esFilters, esQuery, Query } from '../../../../../plugins/data/public'; +import { TimeRange, Filter, esQuery, Query } from '../../../../../plugins/data/public'; import { timezoneProvider } from '../legacy_imports'; import { TimelionVisDependencies } from '../plugin'; @@ -75,7 +75,7 @@ export function getTimelionRequestHandler({ visParams, }: { timeRange: TimeRange; - filters: esFilters.Filter[]; + filters: Filter[]; query: Query; visParams: VisParams; forceFetch?: boolean; diff --git a/src/legacy/core_plugins/vis_type_vega/index.ts b/src/legacy/core_plugins/vis_type_vega/index.ts index 52c253c6ac0b52..ccef24f8f97465 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 b2ad45b5d7b6d9..868e5729bd494a 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 a7928c7d65e816..38ce706ed13ef6 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 9721de9848cfca..b354433330caf8 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 afb476472a2730..2a0da81a31a96c 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/vis_type_vega/public/vega_request_handler.ts b/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts index 576786567a6f9d..f63efc0007c3bc 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts @@ -19,7 +19,7 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getSearchService } from '../../../../plugins/data/public/services'; -import { esFilters, esQuery, TimeRange, Query } from '../../../../plugins/data/public'; +import { Filter, esQuery, TimeRange, Query } from '../../../../plugins/data/public'; // @ts-ignore import { VegaParser } from './data_model/vega_parser'; @@ -33,7 +33,7 @@ import { VisParams } from './vega_fn'; interface VegaRequestHandlerParams { query: Query; - filters: esFilters.Filter; + filters: Filter; timeRange: TimeRange; visParams: VisParams; } diff --git a/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts b/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts index 50a91df01de7c8..3d04c04f9b1a6e 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts @@ -18,10 +18,6 @@ */ export { AggType, AggGroupNames, IAggConfig, IAggType, Schemas } from 'ui/agg_types'; -// @ts-ignore -export { SimpleEmitter } from 'ui/utils/simple_emitter'; -// @ts-ignore -export { Binder } from 'ui/binder'; export { getFormat, getTableAggs } from 'ui/visualize/loader/pipeline_helpers/utilities'; // @ts-ignore export { tabifyAggResponse } from 'ui/agg_response/tabify'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch.js index 760c2e80d84284..a5d8eb80419a11 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch.js @@ -25,7 +25,6 @@ import expect from '@kbn/expect'; import data from './fixtures/mock_data/date_histogram/_series'; import { getVis, getMockUiState } from './fixtures/_vis_fixture'; -import { SimpleEmitter } from '../../../legacy_imports'; describe('Vislib Dispatch Class Test Suite', function() { function destroyVis(vis) { @@ -54,11 +53,13 @@ describe('Vislib Dispatch Class Test Suite', function() { destroyVis(vis); }); - it('extends the SimpleEmitter class', function() { + it('implements on, off, emit methods', function() { const events = _.pluck(vis.handler.charts, 'events'); expect(events.length).to.be.above(0); events.forEach(function(dispatch) { - expect(dispatch).to.be.a(SimpleEmitter); + expect(dispatch).to.have.property('on'); + expect(dispatch).to.have.property('off'); + expect(dispatch).to.have.property('emit'); }); }); }); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/tooltip.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/tooltip.js index f7d29164eec6f1..2c482db0a9dd94 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/tooltip.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/tooltip.js @@ -21,7 +21,7 @@ import d3 from 'd3'; import _ from 'lodash'; import $ from 'jquery'; -import { Binder } from '../../../legacy_imports'; +import { Binder } from '../../lib/binder'; import { positionTooltip } from './position_tooltip'; import theme from '@elastic/eui/dist/eui_theme_light.json'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/binder.ts b/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/binder.ts new file mode 100644 index 00000000000000..87221333c0ba50 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/binder.ts @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import d3 from 'd3'; +import $ from 'jquery'; +import { IScope } from 'angular'; + +export interface Emitter { + on: (...args: any[]) => void; + off: (...args: any[]) => void; + addListener: Emitter['on']; + removeListener: Emitter['off']; +} + +export class Binder { + private disposal: Array<() => void> = []; + + constructor($scope: IScope) { + // support auto-binding to $scope objects + if ($scope) { + $scope.$on('$destroy', () => this.destroy()); + } + } + + public on(emitter: Emitter, ...args: any[]) { + const on = emitter.on || emitter.addListener; + const off = emitter.off || emitter.removeListener; + + on.apply(emitter, args); + this.disposal.push(() => off.apply(emitter, args)); + } + + public destroy() { + const destroyers = this.disposal; + this.disposal = []; + destroyers.forEach(fn => fn()); + } + + jqOn(el: HTMLElement, ...args: [string, (event: JQueryEventObject) => void]) { + const $el = $(el); + $el.on(...args); + this.disposal.push(() => $el.off(...args)); + } + + fakeD3Bind(el: HTMLElement, event: string, handler: (event: JQueryEventObject) => void) { + this.jqOn(el, event, (e: JQueryEventObject) => { + // mimic https://github.com/mbostock/d3/blob/3abb00113662463e5c19eb87cd33f6d0ddc23bc0/src/selection/on.js#L87-L94 + const o = d3.event; // Events can be reentrant (e.g., focus). + d3.event = e; + try { + // @ts-ignore + handler.apply(this, [this.__data__]); + } finally { + d3.event = o; + } + }); + } +} diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/dispatch.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/dispatch.js index 404f7ef82d97f9..b36ba336dbfe57 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/dispatch.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/dispatch.js @@ -18,11 +18,9 @@ */ import d3 from 'd3'; -import { get } from 'lodash'; +import { get, pull, restParam, size, reduce } from 'lodash'; import $ from 'jquery'; -import { SimpleEmitter } from '../../legacy_imports'; - /** * Handles event responses * @@ -30,14 +28,112 @@ import { SimpleEmitter } from '../../legacy_imports'; * @constructor * @param handler {Object} Reference to Handler Class Object */ -export class Dispatch extends SimpleEmitter { +export class Dispatch { constructor(handler, uiSettings) { - super(); this.handler = handler; this.uiSettings = uiSettings; this._listeners = {}; } + /** + * Add an event handler + * + * @param {string} name + * @param {function} handler + * @return {Dispatch} - this, for chaining + */ + on(name, handler) { + let handlers = this._listeners[name]; + if (!handlers) { + this._listeners[name] = []; + handlers = this._listeners[name]; + } + + handlers.push(handler); + + return this; + } + + /** + * Remove an event handler + * + * @param {string} name + * @param {function} [handler] - optional handler to remove, if no handler is + * passed then all are removed + * @return {Dispatch} - this, for chaining + */ + off(name, handler) { + if (!this._listeners[name]) { + return this; + } + + // remove a specific handler + if (handler) { + pull(this._listeners[name], handler); + } + // or remove all listeners + else { + this._listeners[name] = null; + } + + return this; + } + + /** + * Remove all event listeners bound to this emitter. + * + * @return {Dispatch} - this, for chaining + */ + removeAllListeners() { + this._listeners = {}; + return this; + } + + /** + * Emit an event and all arguments to all listeners for an event name + * + * @param {string} name + * @param {*} [arg...] - any number of arguments that will be applied to each handler + * @return {Dispatch} - this, for chaining + */ + emit = restParam(function(name, args) { + if (!this._listeners[name]) { + return this; + } + const listeners = this.listeners(name); + let i = -1; + + while (++i < listeners.length) { + listeners[i].apply(this, args); + } + + return this; + }); + + /** + * Get a list of the handler functions for a specific event + * + * @param {string} name + * @return {array[function]} + */ + listeners(name) { + return this._listeners[name] ? this._listeners[name].slice(0) : []; + } + + /** + * Get the count of handlers for a specific event + * + * @param {string} [name] - optional event name to filter by + * @return {number} + */ + listenerCount(name) { + if (name) { + return size(this._listeners[name]); + } + + return reduce(this._listeners, (count, handlers) => count + size(handlers), 0); + } + _pieClickResponse(data) { const points = []; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/handler.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/handler.js index b887b61578cc41..ecf67ee3e017c5 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/handler.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/handler.js @@ -28,7 +28,7 @@ import { Alerts } from './alerts'; import { Axis } from './axis/axis'; import { ChartGrid as Grid } from './chart_grid'; import { visTypes as chartTypes } from '../visualizations/vis_types'; -import { Binder } from '../../legacy_imports'; +import { Binder } from './binder'; import { dispatchRenderComplete } from '../../../../../../plugins/kibana_utils/public'; const markdownIt = new MarkdownIt({ diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/query_geohash_bounds.ts b/src/legacy/core_plugins/visualizations/public/embeddable/query_geohash_bounds.ts index 719d69e21a826d..f37bc858efab09 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/query_geohash_bounds.ts +++ b/src/legacy/core_plugins/visualizations/public/embeddable/query_geohash_bounds.ts @@ -24,10 +24,10 @@ import { toastNotifications } from 'ui/notify'; import { IAggConfig } from 'ui/agg_types'; import { timefilter } from 'ui/timefilter'; import { Vis } from '../np_ready/public'; -import { esFilters, Query, SearchSource, ISearchSource } from '../../../../../plugins/data/public'; +import { Filter, Query, SearchSource, ISearchSource } from '../../../../../plugins/data/public'; interface QueryGeohashBoundsParams { - filters?: esFilters.Filter[]; + filters?: Filter[]; query?: Query; searchSource?: ISearchSource; } @@ -78,7 +78,7 @@ export async function queryGeohashBounds(vis: Vis, params: QueryGeohashBoundsPar const useTimeFilter = !!indexPattern.timeFieldName; if (useTimeFilter) { const filter = timefilter.createFilter(indexPattern); - if (filter) activeFilters.push((filter as any) as esFilters.Filter); + if (filter) activeFilters.push((filter as any) as Filter); } return activeFilters; }); 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 049dec792ff4d2..5e593398333c9b 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'; @@ -31,8 +31,8 @@ import { IIndexPattern, TimeRange, Query, - onlyDisabledFiltersChanged, esFilters, + Filter, ISearchSource, } from '../../../../../plugins/data/public'; import { @@ -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; @@ -75,7 +74,7 @@ export interface VisualizeEmbeddableConfiguration { export interface VisualizeInput extends EmbeddableInput { timeRange?: TimeRange; query?: Query; - filters?: esFilters.Filter[]; + filters?: Filter[]; vis?: { colors?: { [key: string]: string }; }; @@ -100,7 +99,7 @@ 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.test.js b/src/legacy/server/sass/build.test.js index 7092f6ad129217..46a898c30f84e1 100644 --- a/src/legacy/server/sass/build.test.js +++ b/src/legacy/server/sass/build.test.js @@ -47,28 +47,7 @@ it('builds light themed SASS', async () => { expect(readFileSync(targetPath, 'utf8').replace(/(\/\*# sourceMappingURL=).*( \*\/)/, '$1...$2')) .toMatchInlineSnapshot(` - "/* 1 */ - /* 1 */ - /** - * 1. Extend beta badges to at least 40% of the container's width - * 2. Fix for IE to ensure badges are visible outside of a