diff --git a/.eslintignore b/.eslintignore index 93c69b4f9b207..7f3e3ef597cbb 100644 --- a/.eslintignore +++ b/.eslintignore @@ -19,19 +19,14 @@ target # plugin overrides /src/core/lib/kbn_internal_native_observable /src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken -/src/legacy/ui/public/flot-charts /src/plugins/data/common/es_query/kuery/ast/_generated_/** /src/plugins/vis_type_timelion/public/_generated_/** -/src/plugins/vis_type_timelion/public/flot/jquery.flot.* -/src/plugins/timelion/public/flot/jquery.flot.* /x-pack/legacy/plugins/**/__tests__/fixtures/** /x-pack/plugins/apm/e2e/**/snapshots.js /x-pack/plugins/apm/e2e/tmp/* /x-pack/plugins/canvas/canvas_plugin -/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts /x-pack/plugins/canvas/shareable_runtime/build /x-pack/plugins/canvas/storybook/build -/x-pack/plugins/monitoring/public/lib/jquery_flot /x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/** /x-pack/legacy/plugins/infra/common/graphql/types.ts /x-pack/legacy/plugins/infra/public/graphql/types.ts @@ -48,4 +43,4 @@ target /packages/kbn-ui-framework/dist /packages/kbn-ui-framework/doc_site/build /packages/kbn-ui-framework/generator-kui/*/templates/ - +/packages/kbn-ui-shared-deps/flot_charts diff --git a/.eslintrc.js b/.eslintrc.js index 27dacd51be6f2..24ae50791d91d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1178,13 +1178,7 @@ module.exports = { }, }, { - files: ['x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/**/*.js'], - env: { - jquery: true, - }, - }, - { - files: ['x-pack/plugins/monitoring/public/lib/jquery_flot/**/*.js'], + files: ['packages/kbn-ui-shared-deps/flot_charts/**/*.js'], env: { jquery: true, }, diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5dd41581914ed..8f2c27ac7c3cf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -6,20 +6,16 @@ # used for the 'team' designator within Kibana Stats # App -/x-pack/plugins/dashboard_enhanced/ @elastic/kibana-app /x-pack/plugins/discover_enhanced/ @elastic/kibana-app /x-pack/plugins/lens/ @elastic/kibana-app /x-pack/plugins/graph/ @elastic/kibana-app /src/plugins/advanced_settings/ @elastic/kibana-app /src/plugins/charts/ @elastic/kibana-app -/src/plugins/dashboard/ @elastic/kibana-app /src/plugins/discover/ @elastic/kibana-app -/src/plugins/input_control_vis/ @elastic/kibana-app /src/plugins/management/ @elastic/kibana-app /src/plugins/kibana_legacy/ @elastic/kibana-app /src/plugins/timelion/ @elastic/kibana-app /src/plugins/vis_default_editor/ @elastic/kibana-app -/src/plugins/vis_type_markdown/ @elastic/kibana-app /src/plugins/vis_type_metric/ @elastic/kibana-app /src/plugins/vis_type_table/ @elastic/kibana-app /src/plugins/vis_type_tagcloud/ @elastic/kibana-app @@ -35,10 +31,8 @@ #CC# /src/legacy/core_plugins/kibana/common/utils @elastic/kibana-app #CC# /src/legacy/core_plugins/kibana/migrations @elastic/kibana-app #CC# /src/legacy/core_plugins/kibana/public @elastic/kibana-app -#CC# /src/legacy/core_plugins/kibana/public/dashboard/ @elastic/kibana-app #CC# /src/legacy/core_plugins/kibana/public/discover/ @elastic/kibana-app #CC# /src/legacy/core_plugins/kibana/public/local_application_service/ @elastic/kibana-app -#CC# /src/legacy/core_plugins/input_control_vis @elastic/kibana-app #CC# /src/legacy/core_plugins/timelion @elastic/kibana-app #CC# /src/legacy/core_plugins/vis_type_tagcloud @elastic/kibana-app #CC# /src/legacy/core_plugins/vis_type_vega @elastic/kibana-app @@ -46,8 +40,6 @@ #CC# /src/legacy/server/url_shortening/ @elastic/kibana-app #CC# /src/legacy/ui/public/state_management @elastic/kibana-app #CC# /src/plugins/index_pattern_management/public @elastic/kibana-app -#CC# /x-pack/legacy/plugins/dashboard_mode/ @elastic/kibana-app -#CC# /x-pack/plugins/dashboard_mode @elastic/kibana-app # App Architecture /examples/bfetch_explorer/ @elastic/kibana-app-arch @@ -127,10 +119,18 @@ #CC# /x-pack/plugins/beats_management/ @elastic/beats # Canvas +/src/plugins/dashboard/ @elastic/kibana-app +/src/plugins/input_control_vis/ @elastic/kibana-app +/src/plugins/vis_type_markdown/ @elastic/kibana-app /x-pack/plugins/canvas/ @elastic/kibana-canvas +/x-pack/plugins/dashboard_enhanced/ @elastic/kibana-app /x-pack/test/functional/apps/canvas/ @elastic/kibana-canvas +#CC# /src/legacy/core_plugins/kibana/public/dashboard/ @elastic/kibana-app +#CC# /src/legacy/core_plugins/input_control_vis @elastic/kibana-app #CC# /src/plugins/kibana_react/public/code_editor/ @elastic/kibana-canvas #CC# /x-pack/legacy/plugins/canvas/ @elastic/kibana-canvas +#CC# /x-pack/plugins/dashboard_mode @elastic/kibana-app +#CC# /x-pack/legacy/plugins/dashboard_mode/ @elastic/kibana-app # Core UI # Exclude tutorials folder for now because they are not owned by Kibana app and most will move out soon diff --git a/.github/ISSUE_TEMPLATE/security_solution_bug_report.md b/.github/ISSUE_TEMPLATE/security_solution_bug_report.md new file mode 100644 index 0000000000000..a7a12a6eb379d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/security_solution_bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report for Security Solution +about: Help us identify bugs in Elastic Security, SIEM, and Endpoint so we can fix them! +title: '[Security Solution]' +labels: 'Team: Security Solution' +--- + +**Describe the bug:** + +**Kibana/Elasticsearch Stack version:** + +**Server OS version:** + +**Browser and Browser OS versions:** + +**Elastic Endpoint version:** + +**Original install method (e.g. download page, yum, from source, etc.):** + +**Functional Area (e.g. Endpoint management, timelines, resolver, etc.):** + +**Steps to reproduce:** + +1. +2. +3. + +**Current behavior:** + +**Expected behavior:** + +**Screenshots (if relevant):** + +**Errors in browser console (if relevant):** + +**Provide logs and/or server output (if relevant):** + +**Any additional context (logs, chat logs, magical formulas, etc.):** diff --git a/.i18nrc.json b/.i18nrc.json index e0281b0a5bc21..68e38d3976a68 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -11,6 +11,7 @@ "uiActionsExamples": "examples/ui_action_examples", "share": "src/plugins/share", "home": "src/plugins/home", + "flot": "packages/kbn-ui-shared-deps/flot_charts", "charts": "src/plugins/charts", "esUi": "src/plugins/es_ui_shared", "devTools": "src/plugins/dev_tools", diff --git a/NOTICE.txt b/NOTICE.txt index 24940e232e88f..0504b7f7d6db2 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -118,212 +118,6 @@ THE SOFTWARE. This product uses Noto fonts that are licensed under the SIL Open Font License, Version 1.1. ---- -We include the `firstValueFrom()` and `lastValueFrom()` helpers -extracted from the v7-beta.7 version of the RxJS library. - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - --- Based on the scroll-into-view-if-necessary module from npm https://github.com/stipsan/compute-scroll-into-view/blob/master/src/index.ts#L269-L340 diff --git a/docs/developer/best-practices/typescript.asciidoc b/docs/developer/best-practices/typescript.asciidoc index a2cda1e0b1e87..583a98f296de5 100644 --- a/docs/developer/best-practices/typescript.asciidoc +++ b/docs/developer/best-practices/typescript.asciidoc @@ -28,7 +28,7 @@ This architecture imposes several limitations to which we must comply: [discrete] ==== Prerequisites Since project refs rely on generated `d.ts` files, the migration order does matter. You can migrate your plugin only when all the plugin dependencies already have migrated. It creates a situation where commonly used plugins (such as `data` or `kibana_react`) have to migrate first. -https://github.com/elastic/kibana/issues/79343 is going to provide a tool for identifying a plugin dependency tree. +Run `node scripts/find_plugins_without_ts_refs.js --id your_plugin_id` to get a list of plugins that should be switched to TS project refs to unblock your plugin migration. [discrete] ==== Implementation diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 21c51f8cabd32..3e849ca80db72 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -435,8 +435,9 @@ using the CURL scripts in the scripts folder. |This plugin provides access to the detailed tile map services from Elastic. -|{kib-repo}blob/{branch}/x-pack/plugins/ml[ml] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/ml/readme.md[ml] +|This plugin provides access to the machine learning features provided by +Elastic. |{kib-repo}blob/{branch}/x-pack/plugins/monitoring[monitoring] @@ -498,8 +499,8 @@ routes, etc. |Gathers all usage collection, retrieving them from both: OSS and X-Pack plugins. -|{kib-repo}blob/{branch}/x-pack/plugins/transform[transform] -|WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/transform/readme.md[transform] +|This plugin provides access to the transforms features provided by Elastic. |{kib-repo}blob/{branch}/x-pack/plugins/translations[translations] diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md index c9018b0048aa3..76d0914173447 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.md @@ -15,5 +15,6 @@ export interface ISearchOptions | Property | Type | Description | | --- | --- | --- | | [abortSignal](./kibana-plugin-plugins-data-public.isearchoptions.abortsignal.md) | AbortSignal | An AbortSignal that allows the caller of search to abort a search request. | +| [sessionId](./kibana-plugin-plugins-data-public.isearchoptions.sessionid.md) | string | A session ID, grouping multiple search requests into a single session. | | [strategy](./kibana-plugin-plugins-data-public.isearchoptions.strategy.md) | string | Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.sessionid.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.sessionid.md new file mode 100644 index 0000000000000..b1d569e58bf1d --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchoptions.sessionid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) > [sessionId](./kibana-plugin-plugins-data-public.isearchoptions.sessionid.md) + +## ISearchOptions.sessionId property + +A session ID, grouping multiple search requests into a single session. + +Signature: + +```typescript +sessionId?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md index b68c4d61e4e03..bbf856480aedd 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md @@ -17,5 +17,6 @@ export interface ISearchSetup | Property | Type | Description | | --- | --- | --- | | [aggs](./kibana-plugin-plugins-data-public.isearchsetup.aggs.md) | AggsSetup | | +| [session](./kibana-plugin-plugins-data-public.isearchsetup.session.md) | ISessionService | session management | | [usageCollector](./kibana-plugin-plugins-data-public.isearchsetup.usagecollector.md) | SearchUsageCollector | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md new file mode 100644 index 0000000000000..7f39d9714a3a3 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.session.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchSetup](./kibana-plugin-plugins-data-public.isearchsetup.md) > [session](./kibana-plugin-plugins-data-public.isearchsetup.session.md) + +## ISearchSetup.session property + +session management + +Signature: + +```typescript +session: ISessionService; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md index 5defe4a647614..4a69e94dd6f58 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md @@ -19,5 +19,6 @@ export interface ISearchStart | [aggs](./kibana-plugin-plugins-data-public.isearchstart.aggs.md) | AggsStart | agg config sub service [AggsStart](./kibana-plugin-plugins-data-public.aggsstart.md) | | [search](./kibana-plugin-plugins-data-public.isearchstart.search.md) | ISearchGeneric | low level search [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | | [searchSource](./kibana-plugin-plugins-data-public.isearchstart.searchsource.md) | ISearchStartSearchSource | high level search [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) | +| [session](./kibana-plugin-plugins-data-public.isearchstart.session.md) | ISessionService | session management | | [showError](./kibana-plugin-plugins-data-public.isearchstart.showerror.md) | (e: Error) => void | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md new file mode 100644 index 0000000000000..de25cccd6d27a --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.session.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) > [session](./kibana-plugin-plugins-data-public.isearchstart.session.md) + +## ISearchStart.session property + +session management + +Signature: + +```typescript +session: ISessionService; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md index 02db74b1a9e91..1c8b6eb41a72e 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md @@ -7,7 +7,7 @@ Signature: ```typescript -protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal: AbortSignal, appAbortSignal?: AbortSignal): Error; +protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal: AbortSignal, options?: ISearchOptions): Error; ``` ## Parameters @@ -17,7 +17,7 @@ protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal | e | any | | | request | IKibanaSearchRequest | | | timeoutSignal | AbortSignal | | -| appAbortSignal | AbortSignal | | +| options | ISearchOptions | | Returns: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md index a02a6116d7ae0..40c7055e4c059 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md @@ -27,7 +27,7 @@ export declare class SearchInterceptor | Method | Modifiers | Description | | --- | --- | --- | | [getTimeoutMode()](./kibana-plugin-plugins-data-public.searchinterceptor.gettimeoutmode.md) | | | -| [handleSearchError(e, request, timeoutSignal, appAbortSignal)](./kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md) | | | +| [handleSearchError(e, request, timeoutSignal, options)](./kibana-plugin-plugins-data-public.searchinterceptor.handlesearcherror.md) | | | | [search(request, options)](./kibana-plugin-plugins-data-public.searchinterceptor.search.md) | | Searches using the given search method. Overrides the AbortSignal with one that will abort either when cancelPending is called, when the request times out, or when the original AbortSignal is aborted. Updates pendingCount$ when the request is started/finalized. | | [showError(e)](./kibana-plugin-plugins-data-public.searchinterceptor.showerror.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md index 63eb67ce48246..3653394d28b92 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md @@ -15,6 +15,7 @@ export interface SearchInterceptorDeps | Property | Type | Description | | --- | --- | --- | | [http](./kibana-plugin-plugins-data-public.searchinterceptordeps.http.md) | CoreSetup['http'] | | +| [session](./kibana-plugin-plugins-data-public.searchinterceptordeps.session.md) | ISessionService | | | [startServices](./kibana-plugin-plugins-data-public.searchinterceptordeps.startservices.md) | Promise<[CoreStart, any, unknown]> | | | [toasts](./kibana-plugin-plugins-data-public.searchinterceptordeps.toasts.md) | ToastsSetup | | | [uiSettings](./kibana-plugin-plugins-data-public.searchinterceptordeps.uisettings.md) | CoreSetup['uiSettings'] | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.session.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.session.md new file mode 100644 index 0000000000000..40d00483317ba --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.session.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) > [session](./kibana-plugin-plugins-data-public.searchinterceptordeps.session.md) + +## SearchInterceptorDeps.session property + +Signature: + +```typescript +session: ISessionService; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md index 21ddaef3a0b94..af96e1413ba0c 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.md @@ -15,5 +15,6 @@ export interface ISearchOptions | Property | Type | Description | | --- | --- | --- | | [abortSignal](./kibana-plugin-plugins-data-server.isearchoptions.abortsignal.md) | AbortSignal | An AbortSignal that allows the caller of search to abort a search request. | +| [sessionId](./kibana-plugin-plugins-data-server.isearchoptions.sessionid.md) | string | A session ID, grouping multiple search requests into a single session. | | [strategy](./kibana-plugin-plugins-data-server.isearchoptions.strategy.md) | string | Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.sessionid.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.sessionid.md new file mode 100644 index 0000000000000..03043de5193d2 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchoptions.sessionid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) > [sessionId](./kibana-plugin-plugins-data-server.isearchoptions.sessionid.md) + +## ISearchOptions.sessionId property + +A session ID, grouping multiple search requests into a single session. + +Signature: + +```typescript +sessionId?: string; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.range.label.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.range.label.md new file mode 100644 index 0000000000000..26d1e7810f9e7 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.range.label.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [Range](./kibana-plugin-plugins-expressions-public.range.md) > [label](./kibana-plugin-plugins-expressions-public.range.label.md) + +## Range.label property + +Signature: + +```typescript +label?: string; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.range.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.range.md index cf0cf4cb50b71..83d4b9bd35090 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.range.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.range.md @@ -15,6 +15,7 @@ export interface Range | Property | Type | Description | | --- | --- | --- | | [from](./kibana-plugin-plugins-expressions-public.range.from.md) | number | | +| [label](./kibana-plugin-plugins-expressions-public.range.label.md) | string | | | [to](./kibana-plugin-plugins-expressions-public.range.to.md) | number | | | [type](./kibana-plugin-plugins-expressions-public.range.type.md) | typeof name | | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.range.label.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.range.label.md new file mode 100644 index 0000000000000..767f6011290a1 --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.range.label.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [Range](./kibana-plugin-plugins-expressions-server.range.md) > [label](./kibana-plugin-plugins-expressions-server.range.label.md) + +## Range.label property + +Signature: + +```typescript +label?: string; +``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.range.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.range.md index d369d882757fc..4e6ae12217f2e 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.range.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.range.md @@ -15,6 +15,7 @@ export interface Range | Property | Type | Description | | --- | --- | --- | | [from](./kibana-plugin-plugins-expressions-server.range.from.md) | number | | +| [label](./kibana-plugin-plugins-expressions-server.range.label.md) | string | | | [to](./kibana-plugin-plugins-expressions-server.range.to.md) | number | | | [type](./kibana-plugin-plugins-expressions-server.range.type.md) | typeof name | | diff --git a/docs/fleet/fleet.asciidoc b/docs/fleet/fleet.asciidoc index 7039468f4b185..06b2b96c0035c 100644 --- a/docs/fleet/fleet.asciidoc +++ b/docs/fleet/fleet.asciidoc @@ -3,7 +3,7 @@ [[fleet]] = {fleet} -experimental[] +beta[] {fleet} in {kib} enables you to add and manage integrations for popular services and platforms, as well as manage {elastic-agent} installations in diff --git a/examples/ui_actions_explorer/public/context_menu_examples/panel_edit_with_drilldowns_and_context_actions.tsx b/examples/ui_actions_explorer/public/context_menu_examples/panel_edit_with_drilldowns_and_context_actions.tsx index e9543814ff015..5ef2cb73b5937 100644 --- a/examples/ui_actions_explorer/public/context_menu_examples/panel_edit_with_drilldowns_and_context_actions.tsx +++ b/examples/ui_actions_explorer/public/context_menu_examples/panel_edit_with_drilldowns_and_context_actions.tsx @@ -39,7 +39,7 @@ export const PanelEditWithDrilldownsAndContextActions: React.FC = () => { const customActionGrouping: Action['grouping'] = [ { id: 'actions', - getDisplayName: () => 'Custom actions', + getDisplayName: () => 'API actions', getIconType: () => 'cloudStormy', order: 20, }, diff --git a/package.json b/package.json index 732ee1fd3038b..8a87598aec56d 100644 --- a/package.json +++ b/package.json @@ -131,10 +131,7 @@ "@kbn/i18n": "1.0.0", "@kbn/interpreter": "1.0.0", "@kbn/logging": "1.0.0", - "@kbn/pm": "1.0.0", "@kbn/std": "1.0.0", - "@kbn/telemetry-tools": "1.0.0", - "@kbn/test-subj-selector": "0.2.1", "@kbn/ui-framework": "1.0.0", "@kbn/ace": "1.0.0", "@kbn/monaco": "1.0.0", @@ -247,12 +244,22 @@ "@kbn/expect": "1.0.0", "@kbn/optimizer": "1.0.0", "@kbn/plugin-generator": "1.0.0", + "@kbn/pm": "1.0.0", "@kbn/release-notes": "1.0.0", + "@kbn/telemetry-tools": "1.0.0", "@kbn/test": "1.0.0", + "@kbn/test-subj-selector": "0.2.1", "@kbn/utility-types": "1.0.0", "@microsoft/api-documenter": "7.7.2", "@microsoft/api-extractor": "7.7.0", "@percy/agent": "^0.26.0", + "@storybook/addon-a11y": "^6.0.26", + "@storybook/addon-actions": "^6.0.26", + "@storybook/addon-essentials": "^6.0.26", + "@storybook/addon-knobs": "^6.0.26", + "@storybook/addon-storyshots": "^6.0.26", + "@storybook/react": "^6.0.26", + "@storybook/theming": "^6.0.26", "@testing-library/dom": "^7.24.2", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.0.4", @@ -301,6 +308,7 @@ "@types/json5": "^0.0.30", "@types/license-checker": "15.0.0", "@types/listr": "^0.14.0", + "@types/loader-utils": "^1.1.3", "@types/lodash": "^4.14.159", "@types/lru-cache": "^5.1.0", "@types/markdown-it": "^0.0.7", @@ -346,6 +354,7 @@ "@types/vinyl-fs": "^2.4.11", "@types/webpack": "^4.41.3", "@types/webpack-env": "^1.15.2", + "@types/webpack-merge": "^4.1.5", "@types/zen-observable": "^0.8.0", "@typescript-eslint/eslint-plugin": "^3.10.0", "@typescript-eslint/parser": "^3.10.0", diff --git a/packages/elastic-eslint-config-kibana/package.json b/packages/elastic-eslint-config-kibana/package.json index 3f2c6e9edb261..9d0d579086543 100644 --- a/packages/elastic-eslint-config-kibana/package.json +++ b/packages/elastic-eslint-config-kibana/package.json @@ -7,6 +7,9 @@ "type": "git", "url": "git+https://github.com/elastic/kibana.git" }, + "kibana": { + "devOnly": true + }, "keywords": [], "author": "Spencer Alger ", "license": "Apache-2.0", diff --git a/packages/kbn-babel-preset/package.json b/packages/kbn-babel-preset/package.json index 79d2fd8687dae..2fab970c5c71f 100644 --- a/packages/kbn-babel-preset/package.json +++ b/packages/kbn-babel-preset/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "private": true, "license": "Apache-2.0", + "kibana": { + "devOnly": true + }, "dependencies": { "@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/plugin-proposal-export-namespace-from": "^7.10.4", diff --git a/packages/kbn-config/package.json b/packages/kbn-config/package.json index 6d2d56b929ead..f994836af8847 100644 --- a/packages/kbn-config/package.json +++ b/packages/kbn-config/package.json @@ -12,10 +12,8 @@ "dependencies": { "@elastic/safer-lodash-set": "0.0.0", "@kbn/config-schema": "1.0.0", - "@kbn/dev-utils": "1.0.0", "@kbn/logging": "1.0.0", "@kbn/std": "1.0.0", - "@kbn/utility-types": "1.0.0", "js-yaml": "^3.14.0", "load-json-file": "^6.2.0", "lodash": "^4.17.20", @@ -24,6 +22,8 @@ "type-detect": "^4.0.8" }, "devDependencies": { + "@kbn/dev-utils": "1.0.0", + "@kbn/utility-types": "1.0.0", "typescript": "4.0.2", "tsd": "^0.13.1" } diff --git a/packages/kbn-config/src/env.ts b/packages/kbn-config/src/env.ts index e4585056696f9..e7b4658262235 100644 --- a/packages/kbn-config/src/env.ts +++ b/packages/kbn-config/src/env.ts @@ -19,6 +19,7 @@ import { resolve, join } from 'path'; import loadJsonFile from 'load-json-file'; +import { getPluginSearchPaths } from './plugins'; import { PackageInfo, EnvironmentMode } from './types'; /** @internal */ @@ -114,21 +115,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')]), - resolve(this.homeDir, 'plugins'), - ...(options.cliArgs.runExamples ? [resolve(this.homeDir, 'examples')] : []), - ...(options.cliArgs.runExamples && !options.cliArgs.oss - ? [resolve(this.homeDir, 'x-pack', 'examples')] - : []), - resolve(this.homeDir, '..', 'kibana-extra'), - ]; + this.pluginSearchPaths = getPluginSearchPaths({ + rootDir: this.homeDir, + oss: options.cliArgs.oss, + examples: options.cliArgs.runExamples, + }); this.cliArgs = Object.freeze(options.cliArgs); this.configs = Object.freeze(options.configs); diff --git a/packages/kbn-config/src/index.ts b/packages/kbn-config/src/index.ts index f02514a92e606..68609c6d5c7c3 100644 --- a/packages/kbn-config/src/index.ts +++ b/packages/kbn-config/src/index.ts @@ -35,3 +35,4 @@ export { ObjectToConfigAdapter } from './object_to_config_adapter'; export { CliArgs, Env, RawPackageInfo } from './env'; export { EnvironmentMode, PackageInfo } from './types'; export { LegacyObjectToConfigAdapter, LegacyLoggingConfig } from './legacy'; +export { getPluginSearchPaths } from './plugins'; diff --git a/packages/kbn-config/src/plugins/index.ts b/packages/kbn-config/src/plugins/index.ts new file mode 100644 index 0000000000000..7d02f9fb984c2 --- /dev/null +++ b/packages/kbn-config/src/plugins/index.ts @@ -0,0 +1,19 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { getPluginSearchPaths } from './plugin_search_paths'; diff --git a/packages/kbn-config/src/plugins/plugin_search_paths.ts b/packages/kbn-config/src/plugins/plugin_search_paths.ts new file mode 100644 index 0000000000000..a7d151c3275c8 --- /dev/null +++ b/packages/kbn-config/src/plugins/plugin_search_paths.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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'; + +interface SearchOptions { + rootDir: string; + oss: boolean; + examples: boolean; +} + +export function getPluginSearchPaths({ rootDir, oss, examples }: SearchOptions) { + return [ + resolve(rootDir, 'src', 'plugins'), + ...(oss ? [] : [resolve(rootDir, 'x-pack', 'plugins')]), + resolve(rootDir, 'plugins'), + ...(examples ? [resolve(rootDir, 'examples')] : []), + ...(examples && !oss ? [resolve(rootDir, 'x-pack', 'examples')] : []), + resolve(rootDir, '..', 'kibana-extra'), + ]; +} diff --git a/packages/kbn-dev-utils/package.json b/packages/kbn-dev-utils/package.json index a51734168cf76..7fd9a9e7d67e1 100644 --- a/packages/kbn-dev-utils/package.json +++ b/packages/kbn-dev-utils/package.json @@ -9,6 +9,9 @@ "kbn:bootstrap": "yarn build", "kbn:watch": "yarn build --watch" }, + "kibana": { + "devOnly": true + }, "dependencies": { "@babel/core": "^7.11.6", "@kbn/utils": "1.0.0", diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index 6a845825f0fd4..98385b49dafa9 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -40,7 +40,6 @@ export * from './axios'; export * from './stdio'; export * from './ci_stats_reporter'; export * from './plugin_list'; -export * from './simple_kibana_platform_plugin_discovery'; +export * from './plugins'; export * from './streams'; export * from './babel'; -export * from './parse_kibana_platform_plugin'; diff --git a/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts b/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts index e8f6735205b19..9782067e61343 100644 --- a/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts +++ b/packages/kbn-dev-utils/src/plugin_list/discover_plugins.ts @@ -24,7 +24,7 @@ import MarkdownIt from 'markdown-it'; import cheerio from 'cheerio'; import { REPO_ROOT } from '@kbn/utils'; -import { simpleKibanaPlatformPluginDiscovery } from '../simple_kibana_platform_plugin_discovery'; +import { simpleKibanaPlatformPluginDiscovery } from '../plugins'; import { extractAsciidocInfo } from './extract_asciidoc_info'; export interface Plugin { diff --git a/src/plugins/vis_type_timelion/public/flot/index.js b/packages/kbn-dev-utils/src/plugins/index.ts similarity index 78% rename from src/plugins/vis_type_timelion/public/flot/index.js rename to packages/kbn-dev-utils/src/plugins/index.ts index a066fd3ab8607..8705682f355c7 100644 --- a/src/plugins/vis_type_timelion/public/flot/index.js +++ b/packages/kbn-dev-utils/src/plugins/index.ts @@ -17,10 +17,5 @@ * under the License. */ -import './jquery.flot'; -import './jquery.flot.time'; -import './jquery.flot.symbol'; -import './jquery.flot.crosshair'; -import './jquery.flot.selection'; -import './jquery.flot.stack'; -import './jquery.flot.axislabels'; +export * from './parse_kibana_platform_plugin'; +export * from './simple_kibana_platform_plugin_discovery'; diff --git a/packages/kbn-dev-utils/src/parse_kibana_platform_plugin.ts b/packages/kbn-dev-utils/src/plugins/parse_kibana_platform_plugin.ts similarity index 56% rename from packages/kbn-dev-utils/src/parse_kibana_platform_plugin.ts rename to packages/kbn-dev-utils/src/plugins/parse_kibana_platform_plugin.ts index 83d8c2684d7ca..16aaecb3e478d 100644 --- a/packages/kbn-dev-utils/src/parse_kibana_platform_plugin.ts +++ b/packages/kbn-dev-utils/src/plugins/parse_kibana_platform_plugin.ts @@ -23,12 +23,27 @@ import loadJsonFile from 'load-json-file'; export interface KibanaPlatformPlugin { readonly directory: string; readonly manifestPath: string; - readonly manifest: { - id: string; - ui: boolean; - server: boolean; - [key: string]: unknown; - }; + readonly manifest: Manifest; +} + +function isValidDepsDeclaration(input: unknown, type: string): string[] { + if (typeof input === 'undefined') return []; + if (Array.isArray(input) && input.every((i) => typeof i === 'string')) { + return input; + } + throw new TypeError(`The "${type}" in plugin manifest should be an array of strings.`); +} + +interface Manifest { + id: string; + ui: boolean; + server: boolean; + kibanaVersion: string; + version: string; + requiredPlugins: readonly string[]; + optionalPlugins: readonly string[]; + requiredBundles: readonly string[]; + extraPublicDirs: readonly string[]; } export function parseKibanaPlatformPlugin(manifestPath: string): KibanaPlatformPlugin { @@ -36,7 +51,7 @@ export function parseKibanaPlatformPlugin(manifestPath: string): KibanaPlatformP throw new TypeError('expected new platform manifest path to be absolute'); } - const manifest = loadJsonFile.sync(manifestPath); + const manifest: Partial = 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'); } @@ -45,6 +60,10 @@ export function parseKibanaPlatformPlugin(manifestPath: string): KibanaPlatformP throw new TypeError('expected new platform plugin manifest to have a string id'); } + if (typeof manifest.version !== 'string') { + throw new TypeError('expected new platform plugin manifest to have a string version'); + } + return { directory: Path.dirname(manifestPath), manifestPath, @@ -54,6 +73,12 @@ export function parseKibanaPlatformPlugin(manifestPath: string): KibanaPlatformP ui: !!manifest.ui, server: !!manifest.server, id: manifest.id, + version: manifest.version, + kibanaVersion: manifest.kibanaVersion || manifest.version, + requiredPlugins: isValidDepsDeclaration(manifest.requiredPlugins, 'requiredPlugins'), + optionalPlugins: isValidDepsDeclaration(manifest.optionalPlugins, 'optionalPlugins'), + requiredBundles: isValidDepsDeclaration(manifest.requiredBundles, 'requiredBundles'), + extraPublicDirs: isValidDepsDeclaration(manifest.extraPublicDirs, 'extraPublicDirs'), }, }; } diff --git a/packages/kbn-dev-utils/src/simple_kibana_platform_plugin_discovery.ts b/packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts similarity index 100% rename from packages/kbn-dev-utils/src/simple_kibana_platform_plugin_discovery.ts rename to packages/kbn-dev-utils/src/plugins/simple_kibana_platform_plugin_discovery.ts diff --git a/packages/kbn-es-archiver/package.json b/packages/kbn-es-archiver/package.json index 81c1747bb2727..645abd6195909 100644 --- a/packages/kbn-es-archiver/package.json +++ b/packages/kbn-es-archiver/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "license": "Apache-2.0", "main": "target/index.js", + "kibana": { + "devOnly": true + }, "scripts": { "kbn:bootstrap": "rm -rf target && tsc", "kbn:watch": "rm -rf target && tsc --watch" diff --git a/packages/kbn-es/package.json b/packages/kbn-es/package.json index c3733094350be..6ed3ae2eb2fb7 100644 --- a/packages/kbn-es/package.json +++ b/packages/kbn-es/package.json @@ -4,6 +4,9 @@ "version": "1.0.0", "license": "Apache-2.0", "private": true, + "kibana": { + "devOnly": true + }, "scripts": { "kbn:bootstrap": "node scripts/build", "kbn:watch": "node scripts/build --watch" diff --git a/packages/kbn-eslint-import-resolver-kibana/package.json b/packages/kbn-eslint-import-resolver-kibana/package.json index 223c73e97908e..ffbd94810a405 100755 --- a/packages/kbn-eslint-import-resolver-kibana/package.json +++ b/packages/kbn-eslint-import-resolver-kibana/package.json @@ -5,6 +5,9 @@ "version": "2.0.0", "main": "import_resolver_kibana.js", "license": "Apache-2.0", + "kibana": { + "devOnly": true + }, "repository": { "type": "git", "url": "https://github.com/elastic/kibana/tree/master/packages/kbn-eslint-import-resolver-kibana" diff --git a/packages/kbn-eslint-plugin-eslint/package.json b/packages/kbn-eslint-plugin-eslint/package.json index 026938213ac83..72b8577cb0945 100644 --- a/packages/kbn-eslint-plugin-eslint/package.json +++ b/packages/kbn-eslint-plugin-eslint/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "private": true, "license": "Apache-2.0", + "kibana": { + "devOnly": true + }, "peerDependencies": { "eslint": "6.8.0", "babel-eslint": "^10.0.3" diff --git a/packages/kbn-expect/package.json b/packages/kbn-expect/package.json index 0975f5762fa1c..8ca37c7c88673 100644 --- a/packages/kbn-expect/package.json +++ b/packages/kbn-expect/package.json @@ -3,5 +3,8 @@ "main": "./expect.js", "version": "1.0.0", "license": "MIT", - "private": true + "private": true, + "kibana": { + "devOnly": true + } } diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index c9e414dbc5177..63146fc7a1834 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -14,6 +14,7 @@ "@babel/core": "^7.11.6", "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", + "@kbn/config": "1.0.0", "@kbn/std": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", "autoprefixer": "^9.7.4", 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 index 33f53e336598d..a5e9f34a22aa6 100644 --- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json @@ -1,5 +1,6 @@ { "id": "bar", "ui": true, - "requiredBundles": ["foo"] + "requiredBundles": ["foo"], + "version": "8.0.0" } 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 index 256856181ccd8..27730df199887 100644 --- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/kibana.json +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/kibana.json @@ -1,4 +1,5 @@ { "id": "foo", - "ui": true + "ui": true, + "version": "8.0.0" } diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/nested/baz/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/nested/baz/kibana.json index 6e4e9c70a115c..a8f991ee11465 100644 --- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/nested/baz/kibana.json +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/nested/baz/kibana.json @@ -1,3 +1,4 @@ { - "id": "baz" + "id": "baz", + "version": "8.0.0" } 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 index b9e044523a6a5..d8a8b2e548e4a 100644 --- 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 @@ -1,3 +1,4 @@ { - "id": "test_baz" + "id": "test_baz", + "version": "8.0.0" } diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json index 10602d2e7981a..64ec7ff5ccf3e 100644 --- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json +++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/x-pack/baz/kibana.json @@ -1,4 +1,5 @@ { "id": "baz", - "ui": true + "ui": true, + "version": "8.0.0" } diff --git a/packages/kbn-optimizer/src/cli.ts b/packages/kbn-optimizer/src/cli.ts index a822773052cae..28b3e37380b4e 100644 --- a/packages/kbn-optimizer/src/cli.ts +++ b/packages/kbn-optimizer/src/cli.ts @@ -95,6 +95,11 @@ run( throw createFlagError('expected --filter to be one or more strings'); } + const focus = typeof flags.focus === 'string' ? [flags.focus] : flags.focus; + if (!Array.isArray(focus) || !focus.every((f) => typeof f === 'string')) { + throw createFlagError('expected --focus to be one or more strings'); + } + const validateLimits = flags['validate-limits'] ?? false; if (typeof validateLimits !== 'boolean') { throw createFlagError('expected --validate-limits to have no value'); @@ -118,6 +123,7 @@ run( inspectWorkers, includeCoreBundle, filter, + focus, }); if (validateLimits) { @@ -165,6 +171,7 @@ run( cache: true, 'inspect-workers': true, filter: [], + focus: [], }, help: ` --watch run the optimizer in watch mode @@ -173,6 +180,7 @@ run( --profile profile the webpack builds and write stats.json files to build outputs --no-core disable generating the core bundle --no-cache disable the cache + --focus just like --filter, except dependencies are automatically included, --filter applies to result --filter comma-separated list of bundle id filters, results from multiple flags are merged, * and ! are supported --no-examples don't build the example plugins --dist create bundles that are suitable for inclusion in the Kibana distributable, enabled when running with --update-limits diff --git a/packages/kbn-optimizer/src/optimizer/focus_bundles.test.ts b/packages/kbn-optimizer/src/optimizer/focus_bundles.test.ts new file mode 100644 index 0000000000000..0e31899e6e425 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/focus_bundles.test.ts @@ -0,0 +1,80 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { focusBundles } from './focus_bundles'; +import { Bundle } from '../common'; + +function createBundle(id: string, deps: ReturnType) { + const bundle = new Bundle({ + type: id === 'core' ? 'entry' : 'plugin', + id, + contextDir: Path.resolve('/kibana/plugins', id), + outputDir: Path.resolve('/kibana/plugins', id, 'target/public'), + publicDirNames: ['public'], + sourceRoot: Path.resolve('/kibana'), + }); + + jest.spyOn(bundle, 'readBundleDeps').mockReturnValue(deps); + + return bundle; +} + +const BUNDLES = [ + createBundle('core', { + implicit: [], + explicit: [], + }), + createBundle('foo', { + implicit: ['core'], + explicit: [], + }), + createBundle('bar', { + implicit: ['core'], + explicit: ['foo'], + }), + createBundle('baz', { + implicit: ['core'], + explicit: ['bar'], + }), + createBundle('box', { + implicit: ['core'], + explicit: ['foo'], + }), +]; + +function test(filters: string[]) { + return focusBundles(filters, BUNDLES) + .map((b) => b.id) + .sort((a, b) => a.localeCompare(b)) + .join(', '); +} + +it('returns all bundles when no focus filters are defined', () => { + expect(test([])).toMatchInlineSnapshot(`"bar, baz, box, core, foo"`); +}); + +it('includes a single instance of all implicit and explicit dependencies', () => { + expect(test(['core'])).toMatchInlineSnapshot(`"core"`); + expect(test(['foo'])).toMatchInlineSnapshot(`"core, foo"`); + expect(test(['bar'])).toMatchInlineSnapshot(`"bar, core, foo"`); + expect(test(['baz'])).toMatchInlineSnapshot(`"bar, baz, core, foo"`); + expect(test(['box'])).toMatchInlineSnapshot(`"box, core, foo"`); +}); diff --git a/packages/kbn-optimizer/src/optimizer/focus_bundles.ts b/packages/kbn-optimizer/src/optimizer/focus_bundles.ts new file mode 100644 index 0000000000000..67c6d02364668 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/focus_bundles.ts @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Bundle } from '../common'; +import { filterById } from './filter_by_id'; + +export function focusBundles(filters: string[], bundles: Bundle[]) { + if (!filters.length) { + return [...bundles]; + } + + const queue = new Set(filterById(filters, bundles)); + const focused: Bundle[] = []; + + for (const bundle of queue) { + focused.push(bundle); + + const { explicit, implicit } = bundle.readBundleDeps(); + const depIds = [...explicit, ...implicit]; + if (depIds.length) { + for (const dep of filterById(depIds, bundles)) { + queue.add(dep); + } + } + } + + return focused; +} diff --git a/packages/kbn-optimizer/src/optimizer/get_output_stats.ts b/packages/kbn-optimizer/src/optimizer/get_output_stats.ts index cc4cd05f42c3f..82d1a276ccdad 100644 --- a/packages/kbn-optimizer/src/optimizer/get_output_stats.ts +++ b/packages/kbn-optimizer/src/optimizer/get_output_stats.ts @@ -109,6 +109,11 @@ export function getMetrics(log: ToolingLog, config: OptimizerConfig) { id: bundle.id, value: sumSize(asyncChunks), }, + { + group: `async chunk count`, + id: bundle.id, + value: asyncChunks.length, + }, { group: `miscellaneous assets size`, id: bundle.id, diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts index 948ba520931e5..c3f3505197038 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts @@ -22,6 +22,7 @@ jest.mock('./kibana_platform_plugins.ts'); jest.mock('./get_plugin_bundles.ts'); jest.mock('../common/theme_tags.ts'); jest.mock('./filter_by_id.ts'); +jest.mock('./focus_bundles'); jest.mock('../limits.ts'); jest.mock('os', () => { @@ -121,6 +122,7 @@ describe('OptimizerConfig::parseOptions()', () => { "cache": true, "dist": false, "filters": Array [], + "focus": Array [], "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, @@ -149,6 +151,7 @@ describe('OptimizerConfig::parseOptions()', () => { "cache": false, "dist": false, "filters": Array [], + "focus": Array [], "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, @@ -177,6 +180,7 @@ describe('OptimizerConfig::parseOptions()', () => { "cache": true, "dist": false, "filters": Array [], + "focus": Array [], "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, @@ -207,6 +211,7 @@ describe('OptimizerConfig::parseOptions()', () => { "cache": true, "dist": false, "filters": Array [], + "focus": Array [], "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, @@ -234,6 +239,7 @@ describe('OptimizerConfig::parseOptions()', () => { "cache": true, "dist": false, "filters": Array [], + "focus": Array [], "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, @@ -261,6 +267,7 @@ describe('OptimizerConfig::parseOptions()', () => { "cache": true, "dist": false, "filters": Array [], + "focus": Array [], "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, @@ -285,6 +292,7 @@ describe('OptimizerConfig::parseOptions()', () => { "cache": false, "dist": false, "filters": Array [], + "focus": Array [], "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, @@ -309,6 +317,7 @@ describe('OptimizerConfig::parseOptions()', () => { "cache": false, "dist": false, "filters": Array [], + "focus": Array [], "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, @@ -334,6 +343,7 @@ describe('OptimizerConfig::parseOptions()', () => { "cache": false, "dist": false, "filters": Array [], + "focus": Array [], "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, @@ -359,6 +369,7 @@ describe('OptimizerConfig::parseOptions()', () => { "cache": true, "dist": false, "filters": Array [], + "focus": Array [], "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, @@ -386,6 +397,7 @@ describe('OptimizerConfig::create()', () => { .findKibanaPlatformPlugins; const getPluginBundles: jest.Mock = jest.requireMock('./get_plugin_bundles.ts').getPluginBundles; const filterById: jest.Mock = jest.requireMock('./filter_by_id.ts').filterById; + const focusBundles: jest.Mock = jest.requireMock('./focus_bundles').focusBundles; const readLimits: jest.Mock = jest.requireMock('../limits.ts').readLimits; beforeEach(() => { @@ -400,6 +412,7 @@ describe('OptimizerConfig::create()', () => { findKibanaPlatformPlugins.mockReturnValue(Symbol('new platform plugins')); getPluginBundles.mockReturnValue([Symbol('bundle1'), Symbol('bundle2')]); filterById.mockReturnValue(Symbol('filtered bundles')); + focusBundles.mockReturnValue(Symbol('focused bundles')); readLimits.mockReturnValue(Symbol('limits')); jest.spyOn(OptimizerConfig, 'parseOptions').mockImplementation((): { @@ -417,6 +430,7 @@ describe('OptimizerConfig::create()', () => { inspectWorkers: Symbol('parsed inspect workers'), profileWebpack: Symbol('parsed profile webpack'), filters: [], + focus: [], includeCoreBundle: false, })); }); @@ -470,17 +484,14 @@ describe('OptimizerConfig::create()', () => { "calls": Array [ Array [ Array [], - Array [ - Symbol(bundle1), - Symbol(bundle2), - ], + Symbol(focused bundles), ], ], "instances": Array [ [Window], ], "invocationCallOrder": Array [ - 23, + 24, ], "results": Array [ Object { diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts index b685d6ea01591..1443fccda04d8 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -19,6 +19,7 @@ import Path from 'path'; import Os from 'os'; +import { getPluginSearchPaths } from '@kbn/config'; import { Bundle, @@ -32,6 +33,7 @@ import { import { findKibanaPlatformPlugins, KibanaPlatformPlugin } from './kibana_platform_plugins'; import { getPluginBundles } from './get_plugin_bundles'; import { filterById } from './filter_by_id'; +import { focusBundles } from './focus_bundles'; import { readLimits } from '../limits'; export interface Limits { @@ -104,6 +106,11 @@ interface Options { * --filter f*r # [foobar], excludes [foo, bar] */ filter?: string[]; + /** + * behaves just like filter, but includes required bundles and plugins of the + * listed bundle ids. Filters only apply to bundles selected by focus + */ + focus?: string[]; /** flag that causes the core bundle to be built along with plugins */ includeCoreBundle?: boolean; @@ -132,6 +139,7 @@ export interface ParsedOptions { pluginPaths: string[]; pluginScanDirs: string[]; filters: string[]; + focus: string[]; inspectWorkers: boolean; includeCoreBundle: boolean; themeTags: ThemeTags; @@ -148,6 +156,7 @@ export class OptimizerConfig { const cache = options.cache !== false && !process.env.KBN_OPTIMIZER_NO_CACHE; const includeCoreBundle = !!options.includeCoreBundle; const filters = options.filter || []; + const focus = options.focus || []; const repoRoot = options.repoRoot; if (!Path.isAbsolute(repoRoot)) { @@ -159,19 +168,14 @@ export class OptimizerConfig { throw new TypeError('outputRoot must be an absolute path'); } - /** - * BEWARE: this needs to stay roughly synchronized with - * `src/core/server/config/env.ts` which determines 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')] : []), - ...(examples && !oss ? [Path.resolve('x-pack/examples')] : []), - Path.resolve(repoRoot, '../kibana-extra'), - ]; + const pluginScanDirs = + options.pluginScanDirs || + getPluginSearchPaths({ + rootDir: repoRoot, + oss, + examples, + }); + if (!pluginScanDirs.every((p) => Path.isAbsolute(p))) { throw new TypeError('pluginScanDirs must all be absolute paths'); } @@ -210,6 +214,7 @@ export class OptimizerConfig { pluginScanDirs, pluginPaths, filters, + focus, inspectWorkers, includeCoreBundle, themeTags, @@ -236,7 +241,7 @@ export class OptimizerConfig { ]; return new OptimizerConfig( - filterById(options.filters, bundles), + filterById(options.filters, focusBundles(options.focus, bundles)), options.cache, options.watch, options.inspectWorkers, diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 9678dd5de868b..7987dd71f765c 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -23,7 +23,6 @@ import { stringifyRequest } from 'loader-utils'; import webpack from 'webpack'; // @ts-expect-error import TerserPlugin from 'terser-webpack-plugin'; -// @ts-expect-error import webpackMerge from 'webpack-merge'; import { CleanWebpackPlugin } from 'clean-webpack-plugin'; import CompressionPlugin from 'compression-webpack-plugin'; @@ -64,6 +63,14 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: optimization: { noEmitOnErrors: true, + splitChunks: { + maxAsyncRequests: 10, + cacheGroups: { + default: { + reuseExistingChunk: false, + }, + }, + }, }, externals: [UiSharedDeps.externals], diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 2e50f4214beb4..c053445285c03 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -150,7 +150,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(5); /* harmony import */ var _kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _commands__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(128); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(500); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(501); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(144); /* * Licensed to Elasticsearch B.V. under one or more contributor @@ -8897,9 +8897,9 @@ exports.ToolingLogCollectingWriter = ToolingLogCollectingWriter; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "commands", function() { return commands; }); /* harmony import */ var _bootstrap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(129); -/* harmony import */ var _clean__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(280); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(399); -/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(400); +/* harmony import */ var _clean__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(281); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(400); +/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(401); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -8943,7 +8943,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_project_checksums__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(273); /* harmony import */ var _utils_bootstrap_cache_file__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(278); /* harmony import */ var _utils_yarn_lock__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(275); -/* harmony import */ var _utils_validate_yarn_lock__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(279); +/* harmony import */ var _utils_validate_dependencies__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(279); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -9002,7 +9002,7 @@ const BootstrapCommand = { const yarnLock = await Object(_utils_yarn_lock__WEBPACK_IMPORTED_MODULE_6__["readYarnLock"])(kbn); if (options.validate) { - await Object(_utils_validate_yarn_lock__WEBPACK_IMPORTED_MODULE_7__["validateYarnLock"])(kbn, yarnLock); + await Object(_utils_validate_dependencies__WEBPACK_IMPORTED_MODULE_7__["validateDependencies"])(kbn, yarnLock); } await Object(_utils_link_project_executables__WEBPACK_IMPORTED_MODULE_0__["linkProjectExecutables"])(projects, projectGraph); @@ -14713,6 +14713,10 @@ class Project { return this.json.kibana && this.json.kibana.clean || {}; } + isFlaggedAsDevOnly() { + return !!(this.json.kibana && this.json.kibana.devOnly); + } + hasScript(name) { return name in this.scripts; } @@ -37954,13 +37958,16 @@ class BootstrapCacheFile { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "validateYarnLock", function() { return validateYarnLock; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "validateDependencies", function() { return validateDependencies; }); /* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(276); /* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var dedent__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2); /* harmony import */ var dedent__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(dedent__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var _fs__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(131); -/* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(144); +/* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(113); +/* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_2__); +/* harmony import */ var _fs__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(131); +/* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(144); +/* harmony import */ var _projects_tree__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(280); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -37984,7 +37991,9 @@ __webpack_require__.r(__webpack_exports__); -async function validateYarnLock(kbn, yarnLock) { + + +async function validateDependencies(kbn, yarnLock) { // look through all of the packages in the yarn.lock file to see if // we have accidentally installed multiple lodash v4 versions const lodash4Versions = new Set(); @@ -38005,8 +38014,8 @@ async function validateYarnLock(kbn, yarnLock) { delete yarnLock[req]; } - await Object(_fs__WEBPACK_IMPORTED_MODULE_2__["writeFile"])(kbn.getAbsolute('yarn.lock'), Object(_yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__["stringify"])(yarnLock), 'utf8'); - _log__WEBPACK_IMPORTED_MODULE_3__["log"].error(dedent__WEBPACK_IMPORTED_MODULE_1___default.a` + await Object(_fs__WEBPACK_IMPORTED_MODULE_3__["writeFile"])(kbn.getAbsolute('yarn.lock'), Object(_yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__["stringify"])(yarnLock), 'utf8'); + _log__WEBPACK_IMPORTED_MODULE_4__["log"].error(dedent__WEBPACK_IMPORTED_MODULE_1___default.a` Multiple version of lodash v4 were detected, so they have been removed from the yarn.lock file. Please rerun yarn kbn bootstrap to coalese the @@ -38025,7 +38034,7 @@ async function validateYarnLock(kbn, yarnLock) { // of lodash v3 in the distributable - const prodDependencies = kbn.resolveAllProductionDependencies(yarnLock, _log__WEBPACK_IMPORTED_MODULE_3__["log"]); + const prodDependencies = kbn.resolveAllProductionDependencies(yarnLock, _log__WEBPACK_IMPORTED_MODULE_4__["log"]); const lodash3Versions = new Set(); for (const dep of prodDependencies.values()) { @@ -38036,7 +38045,7 @@ async function validateYarnLock(kbn, yarnLock) { if (lodash3Versions.size) { - _log__WEBPACK_IMPORTED_MODULE_3__["log"].error(dedent__WEBPACK_IMPORTED_MODULE_1___default.a` + _log__WEBPACK_IMPORTED_MODULE_4__["log"].error(dedent__WEBPACK_IMPORTED_MODULE_1___default.a` Due to changes in the yarn.lock file and/or package.json files a version of lodash 3 is now included in the production dependencies. To reduce the size of @@ -38088,7 +38097,7 @@ async function validateYarnLock(kbn, yarnLock) { }) => ` ${range} => ${projects.map(p => p.name).join(', ')}`)], []).join('\n '); if (duplicateRanges) { - _log__WEBPACK_IMPORTED_MODULE_3__["log"].error(dedent__WEBPACK_IMPORTED_MODULE_1___default.a` + _log__WEBPACK_IMPORTED_MODULE_4__["log"].error(dedent__WEBPACK_IMPORTED_MODULE_1___default.a` [single_version_dependencies] Multiple version ranges for the same dependency were found declared across different package.json files. Please consolidate @@ -38102,21 +38111,207 @@ async function validateYarnLock(kbn, yarnLock) { ${duplicateRanges} `); process.exit(1); + } // look for packages that have the the `kibana.devOnly` flag in their package.json + // and make sure they aren't included in the production dependencies of Kibana + + + const devOnlyProjectsInProduction = getDevOnlyProductionDepsTree(kbn, 'kibana'); + + if (devOnlyProjectsInProduction) { + _log__WEBPACK_IMPORTED_MODULE_4__["log"].error(dedent__WEBPACK_IMPORTED_MODULE_1___default.a` + Some of the packages in the production dependency chain for Kibana and X-Pack are + flagged with "kibana.devOnly" in their package.json. Please check changes made to + packages and their dependencies to ensure they don't end up in production. + + The devOnly dependencies that are being dependend on in production are: + + ${Object(_projects_tree__WEBPACK_IMPORTED_MODULE_5__["treeToString"])(devOnlyProjectsInProduction).split('\n').join('\n ')} + `); + process.exit(1); } - _log__WEBPACK_IMPORTED_MODULE_3__["log"].success('yarn.lock analysis completed without any issues'); + _log__WEBPACK_IMPORTED_MODULE_4__["log"].success('yarn.lock analysis completed without any issues'); +} + +function getDevOnlyProductionDepsTree(kbn, projectName) { + const project = kbn.getProject(projectName); + const childProjectNames = [...Object.keys(project.productionDependencies).filter(name => kbn.hasProject(name)), ...(projectName === 'kibana' ? ['x-pack'] : [])]; + const children = childProjectNames.map(n => getDevOnlyProductionDepsTree(kbn, n)).filter(t => !!t); + + if (!children.length && !project.isFlaggedAsDevOnly()) { + return; + } + + const tree = { + name: project.isFlaggedAsDevOnly() ? chalk__WEBPACK_IMPORTED_MODULE_2___default.a.red.bold(projectName) : projectName, + children + }; + return tree; } /***/ }), /* 280 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "renderProjectsTree", function() { return renderProjectsTree; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "treeToString", function() { return treeToString; }); +/* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(113); +/* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); +/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__); +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 projectKey = Symbol('__project'); +function renderProjectsTree(rootPath, projects) { + const projectsTree = buildProjectsTree(rootPath, projects); + return treeToString(createTreeStructure(projectsTree)); +} +function treeToString(tree) { + return [tree.name].concat(childrenToStrings(tree.children, '')).join('\n'); +} + +function childrenToStrings(tree, treePrefix) { + if (tree === undefined) { + return []; + } + + let strings = []; + tree.forEach((node, index) => { + const isLastNode = tree.length - 1 === index; + const nodePrefix = isLastNode ? '└── ' : '├── '; + const childPrefix = isLastNode ? ' ' : '│ '; + const childrenPrefix = treePrefix + childPrefix; + strings.push(`${treePrefix}${nodePrefix}${node.name}`); + strings = strings.concat(childrenToStrings(node.children, childrenPrefix)); + }); + return strings; +} + +function createTreeStructure(tree) { + let name; + const children = []; + + for (const [dir, project] of tree.entries()) { + // This is a leaf node (aka a project) + if (typeof project === 'string') { + name = chalk__WEBPACK_IMPORTED_MODULE_0___default.a.green(project); + continue; + } // If there's only one project and the key indicates it's a leaf node, we + // know that we're at a package folder that contains a package.json, so we + // "inline it" so we don't get unnecessary levels, i.e. we'll just see + // `foo` instead of `foo -> foo`. + + + if (project.size === 1 && project.has(projectKey)) { + const projectName = project.get(projectKey); + children.push({ + children: [], + name: dirOrProjectName(dir, projectName) + }); + continue; + } + + const subtree = createTreeStructure(project); // If the name is specified, we know there's a package at the "root" of the + // subtree itself. + + if (subtree.name !== undefined) { + const projectName = subtree.name; + children.push({ + children: subtree.children, + name: dirOrProjectName(dir, projectName) + }); + continue; + } // Special-case whenever we have one child, so we don't get unnecessary + // folders in the output. E.g. instead of `foo -> bar -> baz` we get + // `foo/bar/baz` instead. + + + if (subtree.children && subtree.children.length === 1) { + const child = subtree.children[0]; + const newName = chalk__WEBPACK_IMPORTED_MODULE_0___default.a.dim(path__WEBPACK_IMPORTED_MODULE_1___default.a.join(dir.toString(), child.name)); + children.push({ + children: child.children, + name: newName + }); + continue; + } + + children.push({ + children: subtree.children, + name: chalk__WEBPACK_IMPORTED_MODULE_0___default.a.dim(dir.toString()) + }); + } + + return { + name, + children + }; +} + +function dirOrProjectName(dir, projectName) { + return dir === projectName ? chalk__WEBPACK_IMPORTED_MODULE_0___default.a.green(dir) : chalk__WEBPACK_IMPORTED_MODULE_0___default.a`{dim ${dir.toString()} ({reset.green ${projectName}})}`; +} + +function buildProjectsTree(rootPath, projects) { + const tree = new Map(); + + for (const project of projects.values()) { + if (rootPath === project.path) { + tree.set(projectKey, project.name); + } else { + const relativeProjectPath = path__WEBPACK_IMPORTED_MODULE_1___default.a.relative(rootPath, project.path); + addProjectToTree(tree, relativeProjectPath.split(path__WEBPACK_IMPORTED_MODULE_1___default.a.sep), project); + } + } + + return tree; +} + +function addProjectToTree(tree, pathParts, project) { + if (pathParts.length === 0) { + tree.set(projectKey, project.name); + } else { + const [currentDir, ...rest] = pathParts; + + if (!tree.has(currentDir)) { + tree.set(currentDir, new Map()); + } + + const subtree = tree.get(currentDir); + addProjectToTree(subtree, rest, project); + } +} + +/***/ }), +/* 281 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "CleanCommand", function() { return CleanCommand; }); -/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(281); +/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(282); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(367); +/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(368); /* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(ora__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); @@ -38216,21 +38411,21 @@ const CleanCommand = { }; /***/ }), -/* 281 */ +/* 282 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(112); const path = __webpack_require__(4); -const globby = __webpack_require__(282); -const isGlob = __webpack_require__(294); -const slash = __webpack_require__(358); +const globby = __webpack_require__(283); +const isGlob = __webpack_require__(295); +const slash = __webpack_require__(359); const gracefulFs = __webpack_require__(133); -const isPathCwd = __webpack_require__(360); -const isPathInside = __webpack_require__(361); -const rimraf = __webpack_require__(362); -const pMap = __webpack_require__(363); +const isPathCwd = __webpack_require__(361); +const isPathInside = __webpack_require__(362); +const rimraf = __webpack_require__(363); +const pMap = __webpack_require__(364); const rimrafP = promisify(rimraf); @@ -38344,19 +38539,19 @@ module.exports.sync = (patterns, {force, dryRun, cwd = process.cwd(), ...options /***/ }), -/* 282 */ +/* 283 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const arrayUnion = __webpack_require__(283); -const merge2 = __webpack_require__(284); +const arrayUnion = __webpack_require__(284); +const merge2 = __webpack_require__(285); const glob = __webpack_require__(147); -const fastGlob = __webpack_require__(285); -const dirGlob = __webpack_require__(354); -const gitignore = __webpack_require__(356); -const {FilterStream, UniqueStream} = __webpack_require__(359); +const fastGlob = __webpack_require__(286); +const dirGlob = __webpack_require__(355); +const gitignore = __webpack_require__(357); +const {FilterStream, UniqueStream} = __webpack_require__(360); const DEFAULT_FILTER = () => false; @@ -38529,7 +38724,7 @@ module.exports.gitignore = gitignore; /***/ }), -/* 283 */ +/* 284 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38541,7 +38736,7 @@ module.exports = (...arguments_) => { /***/ }), -/* 284 */ +/* 285 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38692,17 +38887,17 @@ function pauseStreams (streams, options) { /***/ }), -/* 285 */ +/* 286 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const taskManager = __webpack_require__(286); -const async_1 = __webpack_require__(315); -const stream_1 = __webpack_require__(350); -const sync_1 = __webpack_require__(351); -const settings_1 = __webpack_require__(353); -const utils = __webpack_require__(287); +const taskManager = __webpack_require__(287); +const async_1 = __webpack_require__(316); +const stream_1 = __webpack_require__(351); +const sync_1 = __webpack_require__(352); +const settings_1 = __webpack_require__(354); +const utils = __webpack_require__(288); async function FastGlob(source, options) { assertPatternsInput(source); const works = getWorks(source, async_1.default, options); @@ -38766,13 +38961,13 @@ module.exports = FastGlob; /***/ }), -/* 286 */ +/* 287 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(287); +const utils = __webpack_require__(288); function generate(patterns, settings) { const positivePatterns = getPositivePatterns(patterns); const negativePatterns = getNegativePatternsAsPositive(patterns, settings.ignore); @@ -38837,30 +39032,30 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 287 */ +/* 288 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const array = __webpack_require__(288); +const array = __webpack_require__(289); exports.array = array; -const errno = __webpack_require__(289); +const errno = __webpack_require__(290); exports.errno = errno; -const fs = __webpack_require__(290); +const fs = __webpack_require__(291); exports.fs = fs; -const path = __webpack_require__(291); +const path = __webpack_require__(292); exports.path = path; -const pattern = __webpack_require__(292); +const pattern = __webpack_require__(293); exports.pattern = pattern; -const stream = __webpack_require__(313); +const stream = __webpack_require__(314); exports.stream = stream; -const string = __webpack_require__(314); +const string = __webpack_require__(315); exports.string = string; /***/ }), -/* 288 */ +/* 289 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38888,7 +39083,7 @@ exports.splitWhen = splitWhen; /***/ }), -/* 289 */ +/* 290 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38901,7 +39096,7 @@ exports.isEnoentCodeError = isEnoentCodeError; /***/ }), -/* 290 */ +/* 291 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38926,7 +39121,7 @@ exports.createDirentFromStats = createDirentFromStats; /***/ }), -/* 291 */ +/* 292 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -38965,16 +39160,16 @@ exports.removeLeadingDotSegment = removeLeadingDotSegment; /***/ }), -/* 292 */ +/* 293 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); -const globParent = __webpack_require__(293); -const micromatch = __webpack_require__(296); -const picomatch = __webpack_require__(307); +const globParent = __webpack_require__(294); +const micromatch = __webpack_require__(297); +const picomatch = __webpack_require__(308); const GLOBSTAR = '**'; const ESCAPE_SYMBOL = '\\'; const COMMON_GLOB_SYMBOLS_RE = /[*?]|^!/; @@ -39084,13 +39279,13 @@ exports.matchAny = matchAny; /***/ }), -/* 293 */ +/* 294 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isGlob = __webpack_require__(294); +var isGlob = __webpack_require__(295); var pathPosixDirname = __webpack_require__(4).posix.dirname; var isWin32 = __webpack_require__(121).platform() === 'win32'; @@ -39132,7 +39327,7 @@ module.exports = function globParent(str, opts) { /***/ }), -/* 294 */ +/* 295 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -39142,7 +39337,7 @@ module.exports = function globParent(str, opts) { * Released under the MIT License. */ -var isExtglob = __webpack_require__(295); +var isExtglob = __webpack_require__(296); var chars = { '{': '}', '(': ')', '[': ']'}; var strictRegex = /\\(.)|(^!|\*|[\].+)]\?|\[[^\\\]]+\]|\{[^\\}]+\}|\(\?[:!=][^\\)]+\)|\([^|]+\|[^\\)]+\))/; var relaxedRegex = /\\(.)|(^!|[*?{}()[\]]|\(\?)/; @@ -39186,7 +39381,7 @@ module.exports = function isGlob(str, options) { /***/ }), -/* 295 */ +/* 296 */ /***/ (function(module, exports) { /*! @@ -39212,16 +39407,16 @@ module.exports = function isExtglob(str) { /***/ }), -/* 296 */ +/* 297 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const util = __webpack_require__(112); -const braces = __webpack_require__(297); -const picomatch = __webpack_require__(307); -const utils = __webpack_require__(310); +const braces = __webpack_require__(298); +const picomatch = __webpack_require__(308); +const utils = __webpack_require__(311); const isEmptyString = val => typeof val === 'string' && (val === '' || val === './'); /** @@ -39686,16 +39881,16 @@ module.exports = micromatch; /***/ }), -/* 297 */ +/* 298 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stringify = __webpack_require__(298); -const compile = __webpack_require__(300); -const expand = __webpack_require__(304); -const parse = __webpack_require__(305); +const stringify = __webpack_require__(299); +const compile = __webpack_require__(301); +const expand = __webpack_require__(305); +const parse = __webpack_require__(306); /** * Expand the given pattern or create a regex-compatible string. @@ -39863,13 +40058,13 @@ module.exports = braces; /***/ }), -/* 298 */ +/* 299 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const utils = __webpack_require__(299); +const utils = __webpack_require__(300); module.exports = (ast, options = {}) => { let stringify = (node, parent = {}) => { @@ -39902,7 +40097,7 @@ module.exports = (ast, options = {}) => { /***/ }), -/* 299 */ +/* 300 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40021,14 +40216,14 @@ exports.flatten = (...args) => { /***/ }), -/* 300 */ +/* 301 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const fill = __webpack_require__(301); -const utils = __webpack_require__(299); +const fill = __webpack_require__(302); +const utils = __webpack_require__(300); const compile = (ast, options = {}) => { let walk = (node, parent = {}) => { @@ -40085,7 +40280,7 @@ module.exports = compile; /***/ }), -/* 301 */ +/* 302 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40099,7 +40294,7 @@ module.exports = compile; const util = __webpack_require__(112); -const toRegexRange = __webpack_require__(302); +const toRegexRange = __webpack_require__(303); const isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); @@ -40341,7 +40536,7 @@ module.exports = fill; /***/ }), -/* 302 */ +/* 303 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40354,7 +40549,7 @@ module.exports = fill; -const isNumber = __webpack_require__(303); +const isNumber = __webpack_require__(304); const toRegexRange = (min, max, options) => { if (isNumber(min) === false) { @@ -40636,7 +40831,7 @@ module.exports = toRegexRange; /***/ }), -/* 303 */ +/* 304 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40661,15 +40856,15 @@ module.exports = function(num) { /***/ }), -/* 304 */ +/* 305 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const fill = __webpack_require__(301); -const stringify = __webpack_require__(298); -const utils = __webpack_require__(299); +const fill = __webpack_require__(302); +const stringify = __webpack_require__(299); +const utils = __webpack_require__(300); const append = (queue = '', stash = '', enclose = false) => { let result = []; @@ -40781,13 +40976,13 @@ module.exports = expand; /***/ }), -/* 305 */ +/* 306 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stringify = __webpack_require__(298); +const stringify = __webpack_require__(299); /** * Constants @@ -40809,7 +41004,7 @@ const { CHAR_SINGLE_QUOTE, /* ' */ CHAR_NO_BREAK_SPACE, CHAR_ZERO_WIDTH_NOBREAK_SPACE -} = __webpack_require__(306); +} = __webpack_require__(307); /** * parse @@ -41121,7 +41316,7 @@ module.exports = parse; /***/ }), -/* 306 */ +/* 307 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -41185,27 +41380,27 @@ module.exports = { /***/ }), -/* 307 */ +/* 308 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -module.exports = __webpack_require__(308); +module.exports = __webpack_require__(309); /***/ }), -/* 308 */ +/* 309 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const scan = __webpack_require__(309); -const parse = __webpack_require__(312); -const utils = __webpack_require__(310); -const constants = __webpack_require__(311); +const scan = __webpack_require__(310); +const parse = __webpack_require__(313); +const utils = __webpack_require__(311); +const constants = __webpack_require__(312); const isObject = val => val && typeof val === 'object' && !Array.isArray(val); /** @@ -41541,13 +41736,13 @@ module.exports = picomatch; /***/ }), -/* 309 */ +/* 310 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const utils = __webpack_require__(310); +const utils = __webpack_require__(311); const { CHAR_ASTERISK, /* * */ CHAR_AT, /* @ */ @@ -41564,7 +41759,7 @@ const { CHAR_RIGHT_CURLY_BRACE, /* } */ CHAR_RIGHT_PARENTHESES, /* ) */ CHAR_RIGHT_SQUARE_BRACKET /* ] */ -} = __webpack_require__(311); +} = __webpack_require__(312); const isPathSeparator = code => { return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH; @@ -41931,7 +42126,7 @@ module.exports = scan; /***/ }), -/* 310 */ +/* 311 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -41944,7 +42139,7 @@ const { REGEX_REMOVE_BACKSLASH, REGEX_SPECIAL_CHARS, REGEX_SPECIAL_CHARS_GLOBAL -} = __webpack_require__(311); +} = __webpack_require__(312); exports.isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); exports.hasRegexChars = str => REGEX_SPECIAL_CHARS.test(str); @@ -42002,7 +42197,7 @@ exports.wrapOutput = (input, state = {}, options = {}) => { /***/ }), -/* 311 */ +/* 312 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -42188,14 +42383,14 @@ module.exports = { /***/ }), -/* 312 */ +/* 313 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const constants = __webpack_require__(311); -const utils = __webpack_require__(310); +const constants = __webpack_require__(312); +const utils = __webpack_require__(311); /** * Constants @@ -43273,13 +43468,13 @@ module.exports = parse; /***/ }), -/* 313 */ +/* 314 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const merge2 = __webpack_require__(284); +const merge2 = __webpack_require__(285); function merge(streams) { const mergedStream = merge2(streams); streams.forEach((stream) => { @@ -43296,7 +43491,7 @@ function propagateCloseEventToSources(streams) { /***/ }), -/* 314 */ +/* 315 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43313,14 +43508,14 @@ exports.isEmpty = isEmpty; /***/ }), -/* 315 */ +/* 316 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const stream_1 = __webpack_require__(316); -const provider_1 = __webpack_require__(343); +const stream_1 = __webpack_require__(317); +const provider_1 = __webpack_require__(344); class ProviderAsync extends provider_1.default { constructor() { super(...arguments); @@ -43348,16 +43543,16 @@ exports.default = ProviderAsync; /***/ }), -/* 316 */ +/* 317 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(138); -const fsStat = __webpack_require__(317); -const fsWalk = __webpack_require__(322); -const reader_1 = __webpack_require__(342); +const fsStat = __webpack_require__(318); +const fsWalk = __webpack_require__(323); +const reader_1 = __webpack_require__(343); class ReaderStream extends reader_1.default { constructor() { super(...arguments); @@ -43410,15 +43605,15 @@ exports.default = ReaderStream; /***/ }), -/* 317 */ +/* 318 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async = __webpack_require__(318); -const sync = __webpack_require__(319); -const settings_1 = __webpack_require__(320); +const async = __webpack_require__(319); +const sync = __webpack_require__(320); +const settings_1 = __webpack_require__(321); exports.Settings = settings_1.default; function stat(path, optionsOrSettingsOrCallback, callback) { if (typeof optionsOrSettingsOrCallback === 'function') { @@ -43441,7 +43636,7 @@ function getSettings(settingsOrOptions = {}) { /***/ }), -/* 318 */ +/* 319 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43479,7 +43674,7 @@ function callSuccessCallback(callback, result) { /***/ }), -/* 319 */ +/* 320 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43508,13 +43703,13 @@ exports.read = read; /***/ }), -/* 320 */ +/* 321 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = __webpack_require__(321); +const fs = __webpack_require__(322); class Settings { constructor(_options = {}) { this._options = _options; @@ -43531,7 +43726,7 @@ exports.default = Settings; /***/ }), -/* 321 */ +/* 322 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43554,16 +43749,16 @@ exports.createFileSystemAdapter = createFileSystemAdapter; /***/ }), -/* 322 */ +/* 323 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async_1 = __webpack_require__(323); -const stream_1 = __webpack_require__(338); -const sync_1 = __webpack_require__(339); -const settings_1 = __webpack_require__(341); +const async_1 = __webpack_require__(324); +const stream_1 = __webpack_require__(339); +const sync_1 = __webpack_require__(340); +const settings_1 = __webpack_require__(342); exports.Settings = settings_1.default; function walk(directory, optionsOrSettingsOrCallback, callback) { if (typeof optionsOrSettingsOrCallback === 'function') { @@ -43593,13 +43788,13 @@ function getSettings(settingsOrOptions = {}) { /***/ }), -/* 323 */ +/* 324 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async_1 = __webpack_require__(324); +const async_1 = __webpack_require__(325); class AsyncProvider { constructor(_root, _settings) { this._root = _root; @@ -43630,17 +43825,17 @@ function callSuccessCallback(callback, entries) { /***/ }), -/* 324 */ +/* 325 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const events_1 = __webpack_require__(156); -const fsScandir = __webpack_require__(325); -const fastq = __webpack_require__(334); -const common = __webpack_require__(336); -const reader_1 = __webpack_require__(337); +const fsScandir = __webpack_require__(326); +const fastq = __webpack_require__(335); +const common = __webpack_require__(337); +const reader_1 = __webpack_require__(338); class AsyncReader extends reader_1.default { constructor(_root, _settings) { super(_root, _settings); @@ -43730,15 +43925,15 @@ exports.default = AsyncReader; /***/ }), -/* 325 */ +/* 326 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async = __webpack_require__(326); -const sync = __webpack_require__(331); -const settings_1 = __webpack_require__(332); +const async = __webpack_require__(327); +const sync = __webpack_require__(332); +const settings_1 = __webpack_require__(333); exports.Settings = settings_1.default; function scandir(path, optionsOrSettingsOrCallback, callback) { if (typeof optionsOrSettingsOrCallback === 'function') { @@ -43761,16 +43956,16 @@ function getSettings(settingsOrOptions = {}) { /***/ }), -/* 326 */ +/* 327 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsStat = __webpack_require__(317); -const rpl = __webpack_require__(327); -const constants_1 = __webpack_require__(328); -const utils = __webpack_require__(329); +const fsStat = __webpack_require__(318); +const rpl = __webpack_require__(328); +const constants_1 = __webpack_require__(329); +const utils = __webpack_require__(330); function read(directory, settings, callback) { if (!settings.stats && constants_1.IS_SUPPORT_READDIR_WITH_FILE_TYPES) { return readdirWithFileTypes(directory, settings, callback); @@ -43858,7 +44053,7 @@ function callSuccessCallback(callback, result) { /***/ }), -/* 327 */ +/* 328 */ /***/ (function(module, exports) { module.exports = runParallel @@ -43912,7 +44107,7 @@ function runParallel (tasks, cb) { /***/ }), -/* 328 */ +/* 329 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43932,18 +44127,18 @@ exports.IS_SUPPORT_READDIR_WITH_FILE_TYPES = IS_MATCHED_BY_MAJOR || IS_MATCHED_B /***/ }), -/* 329 */ +/* 330 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = __webpack_require__(330); +const fs = __webpack_require__(331); exports.fs = fs; /***/ }), -/* 330 */ +/* 331 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -43968,15 +44163,15 @@ exports.createDirentFromStats = createDirentFromStats; /***/ }), -/* 331 */ +/* 332 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsStat = __webpack_require__(317); -const constants_1 = __webpack_require__(328); -const utils = __webpack_require__(329); +const fsStat = __webpack_require__(318); +const constants_1 = __webpack_require__(329); +const utils = __webpack_require__(330); function read(directory, settings) { if (!settings.stats && constants_1.IS_SUPPORT_READDIR_WITH_FILE_TYPES) { return readdirWithFileTypes(directory, settings); @@ -44027,15 +44222,15 @@ exports.readdir = readdir; /***/ }), -/* 332 */ +/* 333 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); -const fsStat = __webpack_require__(317); -const fs = __webpack_require__(333); +const fsStat = __webpack_require__(318); +const fs = __webpack_require__(334); class Settings { constructor(_options = {}) { this._options = _options; @@ -44058,7 +44253,7 @@ exports.default = Settings; /***/ }), -/* 333 */ +/* 334 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -44083,13 +44278,13 @@ exports.createFileSystemAdapter = createFileSystemAdapter; /***/ }), -/* 334 */ +/* 335 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var reusify = __webpack_require__(335) +var reusify = __webpack_require__(336) function fastqueue (context, worker, concurrency) { if (typeof context === 'function') { @@ -44263,7 +44458,7 @@ module.exports = fastqueue /***/ }), -/* 335 */ +/* 336 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -44303,7 +44498,7 @@ module.exports = reusify /***/ }), -/* 336 */ +/* 337 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -44334,13 +44529,13 @@ exports.joinPathSegments = joinPathSegments; /***/ }), -/* 337 */ +/* 338 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const common = __webpack_require__(336); +const common = __webpack_require__(337); class Reader { constructor(_root, _settings) { this._root = _root; @@ -44352,14 +44547,14 @@ exports.default = Reader; /***/ }), -/* 338 */ +/* 339 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(138); -const async_1 = __webpack_require__(324); +const async_1 = __webpack_require__(325); class StreamProvider { constructor(_root, _settings) { this._root = _root; @@ -44389,13 +44584,13 @@ exports.default = StreamProvider; /***/ }), -/* 339 */ +/* 340 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const sync_1 = __webpack_require__(340); +const sync_1 = __webpack_require__(341); class SyncProvider { constructor(_root, _settings) { this._root = _root; @@ -44410,15 +44605,15 @@ exports.default = SyncProvider; /***/ }), -/* 340 */ +/* 341 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsScandir = __webpack_require__(325); -const common = __webpack_require__(336); -const reader_1 = __webpack_require__(337); +const fsScandir = __webpack_require__(326); +const common = __webpack_require__(337); +const reader_1 = __webpack_require__(338); class SyncReader extends reader_1.default { constructor() { super(...arguments); @@ -44476,14 +44671,14 @@ exports.default = SyncReader; /***/ }), -/* 341 */ +/* 342 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); -const fsScandir = __webpack_require__(325); +const fsScandir = __webpack_require__(326); class Settings { constructor(_options = {}) { this._options = _options; @@ -44509,15 +44704,15 @@ exports.default = Settings; /***/ }), -/* 342 */ +/* 343 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); -const fsStat = __webpack_require__(317); -const utils = __webpack_require__(287); +const fsStat = __webpack_require__(318); +const utils = __webpack_require__(288); class Reader { constructor(_settings) { this._settings = _settings; @@ -44549,17 +44744,17 @@ exports.default = Reader; /***/ }), -/* 343 */ +/* 344 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); -const deep_1 = __webpack_require__(344); -const entry_1 = __webpack_require__(347); -const error_1 = __webpack_require__(348); -const entry_2 = __webpack_require__(349); +const deep_1 = __webpack_require__(345); +const entry_1 = __webpack_require__(348); +const error_1 = __webpack_require__(349); +const entry_2 = __webpack_require__(350); class Provider { constructor(_settings) { this._settings = _settings; @@ -44604,14 +44799,14 @@ exports.default = Provider; /***/ }), -/* 344 */ +/* 345 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(287); -const partial_1 = __webpack_require__(345); +const utils = __webpack_require__(288); +const partial_1 = __webpack_require__(346); class DeepFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -44665,13 +44860,13 @@ exports.default = DeepFilter; /***/ }), -/* 345 */ +/* 346 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const matcher_1 = __webpack_require__(346); +const matcher_1 = __webpack_require__(347); class PartialMatcher extends matcher_1.default { match(filepath) { const parts = filepath.split('/'); @@ -44710,13 +44905,13 @@ exports.default = PartialMatcher; /***/ }), -/* 346 */ +/* 347 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(287); +const utils = __webpack_require__(288); class Matcher { constructor(_patterns, _settings, _micromatchOptions) { this._patterns = _patterns; @@ -44767,13 +44962,13 @@ exports.default = Matcher; /***/ }), -/* 347 */ +/* 348 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(287); +const utils = __webpack_require__(288); class EntryFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -44829,13 +45024,13 @@ exports.default = EntryFilter; /***/ }), -/* 348 */ +/* 349 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(287); +const utils = __webpack_require__(288); class ErrorFilter { constructor(_settings) { this._settings = _settings; @@ -44851,13 +45046,13 @@ exports.default = ErrorFilter; /***/ }), -/* 349 */ +/* 350 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(287); +const utils = __webpack_require__(288); class EntryTransformer { constructor(_settings) { this._settings = _settings; @@ -44884,15 +45079,15 @@ exports.default = EntryTransformer; /***/ }), -/* 350 */ +/* 351 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(138); -const stream_2 = __webpack_require__(316); -const provider_1 = __webpack_require__(343); +const stream_2 = __webpack_require__(317); +const provider_1 = __webpack_require__(344); class ProviderStream extends provider_1.default { constructor() { super(...arguments); @@ -44922,14 +45117,14 @@ exports.default = ProviderStream; /***/ }), -/* 351 */ +/* 352 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const sync_1 = __webpack_require__(352); -const provider_1 = __webpack_require__(343); +const sync_1 = __webpack_require__(353); +const provider_1 = __webpack_require__(344); class ProviderSync extends provider_1.default { constructor() { super(...arguments); @@ -44952,15 +45147,15 @@ exports.default = ProviderSync; /***/ }), -/* 352 */ +/* 353 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsStat = __webpack_require__(317); -const fsWalk = __webpack_require__(322); -const reader_1 = __webpack_require__(342); +const fsStat = __webpack_require__(318); +const fsWalk = __webpack_require__(323); +const reader_1 = __webpack_require__(343); class ReaderSync extends reader_1.default { constructor() { super(...arguments); @@ -45002,7 +45197,7 @@ exports.default = ReaderSync; /***/ }), -/* 353 */ +/* 354 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -45061,13 +45256,13 @@ exports.default = Settings; /***/ }), -/* 354 */ +/* 355 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const pathType = __webpack_require__(355); +const pathType = __webpack_require__(356); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -45143,7 +45338,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 355 */ +/* 356 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -45193,7 +45388,7 @@ exports.isSymlinkSync = isTypeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 356 */ +/* 357 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -45201,9 +45396,9 @@ exports.isSymlinkSync = isTypeSync.bind(null, 'lstatSync', 'isSymbolicLink'); const {promisify} = __webpack_require__(112); const fs = __webpack_require__(134); const path = __webpack_require__(4); -const fastGlob = __webpack_require__(285); -const gitIgnore = __webpack_require__(357); -const slash = __webpack_require__(358); +const fastGlob = __webpack_require__(286); +const gitIgnore = __webpack_require__(358); +const slash = __webpack_require__(359); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -45317,7 +45512,7 @@ module.exports.sync = options => { /***/ }), -/* 357 */ +/* 358 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -45920,7 +46115,7 @@ if ( /***/ }), -/* 358 */ +/* 359 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -45938,7 +46133,7 @@ module.exports = path => { /***/ }), -/* 359 */ +/* 360 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -45991,7 +46186,7 @@ module.exports = { /***/ }), -/* 360 */ +/* 361 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -46013,7 +46208,7 @@ module.exports = path_ => { /***/ }), -/* 361 */ +/* 362 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -46041,7 +46236,7 @@ module.exports = (childPath, parentPath) => { /***/ }), -/* 362 */ +/* 363 */ /***/ (function(module, exports, __webpack_require__) { const assert = __webpack_require__(140) @@ -46407,12 +46602,12 @@ rimraf.sync = rimrafSync /***/ }), -/* 363 */ +/* 364 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const AggregateError = __webpack_require__(364); +const AggregateError = __webpack_require__(365); module.exports = async ( iterable, @@ -46495,13 +46690,13 @@ module.exports = async ( /***/ }), -/* 364 */ +/* 365 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const indentString = __webpack_require__(365); -const cleanStack = __webpack_require__(366); +const indentString = __webpack_require__(366); +const cleanStack = __webpack_require__(367); const cleanInternalStack = stack => stack.replace(/\s+at .*aggregate-error\/index.js:\d+:\d+\)?/g, ''); @@ -46549,7 +46744,7 @@ module.exports = AggregateError; /***/ }), -/* 365 */ +/* 366 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -46591,7 +46786,7 @@ module.exports = (string, count = 1, options) => { /***/ }), -/* 366 */ +/* 367 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -46638,20 +46833,20 @@ module.exports = (stack, options) => { /***/ }), -/* 367 */ +/* 368 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readline = __webpack_require__(368); -const chalk = __webpack_require__(369); -const cliCursor = __webpack_require__(376); -const cliSpinners = __webpack_require__(380); -const logSymbols = __webpack_require__(382); -const stripAnsi = __webpack_require__(391); -const wcwidth = __webpack_require__(393); -const isInteractive = __webpack_require__(397); -const MuteStream = __webpack_require__(398); +const readline = __webpack_require__(369); +const chalk = __webpack_require__(370); +const cliCursor = __webpack_require__(377); +const cliSpinners = __webpack_require__(381); +const logSymbols = __webpack_require__(383); +const stripAnsi = __webpack_require__(392); +const wcwidth = __webpack_require__(394); +const isInteractive = __webpack_require__(398); +const MuteStream = __webpack_require__(399); const TEXT = Symbol('text'); const PREFIX_TEXT = Symbol('prefixText'); @@ -47004,23 +47199,23 @@ module.exports.promise = (action, options) => { /***/ }), -/* 368 */ +/* 369 */ /***/ (function(module, exports) { module.exports = require("readline"); /***/ }), -/* 369 */ +/* 370 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiStyles = __webpack_require__(370); +const ansiStyles = __webpack_require__(371); const {stdout: stdoutColor, stderr: stderrColor} = __webpack_require__(120); const { stringReplaceAll, stringEncaseCRLFWithFirstIndex -} = __webpack_require__(374); +} = __webpack_require__(375); // `supportsColor.level` → `ansiStyles.color[name]` mapping const levelMapping = [ @@ -47221,7 +47416,7 @@ const chalkTag = (chalk, ...strings) => { } if (template === undefined) { - template = __webpack_require__(375); + template = __webpack_require__(376); } return template(chalk, parts.join('')); @@ -47250,7 +47445,7 @@ module.exports = chalk; /***/ }), -/* 370 */ +/* 371 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -47296,7 +47491,7 @@ const setLazyProperty = (object, property, get) => { let colorConvert; const makeDynamicStyles = (wrap, targetSpace, identity, isBackground) => { if (colorConvert === undefined) { - colorConvert = __webpack_require__(371); + colorConvert = __webpack_require__(372); } const offset = isBackground ? 10 : 0; @@ -47421,11 +47616,11 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(115)(module))) /***/ }), -/* 371 */ +/* 372 */ /***/ (function(module, exports, __webpack_require__) { -const conversions = __webpack_require__(372); -const route = __webpack_require__(373); +const conversions = __webpack_require__(373); +const route = __webpack_require__(374); const convert = {}; @@ -47508,7 +47703,7 @@ module.exports = convert; /***/ }), -/* 372 */ +/* 373 */ /***/ (function(module, exports, __webpack_require__) { /* MIT license */ @@ -48353,10 +48548,10 @@ convert.rgb.gray = function (rgb) { /***/ }), -/* 373 */ +/* 374 */ /***/ (function(module, exports, __webpack_require__) { -const conversions = __webpack_require__(372); +const conversions = __webpack_require__(373); /* This function routes a model to all other models. @@ -48456,7 +48651,7 @@ module.exports = function (fromModel) { /***/ }), -/* 374 */ +/* 375 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -48502,7 +48697,7 @@ module.exports = { /***/ }), -/* 375 */ +/* 376 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -48643,12 +48838,12 @@ module.exports = (chalk, temporary) => { /***/ }), -/* 376 */ +/* 377 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const restoreCursor = __webpack_require__(377); +const restoreCursor = __webpack_require__(378); let isHidden = false; @@ -48685,12 +48880,12 @@ exports.toggle = (force, writableStream) => { /***/ }), -/* 377 */ +/* 378 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const onetime = __webpack_require__(378); +const onetime = __webpack_require__(379); const signalExit = __webpack_require__(218); module.exports = onetime(() => { @@ -48701,12 +48896,12 @@ module.exports = onetime(() => { /***/ }), -/* 378 */ +/* 379 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const mimicFn = __webpack_require__(379); +const mimicFn = __webpack_require__(380); const calledFunctions = new WeakMap(); @@ -48758,7 +48953,7 @@ module.exports.callCount = fn => { /***/ }), -/* 379 */ +/* 380 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -48778,13 +48973,13 @@ module.exports.default = mimicFn; /***/ }), -/* 380 */ +/* 381 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const spinners = Object.assign({}, __webpack_require__(381)); +const spinners = Object.assign({}, __webpack_require__(382)); const spinnersList = Object.keys(spinners); @@ -48802,18 +48997,18 @@ module.exports.default = spinners; /***/ }), -/* 381 */ +/* 382 */ /***/ (function(module) { module.exports = JSON.parse("{\"dots\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠹\",\"⠸\",\"⠼\",\"⠴\",\"⠦\",\"⠧\",\"⠇\",\"⠏\"]},\"dots2\":{\"interval\":80,\"frames\":[\"⣾\",\"⣽\",\"⣻\",\"⢿\",\"⡿\",\"⣟\",\"⣯\",\"⣷\"]},\"dots3\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠞\",\"⠖\",\"⠦\",\"⠴\",\"⠲\",\"⠳\",\"⠓\"]},\"dots4\":{\"interval\":80,\"frames\":[\"⠄\",\"⠆\",\"⠇\",\"⠋\",\"⠙\",\"⠸\",\"⠰\",\"⠠\",\"⠰\",\"⠸\",\"⠙\",\"⠋\",\"⠇\",\"⠆\"]},\"dots5\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\"]},\"dots6\":{\"interval\":80,\"frames\":[\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠴\",\"⠲\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠚\",\"⠙\",\"⠉\",\"⠁\"]},\"dots7\":{\"interval\":80,\"frames\":[\"⠈\",\"⠉\",\"⠋\",\"⠓\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠖\",\"⠦\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\"]},\"dots8\":{\"interval\":80,\"frames\":[\"⠁\",\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\",\"⠈\"]},\"dots9\":{\"interval\":80,\"frames\":[\"⢹\",\"⢺\",\"⢼\",\"⣸\",\"⣇\",\"⡧\",\"⡗\",\"⡏\"]},\"dots10\":{\"interval\":80,\"frames\":[\"⢄\",\"⢂\",\"⢁\",\"⡁\",\"⡈\",\"⡐\",\"⡠\"]},\"dots11\":{\"interval\":100,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⡀\",\"⢀\",\"⠠\",\"⠐\",\"⠈\"]},\"dots12\":{\"interval\":80,\"frames\":[\"⢀⠀\",\"⡀⠀\",\"⠄⠀\",\"⢂⠀\",\"⡂⠀\",\"⠅⠀\",\"⢃⠀\",\"⡃⠀\",\"⠍⠀\",\"⢋⠀\",\"⡋⠀\",\"⠍⠁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⢈⠩\",\"⡀⢙\",\"⠄⡙\",\"⢂⠩\",\"⡂⢘\",\"⠅⡘\",\"⢃⠨\",\"⡃⢐\",\"⠍⡐\",\"⢋⠠\",\"⡋⢀\",\"⠍⡁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⠈⠩\",\"⠀⢙\",\"⠀⡙\",\"⠀⠩\",\"⠀⢘\",\"⠀⡘\",\"⠀⠨\",\"⠀⢐\",\"⠀⡐\",\"⠀⠠\",\"⠀⢀\",\"⠀⡀\"]},\"dots8Bit\":{\"interval\":80,\"frames\":[\"⠀\",\"⠁\",\"⠂\",\"⠃\",\"⠄\",\"⠅\",\"⠆\",\"⠇\",\"⡀\",\"⡁\",\"⡂\",\"⡃\",\"⡄\",\"⡅\",\"⡆\",\"⡇\",\"⠈\",\"⠉\",\"⠊\",\"⠋\",\"⠌\",\"⠍\",\"⠎\",\"⠏\",\"⡈\",\"⡉\",\"⡊\",\"⡋\",\"⡌\",\"⡍\",\"⡎\",\"⡏\",\"⠐\",\"⠑\",\"⠒\",\"⠓\",\"⠔\",\"⠕\",\"⠖\",\"⠗\",\"⡐\",\"⡑\",\"⡒\",\"⡓\",\"⡔\",\"⡕\",\"⡖\",\"⡗\",\"⠘\",\"⠙\",\"⠚\",\"⠛\",\"⠜\",\"⠝\",\"⠞\",\"⠟\",\"⡘\",\"⡙\",\"⡚\",\"⡛\",\"⡜\",\"⡝\",\"⡞\",\"⡟\",\"⠠\",\"⠡\",\"⠢\",\"⠣\",\"⠤\",\"⠥\",\"⠦\",\"⠧\",\"⡠\",\"⡡\",\"⡢\",\"⡣\",\"⡤\",\"⡥\",\"⡦\",\"⡧\",\"⠨\",\"⠩\",\"⠪\",\"⠫\",\"⠬\",\"⠭\",\"⠮\",\"⠯\",\"⡨\",\"⡩\",\"⡪\",\"⡫\",\"⡬\",\"⡭\",\"⡮\",\"⡯\",\"⠰\",\"⠱\",\"⠲\",\"⠳\",\"⠴\",\"⠵\",\"⠶\",\"⠷\",\"⡰\",\"⡱\",\"⡲\",\"⡳\",\"⡴\",\"⡵\",\"⡶\",\"⡷\",\"⠸\",\"⠹\",\"⠺\",\"⠻\",\"⠼\",\"⠽\",\"⠾\",\"⠿\",\"⡸\",\"⡹\",\"⡺\",\"⡻\",\"⡼\",\"⡽\",\"⡾\",\"⡿\",\"⢀\",\"⢁\",\"⢂\",\"⢃\",\"⢄\",\"⢅\",\"⢆\",\"⢇\",\"⣀\",\"⣁\",\"⣂\",\"⣃\",\"⣄\",\"⣅\",\"⣆\",\"⣇\",\"⢈\",\"⢉\",\"⢊\",\"⢋\",\"⢌\",\"⢍\",\"⢎\",\"⢏\",\"⣈\",\"⣉\",\"⣊\",\"⣋\",\"⣌\",\"⣍\",\"⣎\",\"⣏\",\"⢐\",\"⢑\",\"⢒\",\"⢓\",\"⢔\",\"⢕\",\"⢖\",\"⢗\",\"⣐\",\"⣑\",\"⣒\",\"⣓\",\"⣔\",\"⣕\",\"⣖\",\"⣗\",\"⢘\",\"⢙\",\"⢚\",\"⢛\",\"⢜\",\"⢝\",\"⢞\",\"⢟\",\"⣘\",\"⣙\",\"⣚\",\"⣛\",\"⣜\",\"⣝\",\"⣞\",\"⣟\",\"⢠\",\"⢡\",\"⢢\",\"⢣\",\"⢤\",\"⢥\",\"⢦\",\"⢧\",\"⣠\",\"⣡\",\"⣢\",\"⣣\",\"⣤\",\"⣥\",\"⣦\",\"⣧\",\"⢨\",\"⢩\",\"⢪\",\"⢫\",\"⢬\",\"⢭\",\"⢮\",\"⢯\",\"⣨\",\"⣩\",\"⣪\",\"⣫\",\"⣬\",\"⣭\",\"⣮\",\"⣯\",\"⢰\",\"⢱\",\"⢲\",\"⢳\",\"⢴\",\"⢵\",\"⢶\",\"⢷\",\"⣰\",\"⣱\",\"⣲\",\"⣳\",\"⣴\",\"⣵\",\"⣶\",\"⣷\",\"⢸\",\"⢹\",\"⢺\",\"⢻\",\"⢼\",\"⢽\",\"⢾\",\"⢿\",\"⣸\",\"⣹\",\"⣺\",\"⣻\",\"⣼\",\"⣽\",\"⣾\",\"⣿\"]},\"line\":{\"interval\":130,\"frames\":[\"-\",\"\\\\\",\"|\",\"/\"]},\"line2\":{\"interval\":100,\"frames\":[\"⠂\",\"-\",\"–\",\"—\",\"–\",\"-\"]},\"pipe\":{\"interval\":100,\"frames\":[\"┤\",\"┘\",\"┴\",\"└\",\"├\",\"┌\",\"┬\",\"┐\"]},\"simpleDots\":{\"interval\":400,\"frames\":[\". \",\".. \",\"...\",\" \"]},\"simpleDotsScrolling\":{\"interval\":200,\"frames\":[\". \",\".. \",\"...\",\" ..\",\" .\",\" \"]},\"star\":{\"interval\":70,\"frames\":[\"✶\",\"✸\",\"✹\",\"✺\",\"✹\",\"✷\"]},\"star2\":{\"interval\":80,\"frames\":[\"+\",\"x\",\"*\"]},\"flip\":{\"interval\":70,\"frames\":[\"_\",\"_\",\"_\",\"-\",\"`\",\"`\",\"'\",\"´\",\"-\",\"_\",\"_\",\"_\"]},\"hamburger\":{\"interval\":100,\"frames\":[\"☱\",\"☲\",\"☴\"]},\"growVertical\":{\"interval\":120,\"frames\":[\"▁\",\"▃\",\"▄\",\"▅\",\"▆\",\"▇\",\"▆\",\"▅\",\"▄\",\"▃\"]},\"growHorizontal\":{\"interval\":120,\"frames\":[\"▏\",\"▎\",\"▍\",\"▌\",\"▋\",\"▊\",\"▉\",\"▊\",\"▋\",\"▌\",\"▍\",\"▎\"]},\"balloon\":{\"interval\":140,\"frames\":[\" \",\".\",\"o\",\"O\",\"@\",\"*\",\" \"]},\"balloon2\":{\"interval\":120,\"frames\":[\".\",\"o\",\"O\",\"°\",\"O\",\"o\",\".\"]},\"noise\":{\"interval\":100,\"frames\":[\"▓\",\"▒\",\"░\"]},\"bounce\":{\"interval\":120,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⠂\"]},\"boxBounce\":{\"interval\":120,\"frames\":[\"▖\",\"▘\",\"▝\",\"▗\"]},\"boxBounce2\":{\"interval\":100,\"frames\":[\"▌\",\"▀\",\"▐\",\"▄\"]},\"triangle\":{\"interval\":50,\"frames\":[\"◢\",\"◣\",\"◤\",\"◥\"]},\"arc\":{\"interval\":100,\"frames\":[\"◜\",\"◠\",\"◝\",\"◞\",\"◡\",\"◟\"]},\"circle\":{\"interval\":120,\"frames\":[\"◡\",\"⊙\",\"◠\"]},\"squareCorners\":{\"interval\":180,\"frames\":[\"◰\",\"◳\",\"◲\",\"◱\"]},\"circleQuarters\":{\"interval\":120,\"frames\":[\"◴\",\"◷\",\"◶\",\"◵\"]},\"circleHalves\":{\"interval\":50,\"frames\":[\"◐\",\"◓\",\"◑\",\"◒\"]},\"squish\":{\"interval\":100,\"frames\":[\"╫\",\"╪\"]},\"toggle\":{\"interval\":250,\"frames\":[\"⊶\",\"⊷\"]},\"toggle2\":{\"interval\":80,\"frames\":[\"▫\",\"▪\"]},\"toggle3\":{\"interval\":120,\"frames\":[\"□\",\"■\"]},\"toggle4\":{\"interval\":100,\"frames\":[\"■\",\"□\",\"▪\",\"▫\"]},\"toggle5\":{\"interval\":100,\"frames\":[\"▮\",\"▯\"]},\"toggle6\":{\"interval\":300,\"frames\":[\"ဝ\",\"၀\"]},\"toggle7\":{\"interval\":80,\"frames\":[\"⦾\",\"⦿\"]},\"toggle8\":{\"interval\":100,\"frames\":[\"◍\",\"◌\"]},\"toggle9\":{\"interval\":100,\"frames\":[\"◉\",\"◎\"]},\"toggle10\":{\"interval\":100,\"frames\":[\"㊂\",\"㊀\",\"㊁\"]},\"toggle11\":{\"interval\":50,\"frames\":[\"⧇\",\"⧆\"]},\"toggle12\":{\"interval\":120,\"frames\":[\"☗\",\"☖\"]},\"toggle13\":{\"interval\":80,\"frames\":[\"=\",\"*\",\"-\"]},\"arrow\":{\"interval\":100,\"frames\":[\"←\",\"↖\",\"↑\",\"↗\",\"→\",\"↘\",\"↓\",\"↙\"]},\"arrow2\":{\"interval\":80,\"frames\":[\"⬆️ \",\"↗️ \",\"➡️ \",\"↘️ \",\"⬇️ \",\"↙️ \",\"⬅️ \",\"↖️ \"]},\"arrow3\":{\"interval\":120,\"frames\":[\"▹▹▹▹▹\",\"▸▹▹▹▹\",\"▹▸▹▹▹\",\"▹▹▸▹▹\",\"▹▹▹▸▹\",\"▹▹▹▹▸\"]},\"bouncingBar\":{\"interval\":80,\"frames\":[\"[ ]\",\"[= ]\",\"[== ]\",\"[=== ]\",\"[ ===]\",\"[ ==]\",\"[ =]\",\"[ ]\",\"[ =]\",\"[ ==]\",\"[ ===]\",\"[====]\",\"[=== ]\",\"[== ]\",\"[= ]\"]},\"bouncingBall\":{\"interval\":80,\"frames\":[\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ●)\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"(● )\"]},\"smiley\":{\"interval\":200,\"frames\":[\"😄 \",\"😝 \"]},\"monkey\":{\"interval\":300,\"frames\":[\"🙈 \",\"🙈 \",\"🙉 \",\"🙊 \"]},\"hearts\":{\"interval\":100,\"frames\":[\"💛 \",\"💙 \",\"💜 \",\"💚 \",\"❤️ \"]},\"clock\":{\"interval\":100,\"frames\":[\"🕛 \",\"🕐 \",\"🕑 \",\"🕒 \",\"🕓 \",\"🕔 \",\"🕕 \",\"🕖 \",\"🕗 \",\"🕘 \",\"🕙 \",\"🕚 \"]},\"earth\":{\"interval\":180,\"frames\":[\"🌍 \",\"🌎 \",\"🌏 \"]},\"material\":{\"interval\":17,\"frames\":[\"█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"███▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"████▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"███████▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"████████▁▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"██████████▁▁▁▁▁▁▁▁▁▁\",\"███████████▁▁▁▁▁▁▁▁▁\",\"█████████████▁▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁▁██████████████▁▁▁▁\",\"▁▁▁██████████████▁▁▁\",\"▁▁▁▁█████████████▁▁▁\",\"▁▁▁▁██████████████▁▁\",\"▁▁▁▁██████████████▁▁\",\"▁▁▁▁▁██████████████▁\",\"▁▁▁▁▁██████████████▁\",\"▁▁▁▁▁██████████████▁\",\"▁▁▁▁▁▁██████████████\",\"▁▁▁▁▁▁██████████████\",\"▁▁▁▁▁▁▁█████████████\",\"▁▁▁▁▁▁▁█████████████\",\"▁▁▁▁▁▁▁▁████████████\",\"▁▁▁▁▁▁▁▁████████████\",\"▁▁▁▁▁▁▁▁▁███████████\",\"▁▁▁▁▁▁▁▁▁███████████\",\"▁▁▁▁▁▁▁▁▁▁██████████\",\"▁▁▁▁▁▁▁▁▁▁██████████\",\"▁▁▁▁▁▁▁▁▁▁▁▁████████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁██████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\"█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"███▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"████▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"██████▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"████████▁▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"███████████▁▁▁▁▁▁▁▁▁\",\"████████████▁▁▁▁▁▁▁▁\",\"████████████▁▁▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁▁▁█████████████▁▁▁▁\",\"▁▁▁▁▁████████████▁▁▁\",\"▁▁▁▁▁████████████▁▁▁\",\"▁▁▁▁▁▁███████████▁▁▁\",\"▁▁▁▁▁▁▁▁█████████▁▁▁\",\"▁▁▁▁▁▁▁▁█████████▁▁▁\",\"▁▁▁▁▁▁▁▁▁█████████▁▁\",\"▁▁▁▁▁▁▁▁▁█████████▁▁\",\"▁▁▁▁▁▁▁▁▁▁█████████▁\",\"▁▁▁▁▁▁▁▁▁▁▁████████▁\",\"▁▁▁▁▁▁▁▁▁▁▁████████▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁███████▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁███████▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\"]},\"moon\":{\"interval\":80,\"frames\":[\"🌑 \",\"🌒 \",\"🌓 \",\"🌔 \",\"🌕 \",\"🌖 \",\"🌗 \",\"🌘 \"]},\"runner\":{\"interval\":140,\"frames\":[\"🚶 \",\"🏃 \"]},\"pong\":{\"interval\":80,\"frames\":[\"▐⠂ ▌\",\"▐⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂▌\",\"▐ ⠠▌\",\"▐ ⡀▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐⠠ ▌\"]},\"shark\":{\"interval\":120,\"frames\":[\"▐|\\\\____________▌\",\"▐_|\\\\___________▌\",\"▐__|\\\\__________▌\",\"▐___|\\\\_________▌\",\"▐____|\\\\________▌\",\"▐_____|\\\\_______▌\",\"▐______|\\\\______▌\",\"▐_______|\\\\_____▌\",\"▐________|\\\\____▌\",\"▐_________|\\\\___▌\",\"▐__________|\\\\__▌\",\"▐___________|\\\\_▌\",\"▐____________|\\\\▌\",\"▐____________/|▌\",\"▐___________/|_▌\",\"▐__________/|__▌\",\"▐_________/|___▌\",\"▐________/|____▌\",\"▐_______/|_____▌\",\"▐______/|______▌\",\"▐_____/|_______▌\",\"▐____/|________▌\",\"▐___/|_________▌\",\"▐__/|__________▌\",\"▐_/|___________▌\",\"▐/|____________▌\"]},\"dqpb\":{\"interval\":100,\"frames\":[\"d\",\"q\",\"p\",\"b\"]},\"weather\":{\"interval\":100,\"frames\":[\"☀️ \",\"☀️ \",\"☀️ \",\"🌤 \",\"⛅️ \",\"🌥 \",\"☁️ \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"⛈ \",\"🌨 \",\"🌧 \",\"🌨 \",\"☁️ \",\"🌥 \",\"⛅️ \",\"🌤 \",\"☀️ \",\"☀️ \"]},\"christmas\":{\"interval\":400,\"frames\":[\"🌲\",\"🎄\"]},\"grenade\":{\"interval\":80,\"frames\":[\"، \",\"′ \",\" ´ \",\" ‾ \",\" ⸌\",\" ⸊\",\" |\",\" ⁎\",\" ⁕\",\" ෴ \",\" ⁓\",\" \",\" \",\" \"]},\"point\":{\"interval\":125,\"frames\":[\"∙∙∙\",\"●∙∙\",\"∙●∙\",\"∙∙●\",\"∙∙∙\"]},\"layer\":{\"interval\":150,\"frames\":[\"-\",\"=\",\"≡\"]},\"betaWave\":{\"interval\":80,\"frames\":[\"ρββββββ\",\"βρβββββ\",\"ββρββββ\",\"βββρβββ\",\"ββββρββ\",\"βββββρβ\",\"ββββββρ\"]}}"); /***/ }), -/* 382 */ +/* 383 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const chalk = __webpack_require__(383); +const chalk = __webpack_require__(384); const isSupported = process.platform !== 'win32' || process.env.CI || process.env.TERM === 'xterm-256color'; @@ -48835,16 +49030,16 @@ module.exports = isSupported ? main : fallbacks; /***/ }), -/* 383 */ +/* 384 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const escapeStringRegexp = __webpack_require__(179); -const ansiStyles = __webpack_require__(384); -const stdoutColor = __webpack_require__(389).stdout; +const ansiStyles = __webpack_require__(385); +const stdoutColor = __webpack_require__(390).stdout; -const template = __webpack_require__(390); +const template = __webpack_require__(391); const isSimpleWindowsTerm = process.platform === 'win32' && !(process.env.TERM || '').toLowerCase().startsWith('xterm'); @@ -49070,12 +49265,12 @@ module.exports.default = module.exports; // For TypeScript /***/ }), -/* 384 */ +/* 385 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /* WEBPACK VAR INJECTION */(function(module) { -const colorConvert = __webpack_require__(385); +const colorConvert = __webpack_require__(386); const wrapAnsi16 = (fn, offset) => function () { const code = fn.apply(colorConvert, arguments); @@ -49243,11 +49438,11 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(115)(module))) /***/ }), -/* 385 */ +/* 386 */ /***/ (function(module, exports, __webpack_require__) { -var conversions = __webpack_require__(386); -var route = __webpack_require__(388); +var conversions = __webpack_require__(387); +var route = __webpack_require__(389); var convert = {}; @@ -49327,11 +49522,11 @@ module.exports = convert; /***/ }), -/* 386 */ +/* 387 */ /***/ (function(module, exports, __webpack_require__) { /* MIT license */ -var cssKeywords = __webpack_require__(387); +var cssKeywords = __webpack_require__(388); // NOTE: conversions should only return primitive values (i.e. arrays, or // values that give correct `typeof` results). @@ -50201,7 +50396,7 @@ convert.rgb.gray = function (rgb) { /***/ }), -/* 387 */ +/* 388 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -50360,10 +50555,10 @@ module.exports = { /***/ }), -/* 388 */ +/* 389 */ /***/ (function(module, exports, __webpack_require__) { -var conversions = __webpack_require__(386); +var conversions = __webpack_require__(387); /* this function routes a model to all other models. @@ -50463,7 +50658,7 @@ module.exports = function (fromModel) { /***/ }), -/* 389 */ +/* 390 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -50601,7 +50796,7 @@ module.exports = { /***/ }), -/* 390 */ +/* 391 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -50736,18 +50931,18 @@ module.exports = (chalk, tmp) => { /***/ }), -/* 391 */ +/* 392 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiRegex = __webpack_require__(392); +const ansiRegex = __webpack_require__(393); module.exports = string => typeof string === 'string' ? string.replace(ansiRegex(), '') : string; /***/ }), -/* 392 */ +/* 393 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -50764,14 +50959,14 @@ module.exports = ({onlyFirst = false} = {}) => { /***/ }), -/* 393 */ +/* 394 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var defaults = __webpack_require__(394) -var combining = __webpack_require__(396) +var defaults = __webpack_require__(395) +var combining = __webpack_require__(397) var DEFAULTS = { nul: 0, @@ -50870,10 +51065,10 @@ function bisearch(ucs) { /***/ }), -/* 394 */ +/* 395 */ /***/ (function(module, exports, __webpack_require__) { -var clone = __webpack_require__(395); +var clone = __webpack_require__(396); module.exports = function(options, defaults) { options = options || {}; @@ -50888,7 +51083,7 @@ module.exports = function(options, defaults) { }; /***/ }), -/* 395 */ +/* 396 */ /***/ (function(module, exports, __webpack_require__) { var clone = (function() { @@ -51060,7 +51255,7 @@ if ( true && module.exports) { /***/ }), -/* 396 */ +/* 397 */ /***/ (function(module, exports) { module.exports = [ @@ -51116,7 +51311,7 @@ module.exports = [ /***/ }), -/* 397 */ +/* 398 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -51132,7 +51327,7 @@ module.exports = ({stream = process.stdout} = {}) => { /***/ }), -/* 398 */ +/* 399 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(138) @@ -51283,7 +51478,7 @@ MuteStream.prototype.close = proxy('close') /***/ }), -/* 399 */ +/* 400 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -51344,7 +51539,7 @@ const RunCommand = { }; /***/ }), -/* 400 */ +/* 401 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -51354,7 +51549,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(144); /* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(145); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(146); -/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(401); +/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(402); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -51439,14 +51634,14 @@ const WatchCommand = { }; /***/ }), -/* 401 */ +/* 402 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "waitUntilWatchIsReady", function() { return waitUntilWatchIsReady; }); /* harmony import */ var rxjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(8); -/* harmony import */ var rxjs_operators__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(402); +/* harmony import */ var rxjs_operators__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(403); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -51513,141 +51708,141 @@ function waitUntilWatchIsReady(stream, opts = {}) { } /***/ }), -/* 402 */ +/* 403 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(403); +/* harmony import */ var _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(404); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "audit", function() { return _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__["audit"]; }); -/* harmony import */ var _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(404); +/* harmony import */ var _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(405); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "auditTime", function() { return _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__["auditTime"]; }); -/* harmony import */ var _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(405); +/* harmony import */ var _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(406); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buffer", function() { return _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__["buffer"]; }); -/* harmony import */ var _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(406); +/* harmony import */ var _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(407); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferCount", function() { return _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__["bufferCount"]; }); -/* harmony import */ var _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(407); +/* harmony import */ var _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(408); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferTime", function() { return _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__["bufferTime"]; }); -/* harmony import */ var _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(408); +/* harmony import */ var _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(409); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferToggle", function() { return _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__["bufferToggle"]; }); -/* harmony import */ var _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(409); +/* harmony import */ var _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(410); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferWhen", function() { return _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__["bufferWhen"]; }); -/* harmony import */ var _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(410); +/* harmony import */ var _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(411); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "catchError", function() { return _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__["catchError"]; }); -/* harmony import */ var _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(411); +/* harmony import */ var _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(412); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "combineAll", function() { return _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__["combineAll"]; }); -/* harmony import */ var _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(412); +/* harmony import */ var _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(413); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "combineLatest", function() { return _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__["combineLatest"]; }); -/* harmony import */ var _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(413); +/* harmony import */ var _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(414); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concat", function() { return _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__["concat"]; }); /* harmony import */ var _internal_operators_concatAll__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(80); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatAll", function() { return _internal_operators_concatAll__WEBPACK_IMPORTED_MODULE_11__["concatAll"]; }); -/* harmony import */ var _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(414); +/* harmony import */ var _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(415); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatMap", function() { return _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__["concatMap"]; }); -/* harmony import */ var _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(415); +/* harmony import */ var _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(416); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatMapTo", function() { return _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__["concatMapTo"]; }); -/* harmony import */ var _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(416); +/* harmony import */ var _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(417); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "count", function() { return _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__["count"]; }); -/* harmony import */ var _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(417); +/* harmony import */ var _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(418); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "debounce", function() { return _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__["debounce"]; }); -/* harmony import */ var _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(418); +/* harmony import */ var _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(419); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "debounceTime", function() { return _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__["debounceTime"]; }); -/* harmony import */ var _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(419); +/* harmony import */ var _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(420); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "defaultIfEmpty", function() { return _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__["defaultIfEmpty"]; }); -/* harmony import */ var _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(420); +/* harmony import */ var _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(421); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "delay", function() { return _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__["delay"]; }); -/* harmony import */ var _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(422); +/* harmony import */ var _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(423); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "delayWhen", function() { return _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__["delayWhen"]; }); -/* harmony import */ var _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(423); +/* harmony import */ var _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(424); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "dematerialize", function() { return _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__["dematerialize"]; }); -/* harmony import */ var _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(424); +/* harmony import */ var _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(425); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinct", function() { return _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__["distinct"]; }); -/* harmony import */ var _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(425); +/* harmony import */ var _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(426); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinctUntilChanged", function() { return _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__["distinctUntilChanged"]; }); -/* harmony import */ var _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(426); +/* harmony import */ var _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(427); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinctUntilKeyChanged", function() { return _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__["distinctUntilKeyChanged"]; }); -/* harmony import */ var _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(427); +/* harmony import */ var _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(428); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "elementAt", function() { return _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__["elementAt"]; }); -/* harmony import */ var _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(430); +/* harmony import */ var _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(431); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "endWith", function() { return _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__["endWith"]; }); -/* harmony import */ var _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(431); +/* harmony import */ var _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(432); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "every", function() { return _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__["every"]; }); -/* harmony import */ var _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(432); +/* harmony import */ var _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(433); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "exhaust", function() { return _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__["exhaust"]; }); -/* harmony import */ var _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(433); +/* harmony import */ var _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(434); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "exhaustMap", function() { return _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__["exhaustMap"]; }); -/* harmony import */ var _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(434); +/* harmony import */ var _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(435); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "expand", function() { return _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__["expand"]; }); /* harmony import */ var _internal_operators_filter__WEBPACK_IMPORTED_MODULE_30__ = __webpack_require__(105); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "filter", function() { return _internal_operators_filter__WEBPACK_IMPORTED_MODULE_30__["filter"]; }); -/* harmony import */ var _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(435); +/* harmony import */ var _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(436); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "finalize", function() { return _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__["finalize"]; }); -/* harmony import */ var _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(436); +/* harmony import */ var _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(437); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "find", function() { return _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__["find"]; }); -/* harmony import */ var _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(437); +/* harmony import */ var _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(438); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "findIndex", function() { return _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__["findIndex"]; }); -/* harmony import */ var _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(438); +/* harmony import */ var _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(439); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "first", function() { return _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__["first"]; }); /* harmony import */ var _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_35__ = __webpack_require__(31); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "groupBy", function() { return _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_35__["groupBy"]; }); -/* harmony import */ var _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(439); +/* harmony import */ var _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(440); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "ignoreElements", function() { return _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__["ignoreElements"]; }); -/* harmony import */ var _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(440); +/* harmony import */ var _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(441); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "isEmpty", function() { return _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__["isEmpty"]; }); -/* harmony import */ var _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(441); +/* harmony import */ var _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(442); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "last", function() { return _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__["last"]; }); /* harmony import */ var _internal_operators_map__WEBPACK_IMPORTED_MODULE_39__ = __webpack_require__(66); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "map", function() { return _internal_operators_map__WEBPACK_IMPORTED_MODULE_39__["map"]; }); -/* harmony import */ var _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(443); +/* harmony import */ var _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(444); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mapTo", function() { return _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__["mapTo"]; }); -/* harmony import */ var _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(444); +/* harmony import */ var _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(445); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "materialize", function() { return _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__["materialize"]; }); -/* harmony import */ var _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(445); +/* harmony import */ var _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(446); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "max", function() { return _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__["max"]; }); -/* harmony import */ var _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(448); +/* harmony import */ var _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(449); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "merge", function() { return _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__["merge"]; }); /* harmony import */ var _internal_operators_mergeAll__WEBPACK_IMPORTED_MODULE_44__ = __webpack_require__(81); @@ -51658,175 +51853,175 @@ __webpack_require__.r(__webpack_exports__); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "flatMap", function() { return _internal_operators_mergeMap__WEBPACK_IMPORTED_MODULE_45__["flatMap"]; }); -/* harmony import */ var _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(449); +/* harmony import */ var _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(450); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeMapTo", function() { return _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__["mergeMapTo"]; }); -/* harmony import */ var _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(450); +/* harmony import */ var _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(451); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeScan", function() { return _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__["mergeScan"]; }); -/* harmony import */ var _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(451); +/* harmony import */ var _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(452); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "min", function() { return _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__["min"]; }); -/* harmony import */ var _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(452); +/* harmony import */ var _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(453); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "multicast", function() { return _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__["multicast"]; }); /* harmony import */ var _internal_operators_observeOn__WEBPACK_IMPORTED_MODULE_50__ = __webpack_require__(41); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "observeOn", function() { return _internal_operators_observeOn__WEBPACK_IMPORTED_MODULE_50__["observeOn"]; }); -/* harmony import */ var _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(453); +/* harmony import */ var _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(454); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "onErrorResumeNext", function() { return _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__["onErrorResumeNext"]; }); -/* harmony import */ var _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__ = __webpack_require__(454); +/* harmony import */ var _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__ = __webpack_require__(455); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pairwise", function() { return _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__["pairwise"]; }); -/* harmony import */ var _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__ = __webpack_require__(455); +/* harmony import */ var _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__ = __webpack_require__(456); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "partition", function() { return _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__["partition"]; }); -/* harmony import */ var _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__ = __webpack_require__(456); +/* harmony import */ var _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__ = __webpack_require__(457); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pluck", function() { return _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__["pluck"]; }); -/* harmony import */ var _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__ = __webpack_require__(457); +/* harmony import */ var _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__ = __webpack_require__(458); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publish", function() { return _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__["publish"]; }); -/* harmony import */ var _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__ = __webpack_require__(458); +/* harmony import */ var _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__ = __webpack_require__(459); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishBehavior", function() { return _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__["publishBehavior"]; }); -/* harmony import */ var _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__ = __webpack_require__(459); +/* harmony import */ var _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__ = __webpack_require__(460); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishLast", function() { return _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__["publishLast"]; }); -/* harmony import */ var _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__ = __webpack_require__(460); +/* harmony import */ var _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__ = __webpack_require__(461); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishReplay", function() { return _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__["publishReplay"]; }); -/* harmony import */ var _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__ = __webpack_require__(461); +/* harmony import */ var _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__ = __webpack_require__(462); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "race", function() { return _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__["race"]; }); -/* harmony import */ var _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__ = __webpack_require__(446); +/* harmony import */ var _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__ = __webpack_require__(447); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "reduce", function() { return _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__["reduce"]; }); -/* harmony import */ var _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__ = __webpack_require__(462); +/* harmony import */ var _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__ = __webpack_require__(463); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "repeat", function() { return _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__["repeat"]; }); -/* harmony import */ var _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__ = __webpack_require__(463); +/* harmony import */ var _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__ = __webpack_require__(464); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "repeatWhen", function() { return _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__["repeatWhen"]; }); -/* harmony import */ var _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__ = __webpack_require__(464); +/* harmony import */ var _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__ = __webpack_require__(465); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "retry", function() { return _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__["retry"]; }); -/* harmony import */ var _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__ = __webpack_require__(465); +/* harmony import */ var _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__ = __webpack_require__(466); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "retryWhen", function() { return _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__["retryWhen"]; }); /* harmony import */ var _internal_operators_refCount__WEBPACK_IMPORTED_MODULE_65__ = __webpack_require__(30); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "refCount", function() { return _internal_operators_refCount__WEBPACK_IMPORTED_MODULE_65__["refCount"]; }); -/* harmony import */ var _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__ = __webpack_require__(466); +/* harmony import */ var _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__ = __webpack_require__(467); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sample", function() { return _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__["sample"]; }); -/* harmony import */ var _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__ = __webpack_require__(467); +/* harmony import */ var _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__ = __webpack_require__(468); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sampleTime", function() { return _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__["sampleTime"]; }); -/* harmony import */ var _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__ = __webpack_require__(447); +/* harmony import */ var _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__ = __webpack_require__(448); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "scan", function() { return _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__["scan"]; }); -/* harmony import */ var _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__ = __webpack_require__(468); +/* harmony import */ var _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__ = __webpack_require__(469); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sequenceEqual", function() { return _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__["sequenceEqual"]; }); -/* harmony import */ var _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__ = __webpack_require__(469); +/* harmony import */ var _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__ = __webpack_require__(470); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "share", function() { return _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__["share"]; }); -/* harmony import */ var _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__ = __webpack_require__(470); +/* harmony import */ var _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__ = __webpack_require__(471); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "shareReplay", function() { return _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__["shareReplay"]; }); -/* harmony import */ var _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__ = __webpack_require__(471); +/* harmony import */ var _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__ = __webpack_require__(472); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "single", function() { return _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__["single"]; }); -/* harmony import */ var _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__ = __webpack_require__(472); +/* harmony import */ var _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__ = __webpack_require__(473); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skip", function() { return _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__["skip"]; }); -/* harmony import */ var _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__ = __webpack_require__(473); +/* harmony import */ var _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__ = __webpack_require__(474); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipLast", function() { return _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__["skipLast"]; }); -/* harmony import */ var _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__ = __webpack_require__(474); +/* harmony import */ var _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__ = __webpack_require__(475); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipUntil", function() { return _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__["skipUntil"]; }); -/* harmony import */ var _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__ = __webpack_require__(475); +/* harmony import */ var _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__ = __webpack_require__(476); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipWhile", function() { return _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__["skipWhile"]; }); -/* harmony import */ var _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__ = __webpack_require__(476); +/* harmony import */ var _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__ = __webpack_require__(477); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "startWith", function() { return _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__["startWith"]; }); -/* harmony import */ var _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__ = __webpack_require__(477); +/* harmony import */ var _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__ = __webpack_require__(478); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "subscribeOn", function() { return _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__["subscribeOn"]; }); -/* harmony import */ var _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__ = __webpack_require__(479); +/* harmony import */ var _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__ = __webpack_require__(480); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchAll", function() { return _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__["switchAll"]; }); -/* harmony import */ var _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__ = __webpack_require__(480); +/* harmony import */ var _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__ = __webpack_require__(481); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchMap", function() { return _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__["switchMap"]; }); -/* harmony import */ var _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__ = __webpack_require__(481); +/* harmony import */ var _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__ = __webpack_require__(482); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchMapTo", function() { return _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__["switchMapTo"]; }); -/* harmony import */ var _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__ = __webpack_require__(429); +/* harmony import */ var _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__ = __webpack_require__(430); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "take", function() { return _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__["take"]; }); -/* harmony import */ var _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__ = __webpack_require__(442); +/* harmony import */ var _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__ = __webpack_require__(443); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeLast", function() { return _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__["takeLast"]; }); -/* harmony import */ var _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__ = __webpack_require__(482); +/* harmony import */ var _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__ = __webpack_require__(483); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeUntil", function() { return _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__["takeUntil"]; }); -/* harmony import */ var _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__ = __webpack_require__(483); +/* harmony import */ var _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__ = __webpack_require__(484); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeWhile", function() { return _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__["takeWhile"]; }); -/* harmony import */ var _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__ = __webpack_require__(484); +/* harmony import */ var _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__ = __webpack_require__(485); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "tap", function() { return _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__["tap"]; }); -/* harmony import */ var _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__ = __webpack_require__(485); +/* harmony import */ var _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__ = __webpack_require__(486); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throttle", function() { return _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__["throttle"]; }); -/* harmony import */ var _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__ = __webpack_require__(486); +/* harmony import */ var _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__ = __webpack_require__(487); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throttleTime", function() { return _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__["throttleTime"]; }); -/* harmony import */ var _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__ = __webpack_require__(428); +/* harmony import */ var _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__ = __webpack_require__(429); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throwIfEmpty", function() { return _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__["throwIfEmpty"]; }); -/* harmony import */ var _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__ = __webpack_require__(487); +/* harmony import */ var _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__ = __webpack_require__(488); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeInterval", function() { return _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__["timeInterval"]; }); -/* harmony import */ var _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__ = __webpack_require__(488); +/* harmony import */ var _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__ = __webpack_require__(489); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeout", function() { return _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__["timeout"]; }); -/* harmony import */ var _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__ = __webpack_require__(489); +/* harmony import */ var _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__ = __webpack_require__(490); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeoutWith", function() { return _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__["timeoutWith"]; }); -/* harmony import */ var _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__ = __webpack_require__(490); +/* harmony import */ var _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__ = __webpack_require__(491); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timestamp", function() { return _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__["timestamp"]; }); -/* harmony import */ var _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__ = __webpack_require__(491); +/* harmony import */ var _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__ = __webpack_require__(492); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "toArray", function() { return _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__["toArray"]; }); -/* harmony import */ var _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__ = __webpack_require__(492); +/* harmony import */ var _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__ = __webpack_require__(493); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "window", function() { return _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__["window"]; }); -/* harmony import */ var _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__ = __webpack_require__(493); +/* harmony import */ var _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__ = __webpack_require__(494); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowCount", function() { return _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__["windowCount"]; }); -/* harmony import */ var _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__ = __webpack_require__(494); +/* harmony import */ var _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__ = __webpack_require__(495); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowTime", function() { return _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__["windowTime"]; }); -/* harmony import */ var _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__ = __webpack_require__(495); +/* harmony import */ var _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__ = __webpack_require__(496); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowToggle", function() { return _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__["windowToggle"]; }); -/* harmony import */ var _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__ = __webpack_require__(496); +/* harmony import */ var _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__ = __webpack_require__(497); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowWhen", function() { return _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__["windowWhen"]; }); -/* harmony import */ var _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__ = __webpack_require__(497); +/* harmony import */ var _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__ = __webpack_require__(498); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "withLatestFrom", function() { return _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__["withLatestFrom"]; }); -/* harmony import */ var _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__ = __webpack_require__(498); +/* harmony import */ var _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__ = __webpack_require__(499); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "zip", function() { return _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__["zip"]; }); -/* harmony import */ var _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__ = __webpack_require__(499); +/* harmony import */ var _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__ = __webpack_require__(500); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "zipAll", function() { return _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__["zipAll"]; }); /** PURE_IMPORTS_START PURE_IMPORTS_END */ @@ -51937,7 +52132,7 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 403 */ +/* 404 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52016,14 +52211,14 @@ var AuditSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 404 */ +/* 405 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "auditTime", function() { return auditTime; }); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(55); -/* harmony import */ var _audit__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(403); +/* harmony import */ var _audit__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(404); /* harmony import */ var _observable_timer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(108); /** PURE_IMPORTS_START _scheduler_async,_audit,_observable_timer PURE_IMPORTS_END */ @@ -52039,7 +52234,7 @@ function auditTime(duration, scheduler) { /***/ }), -/* 405 */ +/* 406 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52086,7 +52281,7 @@ var BufferSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 406 */ +/* 407 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52187,7 +52382,7 @@ var BufferSkipCountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 407 */ +/* 408 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52348,7 +52543,7 @@ function dispatchBufferClose(arg) { /***/ }), -/* 408 */ +/* 409 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52467,7 +52662,7 @@ var BufferToggleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 409 */ +/* 410 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52560,7 +52755,7 @@ var BufferWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 410 */ +/* 411 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52620,7 +52815,7 @@ var CatchSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 411 */ +/* 412 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52636,7 +52831,7 @@ function combineAll(project) { /***/ }), -/* 412 */ +/* 413 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52668,7 +52863,7 @@ function combineLatest() { /***/ }), -/* 413 */ +/* 414 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52688,7 +52883,7 @@ function concat() { /***/ }), -/* 414 */ +/* 415 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52704,13 +52899,13 @@ function concatMap(project, resultSelector) { /***/ }), -/* 415 */ +/* 416 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "concatMapTo", function() { return concatMapTo; }); -/* harmony import */ var _concatMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(414); +/* harmony import */ var _concatMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(415); /** PURE_IMPORTS_START _concatMap PURE_IMPORTS_END */ function concatMapTo(innerObservable, resultSelector) { @@ -52720,7 +52915,7 @@ function concatMapTo(innerObservable, resultSelector) { /***/ }), -/* 416 */ +/* 417 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52785,7 +52980,7 @@ var CountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 417 */ +/* 418 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52870,7 +53065,7 @@ var DebounceSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 418 */ +/* 419 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52946,7 +53141,7 @@ function dispatchNext(subscriber) { /***/ }), -/* 419 */ +/* 420 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52996,7 +53191,7 @@ var DefaultIfEmptySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 420 */ +/* 421 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53004,7 +53199,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "delay", function() { return delay; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(12); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(55); -/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(421); +/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(422); /* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(11); /* harmony import */ var _Notification__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(42); /** PURE_IMPORTS_START tslib,_scheduler_async,_util_isDate,_Subscriber,_Notification PURE_IMPORTS_END */ @@ -53103,7 +53298,7 @@ var DelayMessage = /*@__PURE__*/ (function () { /***/ }), -/* 421 */ +/* 422 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53117,7 +53312,7 @@ function isDate(value) { /***/ }), -/* 422 */ +/* 423 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53263,7 +53458,7 @@ var SubscriptionDelaySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 423 */ +/* 424 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53301,7 +53496,7 @@ var DeMaterializeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 424 */ +/* 425 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53377,7 +53572,7 @@ var DistinctSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 425 */ +/* 426 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53448,13 +53643,13 @@ var DistinctUntilChangedSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 426 */ +/* 427 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "distinctUntilKeyChanged", function() { return distinctUntilKeyChanged; }); -/* harmony import */ var _distinctUntilChanged__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(425); +/* harmony import */ var _distinctUntilChanged__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(426); /** PURE_IMPORTS_START _distinctUntilChanged PURE_IMPORTS_END */ function distinctUntilKeyChanged(key, compare) { @@ -53464,7 +53659,7 @@ function distinctUntilKeyChanged(key, compare) { /***/ }), -/* 427 */ +/* 428 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53472,9 +53667,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "elementAt", function() { return elementAt; }); /* harmony import */ var _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(62); /* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(105); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(428); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(419); -/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(429); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(429); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(420); +/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(430); /** PURE_IMPORTS_START _util_ArgumentOutOfRangeError,_filter,_throwIfEmpty,_defaultIfEmpty,_take PURE_IMPORTS_END */ @@ -53496,7 +53691,7 @@ function elementAt(index, defaultValue) { /***/ }), -/* 428 */ +/* 429 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53562,7 +53757,7 @@ function defaultErrorFactory() { /***/ }), -/* 429 */ +/* 430 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53624,7 +53819,7 @@ var TakeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 430 */ +/* 431 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53646,7 +53841,7 @@ function endWith() { /***/ }), -/* 431 */ +/* 432 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53708,7 +53903,7 @@ var EverySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 432 */ +/* 433 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53762,7 +53957,7 @@ var SwitchFirstSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 433 */ +/* 434 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53856,7 +54051,7 @@ var ExhaustMapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 434 */ +/* 435 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53968,7 +54163,7 @@ var ExpandSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 435 */ +/* 436 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54006,7 +54201,7 @@ var FinallySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 436 */ +/* 437 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54078,13 +54273,13 @@ var FindValueSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 437 */ +/* 438 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "findIndex", function() { return findIndex; }); -/* harmony import */ var _operators_find__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(436); +/* harmony import */ var _operators_find__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(437); /** PURE_IMPORTS_START _operators_find PURE_IMPORTS_END */ function findIndex(predicate, thisArg) { @@ -54094,7 +54289,7 @@ function findIndex(predicate, thisArg) { /***/ }), -/* 438 */ +/* 439 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54102,9 +54297,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "first", function() { return first; }); /* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(63); /* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(105); -/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(429); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(419); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(428); +/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(430); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(420); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(429); /* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(25); /** PURE_IMPORTS_START _util_EmptyError,_filter,_take,_defaultIfEmpty,_throwIfEmpty,_util_identity PURE_IMPORTS_END */ @@ -54121,7 +54316,7 @@ function first(predicate, defaultValue) { /***/ }), -/* 439 */ +/* 440 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54158,7 +54353,7 @@ var IgnoreElementsSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 440 */ +/* 441 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54202,7 +54397,7 @@ var IsEmptySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 441 */ +/* 442 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54210,9 +54405,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "last", function() { return last; }); /* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(63); /* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(105); -/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(442); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(428); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(419); +/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(443); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(429); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(420); /* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(25); /** PURE_IMPORTS_START _util_EmptyError,_filter,_takeLast,_throwIfEmpty,_defaultIfEmpty,_util_identity PURE_IMPORTS_END */ @@ -54229,7 +54424,7 @@ function last(predicate, defaultValue) { /***/ }), -/* 442 */ +/* 443 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54306,7 +54501,7 @@ var TakeLastSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 443 */ +/* 444 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54345,7 +54540,7 @@ var MapToSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 444 */ +/* 445 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54395,13 +54590,13 @@ var MaterializeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 445 */ +/* 446 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "max", function() { return max; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(446); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(447); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function max(comparer) { @@ -54414,15 +54609,15 @@ function max(comparer) { /***/ }), -/* 446 */ +/* 447 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "reduce", function() { return reduce; }); -/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(447); -/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(442); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(419); +/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(448); +/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(443); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(420); /* harmony import */ var _util_pipe__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(24); /** PURE_IMPORTS_START _scan,_takeLast,_defaultIfEmpty,_util_pipe PURE_IMPORTS_END */ @@ -54443,7 +54638,7 @@ function reduce(accumulator, seed) { /***/ }), -/* 447 */ +/* 448 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54525,7 +54720,7 @@ var ScanSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 448 */ +/* 449 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54545,7 +54740,7 @@ function merge() { /***/ }), -/* 449 */ +/* 450 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54570,7 +54765,7 @@ function mergeMapTo(innerObservable, resultSelector, concurrent) { /***/ }), -/* 450 */ +/* 451 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54679,13 +54874,13 @@ var MergeScanSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 451 */ +/* 452 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "min", function() { return min; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(446); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(447); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function min(comparer) { @@ -54698,7 +54893,7 @@ function min(comparer) { /***/ }), -/* 452 */ +/* 453 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54747,7 +54942,7 @@ var MulticastOperator = /*@__PURE__*/ (function () { /***/ }), -/* 453 */ +/* 454 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54837,7 +55032,7 @@ var OnErrorResumeNextSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 454 */ +/* 455 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54885,7 +55080,7 @@ var PairwiseSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 455 */ +/* 456 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54908,7 +55103,7 @@ function partition(predicate, thisArg) { /***/ }), -/* 456 */ +/* 457 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54948,14 +55143,14 @@ function plucker(props, length) { /***/ }), -/* 457 */ +/* 458 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publish", function() { return publish; }); /* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(27); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(452); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(453); /** PURE_IMPORTS_START _Subject,_multicast PURE_IMPORTS_END */ @@ -54968,14 +55163,14 @@ function publish(selector) { /***/ }), -/* 458 */ +/* 459 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishBehavior", function() { return publishBehavior; }); /* harmony import */ var _BehaviorSubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(32); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(452); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(453); /** PURE_IMPORTS_START _BehaviorSubject,_multicast PURE_IMPORTS_END */ @@ -54986,14 +55181,14 @@ function publishBehavior(value) { /***/ }), -/* 459 */ +/* 460 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishLast", function() { return publishLast; }); /* harmony import */ var _AsyncSubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(50); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(452); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(453); /** PURE_IMPORTS_START _AsyncSubject,_multicast PURE_IMPORTS_END */ @@ -55004,14 +55199,14 @@ function publishLast() { /***/ }), -/* 460 */ +/* 461 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishReplay", function() { return publishReplay; }); /* harmony import */ var _ReplaySubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(33); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(452); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(453); /** PURE_IMPORTS_START _ReplaySubject,_multicast PURE_IMPORTS_END */ @@ -55027,7 +55222,7 @@ function publishReplay(bufferSize, windowTime, selectorOrScheduler, scheduler) { /***/ }), -/* 461 */ +/* 462 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55054,7 +55249,7 @@ function race() { /***/ }), -/* 462 */ +/* 463 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55119,7 +55314,7 @@ var RepeatSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 463 */ +/* 464 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55213,7 +55408,7 @@ var RepeatWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 464 */ +/* 465 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55266,7 +55461,7 @@ var RetrySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 465 */ +/* 466 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55352,7 +55547,7 @@ var RetryWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 466 */ +/* 467 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55407,7 +55602,7 @@ var SampleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 467 */ +/* 468 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55467,7 +55662,7 @@ function dispatchNotification(state) { /***/ }), -/* 468 */ +/* 469 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55590,13 +55785,13 @@ var SequenceEqualCompareToSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 469 */ +/* 470 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "share", function() { return share; }); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(452); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(453); /* harmony import */ var _refCount__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(30); /* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(27); /** PURE_IMPORTS_START _multicast,_refCount,_Subject PURE_IMPORTS_END */ @@ -55613,7 +55808,7 @@ function share() { /***/ }), -/* 470 */ +/* 471 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55682,7 +55877,7 @@ function shareReplayOperator(_a) { /***/ }), -/* 471 */ +/* 472 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55762,7 +55957,7 @@ var SingleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 472 */ +/* 473 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55804,7 +55999,7 @@ var SkipSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 473 */ +/* 474 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55866,7 +56061,7 @@ var SkipLastSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 474 */ +/* 475 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55923,7 +56118,7 @@ var SkipUntilSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 475 */ +/* 476 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55979,7 +56174,7 @@ var SkipWhileSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 476 */ +/* 477 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56008,13 +56203,13 @@ function startWith() { /***/ }), -/* 477 */ +/* 478 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeOn", function() { return subscribeOn; }); -/* harmony import */ var _observable_SubscribeOnObservable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(478); +/* harmony import */ var _observable_SubscribeOnObservable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(479); /** PURE_IMPORTS_START _observable_SubscribeOnObservable PURE_IMPORTS_END */ function subscribeOn(scheduler, delay) { @@ -56039,7 +56234,7 @@ var SubscribeOnOperator = /*@__PURE__*/ (function () { /***/ }), -/* 478 */ +/* 479 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56103,13 +56298,13 @@ var SubscribeOnObservable = /*@__PURE__*/ (function (_super) { /***/ }), -/* 479 */ +/* 480 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "switchAll", function() { return switchAll; }); -/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(480); +/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(481); /* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(25); /** PURE_IMPORTS_START _switchMap,_util_identity PURE_IMPORTS_END */ @@ -56121,7 +56316,7 @@ function switchAll() { /***/ }), -/* 480 */ +/* 481 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56209,13 +56404,13 @@ var SwitchMapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 481 */ +/* 482 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "switchMapTo", function() { return switchMapTo; }); -/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(480); +/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(481); /** PURE_IMPORTS_START _switchMap PURE_IMPORTS_END */ function switchMapTo(innerObservable, resultSelector) { @@ -56225,7 +56420,7 @@ function switchMapTo(innerObservable, resultSelector) { /***/ }), -/* 482 */ +/* 483 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56273,7 +56468,7 @@ var TakeUntilSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 483 */ +/* 484 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56341,7 +56536,7 @@ var TakeWhileSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 484 */ +/* 485 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56429,7 +56624,7 @@ var TapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 485 */ +/* 486 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56531,7 +56726,7 @@ var ThrottleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 486 */ +/* 487 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56540,7 +56735,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(12); /* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(11); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(55); -/* harmony import */ var _throttle__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(485); +/* harmony import */ var _throttle__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(486); /** PURE_IMPORTS_START tslib,_Subscriber,_scheduler_async,_throttle PURE_IMPORTS_END */ @@ -56629,7 +56824,7 @@ function dispatchNext(arg) { /***/ }), -/* 487 */ +/* 488 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56637,7 +56832,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeInterval", function() { return timeInterval; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TimeInterval", function() { return TimeInterval; }); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(55); -/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(447); +/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(448); /* harmony import */ var _observable_defer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(91); /* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(66); /** PURE_IMPORTS_START _scheduler_async,_scan,_observable_defer,_map PURE_IMPORTS_END */ @@ -56673,7 +56868,7 @@ var TimeInterval = /*@__PURE__*/ (function () { /***/ }), -/* 488 */ +/* 489 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56681,7 +56876,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeout", function() { return timeout; }); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(55); /* harmony import */ var _util_TimeoutError__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(64); -/* harmony import */ var _timeoutWith__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(489); +/* harmony import */ var _timeoutWith__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(490); /* harmony import */ var _observable_throwError__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(49); /** PURE_IMPORTS_START _scheduler_async,_util_TimeoutError,_timeoutWith,_observable_throwError PURE_IMPORTS_END */ @@ -56698,7 +56893,7 @@ function timeout(due, scheduler) { /***/ }), -/* 489 */ +/* 490 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56706,7 +56901,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeoutWith", function() { return timeoutWith; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(12); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(55); -/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(421); +/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(422); /* harmony import */ var _innerSubscribe__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(90); /** PURE_IMPORTS_START tslib,_scheduler_async,_util_isDate,_innerSubscribe PURE_IMPORTS_END */ @@ -56777,7 +56972,7 @@ var TimeoutWithSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 490 */ +/* 491 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56807,13 +57002,13 @@ var Timestamp = /*@__PURE__*/ (function () { /***/ }), -/* 491 */ +/* 492 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "toArray", function() { return toArray; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(446); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(447); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function toArrayReducer(arr, item, index) { @@ -56830,7 +57025,7 @@ function toArray() { /***/ }), -/* 492 */ +/* 493 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56908,7 +57103,7 @@ var WindowSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 493 */ +/* 494 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56998,7 +57193,7 @@ var WindowCountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 494 */ +/* 495 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57168,7 +57363,7 @@ function dispatchWindowClose(state) { /***/ }), -/* 495 */ +/* 496 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57311,7 +57506,7 @@ var WindowToggleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 496 */ +/* 497 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57408,7 +57603,7 @@ var WindowSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 497 */ +/* 498 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57503,7 +57698,7 @@ var WithLatestFromSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 498 */ +/* 499 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57525,7 +57720,7 @@ function zip() { /***/ }), -/* 499 */ +/* 500 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57541,7 +57736,7 @@ function zipAll(project) { /***/ }), -/* 500 */ +/* 501 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57550,7 +57745,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_errors__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(163); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(144); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(146); -/* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(501); +/* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(280); /* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(502); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } @@ -57632,159 +57827,6 @@ function toArray(value) { return Array.isArray(value) ? value : [value]; } -/***/ }), -/* 501 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "renderProjectsTree", function() { return renderProjectsTree; }); -/* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(113); -/* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); -/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__); -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT 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 projectKey = Symbol('__project'); -function renderProjectsTree(rootPath, projects) { - const projectsTree = buildProjectsTree(rootPath, projects); - return treeToString(createTreeStructure(projectsTree)); -} - -function treeToString(tree) { - return [tree.name].concat(childrenToStrings(tree.children, '')).join('\n'); -} - -function childrenToStrings(tree, treePrefix) { - if (tree === undefined) { - return []; - } - - let strings = []; - tree.forEach((node, index) => { - const isLastNode = tree.length - 1 === index; - const nodePrefix = isLastNode ? '└── ' : '├── '; - const childPrefix = isLastNode ? ' ' : '│ '; - const childrenPrefix = treePrefix + childPrefix; - strings.push(`${treePrefix}${nodePrefix}${node.name}`); - strings = strings.concat(childrenToStrings(node.children, childrenPrefix)); - }); - return strings; -} - -function createTreeStructure(tree) { - let name; - const children = []; - - for (const [dir, project] of tree.entries()) { - // This is a leaf node (aka a project) - if (typeof project === 'string') { - name = chalk__WEBPACK_IMPORTED_MODULE_0___default.a.green(project); - continue; - } // If there's only one project and the key indicates it's a leaf node, we - // know that we're at a package folder that contains a package.json, so we - // "inline it" so we don't get unnecessary levels, i.e. we'll just see - // `foo` instead of `foo -> foo`. - - - if (project.size === 1 && project.has(projectKey)) { - const projectName = project.get(projectKey); - children.push({ - children: [], - name: dirOrProjectName(dir, projectName) - }); - continue; - } - - const subtree = createTreeStructure(project); // If the name is specified, we know there's a package at the "root" of the - // subtree itself. - - if (subtree.name !== undefined) { - const projectName = subtree.name; - children.push({ - children: subtree.children, - name: dirOrProjectName(dir, projectName) - }); - continue; - } // Special-case whenever we have one child, so we don't get unnecessary - // folders in the output. E.g. instead of `foo -> bar -> baz` we get - // `foo/bar/baz` instead. - - - if (subtree.children && subtree.children.length === 1) { - const child = subtree.children[0]; - const newName = chalk__WEBPACK_IMPORTED_MODULE_0___default.a.dim(path__WEBPACK_IMPORTED_MODULE_1___default.a.join(dir.toString(), child.name)); - children.push({ - children: child.children, - name: newName - }); - continue; - } - - children.push({ - children: subtree.children, - name: chalk__WEBPACK_IMPORTED_MODULE_0___default.a.dim(dir.toString()) - }); - } - - return { - name, - children - }; -} - -function dirOrProjectName(dir, projectName) { - return dir === projectName ? chalk__WEBPACK_IMPORTED_MODULE_0___default.a.green(dir) : chalk__WEBPACK_IMPORTED_MODULE_0___default.a`{dim ${dir.toString()} ({reset.green ${projectName}})}`; -} - -function buildProjectsTree(rootPath, projects) { - const tree = new Map(); - - for (const project of projects.values()) { - if (rootPath === project.path) { - tree.set(projectKey, project.name); - } else { - const relativeProjectPath = path__WEBPACK_IMPORTED_MODULE_1___default.a.relative(rootPath, project.path); - addProjectToTree(tree, relativeProjectPath.split(path__WEBPACK_IMPORTED_MODULE_1___default.a.sep), project); - } - } - - return tree; -} - -function addProjectToTree(tree, pathParts, project) { - if (pathParts.length === 0) { - tree.set(projectKey, project.name); - } else { - const [currentDir, ...rest] = pathParts; - - if (!tree.has(currentDir)) { - tree.set(currentDir, new Map()); - } - - const subtree = tree.get(currentDir); - addProjectToTree(subtree, rest, project); - } -} - /***/ }), /* 502 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { @@ -57796,7 +57838,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(503); /* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(multimatch__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(361); +/* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(362); /* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(is_path_inside__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(275); /* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(146); @@ -58088,7 +58130,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return buildProductionProjects; }); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(509); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(281); +/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(282); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); @@ -58239,7 +58281,7 @@ const os = __webpack_require__(121); const pAll = __webpack_require__(510); const arrify = __webpack_require__(512); const globby = __webpack_require__(513); -const isGlob = __webpack_require__(294); +const isGlob = __webpack_require__(295); const cpFile = __webpack_require__(713); const junk = __webpack_require__(723); const CpyError = __webpack_require__(724); @@ -58957,7 +58999,7 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); var globParent = __webpack_require__(521); -var isGlob = __webpack_require__(294); +var isGlob = __webpack_require__(295); var micromatch = __webpack_require__(524); var GLOBSTAR = '**'; /** @@ -59145,7 +59187,7 @@ module.exports = function globParent(str) { * Licensed under the MIT License. */ -var isExtglob = __webpack_require__(295); +var isExtglob = __webpack_require__(296); module.exports = function isGlob(str) { if (typeof str !== 'string' || str === '') { @@ -82800,7 +82842,7 @@ exports.flatten = flatten; "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var merge2 = __webpack_require__(284); +var merge2 = __webpack_require__(285); /** * Merge multiple streams and propagate their errors into one stream in parallel. */ diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index 8ffd86b84bf76..944fcf5998637 100644 --- a/packages/kbn-pm/package.json +++ b/packages/kbn-pm/package.json @@ -4,6 +4,9 @@ "version": "1.0.0", "license": "Apache-2.0", "private": true, + "kibana": { + "devOnly": true + }, "scripts": { "build": "webpack", "kbn:watch": "webpack --watch --progress", diff --git a/packages/kbn-pm/src/commands/bootstrap.ts b/packages/kbn-pm/src/commands/bootstrap.ts index 0fa3f355ae9d6..9a8048d6fd106 100644 --- a/packages/kbn-pm/src/commands/bootstrap.ts +++ b/packages/kbn-pm/src/commands/bootstrap.ts @@ -26,7 +26,7 @@ import { ICommand } from './'; import { getAllChecksums } from '../utils/project_checksums'; import { BootstrapCacheFile } from '../utils/bootstrap_cache_file'; import { readYarnLock } from '../utils/yarn_lock'; -import { validateYarnLock } from '../utils/validate_yarn_lock'; +import { validateDependencies } from '../utils/validate_dependencies'; export const BootstrapCommand: ICommand = { description: 'Install dependencies and crosslink projects', @@ -59,7 +59,7 @@ export const BootstrapCommand: ICommand = { const yarnLock = await readYarnLock(kbn); if (options.validate) { - await validateYarnLock(kbn, yarnLock); + await validateDependencies(kbn, yarnLock); } await linkProjectExecutables(projects, projectGraph); diff --git a/packages/kbn-pm/src/utils/project.ts b/packages/kbn-pm/src/utils/project.ts index 8f45df52c7a2f..4e4d76544aa35 100644 --- a/packages/kbn-pm/src/utils/project.ts +++ b/packages/kbn-pm/src/utils/project.ts @@ -153,6 +153,10 @@ export class Project { return (this.json.kibana && this.json.kibana.clean) || {}; } + public isFlaggedAsDevOnly() { + return !!(this.json.kibana && this.json.kibana.devOnly); + } + public hasScript(name: string) { return name in this.scripts; } diff --git a/packages/kbn-pm/src/utils/projects_tree.ts b/packages/kbn-pm/src/utils/projects_tree.ts index c7a13ce2de348..4ba000bc1b158 100644 --- a/packages/kbn-pm/src/utils/projects_tree.ts +++ b/packages/kbn-pm/src/utils/projects_tree.ts @@ -29,7 +29,7 @@ export function renderProjectsTree(rootPath: string, projects: Map {} -function treeToString(tree: ITree) { +export function treeToString(tree: ITree) { return [tree.name].concat(childrenToStrings(tree.children, '')).join('\n'); } diff --git a/packages/kbn-pm/src/utils/validate_yarn_lock.ts b/packages/kbn-pm/src/utils/validate_dependencies.ts similarity index 77% rename from packages/kbn-pm/src/utils/validate_yarn_lock.ts rename to packages/kbn-pm/src/utils/validate_dependencies.ts index ec853a3a958fb..045d6332dcc29 100644 --- a/packages/kbn-pm/src/utils/validate_yarn_lock.ts +++ b/packages/kbn-pm/src/utils/validate_dependencies.ts @@ -20,14 +20,16 @@ // @ts-expect-error published types are useless import { stringify as stringifyLockfile } from '@yarnpkg/lockfile'; import dedent from 'dedent'; +import chalk from 'chalk'; import { writeFile } from './fs'; import { Kibana } from './kibana'; import { YarnLock } from './yarn_lock'; import { log } from './log'; import { Project } from './project'; +import { ITree, treeToString } from './projects_tree'; -export async function validateYarnLock(kbn: Kibana, yarnLock: YarnLock) { +export async function validateDependencies(kbn: Kibana, yarnLock: YarnLock) { // look through all of the packages in the yarn.lock file to see if // we have accidentally installed multiple lodash v4 versions const lodash4Versions = new Set(); @@ -157,5 +159,45 @@ export async function validateYarnLock(kbn: Kibana, yarnLock: YarnLock) { process.exit(1); } + // look for packages that have the the `kibana.devOnly` flag in their package.json + // and make sure they aren't included in the production dependencies of Kibana + const devOnlyProjectsInProduction = getDevOnlyProductionDepsTree(kbn, 'kibana'); + if (devOnlyProjectsInProduction) { + log.error(dedent` + Some of the packages in the production dependency chain for Kibana and X-Pack are + flagged with "kibana.devOnly" in their package.json. Please check changes made to + packages and their dependencies to ensure they don't end up in production. + + The devOnly dependencies that are being dependend on in production are: + + ${treeToString(devOnlyProjectsInProduction).split('\n').join('\n ')} + `); + + process.exit(1); + } + log.success('yarn.lock analysis completed without any issues'); } + +function getDevOnlyProductionDepsTree(kbn: Kibana, projectName: string) { + const project = kbn.getProject(projectName); + const childProjectNames = [ + ...Object.keys(project.productionDependencies).filter((name) => kbn.hasProject(name)), + ...(projectName === 'kibana' ? ['x-pack'] : []), + ]; + + const children = childProjectNames + .map((n) => getDevOnlyProductionDepsTree(kbn, n)) + .filter((t): t is ITree => !!t); + + if (!children.length && !project.isFlaggedAsDevOnly()) { + return; + } + + const tree: ITree = { + name: project.isFlaggedAsDevOnly() ? chalk.red.bold(projectName) : projectName, + children, + }; + + return tree; +} diff --git a/packages/kbn-release-notes/package.json b/packages/kbn-release-notes/package.json index 268530c22399a..e3306b7a54917 100644 --- a/packages/kbn-release-notes/package.json +++ b/packages/kbn-release-notes/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "license": "Apache-2.0", "main": "target/index.js", + "kibana": { + "devOnly": true + }, "scripts": { "kbn:bootstrap": "tsc", "kbn:watch": "tsc --watch" diff --git a/packages/kbn-std/package.json b/packages/kbn-std/package.json index a931dd3f3154d..8a5e885c456cd 100644 --- a/packages/kbn-std/package.json +++ b/packages/kbn-std/package.json @@ -9,12 +9,12 @@ "build": "tsc", "kbn:bootstrap": "yarn build" }, + "dependencies": { + "lodash": "^4.17.20" + }, "devDependencies": { + "@kbn/utility-types": "1.0.0", "typescript": "4.0.2", "tsd": "^0.13.1" - }, - "dependencies": { - "@kbn/utility-types": "1.0.0", - "lodash": "^4.17.20" } } diff --git a/packages/kbn-std/src/rxjs_7.test.ts b/packages/kbn-std/src/rxjs_7.test.ts index dcc73602613fa..ff1026e23b7ef 100644 --- a/packages/kbn-std/src/rxjs_7.test.ts +++ b/packages/kbn-std/src/rxjs_7.test.ts @@ -42,7 +42,7 @@ describe('firstValueFrom()', () => { ); }); - it('does not unsubscribe from the source observable that emits synchronously', async () => { + it('unsubscribes from a source observable that emits synchronously', async () => { const values = [1, 2, 3, 4]; let unsubscribed = false; const source = new Rx.Observable((subscriber) => { @@ -54,10 +54,10 @@ describe('firstValueFrom()', () => { }); await expect(firstValueFrom(source)).resolves.toMatchInlineSnapshot(`1`); - if (unsubscribed) { - throw new Error('expected source to not be unsubscribed'); + if (!unsubscribed) { + throw new Error('expected source to be unsubscribed'); } - expect(values).toEqual([]); + expect(values).toEqual([2, 3, 4]); }); it('unsubscribes from the source observable after first async notification', async () => { diff --git a/packages/kbn-std/src/rxjs_7.ts b/packages/kbn-std/src/rxjs_7.ts index f0a1be9125cc8..cb10f9de880fd 100644 --- a/packages/kbn-std/src/rxjs_7.ts +++ b/packages/kbn-std/src/rxjs_7.ts @@ -1,251 +1,30 @@ -/* eslint-disable @kbn/eslint/require-license-header */ - -/** - * @notice - * - * We include the `firstValueFrom()` and `lastValueFrom()` helpers - * extracted from the v7-beta.7 version of the RxJS library. - * - * Apache License - * Version 2.0, January 2004 - * http://www.apache.org/licenses/ - * - * TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - * - * 1. Definitions. - * - * "License" shall mean the terms and conditions for use, reproduction, - * and distribution as defined by Sections 1 through 9 of this document. - * - * "Licensor" shall mean the copyright owner or entity authorized by - * the copyright owner that is granting the License. - * - * "Legal Entity" shall mean the union of the acting entity and all - * other entities that control, are controlled by, or are under common - * control with that entity. For the purposes of this definition, - * "control" means (i) the power, direct or indirect, to cause the - * direction or management of such entity, whether by contract or - * otherwise, or (ii) ownership of fifty percent (50%) or more of the - * outstanding shares, or (iii) beneficial ownership of such entity. - * - * "You" (or "Your") shall mean an individual or Legal Entity - * exercising permissions granted by this License. - * - * "Source" form shall mean the preferred form for making modifications, - * including but not limited to software source code, documentation - * source, and configuration files. - * - * "Object" form shall mean any form resulting from mechanical - * transformation or translation of a Source form, including but - * not limited to compiled object code, generated documentation, - * and conversions to other media types. - * - * "Work" shall mean the work of authorship, whether in Source or - * Object form, made available under the License, as indicated by a - * copyright notice that is included in or attached to the work - * (an example is provided in the Appendix below). - * - * "Derivative Works" shall mean any work, whether in Source or Object - * form, that is based on (or derived from) the Work and for which the - * editorial revisions, annotations, elaborations, or other modifications - * represent, as a whole, an original work of authorship. For the purposes - * of this License, Derivative Works shall not include works that remain - * separable from, or merely link (or bind by name) to the interfaces of, - * the Work and Derivative Works thereof. - * - * "Contribution" shall mean any work of authorship, including - * the original version of the Work and any modifications or additions - * to that Work or Derivative Works thereof, that is intentionally - * submitted to Licensor for inclusion in the Work by the copyright owner - * or by an individual or Legal Entity authorized to submit on behalf of - * the copyright owner. For the purposes of this definition, "submitted" - * means any form of electronic, verbal, or written communication sent - * to the Licensor or its representatives, including but not limited to - * communication on electronic mailing lists, source code control systems, - * and issue tracking systems that are managed by, or on behalf of, the - * Licensor for the purpose of discussing and improving the Work, but - * excluding communication that is conspicuously marked or otherwise - * designated in writing by the copyright owner as "Not a Contribution." - * - * "Contributor" shall mean Licensor and any individual or Legal Entity - * on behalf of whom a Contribution has been received by Licensor and - * subsequently incorporated within the Work. - * - * 2. Grant of Copyright License. Subject to the terms and conditions of - * this License, each Contributor hereby grants to You a perpetual, - * worldwide, non-exclusive, no-charge, royalty-free, irrevocable - * copyright license to reproduce, prepare Derivative Works of, - * publicly display, publicly perform, sublicense, and distribute the - * Work and such Derivative Works in Source or Object form. - * - * 3. Grant of Patent License. Subject to the terms and conditions of - * this License, each Contributor hereby grants to You a perpetual, - * worldwide, non-exclusive, no-charge, royalty-free, irrevocable - * (except as stated in this section) patent license to make, have made, - * use, offer to sell, sell, import, and otherwise transfer the Work, - * where such license applies only to those patent claims licensable - * by such Contributor that are necessarily infringed by their - * Contribution(s) alone or by combination of their Contribution(s) - * with the Work to which such Contribution(s) was submitted. If You - * institute patent litigation against any entity (including a - * cross-claim or counterclaim in a lawsuit) alleging that the Work - * or a Contribution incorporated within the Work constitutes direct - * or contributory patent infringement, then any patent licenses - * granted to You under this License for that Work shall terminate - * as of the date such litigation is filed. - * - * 4. Redistribution. You may reproduce and distribute copies of the - * Work or Derivative Works thereof in any medium, with or without - * modifications, and in Source or Object form, provided that You - * meet the following conditions: - * - * (a) You must give any other recipients of the Work or - * Derivative Works a copy of this License; and - * - * (b) You must cause any modified files to carry prominent notices - * stating that You changed the files; and - * - * (c) You must retain, in the Source form of any Derivative Works - * that You distribute, all copyright, patent, trademark, and - * attribution notices from the Source form of the Work, - * excluding those notices that do not pertain to any part of - * the Derivative Works; and - * - * (d) If the Work includes a "NOTICE" text file as part of its - * distribution, then any Derivative Works that You distribute must - * include a readable copy of the attribution notices contained - * within such NOTICE file, excluding those notices that do not - * pertain to any part of the Derivative Works, in at least one - * of the following places: within a NOTICE text file distributed - * as part of the Derivative Works; within the Source form or - * documentation, if provided along with the Derivative Works; or, - * within a display generated by the Derivative Works, if and - * wherever such third-party notices normally appear. The contents - * of the NOTICE file are for informational purposes only and - * do not modify the License. You may add Your own attribution - * notices within Derivative Works that You distribute, alongside - * or as an addendum to the NOTICE text from the Work, provided - * that such additional attribution notices cannot be construed - * as modifying the License. - * - * You may add Your own copyright statement to Your modifications and - * may provide additional or different license terms and conditions - * for use, reproduction, or distribution of Your modifications, or - * for any such Derivative Works as a whole, provided Your use, - * reproduction, and distribution of the Work otherwise complies with - * the conditions stated in this License. - * - * 5. Submission of Contributions. Unless You explicitly state otherwise, - * any Contribution intentionally submitted for inclusion in the Work - * by You to the Licensor shall be under the terms and conditions of - * this License, without any additional terms or conditions. - * Notwithstanding the above, nothing herein shall supersede or modify - * the terms of any separate license agreement you may have executed - * with Licensor regarding such Contributions. - * - * 6. Trademarks. This License does not grant permission to use the trade - * names, trademarks, service marks, or product names of the Licensor, - * except as required for reasonable and customary use in describing the - * origin of the Work and reproducing the content of the NOTICE file. - * - * 7. Disclaimer of Warranty. Unless required by applicable law or - * agreed to in writing, Licensor provides the Work (and each - * Contributor provides its Contributions) on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - * implied, including, without limitation, any warranties or conditions - * of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - * PARTICULAR PURPOSE. You are solely responsible for determining the - * appropriateness of using or redistributing the Work and assume any - * risks associated with Your exercise of permissions under this License. - * - * 8. Limitation of Liability. In no event and under no legal theory, - * whether in tort (including negligence), contract, or otherwise, - * unless required by applicable law (such as deliberate and grossly - * negligent acts) or agreed to in writing, shall any Contributor be - * liable to You for damages, including any direct, indirect, special, - * incidental, or consequential damages of any character arising as a - * result of this License or out of the use or inability to use the - * Work (including but not limited to damages for loss of goodwill, - * work stoppage, computer failure or malfunction, or any and all - * other commercial damages or losses), even if such Contributor - * has been advised of the possibility of such damages. - * - * 9. Accepting Warranty or Additional Liability. While redistributing - * the Work or Derivative Works thereof, You may choose to offer, - * and charge a fee for, acceptance of support, warranty, indemnity, - * or other liability obligations and/or rights consistent with this - * License. However, in accepting such obligations, You may act only - * on Your own behalf and on Your sole responsibility, not on behalf - * of any other Contributor, and only if You agree to indemnify, - * defend, and hold each Contributor harmless for any liability - * incurred by, or claims asserted against, such Contributor by reason - * of your accepting any such warranty or additional liability. - * - * END OF TERMS AND CONDITIONS - * - * APPENDIX: How to apply the Apache License to your work. - * - * To apply the Apache License to your work, attach the following - * boilerplate notice, with the fields enclosed by brackets "[]" - * replaced with your own identifying information. (Don't include - * the brackets!) The text should be enclosed in the appropriate - * comment syntax for the file format. We also recommend that a - * file or class name and description of purpose be included on the - * same "printed page" as the copyright notice for easier - * identification within third-party archives. - * - * Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ -import { Observable, Subscription, EmptyError } from 'rxjs'; +import { Observable } from 'rxjs'; +import { first, last } from 'rxjs/operators'; export function firstValueFrom(source: Observable) { - return new Promise((resolve, reject) => { - const subs = new Subscription(); - subs.add( - source.subscribe({ - next: (value) => { - resolve(value); - subs.unsubscribe(); - }, - error: reject, - complete: () => { - reject(new EmptyError()); - }, - }) - ); - }); + // we can't use SafeSubscriber the same way that RxJS 7 does, so instead we + return source.pipe(first()).toPromise(); } export function lastValueFrom(source: Observable) { - return new Promise((resolve, reject) => { - let _hasValue = false; - let _value: T; - source.subscribe({ - next: (value) => { - _value = value; - _hasValue = true; - }, - error: reject, - complete: () => { - if (_hasValue) { - resolve(_value); - } else { - reject(new EmptyError()); - } - }, - }); - }); + return source.pipe(last()).toPromise(); } diff --git a/packages/kbn-storybook/lib/default_config.ts b/packages/kbn-storybook/lib/default_config.ts index 1fad9e2a3e087..dc2647b7b5757 100644 --- a/packages/kbn-storybook/lib/default_config.ts +++ b/packages/kbn-storybook/lib/default_config.ts @@ -20,7 +20,12 @@ import { StorybookConfig } from '@storybook/core/types'; export const defaultConfig: StorybookConfig = { - addons: ['@kbn/storybook/preset', '@storybook/addon-knobs', '@storybook/addon-essentials'], + addons: [ + '@kbn/storybook/preset', + '@storybook/addon-a11y', + '@storybook/addon-knobs', + '@storybook/addon-essentials', + ], stories: ['../**/*.stories.tsx'], typescript: { reactDocgen: false, diff --git a/packages/kbn-storybook/package.json b/packages/kbn-storybook/package.json index 58359159e950d..cf0bb1262ea73 100644 --- a/packages/kbn-storybook/package.json +++ b/packages/kbn-storybook/package.json @@ -4,34 +4,13 @@ "private": true, "license": "Apache-2.0", "main": "./target/index.js", - "dependencies": { + "devDependencies": { "@kbn/dev-utils": "1.0.0", - "@storybook/addon-actions": "^6.0.16", - "@storybook/addon-essentials": "^6.0.16", - "@storybook/addon-knobs": "^6.0.16", - "@storybook/addon-storyshots": "^6.0.16", - "@storybook/core": "^6.0.16", - "@storybook/react": "^6.0.16", - "@storybook/theming": "^6.0.16", "@types/loader-utils": "^1.1.3", - "@types/webpack": "^4.41.3", - "@types/webpack-env": "^1.15.2", - "@types/webpack-merge": "^4.1.5", - "@kbn/utils": "1.0.0", - "babel-loader": "^8.0.6", - "copy-webpack-plugin": "^6.0.2", - "fast-glob": "2.2.7", - "glob-watcher": "5.0.3", - "jest-specific-snapshot": "2.0.0", - "jest-styled-components": "^7.0.2", - "mkdirp": "0.5.1", - "mini-css-extract-plugin": "0.8.0", - "normalize-path": "^3.0.0", - "react-docgen-typescript-loader": "^3.1.1", - "rxjs": "^6.5.5", - "serve-static": "1.14.1", - "styled-components": "^5.1.0", - "webpack": "^4.41.5" + "@types/webpack-merge": "^4.1.5" + }, + "kibana": { + "devOnly": true }, "scripts": { "build": "tsc", diff --git a/packages/kbn-storybook/yarn.lock b/packages/kbn-storybook/yarn.lock deleted file mode 120000 index 3f82ebc9cdbae..0000000000000 --- a/packages/kbn-storybook/yarn.lock +++ /dev/null @@ -1 +0,0 @@ -../../yarn.lock \ No newline at end of file diff --git a/packages/kbn-telemetry-tools/package.json b/packages/kbn-telemetry-tools/package.json index 4318cbcf2ec4e..cda2998901d56 100644 --- a/packages/kbn-telemetry-tools/package.json +++ b/packages/kbn-telemetry-tools/package.json @@ -4,6 +4,9 @@ "license": "Apache-2.0", "main": "./target/index.js", "private": true, + "kibana": { + "devOnly": true + }, "scripts": { "build": "babel src --out-dir target --delete-dir-on-start --extensions .ts --source-maps=inline", "kbn:bootstrap": "yarn build", diff --git a/packages/kbn-test-subj-selector/package.json b/packages/kbn-test-subj-selector/package.json index 82a26dc4807be..b823c68f9560b 100755 --- a/packages/kbn-test-subj-selector/package.json +++ b/packages/kbn-test-subj-selector/package.json @@ -5,5 +5,8 @@ "main": "index.js", "keywords": [], "author": "Spencer Alger ", - "license": "Apache-2.0" + "license": "Apache-2.0", + "kibana": { + "devOnly": true + } } diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json index 8422c34c9ed08..24096a41a5fdd 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -9,6 +9,9 @@ "kbn:bootstrap": "yarn build", "kbn:watch": "yarn build --watch" }, + "kibana": { + "devOnly": true + }, "devDependencies": { "@babel/cli": "^7.10.5", "@jest/types": "^26.5.2", diff --git a/packages/kbn-ui-shared-deps/entry.js b/packages/kbn-ui-shared-deps/entry.js index d74b45f973eb1..4700479941eed 100644 --- a/packages/kbn-ui-shared-deps/entry.js +++ b/packages/kbn-ui-shared-deps/entry.js @@ -22,6 +22,7 @@ require('./polyfills'); // must load before angular export const Jquery = require('jquery'); window.$ = window.jQuery = Jquery; +require('./flot_charts'); // stateful deps export const KbnI18n = require('@kbn/i18n'); @@ -50,11 +51,9 @@ export const ElasticEui = require('@elastic/eui'); export const ElasticEuiLibServices = require('@elastic/eui/lib/services'); export const ElasticEuiLibServicesFormat = require('@elastic/eui/lib/services/format'); export const ElasticEuiChartsTheme = require('@elastic/eui/dist/eui_charts_theme'); +export const Theme = require('./theme.ts'); export const Lodash = require('lodash'); export const LodashFp = require('lodash/fp'); // runtime deps which don't need to be copied across all bundles export const TsLib = require('tslib'); - -import * as Theme from './theme.ts'; -export { Theme }; diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/API.md b/packages/kbn-ui-shared-deps/flot_charts/API.md similarity index 100% rename from x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/API.md rename to packages/kbn-ui-shared-deps/flot_charts/API.md diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/index.js b/packages/kbn-ui-shared-deps/flot_charts/index.js similarity index 52% rename from x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/index.js rename to packages/kbn-ui-shared-deps/flot_charts/index.js index 613939256cfc9..6d9872d3ec524 100644 --- a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/index.js +++ b/packages/kbn-ui-shared-deps/flot_charts/index.js @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ /* @notice @@ -32,17 +45,15 @@ * THE SOFTWARE. */ -import $ from 'jquery'; -if (window) window.jQuery = $; -require('./jquery.flot'); -require('./jquery.flot.time'); -require('./jquery.flot.canvas'); -require('./jquery.flot.symbol'); -require('./jquery.flot.crosshair'); -require('./jquery.flot.selection'); -require('./jquery.flot.pie'); -require('./jquery.flot.stack'); -require('./jquery.flot.threshold'); -require('./jquery.flot.fillbetween'); -require('./jquery.flot.log'); -module.exports = $; +import './jquery_flot'; +import './jquery_flot_canvas'; +import './jquery_flot_time'; +import './jquery_flot_symbol'; +import './jquery_flot_crosshair'; +import './jquery_flot_selection'; +import './jquery_flot_pie'; +import './jquery_flot_stack'; +import './jquery_flot_threshold'; +import './jquery_flot_fillbetween'; +import './jquery_flot_log'; +import './jquery_flot_axislabels'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.colorhelpers.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_colorhelpers.js similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.colorhelpers.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_colorhelpers.js diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot.js similarity index 100% rename from x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot.js diff --git a/src/plugins/timelion/public/flot/jquery.flot.axislabels.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_axislabels.js similarity index 100% rename from src/plugins/timelion/public/flot/jquery.flot.axislabels.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_axislabels.js diff --git a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.canvas.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_canvas.js similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.canvas.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_canvas.js diff --git a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.categories.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_categories.js similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.categories.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_categories.js diff --git a/src/plugins/timelion/public/flot/jquery.flot.crosshair.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_crosshair.js similarity index 100% rename from src/plugins/timelion/public/flot/jquery.flot.crosshair.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_crosshair.js diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.errorbars.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_errorbars.js similarity index 100% rename from x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.errorbars.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_errorbars.js diff --git a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.fillbetween.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_fillbetween.js similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.fillbetween.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_fillbetween.js diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.image.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_image.js similarity index 100% rename from x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.image.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_image.js diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.log.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_log.js similarity index 100% rename from x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.log.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_log.js diff --git a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.navigate.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_navigate.js similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.navigate.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_navigate.js diff --git a/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_pie.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_pie.js new file mode 100644 index 0000000000000..c1301a0659bda --- /dev/null +++ b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_pie.js @@ -0,0 +1,896 @@ +/* Flot plugin for rendering pie charts. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The plugin assumes that each series has a single data value, and that each +value is a positive integer or zero. Negative numbers don't make sense for a +pie chart, and have unpredictable results. The values do NOT need to be +passed in as percentages; the plugin will calculate the total and per-slice +percentages internally. + +* Created by Brian Medendorp + +* Updated with contributions from btburnett3, Anthony Aragues and Xavi Ivars + +The plugin supports these options: + + series: { + pie: { + show: true/false + radius: 0-1 for percentage of fullsize, or a specified pixel length, or 'auto' + innerRadius: 0-1 for percentage of fullsize or a specified pixel length, for creating a donut effect + startAngle: 0-2 factor of PI used for starting angle (in radians) i.e 3/2 starts at the top, 0 and 2 have the same result + tilt: 0-1 for percentage to tilt the pie, where 1 is no tilt, and 0 is completely flat (nothing will show) + offset: { + top: integer value to move the pie up or down + left: integer value to move the pie left or right, or 'auto' + }, + stroke: { + color: any hexadecimal color value (other formats may or may not work, so best to stick with something like '#FFF') + width: integer pixel width of the stroke + }, + label: { + show: true/false, or 'auto' + formatter: a user-defined function that modifies the text/style of the label text + radius: 0-1 for percentage of fullsize, or a specified pixel length + background: { + color: any hexadecimal color value (other formats may or may not work, so best to stick with something like '#000') + opacity: 0-1 + }, + threshold: 0-1 for the percentage value at which to hide labels (if they're too small) + }, + combine: { + threshold: 0-1 for the percentage value at which to combine slices (if they're too small) + color: any hexadecimal color value (other formats may or may not work, so best to stick with something like '#CCC'), if null, the plugin will automatically use the color of the first slice to be combined + label: any text value of what the combined slice should be labeled + } + highlight: { + opacity: 0-1 + } + } + } + +More detail and specific examples can be found in the included HTML file. + +*/ + +import { i18n } from '@kbn/i18n'; + +(function($) { + // Maximum redraw attempts when fitting labels within the plot + + var REDRAW_ATTEMPTS = 10; + + // Factor by which to shrink the pie when fitting labels within the plot + + var REDRAW_SHRINK = 0.95; + + function init(plot) { + let canvas = null; + let target = null; + let options = null; + let maxRadius = null; + let centerLeft = null; + let centerTop = null; + let processed = false; + let ctx = null; + + // interactive variables + + let highlights = []; + + // add hook to determine if pie plugin in enabled, and then perform necessary operations + + plot.hooks.processOptions.push(function (plot, options) { + if (options.series.pie.show) { + options.grid.show = false; + + // set labels.show + + if (options.series.pie.label.show === 'auto') { + if (options.legend.show) { + options.series.pie.label.show = false; + } else { + options.series.pie.label.show = true; + } + } + + // set radius + + if (options.series.pie.radius === 'auto') { + if (options.series.pie.label.show) { + options.series.pie.radius = 3 / 4; + } else { + options.series.pie.radius = 1; + } + } + + // ensure sane tilt + + if (options.series.pie.tilt > 1) { + options.series.pie.tilt = 1; + } else if (options.series.pie.tilt < 0) { + options.series.pie.tilt = 0; + } + } + }); + + plot.hooks.bindEvents.push(function (plot, eventHolder) { + const options = plot.getOptions(); + if (options.series.pie.show) { + if (options.grid.hoverable) { + eventHolder.unbind('mousemove').mousemove(onMouseMove); + } + + if (options.grid.clickable) { + eventHolder.unbind('click').click(onClick); + } + } + }); + + plot.hooks.processDatapoints.push(function (plot, series, data, datapoints) { + const options = plot.getOptions(); + if (options.series.pie.show) { + processDatapoints(plot, series, data, datapoints); + } + }); + + plot.hooks.drawOverlay.push(function (plot, octx) { + const options = plot.getOptions(); + if (options.series.pie.show) { + drawOverlay(plot, octx); + } + }); + + plot.hooks.draw.push(function (plot, newCtx) { + const options = plot.getOptions(); + if (options.series.pie.show) { + draw(plot, newCtx); + } + }); + + function processDatapoints(plot) { + if (!processed) { + processed = true; + canvas = plot.getCanvas(); + target = $(canvas).parent(); + options = plot.getOptions(); + plot.setData(combine(plot.getData())); + } + } + + function combine(data) { + let total = 0; + let combined = 0; + let numCombined = 0; + let color = options.series.pie.combine.color; + const newdata = []; + + // Fix up the raw data from Flot, ensuring the data is numeric + + for (let i = 0; i < data.length; ++i) { + let value = data[i].data; + + // If the data is an array, we'll assume that it's a standard + // Flot x-y pair, and are concerned only with the second value. + + // Note how we use the original array, rather than creating a + // new one; this is more efficient and preserves any extra data + // that the user may have stored in higher indexes. + + if (Array.isArray(value) && value.length === 1) { + value = value[0]; + } + + if (Array.isArray(value)) { + // Equivalent to $.isNumeric() but compatible with jQuery < 1.7 + if (!isNaN(parseFloat(value[1])) && isFinite(value[1])) { + value[1] = +value[1]; + } else { + value[1] = 0; + } + } else if (!isNaN(parseFloat(value)) && isFinite(value)) { + value = [1, +value]; + } else { + value = [1, 0]; + } + + data[i].data = [value]; + } + + // Sum up all the slices, so we can calculate percentages for each + + for (let i = 0; i < data.length; ++i) { + total += data[i].data[0][1]; + } + + // Count the number of slices with percentages below the combine + // threshold; if it turns out to be just one, we won't combine. + + for (let i = 0; i < data.length; ++i) { + const value = data[i].data[0][1]; + if (value / total <= options.series.pie.combine.threshold) { + combined += value; + numCombined++; + if (!color) { + color = data[i].color; + } + } + } + + for (let i = 0; i < data.length; ++i) { + const value = data[i].data[0][1]; + if (numCombined < 2 || value / total > options.series.pie.combine.threshold) { + newdata.push( + $.extend(data[i], { + /* extend to allow keeping all other original data values + and using them e.g. in labelFormatter. */ + data: [[1, value]], + color: data[i].color, + label: data[i].label, + angle: (value * Math.PI * 2) / total, + percent: value / (total / 100), + }) + ); + } + } + + if (numCombined > 1) { + newdata.push({ + data: [[1, combined]], + color: color, + label: options.series.pie.combine.label, + angle: (combined * Math.PI * 2) / total, + percent: combined / (total / 100), + }); + } + + return newdata; + } + + function draw(plot, newCtx) { + if (!target) { + return; + } // if no series were passed + + const canvasWidth = plot.getPlaceholder().width(); + const canvasHeight = plot.getPlaceholder().height(); + const legendWidth = target.children().filter('.legend').children().width() || 0; + + ctx = newCtx; + + // WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE! + + // When combining smaller slices into an 'other' slice, we need to + // add a new series. Since Flot gives plugins no way to modify the + // list of series, the pie plugin uses a hack where the first call + // to processDatapoints results in a call to setData with the new + // list of series, then subsequent processDatapoints do nothing. + + // The plugin-global 'processed' flag is used to control this hack; + // it starts out false, and is set to true after the first call to + // processDatapoints. + + // Unfortunately this turns future setData calls into no-ops; they + // call processDatapoints, the flag is true, and nothing happens. + + // To fix this we'll set the flag back to false here in draw, when + // all series have been processed, so the next sequence of calls to + // processDatapoints once again starts out with a slice-combine. + // This is really a hack; in 0.9 we need to give plugins a proper + // way to modify series before any processing begins. + + processed = false; + + // calculate maximum radius and center point + + maxRadius = Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2; + centerTop = canvasHeight / 2 + options.series.pie.offset.top; + centerLeft = canvasWidth / 2; + + if (options.series.pie.offset.left === 'auto') { + if (options.legend.position.match('w')) { + centerLeft += legendWidth / 2; + } else { + centerLeft -= legendWidth / 2; + } + + if (centerLeft < maxRadius) { + centerLeft = maxRadius; + } else if (centerLeft > canvasWidth - maxRadius) { + centerLeft = canvasWidth - maxRadius; + } + } else { + centerLeft += options.series.pie.offset.left; + } + + const slices = plot.getData(); + let attempts = 0; + + // Keep shrinking the pie's radius until drawPie returns true, + // indicating that all the labels fit, or we try too many times. + + do { + if (attempts > 0) { + maxRadius *= REDRAW_SHRINK; + } + + attempts += 1; + clear(); + if (options.series.pie.tilt <= 0.8) { + drawShadow(); + } + } while (!drawPie() && attempts < REDRAW_ATTEMPTS); + + if (attempts >= REDRAW_ATTEMPTS) { + clear(); + const errorMessage = i18n.translate('flot.pie.unableToDrawLabelsInsideCanvasErrorMessage', { + defaultMessage: 'Could not draw pie with labels contained inside canvas', + }); + target.prepend( + `
${errorMessage}
` + ); + } + + if (plot.setSeries && plot.insertLegend) { + plot.setSeries(slices); + plot.insertLegend(); + } + + // we're actually done at this point, just defining internal functions at this point + + function clear() { + ctx.clearRect(0, 0, canvasWidth, canvasHeight); + target.children().filter('.pieLabel, .pieLabelBackground').remove(); + } + + function drawShadow() { + const shadowLeft = options.series.pie.shadow.left; + const shadowTop = options.series.pie.shadow.top; + const edge = 10; + const alpha = options.series.pie.shadow.alpha; + let radius = + options.series.pie.radius > 1 + ? options.series.pie.radius + : maxRadius * options.series.pie.radius; + + if ( + radius >= canvasWidth / 2 - shadowLeft || + radius * options.series.pie.tilt >= canvasHeight / 2 - shadowTop || + radius <= edge + ) { + return; + } // shadow would be outside canvas, so don't draw it + + ctx.save(); + ctx.translate(shadowLeft, shadowTop); + ctx.globalAlpha = alpha; + ctx.fillStyle = '#000'; + + // center and rotate to starting position + + ctx.translate(centerLeft, centerTop); + ctx.scale(1, options.series.pie.tilt); + + //radius -= edge; + + for (let i = 1; i <= edge; i++) { + ctx.beginPath(); + ctx.arc(0, 0, radius, 0, Math.PI * 2, false); + ctx.fill(); + radius -= i; + } + + ctx.restore(); + } + + function drawPie() { + const startAngle = Math.PI * options.series.pie.startAngle; + const radius = + options.series.pie.radius > 1 + ? options.series.pie.radius + : maxRadius * options.series.pie.radius; + + // center and rotate to starting position + + ctx.save(); + ctx.translate(centerLeft, centerTop); + ctx.scale(1, options.series.pie.tilt); + //ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera + + // draw slices + + ctx.save(); + let currentAngle = startAngle; + for (let i = 0; i < slices.length; ++i) { + slices[i].startAngle = currentAngle; + drawSlice(slices[i].angle, slices[i].color, true); + } + ctx.restore(); + + // draw slice outlines + + if (options.series.pie.stroke.width > 0) { + ctx.save(); + ctx.lineWidth = options.series.pie.stroke.width; + currentAngle = startAngle; + for (let i = 0; i < slices.length; ++i) { + drawSlice(slices[i].angle, options.series.pie.stroke.color, false); + } + + ctx.restore(); + } + + // draw donut hole + + drawDonutHole(ctx); + + ctx.restore(); + + // Draw the labels, returning true if they fit within the plot + + if (options.series.pie.label.show) { + return drawLabels(); + } else { + return true; + } + + function drawSlice(angle, color, fill) { + if (angle <= 0 || isNaN(angle)) { + return; + } + + if (fill) { + ctx.fillStyle = color; + } else { + ctx.strokeStyle = color; + ctx.lineJoin = 'round'; + } + + ctx.beginPath(); + if (Math.abs(angle - Math.PI * 2) > 0.000000001) { + ctx.moveTo(0, 0); + } // Center of the pie + + //ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera + ctx.arc(0, 0, radius, currentAngle, currentAngle + angle / 2, false); + ctx.arc(0, 0, radius, currentAngle + angle / 2, currentAngle + angle, false); + ctx.closePath(); + //ctx.rotate(angle); // This doesn't work properly in Opera + currentAngle += angle; + + if (fill) { + ctx.fill(); + } else { + ctx.stroke(); + } + } + + function drawLabels() { + let currentAngle = startAngle; + const radius = + options.series.pie.label.radius > 1 + ? options.series.pie.label.radius + : maxRadius * options.series.pie.label.radius; + + for (let i = 0; i < slices.length; ++i) { + if (slices[i].percent >= options.series.pie.label.threshold * 100) { + if (!drawLabel(slices[i], currentAngle, i)) { + return false; + } + } + + currentAngle += slices[i].angle; + } + + return true; + + function drawLabel(slice, startAngle, index) { + if (slice.data[0][1] === 0) { + return true; + } + + // format label text + + const lf = options.legend.labelFormatter; + let text; + const plf = options.series.pie.label.formatter; + + if (lf) { + text = lf(slice.label, slice); + } else { + text = slice.label; + } + + if (plf) { + text = plf(text, slice); + } + + const halfAngle = (startAngle + slice.angle + startAngle) / 2; + const x = centerLeft + Math.round(Math.cos(halfAngle) * radius); + const y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt; + + const html = + "" + + text + + ''; + target.append(html); + + const label = target.children('#pieLabel' + index); + const labelTop = y - label.height() / 2; + const labelLeft = x - label.width() / 2; + + label.css('top', labelTop); + label.css('left', labelLeft); + + // check to make sure that the label is not outside the canvas + + if ( + 0 - labelTop > 0 || + 0 - labelLeft > 0 || + canvasHeight - (labelTop + label.height()) < 0 || + canvasWidth - (labelLeft + label.width()) < 0 + ) { + return false; + } + + if (options.series.pie.label.background.opacity !== 0) { + // put in the transparent background separately to avoid blended labels and label boxes + + let c = options.series.pie.label.background.color; + + if (c == null) { + c = slice.color; + } + + const pos = 'top:' + labelTop + 'px;left:' + labelLeft + 'px;'; + $( + "
" + ) + .css('opacity', options.series.pie.label.background.opacity) + .insertBefore(label); + } + + return true; + } // end individual label function + } // end drawLabels function + } // end drawPie function + } // end draw function + + // Placed here because it needs to be accessed from multiple locations + + function drawDonutHole(layer) { + if (options.series.pie.innerRadius > 0) { + // subtract the center + + layer.save(); + const innerRadius = + options.series.pie.innerRadius > 1 + ? options.series.pie.innerRadius + : maxRadius * options.series.pie.innerRadius; + layer.globalCompositeOperation = 'destination-out'; // this does not work with excanvas, but it will fall back to using the stroke color + layer.beginPath(); + layer.fillStyle = options.series.pie.stroke.color; + layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); + layer.fill(); + layer.closePath(); + layer.restore(); + + // add inner stroke + // TODO: Canvas forked flot here! + if (options.series.pie.stroke.width > 0) { + layer.save(); + layer.beginPath(); + layer.strokeStyle = options.series.pie.stroke.color; + layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); + layer.stroke(); + layer.closePath(); + layer.restore(); + } + + // TODO: add extra shadow inside hole (with a mask) if the pie is tilted. + } + } + + //-- Additional Interactive related functions -- + + function isPointInPoly(poly, pt) { + let c = false; + const l = poly.length; + let j = l - 1; + for (let i = -1; ++i < l; j = i) { + ((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || + (poly[j][1] <= pt[1] && pt[1] < poly[i][1])) && + pt[0] < + ((poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1])) / (poly[j][1] - poly[i][1]) + + poly[i][0] && + (c = !c); + } + return c; + } + + function findNearbySlice(mouseX, mouseY) { + const slices = plot.getData(); + const options = plot.getOptions(); + const radius = + options.series.pie.radius > 1 + ? options.series.pie.radius + : maxRadius * options.series.pie.radius; + let x; + let y; + + for (let i = 0; i < slices.length; ++i) { + const s = slices[i]; + + if (s.pie.show) { + ctx.save(); + ctx.beginPath(); + ctx.moveTo(0, 0); // Center of the pie + //ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here. + ctx.arc(0, 0, radius, s.startAngle, s.startAngle + s.angle / 2, false); + ctx.arc(0, 0, radius, s.startAngle + s.angle / 2, s.startAngle + s.angle, false); + ctx.closePath(); + x = mouseX - centerLeft; + y = mouseY - centerTop; + + if (ctx.isPointInPath) { + if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) { + ctx.restore(); + return { + datapoint: [s.percent, s.data], + dataIndex: 0, + series: s, + seriesIndex: i, + }; + } + } else { + // excanvas for IE doesn;t support isPointInPath, this is a workaround. + + const p1X = radius * Math.cos(s.startAngle); + const p1Y = radius * Math.sin(s.startAngle); + const p2X = radius * Math.cos(s.startAngle + s.angle / 4); + const p2Y = radius * Math.sin(s.startAngle + s.angle / 4); + const p3X = radius * Math.cos(s.startAngle + s.angle / 2); + const p3Y = radius * Math.sin(s.startAngle + s.angle / 2); + const p4X = radius * Math.cos(s.startAngle + s.angle / 1.5); + const p4Y = radius * Math.sin(s.startAngle + s.angle / 1.5); + const p5X = radius * Math.cos(s.startAngle + s.angle); + const p5Y = radius * Math.sin(s.startAngle + s.angle); + const arrPoly = [ + [0, 0], + [p1X, p1Y], + [p2X, p2Y], + [p3X, p3Y], + [p4X, p4Y], + [p5X, p5Y], + ]; + const arrPoint = [x, y]; + + // TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt? + + if (isPointInPoly(arrPoly, arrPoint)) { + ctx.restore(); + return { + datapoint: [s.percent, s.data], + dataIndex: 0, + series: s, + seriesIndex: i, + }; + } + } + + ctx.restore(); + } + } + + return null; + } + + function onMouseMove(e) { + triggerClickHoverEvent('plothover', e); + } + + function onClick(e) { + triggerClickHoverEvent('plotclick', e); + } + + // trigger click or hover event (they send the same parameters so we share their code) + + function triggerClickHoverEvent(eventname, e) { + const offset = plot.offset(); + const canvasX = parseInt(e.pageX - offset.left, 10); + const canvasY = parseInt(e.pageY - offset.top, 10); + const item = findNearbySlice(canvasX, canvasY); + + if (options.grid.autoHighlight) { + // clear auto-highlights + + for (let i = 0; i < highlights.length; ++i) { + const h = highlights[i]; + if (h.auto === eventname && !(item && h.series === item.series)) { + unhighlight(h.series); + } + } + } + + // highlight the slice + + if (item) { + highlight(item.series, eventname); + } + + // trigger any hover bind events + + const pos = { pageX: e.pageX, pageY: e.pageY }; + target.trigger(eventname, [pos, item]); + } + + function highlight(s, auto) { + //if (typeof s == "number") { + // s = series[s]; + //} + + const i = indexOfHighlight(s); + + if (i === -1) { + highlights.push({ series: s, auto: auto }); + plot.triggerRedrawOverlay(); + } else if (!auto) { + highlights[i].auto = false; + } + } + + function unhighlight(s) { + if (s == null) { + highlights = []; + plot.triggerRedrawOverlay(); + } + + //if (typeof s == "number") { + // s = series[s]; + //} + + const i = indexOfHighlight(s); + + if (i !== -1) { + highlights.splice(i, 1); + plot.triggerRedrawOverlay(); + } + } + + function indexOfHighlight(s) { + for (let i = 0; i < highlights.length; ++i) { + const h = highlights[i]; + if (h.series === s) { + return i; + } + } + return -1; + } + + function drawOverlay(plot, octx) { + const options = plot.getOptions(); + + const radius = + options.series.pie.radius > 1 + ? options.series.pie.radius + : maxRadius * options.series.pie.radius; + + octx.save(); + octx.translate(centerLeft, centerTop); + octx.scale(1, options.series.pie.tilt); + + for (let i = 0; i < highlights.length; ++i) { + drawHighlight(highlights[i].series); + } + + drawDonutHole(octx); + + octx.restore(); + + function drawHighlight(series) { + if (series.angle <= 0 || isNaN(series.angle)) { + return; + } + + //octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString(); + octx.fillStyle = 'rgba(255, 255, 255, ' + options.series.pie.highlight.opacity + ')'; // this is temporary until we have access to parseColor + octx.beginPath(); + if (Math.abs(series.angle - Math.PI * 2) > 0.000000001) { + octx.moveTo(0, 0); + } // Center of the pie + + octx.arc(0, 0, radius, series.startAngle, series.startAngle + series.angle / 2, false); + octx.arc( + 0, + 0, + radius, + series.startAngle + series.angle / 2, + series.startAngle + series.angle, + false + ); + octx.closePath(); + octx.fill(); + } + } + } // end init (plugin body) + + // define pie specific options and their default values + + const options = { + series: { + pie: { + show: false, + radius: 'auto', // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value) + innerRadius: 0 /* for donut */, + startAngle: 3 / 2, + tilt: 1, + shadow: { + left: 5, // shadow left offset + top: 15, // shadow top offset + alpha: 0.02, // shadow alpha + }, + offset: { + top: 0, + left: 'auto', + }, + stroke: { + color: '#fff', + width: 1, + }, + label: { + show: 'auto', + formatter: function (label, slice) { + return ( + "
" + + label + + '
' + + Math.round(slice.percent) + + '%
' + ); + }, // formatter function + radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value) + background: { + color: null, + opacity: 0, + }, + threshold: 0, // percentage at which to hide the label (i.e. the slice is too narrow) + }, + combine: { + threshold: -1, // percentage at which to combine little slices into one larger slice + color: null, // color to give the new slice (auto-generated if null) + label: 'Other', // label to give the new slice + }, + highlight: { + //color: "#fff", // will add this functionality once parseColor is available + opacity: 0.5, + }, + }, + }, + }; + + $.plot.plugins.push({ + init: init, + options: options, + name: "pie", + version: "1.1" + }); + +})(jQuery); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.resize.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_resize.js similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.resize.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_resize.js diff --git a/src/plugins/timelion/public/flot/jquery.flot.selection.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_selection.js similarity index 100% rename from src/plugins/timelion/public/flot/jquery.flot.selection.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_selection.js diff --git a/src/plugins/timelion/public/flot/jquery.flot.stack.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_stack.js similarity index 100% rename from src/plugins/timelion/public/flot/jquery.flot.stack.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_stack.js diff --git a/src/plugins/timelion/public/flot/jquery.flot.symbol.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_symbol.js similarity index 100% rename from src/plugins/timelion/public/flot/jquery.flot.symbol.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_symbol.js diff --git a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.threshold.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_threshold.js similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.threshold.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_threshold.js diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.time.js b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_time.js similarity index 92% rename from x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.time.js rename to packages/kbn-ui-shared-deps/flot_charts/jquery_flot_time.js index 991e87d364e8a..767088d1410e2 100644 --- a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.time.js +++ b/packages/kbn-ui-shared-deps/flot_charts/jquery_flot_time.js @@ -49,47 +49,47 @@ import { i18n } from '@kbn/i18n'; if (monthNames == null) { monthNames = [ - i18n.translate('xpack.monitoring.janLabel', { + i18n.translate('flot.time.janLabel', { defaultMessage: 'Jan', - }), i18n.translate('xpack.monitoring.febLabel', { + }), i18n.translate('flot.time.febLabel', { defaultMessage: 'Feb', - }), i18n.translate('xpack.monitoring.marLabel', { + }), i18n.translate('flot.time.marLabel', { defaultMessage: 'Mar', - }), i18n.translate('xpack.monitoring.aprLabel', { + }), i18n.translate('flot.time.aprLabel', { defaultMessage: 'Apr', - }), i18n.translate('xpack.monitoring.mayLabel', { + }), i18n.translate('flot.time.mayLabel', { defaultMessage: 'May', - }), i18n.translate('xpack.monitoring.junLabel', { + }), i18n.translate('flot.time.junLabel', { defaultMessage: 'Jun', - }), i18n.translate('xpack.monitoring.julLabel', { + }), i18n.translate('flot.time.julLabel', { defaultMessage: 'Jul', - }), i18n.translate('xpack.monitoring.augLabel', { + }), i18n.translate('flot.time.augLabel', { defaultMessage: 'Aug', - }), i18n.translate('xpack.monitoring.sepLabel', { + }), i18n.translate('flot.time.sepLabel', { defaultMessage: 'Sep', - }), i18n.translate('xpack.monitoring.octLabel', { + }), i18n.translate('flot.time.octLabel', { defaultMessage: 'Oct', - }), i18n.translate('xpack.monitoring.novLabel', { + }), i18n.translate('flot.time.novLabel', { defaultMessage: 'Nov', - }), i18n.translate('xpack.monitoring.decLabel', { + }), i18n.translate('flot.time.decLabel', { defaultMessage: 'Dec', })]; } if (dayNames == null) { - dayNames = [i18n.translate('xpack.monitoring.sunLabel', { + dayNames = [i18n.translate('flot.time.sunLabel', { defaultMessage: 'Sun', - }), i18n.translate('xpack.monitoring.monLabel', { + }), i18n.translate('flot.time.monLabel', { defaultMessage: 'Mon', - }), i18n.translate('xpack.monitoring.tueLabel', { + }), i18n.translate('flot.time.tueLabel', { defaultMessage: 'Tue', - }), i18n.translate('xpack.monitoring.wedLabel', { + }), i18n.translate('flot.time.wedLabel', { defaultMessage: 'Wed', - }), i18n.translate('xpack.monitoring.thuLabel', { + }), i18n.translate('flot.time.thuLabel', { defaultMessage: 'Thu', - }), i18n.translate('xpack.monitoring.friLabel', { + }), i18n.translate('flot.time.friLabel', { defaultMessage: 'Fri', - }), i18n.translate('xpack.monitoring.satLabel', { + }), i18n.translate('flot.time.satLabel', { defaultMessage: 'Sat', })]; } diff --git a/packages/kbn-utility-types/package.json b/packages/kbn-utility-types/package.json index d1d7a1c0397cf..6b531efcebace 100644 --- a/packages/kbn-utility-types/package.json +++ b/packages/kbn-utility-types/package.json @@ -5,6 +5,9 @@ "license": "Apache-2.0", "main": "target", "types": "target/index.d.ts", + "kibana": { + "devOnly": true + }, "scripts": { "build": "tsc", "kbn:bootstrap": "tsc", diff --git a/src/plugins/timelion/public/flot/index.js b/scripts/find_plugin_circular_deps.js similarity index 78% rename from src/plugins/timelion/public/flot/index.js rename to scripts/find_plugin_circular_deps.js index a066fd3ab8607..6b0661cb841b4 100644 --- a/src/plugins/timelion/public/flot/index.js +++ b/scripts/find_plugin_circular_deps.js @@ -17,10 +17,5 @@ * under the License. */ -import './jquery.flot'; -import './jquery.flot.time'; -import './jquery.flot.symbol'; -import './jquery.flot.crosshair'; -import './jquery.flot.selection'; -import './jquery.flot.stack'; -import './jquery.flot.axislabels'; +require('../src/setup_node_env'); +require('../src/dev/run_find_plugin_circular_deps'); diff --git a/scripts/find_plugins_without_ts_refs.js b/scripts/find_plugins_without_ts_refs.js new file mode 100644 index 0000000000000..5f543a045f739 --- /dev/null +++ b/scripts/find_plugins_without_ts_refs.js @@ -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. + */ + +require('../src/setup_node_env'); +require('../src/dev/run_find_plugins_without_ts_refs'); diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts b/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts index 86a02d74dea15..d6a4224d9fab0 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/build_docker_sh.template.ts @@ -36,7 +36,33 @@ function generator({ # set -euo pipefail - docker pull ${baseOSImage} + retry_docker_pull() { + image=$1 + attempt=0 + max_retries=5 + + while true + do + attempt=$((attempt+1)) + + if [ $attempt -gt $max_retries ] + then + echo "Docker pull retries exceeded, aborting." + exit 1 + fi + + if docker pull "$image" + then + echo "Docker pull successful." + break + else + echo "Docker pull unsuccessful, attempt '$attempt'." + fi + + done + } + + retry_docker_pull ${baseOSImage} echo "Building: kibana${imageFlavor}${ubiImageFlavor}-docker"; \\ docker build -t ${imageTag}${imageFlavor}${ubiImageFlavor}:${version} -f Dockerfile . || exit 1; diff --git a/src/dev/plugin_discovery/find_plugins.ts b/src/dev/plugin_discovery/find_plugins.ts new file mode 100644 index 0000000000000..4e7c34698c964 --- /dev/null +++ b/src/dev/plugin_discovery/find_plugins.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import Path from 'path'; +import { REPO_ROOT } from '@kbn/dev-utils'; +import { getPluginSearchPaths } from '@kbn/config'; +import { KibanaPlatformPlugin, simpleKibanaPlatformPluginDiscovery } from '@kbn/dev-utils'; + +export interface SearchOptions { + oss: boolean; + examples: boolean; + extraPluginScanDirs: string[]; +} + +export function findPlugins({ + oss, + examples, + extraPluginScanDirs, +}: SearchOptions): Map { + const pluginSearchPaths = getPluginSearchPaths({ + rootDir: REPO_ROOT, + oss, + examples, + }); + + for (const extraScanDir of extraPluginScanDirs) { + if (!Path.isAbsolute(extraScanDir)) { + throw new TypeError('extraPluginScanDirs must all be absolute paths'); + } + pluginSearchPaths.push(extraScanDir); + } + + const plugins = simpleKibanaPlatformPluginDiscovery(pluginSearchPaths, []); + return new Map(plugins.map((p) => [p.manifest.id, p])); +} diff --git a/src/dev/plugin_discovery/get_plugin_deps.ts b/src/dev/plugin_discovery/get_plugin_deps.ts new file mode 100644 index 0000000000000..498feefd97094 --- /dev/null +++ b/src/dev/plugin_discovery/get_plugin_deps.ts @@ -0,0 +1,89 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { KibanaPlatformPlugin } from '@kbn/dev-utils'; + +interface AllOptions { + id: string; + pluginMap: Map; +} + +interface CircularRefsError { + from: string; + to: string; + stack: string[]; +} + +export type SearchErrors = CircularRefsError; + +interface State { + deps: Set; + stack: string[]; + errors: Map; +} + +function traverse(pluginMap: Map, state: State, id: string) { + const plugin = pluginMap.get(id); + if (plugin === undefined) { + throw new Error(`Unknown plugin id: ${id}`); + } + + const prevIndex = state.stack.indexOf(id); + const isVisited = prevIndex > -1; + if (isVisited) { + const from = state.stack[state.stack.length - 1]; + const to = id; + const key = `circular-${[from, to].sort().join('-')}`; + + if (!state.errors.has(key)) { + const error: CircularRefsError = { + from, + to, + // provide sub-stack with circular refs only + stack: state.stack.slice(prevIndex), + }; + state.errors.set(key, error); + } + + return; + } + + state.stack.push(id); + new Set([ + ...plugin.manifest.requiredPlugins, + ...plugin.manifest.optionalPlugins, + ...plugin.manifest.requiredBundles, + ]).forEach((depId) => { + state.deps.add(pluginMap.get(depId)!); + traverse(pluginMap, state, depId); + }); + + state.stack.pop(); +} + +export function getPluginDeps({ pluginMap, id }: AllOptions): State { + const state: State = { + deps: new Set(), + errors: new Map(), + stack: [], + }; + + traverse(pluginMap, state, id); + + return state; +} diff --git a/src/dev/plugin_discovery/index.ts b/src/dev/plugin_discovery/index.ts new file mode 100644 index 0000000000000..4a4be65dfaef0 --- /dev/null +++ b/src/dev/plugin_discovery/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 './find_plugins'; +export * from './get_plugin_deps'; diff --git a/src/dev/run_find_plugin_circular_deps.ts b/src/dev/run_find_plugin_circular_deps.ts new file mode 100644 index 0000000000000..501e2c4fed048 --- /dev/null +++ b/src/dev/run_find_plugin_circular_deps.ts @@ -0,0 +1,73 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { run } from '@kbn/dev-utils'; +import { findPlugins, getPluginDeps, SearchErrors } from './plugin_discovery'; + +interface AllOptions { + examples?: boolean; + extraPluginScanDirs?: string[]; +} + +run( + async ({ flags, log }) => { + const { examples = false, extraPluginScanDirs = [] } = flags as AllOptions; + + const pluginMap = findPlugins({ + oss: false, + examples, + extraPluginScanDirs, + }); + + const allErrors = new Map(); + for (const pluginId of pluginMap.keys()) { + const { errors } = getPluginDeps({ + pluginMap, + id: pluginId, + }); + + for (const [errorId, error] of errors) { + if (!allErrors.has(errorId)) { + allErrors.set(errorId, error); + } + } + } + + if (allErrors.size > 0) { + allErrors.forEach((error) => { + log.warning( + `Circular refs detected: ${[...error.stack, error.to].map((p) => `[${p}]`).join(' --> ')}` + ); + }); + } + }, + { + flags: { + boolean: ['examples'], + default: { + examples: false, + }, + allowUnexpected: false, + help: ` + --examples Include examples folder + --extraPluginScanDirs Include extra scan folder + `, + }, + } +); diff --git a/src/dev/run_find_plugins_without_ts_refs.ts b/src/dev/run_find_plugins_without_ts_refs.ts new file mode 100644 index 0000000000000..ad63884671e24 --- /dev/null +++ b/src/dev/run_find_plugins_without_ts_refs.ts @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { get } from 'lodash'; +import { run } from '@kbn/dev-utils'; +import { getPluginDeps, findPlugins } from './plugin_discovery'; + +interface AllOptions { + id?: string; + examples?: boolean; + extraPluginScanDirs?: string[]; +} + +run( + async ({ flags, log }) => { + const { examples = false, extraPluginScanDirs = [], id } = flags as AllOptions; + + if (!id) { + throw new Error('Plugin id required'); + } + + const pluginMap = findPlugins({ + oss: false, + examples, + extraPluginScanDirs, + }); + + const result = getPluginDeps({ + pluginMap, + id, + }); + + if (result.errors.size > 0) { + result.errors.forEach((error) => { + log.warning( + `Circular refs detected: ${[...error.stack, error.to].map((p) => `[${p}]`).join(' --> ')}` + ); + }); + } + + const notMigratedPlugins = [...result.deps].filter( + (plugin) => !isMigratedToTsProjectRefs(plugin.directory) + ); + if (notMigratedPlugins.length > 0) { + log.info( + `Dependencies haven't been migrated to TS project refs yet:\n${notMigratedPlugins + .map((p) => p.manifest.id) + .join('\n')}` + ); + } + }, + { + flags: { + boolean: ['examples'], + string: ['id'], + default: { + examples: false, + }, + allowUnexpected: false, + help: ` + --id Plugin id to perform deps search for + --examples Include examples folder + --extraPluginScanDirs Include extra scan folder + `, + }, + } +); + +function isMigratedToTsProjectRefs(dir: string): boolean { + try { + const path = Path.join(dir, 'tsconfig.json'); + const content = Fs.readFileSync(path, { encoding: 'utf8' }); + return get(JSON.parse(content), 'compilerOptions.composite', false); + } catch (e) { + return false; + } +} diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx index 755269d1a31be..3f7d05e8692c2 100644 --- a/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx @@ -87,19 +87,19 @@ beforeEach(async () => { }); test('Add to library is compatible when embeddable on dashboard has value type input', async () => { - const action = new AddToLibraryAction(); + const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); embeddable.updateInput(await embeddable.getInputAsValueType()); expect(await action.isCompatible({ embeddable })).toBe(true); }); test('Add to library is not compatible when embeddable input is by reference', async () => { - const action = new AddToLibraryAction(); + const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); embeddable.updateInput(await embeddable.getInputAsRefType()); expect(await action.isCompatible({ embeddable })).toBe(false); }); test('Add to library is not compatible when view mode is set to view', async () => { - const action = new AddToLibraryAction(); + const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); embeddable.updateInput(await embeddable.getInputAsRefType()); embeddable.updateInput({ viewMode: ViewMode.VIEW }); expect(await action.isCompatible({ embeddable })).toBe(false); @@ -120,7 +120,7 @@ test('Add to library is not compatible when embeddable is not in a dashboard con mockedByReferenceInput: { savedObjectId: 'test', id: orphanContactCard.id }, mockedByValueInput: { firstName: 'Kibanana', id: orphanContactCard.id }, }); - const action = new AddToLibraryAction(); + const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); expect(await action.isCompatible({ embeddable: orphanContactCard })).toBe(false); }); @@ -128,7 +128,7 @@ test('Add to library replaces embeddableId but retains panel count', async () => const dashboard = embeddable.getRoot() as IContainer; const originalPanelCount = Object.keys(dashboard.getInput().panels).length; const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels)); - const action = new AddToLibraryAction(); + const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); await action.execute({ embeddable }); expect(Object.keys(container.getInput().panels).length).toEqual(originalPanelCount); @@ -154,7 +154,7 @@ test('Add to library returns reference type input', async () => { }); const dashboard = embeddable.getRoot() as IContainer; const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels)); - const action = new AddToLibraryAction(); + const action = new AddToLibraryAction({ toasts: coreStart.notifications.toasts }); await action.execute({ embeddable }); const newPanelId = Object.keys(container.getInput().panels).find( (key) => !originalPanelKeySet.has(key) diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx index 3cc1a8a1dffe1..d89c38f297e8f 100644 --- a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx +++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx @@ -27,6 +27,7 @@ import { EmbeddableInput, isReferenceOrValueEmbeddable, } from '../../../../embeddable/public'; +import { NotificationsStart } from '../../../../../core/public'; import { DashboardPanelState, DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '..'; export const ACTION_ADD_TO_LIBRARY = 'addToFromLibrary'; @@ -40,7 +41,7 @@ export class AddToLibraryAction implements ActionByType { coreStart = coreMock.createStart(); + unlinkAction = ({ + getDisplayName: () => 'unlink from dat library', + execute: jest.fn(), + } as unknown) as UnlinkFromLibraryAction; + const containerOptions = { ExitFullScreenButton: () => null, SavedObjectFinder: () => null, @@ -81,19 +88,19 @@ beforeEach(async () => { }); test('Notification is shown when embeddable on dashboard has reference type input', async () => { - const action = new LibraryNotificationAction(); + const action = new LibraryNotificationAction(unlinkAction); embeddable.updateInput(await embeddable.getInputAsRefType()); expect(await action.isCompatible({ embeddable })).toBe(true); }); test('Notification is not shown when embeddable input is by value', async () => { - const action = new LibraryNotificationAction(); + const action = new LibraryNotificationAction(unlinkAction); embeddable.updateInput(await embeddable.getInputAsValueType()); expect(await action.isCompatible({ embeddable })).toBe(false); }); test('Notification is not shown when view mode is set to view', async () => { - const action = new LibraryNotificationAction(); + const action = new LibraryNotificationAction(unlinkAction); embeddable.updateInput(await embeddable.getInputAsRefType()); embeddable.updateInput({ viewMode: ViewMode.VIEW }); expect(await action.isCompatible({ embeddable })).toBe(false); diff --git a/src/plugins/dashboard/public/application/actions/library_notification_action.tsx b/src/plugins/dashboard/public/application/actions/library_notification_action.tsx index bff0236c802f1..6a0b71d8250be 100644 --- a/src/plugins/dashboard/public/application/actions/library_notification_action.tsx +++ b/src/plugins/dashboard/public/application/actions/library_notification_action.tsx @@ -17,12 +17,13 @@ * under the License. */ -import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiBadge } from '@elastic/eui'; +import React from 'react'; import { IEmbeddable, ViewMode, isReferenceOrValueEmbeddable } from '../../embeddable_plugin'; import { ActionByType, IncompatibleActionError } from '../../ui_actions_plugin'; import { reactToUiComponent } from '../../../../kibana_react/public'; +import { UnlinkFromLibraryAction } from '.'; +import { LibraryNotificationPopover } from './library_notification_popover'; export const ACTION_LIBRARY_NOTIFICATION = 'ACTION_LIBRARY_NOTIFICATION'; @@ -35,23 +36,32 @@ export class LibraryNotificationAction implements ActionByType ( - - {this.displayName} - - )); + private LibraryNotification: React.FC<{ context: LibraryNotificationActionContext }> = ({ + context, + }: { + context: LibraryNotificationActionContext; + }) => { + const { embeddable } = context; + return ( + + ); + }; + + public readonly MenuItem = reactToUiComponent(this.LibraryNotification); public getDisplayName({ embeddable }: LibraryNotificationActionContext) { if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) { @@ -67,16 +77,6 @@ export class LibraryNotificationAction implements ActionByType { - if (!embeddable.getRoot() || !embeddable.getRoot().isContainer) { - throw new IncompatibleActionError(); - } - return i18n.translate('dashboard.panel.libraryNotification.toolTip', { - defaultMessage: - 'This panel is linked to a Library item. Editing the panel might affect other dashboards.', - }); - }; - public isCompatible = async ({ embeddable }: LibraryNotificationActionContext) => { return ( embeddable.getRoot().isContainer && diff --git a/src/plugins/dashboard/public/application/actions/library_notification_popover.test.tsx b/src/plugins/dashboard/public/application/actions/library_notification_popover.test.tsx new file mode 100644 index 0000000000000..c6f223fa45c23 --- /dev/null +++ b/src/plugins/dashboard/public/application/actions/library_notification_popover.test.tsx @@ -0,0 +1,127 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { DashboardContainer } from '..'; +import { isErrorEmbeddable } from '../../embeddable_plugin'; +import { mountWithIntl } from '../../../../../test_utils/public/enzyme_helpers'; +import { embeddablePluginMock } from '../../../../embeddable/public/mocks'; +import { getSampleDashboardInput } from '../test_helpers'; +import { + CONTACT_CARD_EMBEDDABLE, + ContactCardEmbeddableFactory, + ContactCardEmbeddable, + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, +} from '../../embeddable_plugin_test_samples'; +import { + LibraryNotificationPopover, + LibraryNotificationProps, +} from './library_notification_popover'; +import { CoreStart } from '../../../../../core/public'; +import { coreMock } from '../../../../../core/public/mocks'; +import { findTestSubject } from '@elastic/eui/lib/test'; +import { EuiPopover } from '@elastic/eui'; + +describe('LibraryNotificationPopover', () => { + const { setup, doStart } = embeddablePluginMock.createInstance(); + setup.registerEmbeddableFactory( + CONTACT_CARD_EMBEDDABLE, + new ContactCardEmbeddableFactory((() => null) as any, {} as any) + ); + const start = doStart(); + + let container: DashboardContainer; + let defaultProps: LibraryNotificationProps; + let coreStart: CoreStart; + + beforeEach(async () => { + coreStart = coreMock.createStart(); + + const containerOptions = { + ExitFullScreenButton: () => null, + SavedObjectFinder: () => null, + application: {} as any, + embeddable: start, + inspector: {} as any, + notifications: {} as any, + overlays: coreStart.overlays, + savedObjectMetaData: {} as any, + uiActions: {} as any, + }; + + container = new DashboardContainer(getSampleDashboardInput(), containerOptions); + const contactCardEmbeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, { + firstName: 'Kibanana', + }); + + if (isErrorEmbeddable(contactCardEmbeddable)) { + throw new Error('Failed to create embeddable'); + } + + defaultProps = { + unlinkAction: ({ + execute: jest.fn(), + getDisplayName: () => 'test unlink', + } as unknown) as LibraryNotificationProps['unlinkAction'], + displayName: 'test display', + context: { embeddable: contactCardEmbeddable }, + icon: 'testIcon', + id: 'testId', + }; + }); + + function mountComponent(props?: Partial) { + return mountWithIntl(); + } + + test('click library notification badge should open and close popover', () => { + const component = mountComponent(); + const btn = findTestSubject(component, `embeddablePanelNotification-${defaultProps.id}`); + btn.simulate('click'); + let popover = component.find(EuiPopover); + expect(popover.prop('isOpen')).toBe(true); + btn.simulate('click'); + popover = component.find(EuiPopover); + expect(popover.prop('isOpen')).toBe(false); + }); + + test('popover should contain button with unlink action display name', () => { + const component = mountComponent(); + const btn = findTestSubject(component, `embeddablePanelNotification-${defaultProps.id}`); + btn.simulate('click'); + const popover = component.find(EuiPopover); + const unlinkButton = findTestSubject(popover, 'libraryNotificationUnlinkButton'); + expect(unlinkButton.text()).toEqual('test unlink'); + }); + + test('clicking unlink executes unlink action', () => { + const component = mountComponent(); + const btn = findTestSubject(component, `embeddablePanelNotification-${defaultProps.id}`); + btn.simulate('click'); + const popover = component.find(EuiPopover); + const unlinkButton = findTestSubject(popover, 'libraryNotificationUnlinkButton'); + unlinkButton.simulate('click'); + expect(defaultProps.unlinkAction.execute).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/dashboard/public/application/actions/library_notification_popover.tsx b/src/plugins/dashboard/public/application/actions/library_notification_popover.tsx new file mode 100644 index 0000000000000..8bc81b3296c3d --- /dev/null +++ b/src/plugins/dashboard/public/application/actions/library_notification_popover.tsx @@ -0,0 +1,102 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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, { useState } from 'react'; +import { + EuiButton, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiPopoverFooter, + EuiPopoverTitle, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { LibraryNotificationActionContext, UnlinkFromLibraryAction } from '.'; + +export interface LibraryNotificationProps { + context: LibraryNotificationActionContext; + unlinkAction: UnlinkFromLibraryAction; + displayName: string; + icon: string; + id: string; +} + +export function LibraryNotificationPopover({ + unlinkAction, + displayName, + context, + icon, + id, +}: LibraryNotificationProps) { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const { embeddable } = context; + + return ( + setIsPopoverOpen(!isPopoverOpen)} + /> + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + anchorPosition="upCenter" + > + {displayName} +
+ +

+ {i18n.translate('dashboard.panel.libraryNotification.toolTip', { + defaultMessage: + 'This panel is linked to a library item. Editing the panel might affect other dashboards.', + })} +

+
+
+ + + + unlinkAction.execute({ embeddable })} + > + {unlinkAction.getDisplayName({ embeddable })} + + + + +
+ ); +} diff --git a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx index b4178fd40c768..0f61a74cd7036 100644 --- a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx @@ -81,19 +81,19 @@ beforeEach(async () => { }); test('Unlink is compatible when embeddable on dashboard has reference type input', async () => { - const action = new UnlinkFromLibraryAction(); + const action = new UnlinkFromLibraryAction({ toasts: coreStart.notifications.toasts }); embeddable.updateInput(await embeddable.getInputAsRefType()); expect(await action.isCompatible({ embeddable })).toBe(true); }); test('Unlink is not compatible when embeddable input is by value', async () => { - const action = new UnlinkFromLibraryAction(); + const action = new UnlinkFromLibraryAction({ toasts: coreStart.notifications.toasts }); embeddable.updateInput(await embeddable.getInputAsValueType()); expect(await action.isCompatible({ embeddable })).toBe(false); }); test('Unlink is not compatible when view mode is set to view', async () => { - const action = new UnlinkFromLibraryAction(); + const action = new UnlinkFromLibraryAction({ toasts: coreStart.notifications.toasts }); embeddable.updateInput(await embeddable.getInputAsRefType()); embeddable.updateInput({ viewMode: ViewMode.VIEW }); expect(await action.isCompatible({ embeddable })).toBe(false); @@ -114,7 +114,7 @@ test('Unlink is not compatible when embeddable is not in a dashboard container', mockedByReferenceInput: { savedObjectId: 'test', id: orphanContactCard.id }, mockedByValueInput: { firstName: 'Kibanana', id: orphanContactCard.id }, }); - const action = new UnlinkFromLibraryAction(); + const action = new UnlinkFromLibraryAction({ toasts: coreStart.notifications.toasts }); expect(await action.isCompatible({ embeddable: orphanContactCard })).toBe(false); }); @@ -122,7 +122,7 @@ test('Unlink replaces embeddableId but retains panel count', async () => { const dashboard = embeddable.getRoot() as IContainer; const originalPanelCount = Object.keys(dashboard.getInput().panels).length; const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels)); - const action = new UnlinkFromLibraryAction(); + const action = new UnlinkFromLibraryAction({ toasts: coreStart.notifications.toasts }); await action.execute({ embeddable }); expect(Object.keys(container.getInput().panels).length).toEqual(originalPanelCount); @@ -152,7 +152,7 @@ test('Unlink unwraps all attributes from savedObject', async () => { }); const dashboard = embeddable.getRoot() as IContainer; const originalPanelKeySet = new Set(Object.keys(dashboard.getInput().panels)); - const action = new UnlinkFromLibraryAction(); + const action = new UnlinkFromLibraryAction({ toasts: coreStart.notifications.toasts }); await action.execute({ embeddable }); const newPanelId = Object.keys(container.getInput().panels).find( (key) => !originalPanelKeySet.has(key) diff --git a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx index e2a6ec7dd3947..f5cf8b4e866a8 100644 --- a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx +++ b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.tsx @@ -27,6 +27,7 @@ import { EmbeddableInput, isReferenceOrValueEmbeddable, } from '../../../../embeddable/public'; +import { NotificationsStart } from '../../../../../core/public'; import { DashboardPanelState, DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '..'; export const ACTION_UNLINK_FROM_LIBRARY = 'unlinkFromLibrary'; @@ -40,14 +41,14 @@ export class UnlinkFromLibraryAction implements ActionByType `#${DashboardConstants.LANDING_PAGE_PATH}`; const getDashTitle = () => diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 1be884f885551..574d456c10a8d 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -430,14 +430,15 @@ export class DashboardPlugin uiActions.attachAction(CONTEXT_MENU_TRIGGER, clonePanelAction.id); if (this.dashboardFeatureFlagConfig?.allowByValueEmbeddables) { - const addToLibraryAction = new AddToLibraryAction(); + const addToLibraryAction = new AddToLibraryAction({ toasts: notifications.toasts }); uiActions.registerAction(addToLibraryAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, addToLibraryAction.id); - const unlinkFromLibraryAction = new UnlinkFromLibraryAction(); + + const unlinkFromLibraryAction = new UnlinkFromLibraryAction({ toasts: notifications.toasts }); uiActions.registerAction(unlinkFromLibraryAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, unlinkFromLibraryAction.id); - const libraryNotificationAction = new LibraryNotificationAction(); + const libraryNotificationAction = new LibraryNotificationAction(unlinkFromLibraryAction); uiActions.registerAction(libraryNotificationAction); uiActions.attachAction(PANEL_NOTIFICATION_TRIGGER, libraryNotificationAction.id); } diff --git a/src/plugins/data/common/mocks.ts b/src/plugins/data/common/mocks.ts new file mode 100644 index 0000000000000..dde70b1d07443 --- /dev/null +++ b/src/plugins/data/common/mocks.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 { getSessionServiceMock } from './search/session/mocks'; diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts index b53ae44c05075..ead88f924731b 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/range.test.ts @@ -68,6 +68,7 @@ describe('AggConfig Filters', () => { { gte: 1024, lt: 2048.0, + label: 'A custom label', } ); @@ -78,6 +79,7 @@ describe('AggConfig Filters', () => { expect(filter.range).toHaveProperty('bytes'); expect(filter.range.bytes).toHaveProperty('gte', 1024.0); expect(filter.range.bytes).toHaveProperty('lt', 2048.0); + expect(filter.range.bytes).not.toHaveProperty('label'); expect(filter.meta).toHaveProperty('formattedValue'); }); }); diff --git a/src/plugins/data/common/search/aggs/buckets/create_filter/range.ts b/src/plugins/data/common/search/aggs/buckets/create_filter/range.ts index 8dea33a450c5d..bea8e577b21fb 100644 --- a/src/plugins/data/common/search/aggs/buckets/create_filter/range.ts +++ b/src/plugins/data/common/search/aggs/buckets/create_filter/range.ts @@ -25,7 +25,7 @@ import { IBucketAggConfig } from '../bucket_agg_type'; export const createFilterRange = ( getFieldFormatsStart: AggTypesDependencies['getFieldFormatsStart'] ) => { - return (aggConfig: IBucketAggConfig, params: any) => { + return (aggConfig: IBucketAggConfig, { label, ...params }: any) => { const { deserialize } = getFieldFormatsStart(); return buildRangeFilter( aggConfig.params.field, diff --git a/src/plugins/data/common/search/aggs/buckets/range.ts b/src/plugins/data/common/search/aggs/buckets/range.ts index 169b234845274..bdb6ea7cd4b98 100644 --- a/src/plugins/data/common/search/aggs/buckets/range.ts +++ b/src/plugins/data/common/search/aggs/buckets/range.ts @@ -41,6 +41,7 @@ export interface AggParamsRange extends BaseAggParams { ranges?: Array<{ from: number; to: number; + label?: string; }>; } @@ -71,7 +72,7 @@ export const getRangeBucketAgg = ({ getFieldFormatsStart }: RangeBucketAggDepend key = keys.get(id); if (!key) { - key = new RangeKey(bucket); + key = new RangeKey(bucket, agg.params.ranges); keys.set(id, key); } @@ -102,7 +103,11 @@ export const getRangeBucketAgg = ({ getFieldFormatsStart }: RangeBucketAggDepend { from: 1000, to: 2000 }, ], write(aggConfig, output) { - output.params.ranges = aggConfig.params.ranges; + output.params.ranges = (aggConfig.params as AggParamsRange).ranges?.map((range) => ({ + to: range.to, + from: range.from, + })); + output.params.keyed = true; }, }, diff --git a/src/plugins/data/common/search/aggs/buckets/range_key.ts b/src/plugins/data/common/search/aggs/buckets/range_key.ts index cd781f7e082a2..43fdc20e53f55 100644 --- a/src/plugins/data/common/search/aggs/buckets/range_key.ts +++ b/src/plugins/data/common/search/aggs/buckets/range_key.ts @@ -19,14 +19,36 @@ const id = Symbol('id'); +type Ranges = Array< + Partial<{ + from: string | number; + to: string | number; + label: string; + }> +>; + export class RangeKey { [id]: string; gte: string | number; lt: string | number; + label?: string; + + private findCustomLabel( + from: string | number | undefined | null, + to: string | number | undefined | null, + ranges?: Ranges + ) { + return (ranges || []).find( + (range) => + ((from == null && range.from == null) || range.from === from) && + ((to == null && range.to == null) || range.to === to) + )?.label; + } - constructor(bucket: any) { + constructor(bucket: any, allRanges?: Ranges) { this.gte = bucket.from == null ? -Infinity : bucket.from; this.lt = bucket.to == null ? +Infinity : bucket.to; + this.label = this.findCustomLabel(bucket.from, bucket.to, allRanges); this[id] = RangeKey.idBucket(bucket); } diff --git a/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.test.ts b/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.test.ts index 20d8cfc105e49..28646c092c01c 100644 --- a/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.test.ts +++ b/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.test.ts @@ -79,6 +79,16 @@ describe('getFormatWithAggs', () => { expect(getFormat).toHaveBeenCalledTimes(1); }); + test('returns custom label for range if provided', () => { + const mapping = { id: 'range', params: {} }; + const getFieldFormat = getFormatWithAggs(getFormat); + const format = getFieldFormat(mapping); + + expect(format.convert({ gte: 1, lt: 20, label: 'custom' })).toBe('custom'); + // underlying formatter is not called because custom label can be used directly + expect(getFormat).toHaveBeenCalledTimes(0); + }); + test('creates custom format for terms', () => { const mapping = { id: 'terms', diff --git a/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.ts b/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.ts index 01369206ab3cf..a8134619fec0d 100644 --- a/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.ts +++ b/src/plugins/data/common/search/aggs/utils/get_format_with_aggs.ts @@ -48,6 +48,9 @@ export function getFormatWithAggs(getFieldFormat: GetFieldFormat): GetFieldForma const customFormats: Record IFieldFormat> = { range: () => { const RangeFormat = FieldFormat.from((range: any) => { + if (range.label) { + return range.label; + } const nestedFormatter = params as SerializedFieldFormat; const format = getFieldFormat({ id: nestedFormatter.id, diff --git a/src/plugins/data/common/search/es_search/types.ts b/src/plugins/data/common/search/es_search/types.ts index b1c3e5cdd3960..4d3bc088749a9 100644 --- a/src/plugins/data/common/search/es_search/types.ts +++ b/src/plugins/data/common/search/es_search/types.ts @@ -31,6 +31,11 @@ export interface ISearchOptions { * Use this option to force using a specific server side search strategy. Leave empty to use the default strategy. */ strategy?: string; + + /** + * A session ID, grouping multiple search requests into a single session. + */ + sessionId?: string; } export type ISearchRequestParams> = { diff --git a/src/plugins/data/common/search/index.ts b/src/plugins/data/common/search/index.ts index 2ee0db384cf06..e650cf10db87c 100644 --- a/src/plugins/data/common/search/index.ts +++ b/src/plugins/data/common/search/index.ts @@ -23,3 +23,4 @@ export * from './expressions'; export * from './search_source'; export * from './tabify'; export * from './types'; +export * from './session'; diff --git a/src/plugins/data/common/search/session/index.ts b/src/plugins/data/common/search/session/index.ts new file mode 100644 index 0000000000000..d8f7b5091eb8f --- /dev/null +++ b/src/plugins/data/common/search/session/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 './types'; diff --git a/src/plugins/data/common/search/session/mocks.ts b/src/plugins/data/common/search/session/mocks.ts new file mode 100644 index 0000000000000..7d5cd75b57534 --- /dev/null +++ b/src/plugins/data/common/search/session/mocks.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 { ISessionService } from './types'; + +export function getSessionServiceMock(): jest.Mocked { + return { + clear: jest.fn(), + start: jest.fn(), + getSessionId: jest.fn(), + getSession$: jest.fn(), + }; +} diff --git a/src/plugins/data/common/search/session/types.ts b/src/plugins/data/common/search/session/types.ts new file mode 100644 index 0000000000000..80ab74f1aa14d --- /dev/null +++ b/src/plugins/data/common/search/session/types.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable } from 'rxjs'; + +export interface ISessionService { + /** + * Returns the active session ID + * @returns The active session ID + */ + getSessionId: () => string | undefined; + /** + * Returns the observable that emits an update every time the session ID changes + * @returns `Observable` + */ + getSession$: () => Observable; + /** + * Starts a new session + */ + start: () => string; + /** + * Clears the active session. + */ + clear: () => void; +} diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 2ed3e440040de..d280b6f1faf7d 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1389,6 +1389,7 @@ export type ISearchGeneric = void; } @@ -1987,7 +1993,7 @@ export class SearchInterceptor { // (undocumented) protected getTimeoutMode(): TimeoutErrorMode; // (undocumented) - protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal: AbortSignal, appAbortSignal?: AbortSignal): Error; + protected handleSearchError(e: any, request: IKibanaSearchRequest, timeoutSignal: AbortSignal, options?: ISearchOptions): Error; // @internal protected pendingCount$: BehaviorSubject; // @internal (undocumented) @@ -1998,8 +2004,8 @@ export class SearchInterceptor { abortSignal?: AbortSignal; timeout?: number; }): { - combinedSignal: AbortSignal; timeoutSignal: AbortSignal; + combinedSignal: AbortSignal; cleanup: () => void; }; // (undocumented) @@ -2013,6 +2019,8 @@ export interface SearchInterceptorDeps { // (undocumented) http: CoreSetup_2['http']; // (undocumented) + session: ISessionService; + // (undocumented) startServices: Promise<[CoreStart, any, unknown]>; // (undocumented) toasts: ToastsSetup; diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index 1021ef0f91d52..1f72bda44e4ed 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -149,7 +149,9 @@ const handleCourierRequest = async ({ request.stats(getRequestInspectorStats(requestSearchSource)); try { - const response = await requestSearchSource.fetch({ abortSignal }); + const response = await requestSearchSource.fetch({ + abortSignal, + }); request.stats(getResponseInspectorStats(response, searchSource)).ok({ json: response }); diff --git a/src/plugins/data/public/search/mocks.ts b/src/plugins/data/public/search/mocks.ts index 8bad4cd269b3f..836ddb618e746 100644 --- a/src/plugins/data/public/search/mocks.ts +++ b/src/plugins/data/public/search/mocks.ts @@ -20,11 +20,13 @@ import { searchAggsSetupMock, searchAggsStartMock } from './aggs/mocks'; import { searchSourceMock } from './search_source/mocks'; import { ISearchSetup, ISearchStart } from './types'; +import { getSessionServiceMock } from '../../common/mocks'; function createSetupContract(): jest.Mocked { return { aggs: searchAggsSetupMock(), __enhance: jest.fn(), + session: getSessionServiceMock(), }; } @@ -33,6 +35,7 @@ function createStartContract(): jest.Mocked { aggs: searchAggsStartMock(), search: jest.fn(), showError: jest.fn(), + session: getSessionServiceMock(), searchSource: searchSourceMock.createStartContract(), }; } diff --git a/src/plugins/data/public/search/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor.test.ts index ade15adc1c3a3..e8a728bb9cec3 100644 --- a/src/plugins/data/public/search/search_interceptor.test.ts +++ b/src/plugins/data/public/search/search_interceptor.test.ts @@ -17,12 +17,14 @@ * under the License. */ -import { CoreSetup } from '../../../../core/public'; +import { CoreSetup, CoreStart } from '../../../../core/public'; import { coreMock } from '../../../../core/public/mocks'; import { IEsSearchRequest } from '../../common/search'; import { SearchInterceptor } from './search_interceptor'; import { AbortError } from '../../common'; -import { SearchTimeoutError, PainlessError } from './errors'; +import { SearchTimeoutError, PainlessError, TimeoutErrorMode } from './errors'; +import { searchServiceMock } from './mocks'; +import { ISearchStart } from '.'; let searchInterceptor: SearchInterceptor; let mockCoreSetup: MockedKeys; @@ -31,13 +33,61 @@ const flushPromises = () => new Promise((resolve) => setImmediate(resolve)); jest.useFakeTimers(); describe('SearchInterceptor', () => { + let searchMock: jest.Mocked; + let mockCoreStart: MockedKeys; beforeEach(() => { mockCoreSetup = coreMock.createSetup(); + mockCoreStart = coreMock.createStart(); + searchMock = searchServiceMock.createStartContract(); searchInterceptor = new SearchInterceptor({ toasts: mockCoreSetup.notifications.toasts, - startServices: mockCoreSetup.getStartServices(), + startServices: new Promise((resolve) => { + resolve([mockCoreStart, {}, {}]); + }), uiSettings: mockCoreSetup.uiSettings, http: mockCoreSetup.http, + session: searchMock.session, + }); + }); + + describe('showError', () => { + test('Ignores an AbortError', async () => { + searchInterceptor.showError(new AbortError()); + expect(mockCoreSetup.notifications.toasts.addDanger).not.toBeCalled(); + expect(mockCoreSetup.notifications.toasts.addError).not.toBeCalled(); + }); + + test('Ignores a SearchTimeoutError', async () => { + searchInterceptor.showError(new SearchTimeoutError(new Error(), TimeoutErrorMode.UPGRADE)); + expect(mockCoreSetup.notifications.toasts.addDanger).not.toBeCalled(); + expect(mockCoreSetup.notifications.toasts.addError).not.toBeCalled(); + }); + + test('Renders a PainlessError', async () => { + searchInterceptor.showError( + new PainlessError( + { + body: { + attributes: { + error: { + failed_shards: { + reason: 'bananas', + }, + }, + }, + } as any, + }, + {} as any + ) + ); + expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1); + expect(mockCoreSetup.notifications.toasts.addError).not.toBeCalled(); + }); + + test('Renders a general error', async () => { + searchInterceptor.showError(new Error('Oopsy')); + expect(mockCoreSetup.notifications.toasts.addDanger).not.toBeCalled(); + expect(mockCoreSetup.notifications.toasts.addError).toBeCalledTimes(1); }); }); @@ -49,149 +99,172 @@ describe('SearchInterceptor', () => { params: {}, }; const response = searchInterceptor.search(mockRequest); - - const result = await response.toPromise(); - expect(result).toBe(mockResponse); + expect(response.toPromise()).resolves.toBe(mockResponse); }); - test('Observable should fail if fetch has an internal error', async () => { - const mockResponse: any = { result: 500, message: 'Internal Error' }; - mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse); - const mockRequest: IEsSearchRequest = { - params: {}, - }; - const response = searchInterceptor.search(mockRequest); + describe('Should throw typed errors', () => { + test('Observable should fail if fetch has an internal error', async () => { + const mockResponse: any = new Error('Internal Error'); + mockCoreSetup.http.fetch.mockRejectedValue(mockResponse); + const mockRequest: IEsSearchRequest = { + params: {}, + }; + const response = searchInterceptor.search(mockRequest); + await expect(response.toPromise()).rejects.toThrow('Internal Error'); + }); - try { - await response.toPromise(); - } catch (e) { - expect(e).toBe(mockResponse); - } - }); + describe('Should handle Timeout errors', () => { + test('Should throw SearchTimeoutError on server timeout AND show toast', async () => { + const mockResponse: any = { + result: 500, + body: { + message: 'Request timed out', + }, + }; + mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse); + const mockRequest: IEsSearchRequest = { + params: {}, + }; + const response = searchInterceptor.search(mockRequest); + await expect(response.toPromise()).rejects.toThrow(SearchTimeoutError); + expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1); + }); - test('Should throw SearchTimeoutError on server timeout AND show toast', async (done) => { - const mockResponse: any = { - result: 500, - body: { - message: 'Request timed out', - }, - }; - mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse); - const mockRequest: IEsSearchRequest = { - params: {}, - }; - const response = searchInterceptor.search(mockRequest); + test('Timeout error should show multiple times if not in a session', async () => { + const mockResponse: any = { + result: 500, + body: { + message: 'Request timed out', + }, + }; + mockCoreSetup.http.fetch.mockRejectedValue(mockResponse); + const mockRequest: IEsSearchRequest = { + params: {}, + }; - try { - await response.toPromise(); - } catch (e) { - expect(e).toBeInstanceOf(SearchTimeoutError); - expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1); - done(); - } - }); + await expect(searchInterceptor.search(mockRequest).toPromise()).rejects.toThrow( + SearchTimeoutError + ); + await expect(searchInterceptor.search(mockRequest).toPromise()).rejects.toThrow( + SearchTimeoutError + ); + expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(2); + }); - test('Search error should be debounced', async (done) => { - const mockResponse: any = { - result: 500, - body: { - message: 'Request timed out', - }, - }; - mockCoreSetup.http.fetch.mockRejectedValue(mockResponse); - const mockRequest: IEsSearchRequest = { - params: {}, - }; - try { - await searchInterceptor.search(mockRequest).toPromise(); - } catch (e) { - expect(e).toBeInstanceOf(SearchTimeoutError); - try { - await searchInterceptor.search(mockRequest).toPromise(); - } catch (e2) { + test('Timeout error should show once per each session', async () => { + const mockResponse: any = { + result: 500, + body: { + message: 'Request timed out', + }, + }; + mockCoreSetup.http.fetch.mockRejectedValue(mockResponse); + const mockRequest: IEsSearchRequest = { + params: {}, + }; + + await expect( + searchInterceptor.search(mockRequest, { sessionId: 'abc' }).toPromise() + ).rejects.toThrow(SearchTimeoutError); + await expect( + searchInterceptor.search(mockRequest, { sessionId: 'def' }).toPromise() + ).rejects.toThrow(SearchTimeoutError); + expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(2); + }); + + test('Timeout error should show once in a single session', async () => { + const mockResponse: any = { + result: 500, + body: { + message: 'Request timed out', + }, + }; + mockCoreSetup.http.fetch.mockRejectedValue(mockResponse); + const mockRequest: IEsSearchRequest = { + params: {}, + }; + await expect( + searchInterceptor.search(mockRequest, { sessionId: 'abc' }).toPromise() + ).rejects.toThrow(SearchTimeoutError); + await expect( + searchInterceptor.search(mockRequest, { sessionId: 'abc' }).toPromise() + ).rejects.toThrow(SearchTimeoutError); expect(mockCoreSetup.notifications.toasts.addDanger).toBeCalledTimes(1); - done(); - } - } - }); + }); + }); - test('Should throw Painless error on server error with OSS format', async (done) => { - const mockResponse: any = { - result: 500, - body: { - attributes: { - error: { - failed_shards: [ - { - reason: { - lang: 'painless', - script_stack: ['a', 'b'], - reason: 'banana', + test('Should throw Painless error on server error with OSS format', async () => { + const mockResponse: any = { + result: 500, + body: { + attributes: { + error: { + failed_shards: [ + { + reason: { + lang: 'painless', + script_stack: ['a', 'b'], + reason: 'banana', + }, }, - }, - ], + ], + }, }, }, - }, - }; - mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse); - const mockRequest: IEsSearchRequest = { - params: {}, - }; - const response = searchInterceptor.search(mockRequest); + }; + mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse); + const mockRequest: IEsSearchRequest = { + params: {}, + }; + const response = searchInterceptor.search(mockRequest); + await expect(response.toPromise()).rejects.toThrow(PainlessError); + }); - try { - await response.toPromise(); - } catch (e) { - expect(e).toBeInstanceOf(PainlessError); - done(); - } - }); + test('Observable should fail if user aborts (test merged signal)', async () => { + const abortController = new AbortController(); + mockCoreSetup.http.fetch.mockImplementationOnce((options: any) => { + return new Promise((resolve, reject) => { + options.signal.addEventListener('abort', () => { + reject(new AbortError()); + }); - test('Observable should fail if user aborts (test merged signal)', async () => { - const abortController = new AbortController(); - mockCoreSetup.http.fetch.mockImplementationOnce((options: any) => { - return new Promise((resolve, reject) => { - options.signal.addEventListener('abort', () => { - reject(new AbortError()); + setTimeout(resolve, 500); }); - - setTimeout(resolve, 500); }); - }); - const mockRequest: IEsSearchRequest = { - params: {}, - }; - const response = searchInterceptor.search(mockRequest, { - abortSignal: abortController.signal, - }); + const mockRequest: IEsSearchRequest = { + params: {}, + }; + const response = searchInterceptor.search(mockRequest, { + abortSignal: abortController.signal, + }); - const next = jest.fn(); - const error = (e: any) => { - expect(next).not.toBeCalled(); - expect(e).toBeInstanceOf(AbortError); - }; - response.subscribe({ next, error }); - setTimeout(() => abortController.abort(), 200); - jest.advanceTimersByTime(5000); + const next = jest.fn(); + const error = (e: any) => { + expect(next).not.toBeCalled(); + expect(e).toBeInstanceOf(AbortError); + }; + response.subscribe({ next, error }); + setTimeout(() => abortController.abort(), 200); + jest.advanceTimersByTime(5000); - await flushPromises(); - }); + await flushPromises(); + }); - test('Immediately aborts if passed an aborted abort signal', async (done) => { - const abort = new AbortController(); - const mockRequest: IEsSearchRequest = { - params: {}, - }; - const response = searchInterceptor.search(mockRequest, { abortSignal: abort.signal }); - abort.abort(); + test('Immediately aborts if passed an aborted abort signal', async (done) => { + const abort = new AbortController(); + const mockRequest: IEsSearchRequest = { + params: {}, + }; + const response = searchInterceptor.search(mockRequest, { abortSignal: abort.signal }); + abort.abort(); - const error = (e: any) => { - expect(e).toBeInstanceOf(AbortError); - expect(mockCoreSetup.http.fetch).not.toBeCalled(); - done(); - }; - response.subscribe({ error }); + const error = (e: any) => { + expect(e).toBeInstanceOf(AbortError); + expect(mockCoreSetup.http.fetch).not.toBeCalled(); + done(); + }; + response.subscribe({ error }); + }); }); }); }); diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index 2e42635a7f811..e3c6dd3e287d4 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -17,7 +17,7 @@ * under the License. */ -import { get, trimEnd, debounce } from 'lodash'; +import { get, memoize, trimEnd } from 'lodash'; import { BehaviorSubject, throwError, timer, defer, from, Observable, NEVER } from 'rxjs'; import { catchError, finalize } from 'rxjs/operators'; import { CoreStart, CoreSetup, ToastsSetup } from 'kibana/public'; @@ -28,6 +28,7 @@ import { IKibanaSearchResponse, ISearchOptions, ES_SEARCH_STRATEGY, + ISessionService, } from '../../common'; import { SearchUsageCollector } from './collectors'; import { SearchTimeoutError, PainlessError, isPainlessError, TimeoutErrorMode } from './errors'; @@ -39,6 +40,7 @@ export interface SearchInterceptorDeps { startServices: Promise<[CoreStart, any, unknown]>; toasts: ToastsSetup; usageCollector?: SearchUsageCollector; + session: ISessionService; } export class SearchInterceptor { @@ -86,16 +88,17 @@ export class SearchInterceptor { e: any, request: IKibanaSearchRequest, timeoutSignal: AbortSignal, - appAbortSignal?: AbortSignal + options?: ISearchOptions ): Error { if (timeoutSignal.aborted || get(e, 'body.message') === 'Request timed out') { // Handle a client or a server side timeout const err = new SearchTimeoutError(e, this.getTimeoutMode()); // Show the timeout error here, so that it's shown regardless of how an application chooses to handle errors. - this.showTimeoutError(err); + // The timeout error is shown any time a request times out, or once per session, if the request is part of a session. + this.showTimeoutError(err, options?.sessionId); return err; - } else if (appAbortSignal?.aborted) { + } else if (options?.abortSignal?.aborted) { // In the case an application initiated abort, throw the existing AbortError. return e; } else if (isPainlessError(e)) { @@ -162,27 +165,37 @@ export class SearchInterceptor { combinedSignal.addEventListener('abort', cleanup); return { - combinedSignal, timeoutSignal, + combinedSignal, cleanup, }; } + private showTimeoutErrorToast = (e: SearchTimeoutError, sessionId?: string) => { + this.deps.toasts.addDanger({ + title: 'Timed out', + text: toMountPoint(e.getErrorMessage(this.application)), + }); + }; + + private showTimeoutErrorMemoized = memoize( + this.showTimeoutErrorToast, + (_: SearchTimeoutError, sessionId: string) => { + return sessionId; + } + ); + /** - * Right now we are throttling but we will hook this up with background sessions to show only one - * error notification per session. + * Show one error notification per session. * @internal */ - private showTimeoutError = debounce( - (e: SearchTimeoutError) => { - this.deps.toasts.addDanger({ - title: 'Timed out', - text: toMountPoint(e.getErrorMessage(this.application)), - }); - }, - 30000, - { leading: true, trailing: false } - ); + private showTimeoutError = (e: SearchTimeoutError, sessionId?: string) => { + if (sessionId) { + this.showTimeoutErrorMemoized(e, sessionId); + } else { + this.showTimeoutErrorToast(e, sessionId); + } + }; /** * Searches using the given `search` method. Overrides the `AbortSignal` with one that will abort @@ -207,12 +220,9 @@ export class SearchInterceptor { abortSignal: options?.abortSignal, }); this.pendingCount$.next(this.pendingCount$.getValue() + 1); - return this.runSearch(request, combinedSignal, options?.strategy).pipe( - catchError((e: any) => { - return throwError( - this.handleSearchError(e, request, timeoutSignal, options?.abortSignal) - ); + catchError((e: Error) => { + return throwError(this.handleSearchError(e, request, timeoutSignal, options)); }), finalize(() => { this.pendingCount$.next(this.pendingCount$.getValue() - 1); diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 734e88e085661..f955dc5b6ebd5 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -31,6 +31,7 @@ import { ISearchOptions, SearchSourceService, SearchSourceDependencies, + ISessionService, } from '../../common/search'; import { getCallMsearch } from './legacy'; import { AggsService, AggsStartDependencies } from './aggs'; @@ -40,6 +41,7 @@ import { SearchUsageCollector, createUsageCollector } from './collectors'; import { UsageCollectionSetup } from '../../../usage_collection/public'; import { esdsl, esRawResponse } from './expressions'; import { ExpressionsSetup } from '../../../expressions/public'; +import { SessionService } from './session_service'; import { ConfigSchema } from '../../config'; import { SHARD_DELAY_AGG_NAME, @@ -64,6 +66,7 @@ export class SearchService implements Plugin { private readonly searchSourceService = new SearchSourceService(); private searchInterceptor!: ISearchInterceptor; private usageCollector?: SearchUsageCollector; + private sessionService!: ISessionService; constructor(private initializerContext: PluginInitializerContext) {} @@ -73,6 +76,7 @@ export class SearchService implements Plugin { ): ISearchSetup { this.usageCollector = createUsageCollector(getStartServices, usageCollection); + this.sessionService = new SessionService(this.initializerContext, getStartServices); /** * A global object that intercepts all searches and provides convenience methods for cancelling * all pending search requests, as well as getting the number of pending search requests. @@ -83,6 +87,7 @@ export class SearchService implements Plugin { uiSettings, startServices: getStartServices(), usageCollector: this.usageCollector!, + session: this.sessionService, }); expressions.registerFunction(esdsl); @@ -104,6 +109,7 @@ export class SearchService implements Plugin { __enhance: (enhancements: SearchEnhancements) => { this.searchInterceptor = enhancements.searchInterceptor; }, + session: this.sessionService, }; } @@ -142,6 +148,7 @@ export class SearchService implements Plugin { showError: (e: Error) => { this.searchInterceptor.showError(e); }, + session: this.sessionService, searchSource: this.searchSourceService.start(indexPatterns, searchSourceDependencies), }; } diff --git a/src/plugins/data/public/search/session_service.test.ts b/src/plugins/data/public/search/session_service.test.ts new file mode 100644 index 0000000000000..dd64d187f47d6 --- /dev/null +++ b/src/plugins/data/public/search/session_service.test.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SessionService } from './session_service'; +import { ISessionService } from '../../common'; +import { coreMock } from '../../../../core/public/mocks'; + +describe('Session service', () => { + let sessionService: ISessionService; + + beforeEach(() => { + const initializerContext = coreMock.createPluginInitializerContext(); + sessionService = new SessionService( + initializerContext, + coreMock.createSetup().getStartServices + ); + }); + + describe('Session management', () => { + it('Creates and clears a session', async () => { + sessionService.start(); + expect(sessionService.getSessionId()).not.toBeUndefined(); + sessionService.clear(); + expect(sessionService.getSessionId()).toBeUndefined(); + }); + }); +}); diff --git a/src/plugins/data/public/search/session_service.ts b/src/plugins/data/public/search/session_service.ts new file mode 100644 index 0000000000000..31524434af302 --- /dev/null +++ b/src/plugins/data/public/search/session_service.ts @@ -0,0 +1,80 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 uuid from 'uuid'; +import { Subject, Subscription } from 'rxjs'; +import { PluginInitializerContext, StartServicesAccessor } from 'kibana/public'; +import { ISessionService } from '../../common/search'; +import { ConfigSchema } from '../../config'; + +export class SessionService implements ISessionService { + private sessionId?: string; + private session$: Subject = new Subject(); + private appChangeSubscription$?: Subscription; + private curApp?: string; + + constructor( + initializerContext: PluginInitializerContext, + getStartServices: StartServicesAccessor + ) { + /* + Make sure that apps don't leave sessions open. + */ + getStartServices().then(([coreStart]) => { + this.appChangeSubscription$ = coreStart.application.currentAppId$.subscribe((appName) => { + if (this.sessionId) { + const message = `Application '${this.curApp}' had an open session while navigating`; + if (initializerContext.env.mode.dev) { + // TODO: This setTimeout is necessary due to a race condition while navigating. + setTimeout(() => { + coreStart.fatalErrors.add(message); + }, 100); + } else { + // eslint-disable-next-line no-console + console.warn(message); + } + } + this.curApp = appName; + }); + }); + } + + public destroy() { + this.appChangeSubscription$?.unsubscribe(); + } + + public getSessionId() { + return this.sessionId; + } + + public getSession$() { + return this.session$.asObservable(); + } + + public start() { + this.sessionId = uuid.v4(); + this.session$.next(this.sessionId); + return this.sessionId; + } + + public clear() { + this.sessionId = undefined; + this.session$.next(this.sessionId); + } +} diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index 85ef7aa4d97cb..c08d9f4c7be3f 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -21,7 +21,7 @@ import { PackageInfo } from 'kibana/server'; import { ISearchInterceptor } from './search_interceptor'; import { SearchUsageCollector } from './collectors'; import { AggsSetup, AggsSetupDependencies, AggsStartDependencies, AggsStart } from './aggs'; -import { ISearchGeneric, ISearchStartSearchSource } from '../../common/search'; +import { ISearchGeneric, ISessionService, ISearchStartSearchSource } from '../../common/search'; import { IndexPatternsContract } from '../../common/index_patterns/index_patterns'; import { UsageCollectionSetup } from '../../../usage_collection/public'; @@ -38,6 +38,11 @@ export interface SearchEnhancements { export interface ISearchSetup { aggs: AggsSetup; usageCollector?: SearchUsageCollector; + /** + * session management + * {@link ISessionService} + */ + session: ISessionService; /** * @internal */ @@ -67,6 +72,11 @@ export interface ISearchStart { * {@link ISearchStartSearchSource} */ searchSource: ISearchStartSearchSource; + /** + * session management + * {@link ISessionService} + */ + session: ISessionService; } export { SEARCH_EVENT_TYPE } from './collectors'; diff --git a/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx b/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx index 80e1a26163b72..19606cafc5c8a 100644 --- a/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx +++ b/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx @@ -18,17 +18,12 @@ */ import React from 'react'; -import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; import { IIndexPattern, Filter } from '../..'; type CancelFnType = () => void; type SubmitFnType = (filters: Filter[]) => void; -const Fallback = () => ( - - - -); +const Fallback = () =>
; const LazyApplyFiltersPopoverContent = React.lazy(() => import('./apply_filter_popover_content')); diff --git a/src/plugins/data/public/ui/filter_bar/index.tsx b/src/plugins/data/public/ui/filter_bar/index.tsx index b4296bb6615d4..4d9ba69afd48e 100644 --- a/src/plugins/data/public/ui/filter_bar/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/index.tsx @@ -18,14 +18,9 @@ */ import React from 'react'; -import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; import type { FilterLabelProps } from './filter_editor/lib/filter_label'; -const Fallback = () => ( - - - -); +const Fallback = () =>
; const LazyFilterLabel = React.lazy(() => import('./filter_editor/lib/filter_label')); export const FilterLabel = (props: FilterLabelProps) => ( diff --git a/src/plugins/data/public/ui/index_pattern_select/index.tsx b/src/plugins/data/public/ui/index_pattern_select/index.tsx index f0db37eb963fd..c909b202a4094 100644 --- a/src/plugins/data/public/ui/index_pattern_select/index.tsx +++ b/src/plugins/data/public/ui/index_pattern_select/index.tsx @@ -18,14 +18,9 @@ */ import React from 'react'; -import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; import type { IndexPatternSelectInternalProps } from './index_pattern_select'; -const Fallback = () => ( - - - -); +const Fallback = () =>
; const LazyIndexPatternSelect = React.lazy(() => import('./index_pattern_select')); export const IndexPatternSelect = (props: IndexPatternSelectInternalProps) => ( diff --git a/src/plugins/data/public/ui/query_string_input/index.tsx b/src/plugins/data/public/ui/query_string_input/index.tsx index 5bc5bd5097969..eb6641bf3661e 100644 --- a/src/plugins/data/public/ui/query_string_input/index.tsx +++ b/src/plugins/data/public/ui/query_string_input/index.tsx @@ -18,16 +18,11 @@ */ import React from 'react'; -import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; import { withKibana } from '../../../../kibana_react/public'; import type { QueryBarTopRowProps } from './query_bar_top_row'; import type { QueryStringInputProps } from './query_string_input'; -const Fallback = () => ( - - - -); +const Fallback = () =>
; const LazyQueryBarTopRow = React.lazy(() => import('./query_bar_top_row')); export const QueryBarTopRow = (props: QueryBarTopRowProps) => ( diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx index e01fbedbe38de..7a44b924870f0 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx @@ -36,12 +36,14 @@ import { EuiSuperUpdateButton, OnRefreshProps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { Toast } from 'src/core/public'; import { IDataPluginServices, IIndexPattern, TimeRange, TimeHistoryContract, Query } from '../..'; -import { useKibana, toMountPoint } from '../../../../kibana_react/public'; -import { QueryStringInput } from './'; +import { useKibana, toMountPoint, withKibana } from '../../../../kibana_react/public'; +import QueryStringInputUI from './query_string_input'; import { doesKueryExpressionHaveLuceneSyntaxError, UI_SETTINGS } from '../../../common'; import { PersistedLog, getQueryLog } from '../../query'; import { NoDataPopover } from './no_data_popover'; +const QueryStringInput = withKibana(QueryStringInputUI); + // @internal export interface QueryBarTopRowProps { query?: Query; diff --git a/src/plugins/data/public/ui/search_bar/index.tsx b/src/plugins/data/public/ui/search_bar/index.tsx index d81ed7333655d..310542f4b12bd 100644 --- a/src/plugins/data/public/ui/search_bar/index.tsx +++ b/src/plugins/data/public/ui/search_bar/index.tsx @@ -19,15 +19,10 @@ import React from 'react'; import { injectI18n } from '@kbn/i18n/react'; -import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; import { withKibana } from '../../../../kibana_react/public'; import type { SearchBarProps } from './search_bar'; -const Fallback = () => ( - - - -); +const Fallback = () =>
; const LazySearchBar = React.lazy(() => import('./search_bar')); const WrappedSearchBar = (props: SearchBarProps) => ( diff --git a/src/plugins/data/public/ui/search_bar/search_bar.test.tsx b/src/plugins/data/public/ui/search_bar/search_bar.test.tsx index a89b9bb7f91ef..74992f35fffc8 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.test.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.test.tsx @@ -18,10 +18,7 @@ */ import React from 'react'; -import { waitFor } from '@testing-library/dom'; -import { render } from '@testing-library/react'; - -import { SearchBar } from './'; +import SearchBar from './search_bar'; import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; import { I18nProvider } from '@kbn/i18n/react'; @@ -29,6 +26,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { coreMock } from '../../../../../core/public/mocks'; const startMock = coreMock.createStart(); +import { mount } from 'enzyme'; import { IIndexPattern } from '../..'; const mockTimeHistory = { @@ -37,16 +35,14 @@ const mockTimeHistory = { }, }; -jest.mock('..', () => { +jest.mock('../filter_bar/filter_bar', () => { return { FilterBar: () =>
, }; }); -jest.mock('../query_string_input', () => { - return { - QueryBarTopRow: () =>
, - }; +jest.mock('../query_string_input/query_bar_top_row', () => { + return () =>
; }); const noop = jest.fn(); @@ -117,48 +113,42 @@ function wrapSearchBarInContext(testProps: any) { ); } -// FLAKY: https://github.com/elastic/kibana/issues/79910 -describe.skip('SearchBar', () => { - const SEARCH_BAR_TEST_ID = 'globalQueryBar'; +describe('SearchBar', () => { const SEARCH_BAR_ROOT = '.globalQueryBar'; - const FILTER_BAR = '.globalFilterBar'; + const FILTER_BAR = '.filterBar'; const QUERY_BAR = '.queryBar'; beforeEach(() => { jest.clearAllMocks(); }); - it('Should render query bar when no options provided (in reality - timepicker)', async () => { - const { container, getByTestId } = render( + it('Should render query bar when no options provided (in reality - timepicker)', () => { + const component = mount( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], }) ); - await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); - - expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); - expect(container.querySelectorAll(FILTER_BAR).length).toBe(0); - expect(container.querySelectorAll(QUERY_BAR).length).toBe(1); + expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); + expect(component.find(FILTER_BAR).length).toBe(0); + expect(component.find(QUERY_BAR).length).toBe(1); }); - it('Should render empty when timepicker is off and no options provided', async () => { - const { container, getByTestId } = render( + it('Should render empty when timepicker is off and no options provided', () => { + const component = mount( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], showDatePicker: false, }) ); - await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); - - expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); - expect(container.querySelectorAll(FILTER_BAR).length).toBe(0); - expect(container.querySelectorAll(QUERY_BAR).length).toBe(0); + expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); + expect(component.find(FILTER_BAR).length).toBe(0); + expect(component.find(QUERY_BAR).length).toBe(0); }); - it('Should render filter bar, when required fields are provided', async () => { - const { container, getByTestId } = render( + it('Should render filter bar, when required fields are provided', () => { + const component = mount( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], showDatePicker: false, @@ -167,15 +157,13 @@ describe.skip('SearchBar', () => { }) ); - await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); - - expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); - expect(container.querySelectorAll(FILTER_BAR).length).toBe(1); - expect(container.querySelectorAll(QUERY_BAR).length).toBe(0); + expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); + expect(component.find(FILTER_BAR).length).toBe(1); + expect(component.find(QUERY_BAR).length).toBe(0); }); - it('Should NOT render filter bar, if disabled', async () => { - const { container, getByTestId } = render( + it('Should NOT render filter bar, if disabled', () => { + const component = mount( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], showFilterBar: false, @@ -185,15 +173,13 @@ describe.skip('SearchBar', () => { }) ); - await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); - - expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); - expect(container.querySelectorAll(FILTER_BAR).length).toBe(0); - expect(container.querySelectorAll(QUERY_BAR).length).toBe(0); + expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); + expect(component.find(FILTER_BAR).length).toBe(0); + expect(component.find(QUERY_BAR).length).toBe(0); }); - it('Should render query bar, when required fields are provided', async () => { - const { container, getByTestId } = render( + it('Should render query bar, when required fields are provided', () => { + const component = mount( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], screenTitle: 'test screen', @@ -202,15 +188,13 @@ describe.skip('SearchBar', () => { }) ); - await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); - - expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); - expect(container.querySelectorAll(FILTER_BAR).length).toBe(0); - expect(container.querySelectorAll(QUERY_BAR).length).toBe(1); + expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); + expect(component.find(FILTER_BAR).length).toBe(0); + expect(component.find(QUERY_BAR).length).toBe(1); }); - it('Should NOT render query bar, if disabled', async () => { - const { container, getByTestId } = render( + it('Should NOT render query bar, if disabled', () => { + const component = mount( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], screenTitle: 'test screen', @@ -220,15 +204,13 @@ describe.skip('SearchBar', () => { }) ); - await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); - - expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); - expect(container.querySelectorAll(FILTER_BAR).length).toBe(0); - expect(container.querySelectorAll(QUERY_BAR).length).toBe(0); + expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); + expect(component.find(FILTER_BAR).length).toBe(0); + expect(component.find(QUERY_BAR).length).toBe(0); }); - it('Should render query bar and filter bar', async () => { - const { container, getByTestId } = render( + it('Should render query bar and filter bar', () => { + const component = mount( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], screenTitle: 'test screen', @@ -239,10 +221,8 @@ describe.skip('SearchBar', () => { }) ); - await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); - - expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); - expect(container.querySelectorAll(FILTER_BAR).length).toBe(1); - expect(container.querySelectorAll(QUERY_BAR).length).toBe(1); + expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); + expect(component.find(FILTER_BAR).length).toBe(1); + expect(component.find(QUERY_BAR).length).toBe(1); }); }); diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index 95651ac9ed8b3..daa6fa0dd80ab 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -26,7 +26,7 @@ import { get, isEqual } from 'lodash'; import { withKibana, KibanaReactContextValue } from '../../../../kibana_react/public'; -import { QueryBarTopRow } from '../query_string_input'; +import QueryBarTopRow from '../query_string_input/query_bar_top_row'; import { SavedQueryAttributes, TimeHistoryContract, SavedQuery } from '../../query'; import { IDataPluginServices } from '../../types'; import { TimeRange, Query, Filter, IIndexPattern } from '../../../common'; diff --git a/src/plugins/data/public/ui/shard_failure_modal/index.tsx b/src/plugins/data/public/ui/shard_failure_modal/index.tsx index cea882deff365..2ac470573c422 100644 --- a/src/plugins/data/public/ui/shard_failure_modal/index.tsx +++ b/src/plugins/data/public/ui/shard_failure_modal/index.tsx @@ -18,14 +18,9 @@ */ import React from 'react'; -import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; import type { ShardFailureOpenModalButtonProps } from './shard_failure_open_modal_button'; -const Fallback = () => ( - - - -); +const Fallback = () =>
; const LazyShardFailureOpenModalButton = React.lazy( () => import('./shard_failure_open_modal_button') diff --git a/src/plugins/data/public/ui/typeahead/index.tsx b/src/plugins/data/public/ui/typeahead/index.tsx index aa3c2d71300df..58547cd2ccbec 100644 --- a/src/plugins/data/public/ui/typeahead/index.tsx +++ b/src/plugins/data/public/ui/typeahead/index.tsx @@ -18,14 +18,9 @@ */ import React from 'react'; -import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; import type { SuggestionsComponentProps } from './suggestions_component'; -const Fallback = () => ( - - - -); +const Fallback = () =>
; const LazySuggestionsComponent = React.lazy(() => import('./suggestions_component')); export const SuggestionsComponent = (props: SuggestionsComponentProps) => ( diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index f2c8ff5344b9a..0ed296a1d0662 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -20,6 +20,7 @@ import { EnvironmentMode } from '@kbn/config'; import { ErrorToastOptions } from 'src/core/public/notifications'; import { ExpressionAstFunction } from 'src/plugins/expressions/common'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; +import { ISavedObjectsRepository } from 'kibana/server'; import { ISearchOptions as ISearchOptions_2 } from 'src/plugins/data/public'; import { ISearchSource } from 'src/plugins/data/public'; import { KibanaRequest } from 'src/core/server'; @@ -684,6 +685,7 @@ export class IndexPatternsService implements Plugin_3 = { @@ -41,7 +42,7 @@ export const range: ExpressionTypeDefinition = { }, to: { render: (value: Range): ExpressionValueRender<{ text: string }> => { - const text = `from ${value.from} to ${value.to}`; + const text = value?.label || `from ${value.from} to ${value.to}`; return { type: 'render', as: 'text', diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 57ad6f747e5af..c7b6190b96ed7 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -1069,6 +1069,8 @@ export interface Range { // (undocumented) from: number; // (undocumented) + label?: string; + // (undocumented) to: number; // Warning: (ae-forgotten-export) The symbol "name" needs to be exported by the entry point index.d.ts // diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index 1a905de4a3bdf..fd6cc8d742cd4 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -894,6 +894,8 @@ export interface Range { // (undocumented) from: number; // (undocumented) + label?: string; + // (undocumented) to: number; // Warning: (ae-forgotten-export) The symbol "name" needs to be exported by the entry point index.d.ts // diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index b423cbb07ba32..037f97fb63ac6 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -35,6 +35,7 @@ import { Logger, IClusterClient, UiSettingsServiceStart, + SavedObjectsServiceStart, } from '../../../core/server'; import { registerRoutes } from './routes'; import { registerCollection } from './telemetry_collection'; @@ -88,6 +89,7 @@ export class TelemetryPlugin implements Plugin) { this.logger = initializerContext.logger.get(); @@ -110,7 +112,8 @@ export class TelemetryPlugin implements Plugin this.elasticsearchClient + () => this.elasticsearchClient, + () => this.savedObjectsService ); const router = http.createRouter(); @@ -139,6 +142,7 @@ export class TelemetryPlugin implements Plugin { - const usage = await usageCollection.bulkFetch(callWithInternalUser, asInternalUser); + const usage = await usageCollection.bulkFetch(callWithInternalUser, asInternalUser, soClient); return usageCollection.toObject(usage); } diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts index 0c8b0b249f7d1..fcecbca23038e 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts @@ -20,7 +20,10 @@ import { merge, omit } from 'lodash'; import { getLocalStats, handleLocalStats } from './get_local_stats'; -import { usageCollectionPluginMock } from '../../../usage_collection/server/mocks'; +import { + usageCollectionPluginMock, + createCollectorFetchContextMock, +} from '../../../usage_collection/server/mocks'; import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; function mockUsageCollection(kibanaUsage = {}) { @@ -79,6 +82,16 @@ function mockGetLocalStats(clusterInfo: any, clusterStats: any) { return esClient; } +function mockStatsCollectionConfig(clusterInfo: any, clusterStats: any, kibana: {}) { + return { + ...createCollectorFetchContextMock(), + esClient: mockGetLocalStats(clusterInfo, clusterStats), + usageCollection: mockUsageCollection(kibana), + start: '', + end: '', + }; +} + describe('get_local_stats', () => { const clusterUuid = 'abc123'; const clusterName = 'my-cool-cluster'; @@ -224,12 +237,10 @@ describe('get_local_stats', () => { describe('getLocalStats', () => { it('returns expected object with kibana data', async () => { - const callCluster = jest.fn(); - const usageCollection = mockUsageCollection(kibana); - const esClient = mockGetLocalStats(clusterInfo, clusterStats); + const statsCollectionConfig = mockStatsCollectionConfig(clusterInfo, clusterStats, kibana); const response = await getLocalStats( [{ clusterUuid: 'abc123' }], - { callCluster, usageCollection, esClient, start: '', end: '' }, + { ...statsCollectionConfig }, context ); const result = response[0]; @@ -244,14 +255,8 @@ describe('get_local_stats', () => { }); it('returns an empty array when no cluster uuid is provided', async () => { - const callCluster = jest.fn(); - const usageCollection = mockUsageCollection(kibana); - const esClient = mockGetLocalStats(clusterInfo, clusterStats); - const response = await getLocalStats( - [], - { callCluster, usageCollection, esClient, start: '', end: '' }, - context - ); + const statsCollectionConfig = mockStatsCollectionConfig(clusterInfo, clusterStats, kibana); + const response = await getLocalStats([], { ...statsCollectionConfig }, context); expect(response).toBeDefined(); expect(response.length).toEqual(0); }); diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts index 6244c6fac51d3..4aeefb1d81d6a 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts @@ -68,10 +68,10 @@ export type TelemetryLocalStats = ReturnType; */ export const getLocalStats: StatsGetter<{}, TelemetryLocalStats> = async ( clustersDetails, // array of cluster uuid's - config, // contains the new esClient already scoped contains usageCollection, callCluster, esClient, start, end + config, // contains the new esClient already scoped contains usageCollection, callCluster, esClient, start, end and the saved objects client scoped to the request or the internal repository context // StatsCollectionContext contains logger and version (string) ) => { - const { callCluster, usageCollection, esClient } = config; + const { callCluster, usageCollection, esClient, soClient } = config; return await Promise.all( clustersDetails.map(async (clustersDetail) => { @@ -79,7 +79,7 @@ export const getLocalStats: StatsGetter<{}, TelemetryLocalStats> = async ( getClusterInfo(esClient), // cluster info getClusterStats(esClient), // cluster stats (not to be confused with cluster _state_) getNodesUsage(esClient), // nodes_usage info - getKibana(usageCollection, callCluster, esClient), + getKibana(usageCollection, callCluster, esClient, soClient), getDataTelemetry(esClient), ]); return handleLocalStats( diff --git a/src/plugins/telemetry/server/telemetry_collection/register_collection.ts b/src/plugins/telemetry/server/telemetry_collection/register_collection.ts index 9dac4900f5f10..27ca5ae746512 100644 --- a/src/plugins/telemetry/server/telemetry_collection/register_collection.ts +++ b/src/plugins/telemetry/server/telemetry_collection/register_collection.ts @@ -36,7 +36,7 @@ * under the License. */ -import { ILegacyClusterClient } from 'kibana/server'; +import { ILegacyClusterClient, SavedObjectsServiceStart } from 'kibana/server'; import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; import { IClusterClient } from '../../../../../src/core/server'; import { getLocalStats } from './get_local_stats'; @@ -46,11 +46,13 @@ import { getLocalLicense } from './get_local_license'; export function registerCollection( telemetryCollectionManager: TelemetryCollectionManagerPluginSetup, esCluster: ILegacyClusterClient, - esClientGetter: () => IClusterClient | undefined + esClientGetter: () => IClusterClient | undefined, + soServiceGetter: () => SavedObjectsServiceStart | undefined ) { telemetryCollectionManager.setCollection({ esCluster, esClientGetter, + soServiceGetter, title: 'local', priority: 0, statsGetter: getLocalStats, diff --git a/src/plugins/telemetry_collection_manager/server/plugin.ts b/src/plugins/telemetry_collection_manager/server/plugin.ts index ff63262004cf5..4900e75a1936b 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.ts @@ -25,6 +25,7 @@ import { Plugin, Logger, IClusterClient, + SavedObjectsServiceStart, } from '../../../core/server'; import { @@ -90,6 +91,7 @@ export class TelemetryCollectionManagerPlugin priority, esCluster, esClientGetter, + soServiceGetter, statsGetter, clusterDetailsGetter, licenseGetter, @@ -112,6 +114,9 @@ export class TelemetryCollectionManagerPlugin if (!esClientGetter) { throw Error('esClientGetter method not set.'); } + if (!soServiceGetter) { + throw Error('soServiceGetter method not set.'); + } if (!clusterDetailsGetter) { throw Error('Cluster UUIds method is not set.'); } @@ -126,6 +131,7 @@ export class TelemetryCollectionManagerPlugin esCluster, title, esClientGetter, + soServiceGetter, }); this.usageGetterMethodPriority = priority; } @@ -135,6 +141,7 @@ export class TelemetryCollectionManagerPlugin config: StatsGetterConfig, collection: Collection, collectionEsClient: IClusterClient, + collectionSoService: SavedObjectsServiceStart, usageCollection: UsageCollectionSetup ): StatsCollectionConfig { const { start, end, request } = config; @@ -146,7 +153,11 @@ export class TelemetryCollectionManagerPlugin const esClient = config.unencrypted ? collectionEsClient.asScoped(config.request).asCurrentUser : collectionEsClient.asInternalUser; - return { callCluster, start, end, usageCollection, esClient }; + // Scope the saved objects client appropriately and pass to the stats collection config + const soClient = config.unencrypted + ? collectionSoService.getScopedClient(config.request) + : collectionSoService.createInternalRepository(); + return { callCluster, start, end, usageCollection, esClient, soClient }; } private async getOptInStats(optInStatus: boolean, config: StatsGetterConfig) { @@ -156,11 +167,13 @@ export class TelemetryCollectionManagerPlugin for (const collection of this.collections) { // first fetch the client and make sure it's not undefined. const collectionEsClient = collection.esClientGetter(); - if (collectionEsClient !== undefined) { + const collectionSoService = collection.soServiceGetter(); + if (collectionEsClient !== undefined && collectionSoService !== undefined) { const statsCollectionConfig = this.getStatsCollectionConfig( config, collection, collectionEsClient, + collectionSoService, this.usageCollection ); @@ -215,11 +228,13 @@ export class TelemetryCollectionManagerPlugin } for (const collection of this.collections) { const collectionEsClient = collection.esClientGetter(); - if (collectionEsClient !== undefined) { + const collectionSavedObjectsService = collection.soServiceGetter(); + if (collectionEsClient !== undefined && collectionSavedObjectsService !== undefined) { const statsCollectionConfig = this.getStatsCollectionConfig( config, collection, collectionEsClient, + collectionSavedObjectsService, this.usageCollection ); try { diff --git a/src/plugins/telemetry_collection_manager/server/types.ts b/src/plugins/telemetry_collection_manager/server/types.ts index 3b0936fb73a60..d6e4fdce2b188 100644 --- a/src/plugins/telemetry_collection_manager/server/types.ts +++ b/src/plugins/telemetry_collection_manager/server/types.ts @@ -23,6 +23,9 @@ import { KibanaRequest, ILegacyClusterClient, IClusterClient, + SavedObjectsServiceStart, + SavedObjectsClientContract, + ISavedObjectsRepository, } from 'kibana/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { ElasticsearchClient } from '../../../../src/core/server'; @@ -77,6 +80,7 @@ export interface StatsCollectionConfig { start: string | number; end: string | number; esClient: ElasticsearchClient; + soClient: SavedObjectsClientContract | ISavedObjectsRepository; } export interface BasicStatsPayload { @@ -141,6 +145,7 @@ export interface CollectionConfig< priority: number; esCluster: ILegacyClusterClient; esClientGetter: () => IClusterClient | undefined; // --> by now we know that the client getter will return the IClusterClient but we assure that through a code check + soServiceGetter: () => SavedObjectsServiceStart | undefined; // --> by now we know that the service getter will return the SavedObjectsServiceStart but we assure that through a code check statsGetter: StatsGetter; clusterDetailsGetter: ClusterDetailsGetter; licenseGetter: LicenseGetter; @@ -157,5 +162,6 @@ export interface Collection< clusterDetailsGetter: ClusterDetailsGetter; esCluster: ILegacyClusterClient; esClientGetter: () => IClusterClient | undefined; // the collection could still return undefined for the es client getter. + soServiceGetter: () => SavedObjectsServiceStart | undefined; // the collection could still return undefined for the Saved Objects Service getter. title: string; } diff --git a/src/plugins/timelion/public/flot/jquery.flot.js b/src/plugins/timelion/public/flot/jquery.flot.js deleted file mode 100644 index 5d613037cf234..0000000000000 --- a/src/plugins/timelion/public/flot/jquery.flot.js +++ /dev/null @@ -1,3168 +0,0 @@ -/* JavaScript plotting library for jQuery, version 0.8.3. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -*/ - -// first an inline dependency, jquery.colorhelpers.js, we inline it here -// for convenience - -/* Plugin for jQuery for working with colors. - * - * Version 1.1. - * - * Inspiration from jQuery color animation plugin by John Resig. - * - * Released under the MIT license by Ole Laursen, October 2009. - * - * Examples: - * - * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() - * var c = $.color.extract($("#mydiv"), 'background-color'); - * console.log(c.r, c.g, c.b, c.a); - * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" - * - * Note that .scale() and .add() return the same modified object - * instead of making a new one. - * - * V. 1.1: Fix error handling so e.g. parsing an empty string does - * produce a color rather than just crashing. - */ -(function($){$.color={};$.color.make=function(r,g,b,a){var o={};o.r=r||0;o.g=g||0;o.b=b||0;o.a=a!=null?a:1;o.add=function(c,d){for(var i=0;i=1){return"rgb("+[o.r,o.g,o.b].join(",")+")"}else{return"rgba("+[o.r,o.g,o.b,o.a].join(",")+")"}};o.normalize=function(){function clamp(min,value,max){return valuemax?max:value}o.r=clamp(0,parseInt(o.r),255);o.g=clamp(0,parseInt(o.g),255);o.b=clamp(0,parseInt(o.b),255);o.a=clamp(0,o.a,1);return o};o.clone=function(){return $.color.make(o.r,o.b,o.g,o.a)};return o.normalize()};$.color.extract=function(elem,css){var c;do{c=elem.css(css).toLowerCase();if(c!=""&&c!="transparent")break;elem=elem.parent()}while(elem.length&&!$.nodeName(elem.get(0),"body"));if(c=="rgba(0, 0, 0, 0)")c="transparent";return $.color.parse(c)};$.color.parse=function(str){var res,m=$.color.make;if(res=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10));if(res=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10),parseFloat(res[4]));if(res=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55);if(res=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55,parseFloat(res[4]));if(res=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))return m(parseInt(res[1],16),parseInt(res[2],16),parseInt(res[3],16));if(res=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))return m(parseInt(res[1]+res[1],16),parseInt(res[2]+res[2],16),parseInt(res[3]+res[3],16));var name=$.trim(str).toLowerCase();if(name=="transparent")return m(255,255,255,0);else{res=lookupColors[name]||[0,0,0];return m(res[0],res[1],res[2])}};var lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery); - -// the actual Flot code -(function($) { - - // Cache the prototype hasOwnProperty for faster access - - var hasOwnProperty = Object.prototype.hasOwnProperty; - - // A shim to provide 'detach' to jQuery versions prior to 1.4. Using a DOM - // operation produces the same effect as detach, i.e. removing the element - // without touching its jQuery data. - - // Do not merge this into Flot 0.9, since it requires jQuery 1.4.4+. - - if (!$.fn.detach) { - $.fn.detach = function() { - return this.each(function() { - if (this.parentNode) { - this.parentNode.removeChild( this ); - } - }); - }; - } - - /////////////////////////////////////////////////////////////////////////// - // The Canvas object is a wrapper around an HTML5 tag. - // - // @constructor - // @param {string} cls List of classes to apply to the canvas. - // @param {element} container Element onto which to append the canvas. - // - // Requiring a container is a little iffy, but unfortunately canvas - // operations don't work unless the canvas is attached to the DOM. - - function Canvas(cls, container) { - - var element = container.children("." + cls)[0]; - - if (element == null) { - - element = document.createElement("canvas"); - element.className = cls; - - $(element).css({ direction: "ltr", position: "absolute", left: 0, top: 0 }) - .appendTo(container); - - // If HTML5 Canvas isn't available, fall back to [Ex|Flash]canvas - - if (!element.getContext) { - if (window.G_vmlCanvasManager) { - element = window.G_vmlCanvasManager.initElement(element); - } else { - throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode."); - } - } - } - - this.element = element; - - var context = this.context = element.getContext("2d"); - - // Determine the screen's ratio of physical to device-independent - // pixels. This is the ratio between the canvas width that the browser - // advertises and the number of pixels actually present in that space. - - // The iPhone 4, for example, has a device-independent width of 320px, - // but its screen is actually 640px wide. It therefore has a pixel - // ratio of 2, while most normal devices have a ratio of 1. - - var devicePixelRatio = window.devicePixelRatio || 1, - backingStoreRatio = - context.webkitBackingStorePixelRatio || - context.mozBackingStorePixelRatio || - context.msBackingStorePixelRatio || - context.oBackingStorePixelRatio || - context.backingStorePixelRatio || 1; - - this.pixelRatio = devicePixelRatio / backingStoreRatio; - - // Size the canvas to match the internal dimensions of its container - - this.resize(container.width(), container.height()); - - // Collection of HTML div layers for text overlaid onto the canvas - - this.textContainer = null; - this.text = {}; - - // Cache of text fragments and metrics, so we can avoid expensively - // re-calculating them when the plot is re-rendered in a loop. - - this._textCache = {}; - } - - // Resizes the canvas to the given dimensions. - // - // @param {number} width New width of the canvas, in pixels. - // @param {number} width New height of the canvas, in pixels. - - Canvas.prototype.resize = function(width, height) { - - if (width <= 0 || height <= 0) { - throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + height); - } - - var element = this.element, - context = this.context, - pixelRatio = this.pixelRatio; - - // Resize the canvas, increasing its density based on the display's - // pixel ratio; basically giving it more pixels without increasing the - // size of its element, to take advantage of the fact that retina - // displays have that many more pixels in the same advertised space. - - // Resizing should reset the state (excanvas seems to be buggy though) - - if (this.width != width) { - element.width = width * pixelRatio; - element.style.width = width + "px"; - this.width = width; - } - - if (this.height != height) { - element.height = height * pixelRatio; - element.style.height = height + "px"; - this.height = height; - } - - // Save the context, so we can reset in case we get replotted. The - // restore ensure that we're really back at the initial state, and - // should be safe even if we haven't saved the initial state yet. - - context.restore(); - context.save(); - - // Scale the coordinate space to match the display density; so even though we - // may have twice as many pixels, we still want lines and other drawing to - // appear at the same size; the extra pixels will just make them crisper. - - context.scale(pixelRatio, pixelRatio); - }; - - // Clears the entire canvas area, not including any overlaid HTML text - - Canvas.prototype.clear = function() { - this.context.clearRect(0, 0, this.width, this.height); - }; - - // Finishes rendering the canvas, including managing the text overlay. - - Canvas.prototype.render = function() { - - var cache = this._textCache; - - // For each text layer, add elements marked as active that haven't - // already been rendered, and remove those that are no longer active. - - for (var layerKey in cache) { - if (hasOwnProperty.call(cache, layerKey)) { - - var layer = this.getTextLayer(layerKey), - layerCache = cache[layerKey]; - - layer.hide(); - - for (var styleKey in layerCache) { - if (hasOwnProperty.call(layerCache, styleKey)) { - var styleCache = layerCache[styleKey]; - for (var key in styleCache) { - if (hasOwnProperty.call(styleCache, key)) { - - var positions = styleCache[key].positions; - - for (var i = 0, position; position = positions[i]; i++) { - if (position.active) { - if (!position.rendered) { - layer.append(position.element); - position.rendered = true; - } - } else { - positions.splice(i--, 1); - if (position.rendered) { - position.element.detach(); - } - } - } - - if (positions.length == 0) { - delete styleCache[key]; - } - } - } - } - } - - layer.show(); - } - } - }; - - // Creates (if necessary) and returns the text overlay container. - // - // @param {string} classes String of space-separated CSS classes used to - // uniquely identify the text layer. - // @return {object} The jQuery-wrapped text-layer div. - - Canvas.prototype.getTextLayer = function(classes) { - - var layer = this.text[classes]; - - // Create the text layer if it doesn't exist - - if (layer == null) { - - // Create the text layer container, if it doesn't exist - - if (this.textContainer == null) { - this.textContainer = $("
") - .css({ - position: "absolute", - top: 0, - left: 0, - bottom: 0, - right: 0, - 'font-size': "smaller", - color: "#545454" - }) - .insertAfter(this.element); - } - - layer = this.text[classes] = $("
") - .addClass(classes) - .css({ - position: "absolute", - top: 0, - left: 0, - bottom: 0, - right: 0 - }) - .appendTo(this.textContainer); - } - - return layer; - }; - - // Creates (if necessary) and returns a text info object. - // - // The object looks like this: - // - // { - // width: Width of the text's wrapper div. - // height: Height of the text's wrapper div. - // element: The jQuery-wrapped HTML div containing the text. - // positions: Array of positions at which this text is drawn. - // } - // - // The positions array contains objects that look like this: - // - // { - // active: Flag indicating whether the text should be visible. - // rendered: Flag indicating whether the text is currently visible. - // element: The jQuery-wrapped HTML div containing the text. - // x: X coordinate at which to draw the text. - // y: Y coordinate at which to draw the text. - // } - // - // Each position after the first receives a clone of the original element. - // - // The idea is that that the width, height, and general 'identity' of the - // text is constant no matter where it is placed; the placements are a - // secondary property. - // - // Canvas maintains a cache of recently-used text info objects; getTextInfo - // either returns the cached element or creates a new entry. - // - // @param {string} layer A string of space-separated CSS classes uniquely - // identifying the layer containing this text. - // @param {string} text Text string to retrieve info for. - // @param {(string|object)=} font Either a string of space-separated CSS - // classes or a font-spec object, defining the text's font and style. - // @param {number=} angle Angle at which to rotate the text, in degrees. - // Angle is currently unused, it will be implemented in the future. - // @param {number=} width Maximum width of the text before it wraps. - // @return {object} a text info object. - - Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { - - var textStyle, layerCache, styleCache, info; - - // Cast the value to a string, in case we were given a number or such - - text = "" + text; - - // If the font is a font-spec object, generate a CSS font definition - - if (typeof font === "object") { - textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px/" + font.lineHeight + "px " + font.family; - } else { - textStyle = font; - } - - // Retrieve (or create) the cache for the text's layer and styles - - layerCache = this._textCache[layer]; - - if (layerCache == null) { - layerCache = this._textCache[layer] = {}; - } - - styleCache = layerCache[textStyle]; - - if (styleCache == null) { - styleCache = layerCache[textStyle] = {}; - } - - info = styleCache[text]; - - // If we can't find a matching element in our cache, create a new one - - if (info == null) { - - var element = $("
").html(text) - .css({ - position: "absolute", - 'max-width': width, - top: -9999 - }) - .appendTo(this.getTextLayer(layer)); - - if (typeof font === "object") { - element.css({ - font: textStyle, - color: font.color - }); - } else if (typeof font === "string") { - element.addClass(font); - } - - info = styleCache[text] = { - width: element.outerWidth(true), - height: element.outerHeight(true), - element: element, - positions: [] - }; - - element.detach(); - } - - return info; - }; - - // Adds a text string to the canvas text overlay. - // - // The text isn't drawn immediately; it is marked as rendering, which will - // result in its addition to the canvas on the next render pass. - // - // @param {string} layer A string of space-separated CSS classes uniquely - // identifying the layer containing this text. - // @param {number} x X coordinate at which to draw the text. - // @param {number} y Y coordinate at which to draw the text. - // @param {string} text Text string to draw. - // @param {(string|object)=} font Either a string of space-separated CSS - // classes or a font-spec object, defining the text's font and style. - // @param {number=} angle Angle at which to rotate the text, in degrees. - // Angle is currently unused, it will be implemented in the future. - // @param {number=} width Maximum width of the text before it wraps. - // @param {string=} halign Horizontal alignment of the text; either "left", - // "center" or "right". - // @param {string=} valign Vertical alignment of the text; either "top", - // "middle" or "bottom". - - Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { - - var info = this.getTextInfo(layer, text, font, angle, width), - positions = info.positions; - - // Tweak the div's position to match the text's alignment - - if (halign == "center") { - x -= info.width / 2; - } else if (halign == "right") { - x -= info.width; - } - - if (valign == "middle") { - y -= info.height / 2; - } else if (valign == "bottom") { - y -= info.height; - } - - // Determine whether this text already exists at this position. - // If so, mark it for inclusion in the next render pass. - - for (var i = 0, position; position = positions[i]; i++) { - if (position.x == x && position.y == y) { - position.active = true; - return; - } - } - - // If the text doesn't exist at this position, create a new entry - - // For the very first position we'll re-use the original element, - // while for subsequent ones we'll clone it. - - position = { - active: true, - rendered: false, - element: positions.length ? info.element.clone() : info.element, - x: x, - y: y - }; - - positions.push(position); - - // Move the element to its final position within the container - - position.element.css({ - top: Math.round(y), - left: Math.round(x), - 'text-align': halign // In case the text wraps - }); - }; - - // Removes one or more text strings from the canvas text overlay. - // - // If no parameters are given, all text within the layer is removed. - // - // Note that the text is not immediately removed; it is simply marked as - // inactive, which will result in its removal on the next render pass. - // This avoids the performance penalty for 'clear and redraw' behavior, - // where we potentially get rid of all text on a layer, but will likely - // add back most or all of it later, as when redrawing axes, for example. - // - // @param {string} layer A string of space-separated CSS classes uniquely - // identifying the layer containing this text. - // @param {number=} x X coordinate of the text. - // @param {number=} y Y coordinate of the text. - // @param {string=} text Text string to remove. - // @param {(string|object)=} font Either a string of space-separated CSS - // classes or a font-spec object, defining the text's font and style. - // @param {number=} angle Angle at which the text is rotated, in degrees. - // Angle is currently unused, it will be implemented in the future. - - Canvas.prototype.removeText = function(layer, x, y, text, font, angle) { - if (text == null) { - var layerCache = this._textCache[layer]; - if (layerCache != null) { - for (var styleKey in layerCache) { - if (hasOwnProperty.call(layerCache, styleKey)) { - var styleCache = layerCache[styleKey]; - for (var key in styleCache) { - if (hasOwnProperty.call(styleCache, key)) { - var positions = styleCache[key].positions; - for (var i = 0, position; position = positions[i]; i++) { - position.active = false; - } - } - } - } - } - } - } else { - var positions = this.getTextInfo(layer, text, font, angle).positions; - for (var i = 0, position; position = positions[i]; i++) { - if (position.x == x && position.y == y) { - position.active = false; - } - } - } - }; - - /////////////////////////////////////////////////////////////////////////// - // The top-level container for the entire plot. - - function Plot(placeholder, data_, options_, plugins) { - // data is on the form: - // [ series1, series2 ... ] - // where series is either just the data as [ [x1, y1], [x2, y2], ... ] - // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... } - - var series = [], - options = { - // the color theme used for graphs - colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], - legend: { - show: true, - noColumns: 1, // number of columns in legend table - labelFormatter: null, // fn: string -> string - labelBoxBorderColor: "#ccc", // border color for the little label boxes - container: null, // container (as jQuery object) to put legend in, null means default on top of graph - position: "ne", // position of default legend container within plot - margin: 5, // distance from grid edge to default legend container within plot - backgroundColor: null, // null means auto-detect - backgroundOpacity: 0.85, // set to 0 to avoid background - sorted: null // default to no legend sorting - }, - xaxis: { - show: null, // null = auto-detect, true = always, false = never - position: "bottom", // or "top" - mode: null, // null or "time" - font: null, // null (derived from CSS in placeholder) or object like { size: 11, lineHeight: 13, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" } - color: null, // base color, labels, ticks - tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)" - transform: null, // null or f: number -> number to transform axis - inverseTransform: null, // if transform is set, this should be the inverse function - min: null, // min. value to show, null means set automatically - max: null, // max. value to show, null means set automatically - autoscaleMargin: null, // margin in % to add if auto-setting min/max - ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks - tickFormatter: null, // fn: number -> string - labelWidth: null, // size of tick labels in pixels - labelHeight: null, - reserveSpace: null, // whether to reserve space even if axis isn't shown - tickLength: null, // size in pixels of ticks, or "full" for whole line - alignTicksWithAxis: null, // axis number or null for no sync - tickDecimals: null, // no. of decimals, null means auto - tickSize: null, // number or [number, "unit"] - minTickSize: null // number or [number, "unit"] - }, - yaxis: { - autoscaleMargin: 0.02, - position: "left" // or "right" - }, - xaxes: [], - yaxes: [], - series: { - points: { - show: false, - radius: 3, - lineWidth: 2, // in pixels - fill: true, - fillColor: "#ffffff", - symbol: "circle" // or callback - }, - lines: { - // we don't put in show: false so we can see - // whether lines were actively disabled - lineWidth: 2, // in pixels - fill: false, - fillColor: null, - steps: false - // Omit 'zero', so we can later default its value to - // match that of the 'fill' option. - }, - bars: { - show: false, - lineWidth: 2, // in pixels - barWidth: 1, // in units of the x axis - fill: true, - fillColor: null, - align: "left", // "left", "right", or "center" - horizontal: false, - zero: true - }, - shadowSize: 3, - highlightColor: null - }, - grid: { - show: true, - aboveData: false, - color: "#545454", // primary color used for outline and labels - backgroundColor: null, // null for transparent, else color - borderColor: null, // set if different from the grid color - tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)" - margin: 0, // distance from the canvas edge to the grid - labelMargin: 5, // in pixels - axisMargin: 8, // in pixels - borderWidth: 2, // in pixels - minBorderMargin: null, // in pixels, null means taken from points radius - markings: null, // array of ranges or fn: axes -> array of ranges - markingsColor: "#f4f4f4", - markingsLineWidth: 2, - // interactive stuff - clickable: false, - hoverable: false, - autoHighlight: true, // highlight in case mouse is near - mouseActiveRadius: 10 // how far the mouse can be away to activate an item - }, - interaction: { - redrawOverlayInterval: 1000/60 // time between updates, -1 means in same flow - }, - hooks: {} - }, - surface = null, // the canvas for the plot itself - overlay = null, // canvas for interactive stuff on top of plot - eventHolder = null, // jQuery object that events should be bound to - ctx = null, octx = null, - xaxes = [], yaxes = [], - plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, - plotWidth = 0, plotHeight = 0, - hooks = { - processOptions: [], - processRawData: [], - processDatapoints: [], - processOffset: [], - drawBackground: [], - drawSeries: [], - draw: [], - bindEvents: [], - drawOverlay: [], - shutdown: [] - }, - plot = this; - - // public functions - plot.setData = setData; - plot.setupGrid = setupGrid; - plot.draw = draw; - plot.getPlaceholder = function() { return placeholder; }; - plot.getCanvas = function() { return surface.element; }; - plot.getPlotOffset = function() { return plotOffset; }; - plot.width = function () { return plotWidth; }; - plot.height = function () { return plotHeight; }; - plot.offset = function () { - var o = eventHolder.offset(); - o.left += plotOffset.left; - o.top += plotOffset.top; - return o; - }; - plot.getData = function () { return series; }; - plot.getAxes = function () { - var res = {}, i; - $.each(xaxes.concat(yaxes), function (_, axis) { - if (axis) - res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis; - }); - return res; - }; - plot.getXAxes = function () { return xaxes; }; - plot.getYAxes = function () { return yaxes; }; - plot.c2p = canvasToAxisCoords; - plot.p2c = axisToCanvasCoords; - plot.getOptions = function () { return options; }; - plot.highlight = highlight; - plot.unhighlight = unhighlight; - plot.triggerRedrawOverlay = triggerRedrawOverlay; - plot.pointOffset = function(point) { - return { - left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left, 10), - top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top, 10) - }; - }; - plot.shutdown = shutdown; - plot.destroy = function () { - shutdown(); - placeholder.removeData("plot").empty(); - - series = []; - options = null; - surface = null; - overlay = null; - eventHolder = null; - ctx = null; - octx = null; - xaxes = []; - yaxes = []; - hooks = null; - highlights = []; - plot = null; - }; - plot.resize = function () { - var width = placeholder.width(), - height = placeholder.height(); - surface.resize(width, height); - overlay.resize(width, height); - }; - - // public attributes - plot.hooks = hooks; - - // initialize - initPlugins(plot); - parseOptions(options_); - setupCanvases(); - setData(data_); - setupGrid(); - draw(); - bindEvents(); - - - function executeHooks(hook, args) { - args = [plot].concat(args); - for (var i = 0; i < hook.length; ++i) - hook[i].apply(this, args); - } - - function initPlugins() { - - // References to key classes, allowing plugins to modify them - - var classes = { - Canvas: Canvas - }; - - for (var i = 0; i < plugins.length; ++i) { - var p = plugins[i]; - p.init(plot, classes); - if (p.options) - $.extend(true, options, p.options); - } - } - - function parseOptions(opts) { - - $.extend(true, options, opts); - - // $.extend merges arrays, rather than replacing them. When less - // colors are provided than the size of the default palette, we - // end up with those colors plus the remaining defaults, which is - // not expected behavior; avoid it by replacing them here. - - if (opts && opts.colors) { - options.colors = opts.colors; - } - - if (options.xaxis.color == null) - options.xaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); - if (options.yaxis.color == null) - options.yaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); - - if (options.xaxis.tickColor == null) // grid.tickColor for back-compatibility - options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color; - if (options.yaxis.tickColor == null) // grid.tickColor for back-compatibility - options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color; - - if (options.grid.borderColor == null) - options.grid.borderColor = options.grid.color; - if (options.grid.tickColor == null) - options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString(); - - // Fill in defaults for axis options, including any unspecified - // font-spec fields, if a font-spec was provided. - - // If no x/y axis options were provided, create one of each anyway, - // since the rest of the code assumes that they exist. - - var i, axisOptions, axisCount, - fontSize = placeholder.css("font-size"), - fontSizeDefault = fontSize ? +fontSize.replace("px", "") : 13, - fontDefaults = { - style: placeholder.css("font-style"), - size: Math.round(0.8 * fontSizeDefault), - variant: placeholder.css("font-variant"), - weight: placeholder.css("font-weight"), - family: placeholder.css("font-family") - }; - - axisCount = options.xaxes.length || 1; - for (i = 0; i < axisCount; ++i) { - - axisOptions = options.xaxes[i]; - if (axisOptions && !axisOptions.tickColor) { - axisOptions.tickColor = axisOptions.color; - } - - axisOptions = $.extend(true, {}, options.xaxis, axisOptions); - options.xaxes[i] = axisOptions; - - if (axisOptions.font) { - axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); - if (!axisOptions.font.color) { - axisOptions.font.color = axisOptions.color; - } - if (!axisOptions.font.lineHeight) { - axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); - } - } - } - - axisCount = options.yaxes.length || 1; - for (i = 0; i < axisCount; ++i) { - - axisOptions = options.yaxes[i]; - if (axisOptions && !axisOptions.tickColor) { - axisOptions.tickColor = axisOptions.color; - } - - axisOptions = $.extend(true, {}, options.yaxis, axisOptions); - options.yaxes[i] = axisOptions; - - if (axisOptions.font) { - axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); - if (!axisOptions.font.color) { - axisOptions.font.color = axisOptions.color; - } - if (!axisOptions.font.lineHeight) { - axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); - } - } - } - - // backwards compatibility, to be removed in future - if (options.xaxis.noTicks && options.xaxis.ticks == null) - options.xaxis.ticks = options.xaxis.noTicks; - if (options.yaxis.noTicks && options.yaxis.ticks == null) - options.yaxis.ticks = options.yaxis.noTicks; - if (options.x2axis) { - options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis); - options.xaxes[1].position = "top"; - // Override the inherit to allow the axis to auto-scale - if (options.x2axis.min == null) { - options.xaxes[1].min = null; - } - if (options.x2axis.max == null) { - options.xaxes[1].max = null; - } - } - if (options.y2axis) { - options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis); - options.yaxes[1].position = "right"; - // Override the inherit to allow the axis to auto-scale - if (options.y2axis.min == null) { - options.yaxes[1].min = null; - } - if (options.y2axis.max == null) { - options.yaxes[1].max = null; - } - } - if (options.grid.coloredAreas) - options.grid.markings = options.grid.coloredAreas; - if (options.grid.coloredAreasColor) - options.grid.markingsColor = options.grid.coloredAreasColor; - if (options.lines) - $.extend(true, options.series.lines, options.lines); - if (options.points) - $.extend(true, options.series.points, options.points); - if (options.bars) - $.extend(true, options.series.bars, options.bars); - if (options.shadowSize != null) - options.series.shadowSize = options.shadowSize; - if (options.highlightColor != null) - options.series.highlightColor = options.highlightColor; - - // save options on axes for future reference - for (i = 0; i < options.xaxes.length; ++i) - getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i]; - for (i = 0; i < options.yaxes.length; ++i) - getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i]; - - // add hooks from options - for (var n in hooks) - if (options.hooks[n] && options.hooks[n].length) - hooks[n] = hooks[n].concat(options.hooks[n]); - - executeHooks(hooks.processOptions, [options]); - } - - function setData(d) { - series = parseData(d); - fillInSeriesOptions(); - processData(); - } - - function parseData(d) { - var res = []; - for (var i = 0; i < d.length; ++i) { - var s = $.extend(true, {}, options.series); - - if (d[i].data != null) { - s.data = d[i].data; // move the data instead of deep-copy - delete d[i].data; - - $.extend(true, s, d[i]); - - d[i].data = s.data; - } - else - s.data = d[i]; - res.push(s); - } - - return res; - } - - function axisNumber(obj, coord) { - var a = obj[coord + "axis"]; - if (typeof a == "object") // if we got a real axis, extract number - a = a.n; - if (typeof a != "number") - a = 1; // default to first axis - return a; - } - - function allAxes() { - // return flat array without annoying null entries - return $.grep(xaxes.concat(yaxes), function (a) { return a; }); - } - - function canvasToAxisCoords(pos) { - // return an object with x/y corresponding to all used axes - var res = {}, i, axis; - for (i = 0; i < xaxes.length; ++i) { - axis = xaxes[i]; - if (axis && axis.used) - res["x" + axis.n] = axis.c2p(pos.left); - } - - for (i = 0; i < yaxes.length; ++i) { - axis = yaxes[i]; - if (axis && axis.used) - res["y" + axis.n] = axis.c2p(pos.top); - } - - if (res.x1 !== undefined) - res.x = res.x1; - if (res.y1 !== undefined) - res.y = res.y1; - - return res; - } - - function axisToCanvasCoords(pos) { - // get canvas coords from the first pair of x/y found in pos - var res = {}, i, axis, key; - - for (i = 0; i < xaxes.length; ++i) { - axis = xaxes[i]; - if (axis && axis.used) { - key = "x" + axis.n; - if (pos[key] == null && axis.n == 1) - key = "x"; - - if (pos[key] != null) { - res.left = axis.p2c(pos[key]); - break; - } - } - } - - for (i = 0; i < yaxes.length; ++i) { - axis = yaxes[i]; - if (axis && axis.used) { - key = "y" + axis.n; - if (pos[key] == null && axis.n == 1) - key = "y"; - - if (pos[key] != null) { - res.top = axis.p2c(pos[key]); - break; - } - } - } - - return res; - } - - function getOrCreateAxis(axes, number) { - if (!axes[number - 1]) - axes[number - 1] = { - n: number, // save the number for future reference - direction: axes == xaxes ? "x" : "y", - options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis) - }; - - return axes[number - 1]; - } - - function fillInSeriesOptions() { - - var neededColors = series.length, maxIndex = -1, i; - - // Subtract the number of series that already have fixed colors or - // color indexes from the number that we still need to generate. - - for (i = 0; i < series.length; ++i) { - var sc = series[i].color; - if (sc != null) { - neededColors--; - if (typeof sc == "number" && sc > maxIndex) { - maxIndex = sc; - } - } - } - - // If any of the series have fixed color indexes, then we need to - // generate at least as many colors as the highest index. - - if (neededColors <= maxIndex) { - neededColors = maxIndex + 1; - } - - // Generate all the colors, using first the option colors and then - // variations on those colors once they're exhausted. - - var c, colors = [], colorPool = options.colors, - colorPoolSize = colorPool.length, variation = 0; - - for (i = 0; i < neededColors; i++) { - - c = $.color.parse(colorPool[i % colorPoolSize] || "#666"); - - // Each time we exhaust the colors in the pool we adjust - // a scaling factor used to produce more variations on - // those colors. The factor alternates negative/positive - // to produce lighter/darker colors. - - // Reset the variation after every few cycles, or else - // it will end up producing only white or black colors. - - if (i % colorPoolSize == 0 && i) { - if (variation >= 0) { - if (variation < 0.5) { - variation = -variation - 0.2; - } else variation = 0; - } else variation = -variation; - } - - colors[i] = c.scale('rgb', 1 + variation); - } - - // Finalize the series options, filling in their colors - - var colori = 0, s; - for (i = 0; i < series.length; ++i) { - s = series[i]; - - // assign colors - if (s.color == null) { - s.color = colors[colori].toString(); - ++colori; - } - else if (typeof s.color == "number") - s.color = colors[s.color].toString(); - - // turn on lines automatically in case nothing is set - if (s.lines.show == null) { - var v, show = true; - for (v in s) - if (s[v] && s[v].show) { - show = false; - break; - } - if (show) - s.lines.show = true; - } - - // If nothing was provided for lines.zero, default it to match - // lines.fill, since areas by default should extend to zero. - - if (s.lines.zero == null) { - s.lines.zero = !!s.lines.fill; - } - - // setup axes - s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x")); - s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y")); - } - } - - function processData() { - var topSentry = Number.POSITIVE_INFINITY, - bottomSentry = Number.NEGATIVE_INFINITY, - fakeInfinity = Number.MAX_VALUE, - i, j, k, m, length, - s, points, ps, x, y, axis, val, f, p, - data, format; - - function updateAxis(axis, min, max) { - if (min < axis.datamin && min != -fakeInfinity) - axis.datamin = min; - if (max > axis.datamax && max != fakeInfinity) - axis.datamax = max; - } - - $.each(allAxes(), function (_, axis) { - // init axis - axis.datamin = topSentry; - axis.datamax = bottomSentry; - axis.used = false; - }); - - for (i = 0; i < series.length; ++i) { - s = series[i]; - s.datapoints = { points: [] }; - - executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]); - } - - // first pass: clean and copy data - for (i = 0; i < series.length; ++i) { - s = series[i]; - - data = s.data; - format = s.datapoints.format; - - if (!format) { - format = []; - // find out how to copy - format.push({ x: true, number: true, required: true }); - format.push({ y: true, number: true, required: true }); - - if (s.bars.show || (s.lines.show && s.lines.fill)) { - var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); - format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale }); - if (s.bars.horizontal) { - delete format[format.length - 1].y; - format[format.length - 1].x = true; - } - } - - s.datapoints.format = format; - } - - if (s.datapoints.pointsize != null) - continue; // already filled in - - s.datapoints.pointsize = format.length; - - ps = s.datapoints.pointsize; - points = s.datapoints.points; - - var insertSteps = s.lines.show && s.lines.steps; - s.xaxis.used = s.yaxis.used = true; - - for (j = k = 0; j < data.length; ++j, k += ps) { - p = data[j]; - - var nullify = p == null; - if (!nullify) { - for (m = 0; m < ps; ++m) { - val = p[m]; - f = format[m]; - - if (f) { - if (f.number && val != null) { - val = +val; // convert to number - if (isNaN(val)) - val = null; - else if (val == Infinity) - val = fakeInfinity; - else if (val == -Infinity) - val = -fakeInfinity; - } - - if (val == null) { - if (f.required) - nullify = true; - - if (f.defaultValue != null) - val = f.defaultValue; - } - } - - points[k + m] = val; - } - } - - if (nullify) { - for (m = 0; m < ps; ++m) { - val = points[k + m]; - if (val != null) { - f = format[m]; - // extract min/max info - if (f.autoscale !== false) { - if (f.x) { - updateAxis(s.xaxis, val, val); - } - if (f.y) { - updateAxis(s.yaxis, val, val); - } - } - } - points[k + m] = null; - } - } - else { - // a little bit of line specific stuff that - // perhaps shouldn't be here, but lacking - // better means... - if (insertSteps && k > 0 - && points[k - ps] != null - && points[k - ps] != points[k] - && points[k - ps + 1] != points[k + 1]) { - // copy the point to make room for a middle point - for (m = 0; m < ps; ++m) - points[k + ps + m] = points[k + m]; - - // middle point has same y - points[k + 1] = points[k - ps + 1]; - - // we've added a point, better reflect that - k += ps; - } - } - } - } - - // give the hooks a chance to run - for (i = 0; i < series.length; ++i) { - s = series[i]; - - executeHooks(hooks.processDatapoints, [ s, s.datapoints]); - } - - // second pass: find datamax/datamin for auto-scaling - for (i = 0; i < series.length; ++i) { - s = series[i]; - points = s.datapoints.points; - ps = s.datapoints.pointsize; - format = s.datapoints.format; - - var xmin = topSentry, ymin = topSentry, - xmax = bottomSentry, ymax = bottomSentry; - - for (j = 0; j < points.length; j += ps) { - if (points[j] == null) - continue; - - for (m = 0; m < ps; ++m) { - val = points[j + m]; - f = format[m]; - if (!f || f.autoscale === false || val == fakeInfinity || val == -fakeInfinity) - continue; - - if (f.x) { - if (val < xmin) - xmin = val; - if (val > xmax) - xmax = val; - } - if (f.y) { - if (val < ymin) - ymin = val; - if (val > ymax) - ymax = val; - } - } - } - - if (s.bars.show) { - // make sure we got room for the bar on the dancing floor - var delta; - - switch (s.bars.align) { - case "left": - delta = 0; - break; - case "right": - delta = -s.bars.barWidth; - break; - default: - delta = -s.bars.barWidth / 2; - } - - if (s.bars.horizontal) { - ymin += delta; - ymax += delta + s.bars.barWidth; - } - else { - xmin += delta; - xmax += delta + s.bars.barWidth; - } - } - - updateAxis(s.xaxis, xmin, xmax); - updateAxis(s.yaxis, ymin, ymax); - } - - $.each(allAxes(), function (_, axis) { - if (axis.datamin == topSentry) - axis.datamin = null; - if (axis.datamax == bottomSentry) - axis.datamax = null; - }); - } - - function setupCanvases() { - - // Make sure the placeholder is clear of everything except canvases - // from a previous plot in this container that we'll try to re-use. - - placeholder.css("padding", 0) // padding messes up the positioning - .children().filter(function(){ - return !$(this).hasClass("flot-overlay") && !$(this).hasClass('flot-base'); - }).remove(); - - if (placeholder.css("position") == 'static') - placeholder.css("position", "relative"); // for positioning labels and overlay - - surface = new Canvas("flot-base", placeholder); - overlay = new Canvas("flot-overlay", placeholder); // overlay canvas for interactive features - - ctx = surface.context; - octx = overlay.context; - - // define which element we're listening for events on - eventHolder = $(overlay.element).unbind(); - - // If we're re-using a plot object, shut down the old one - - var existing = placeholder.data("plot"); - - if (existing) { - existing.shutdown(); - overlay.clear(); - } - - // save in case we get replotted - placeholder.data("plot", plot); - } - - function bindEvents() { - // bind events - if (options.grid.hoverable) { - eventHolder.mousemove(onMouseMove); - - // Use bind, rather than .mouseleave, because we officially - // still support jQuery 1.2.6, which doesn't define a shortcut - // for mouseenter or mouseleave. This was a bug/oversight that - // was fixed somewhere around 1.3.x. We can return to using - // .mouseleave when we drop support for 1.2.6. - - eventHolder.bind("mouseleave", onMouseLeave); - } - - if (options.grid.clickable) - eventHolder.click(onClick); - - executeHooks(hooks.bindEvents, [eventHolder]); - } - - function shutdown() { - if (redrawTimeout) - clearTimeout(redrawTimeout); - - eventHolder.unbind("mousemove", onMouseMove); - eventHolder.unbind("mouseleave", onMouseLeave); - eventHolder.unbind("click", onClick); - - executeHooks(hooks.shutdown, [eventHolder]); - } - - function setTransformationHelpers(axis) { - // set helper functions on the axis, assumes plot area - // has been computed already - - function identity(x) { return x; } - - var s, m, t = axis.options.transform || identity, - it = axis.options.inverseTransform; - - // precompute how much the axis is scaling a point - // in canvas space - if (axis.direction == "x") { - s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min)); - m = Math.min(t(axis.max), t(axis.min)); - } - else { - s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min)); - s = -s; - m = Math.max(t(axis.max), t(axis.min)); - } - - // data point to canvas coordinate - if (t == identity) // slight optimization - axis.p2c = function (p) { return (p - m) * s; }; - else - axis.p2c = function (p) { return (t(p) - m) * s; }; - // canvas coordinate to data point - if (!it) - axis.c2p = function (c) { return m + c / s; }; - else - axis.c2p = function (c) { return it(m + c / s); }; - } - - function measureTickLabels(axis) { - - var opts = axis.options, - ticks = axis.ticks || [], - labelWidth = opts.labelWidth || 0, - labelHeight = opts.labelHeight || 0, - maxWidth = labelWidth || (axis.direction == "x" ? Math.floor(surface.width / (ticks.length || 1)) : null), - legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", - layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, - font = opts.font || "flot-tick-label tickLabel"; - - for (var i = 0; i < ticks.length; ++i) { - - var t = ticks[i]; - - if (!t.label) - continue; - - var info = surface.getTextInfo(layer, t.label, font, null, maxWidth); - - labelWidth = Math.max(labelWidth, info.width); - labelHeight = Math.max(labelHeight, info.height); - } - - axis.labelWidth = opts.labelWidth || labelWidth; - axis.labelHeight = opts.labelHeight || labelHeight; - } - - function allocateAxisBoxFirstPhase(axis) { - // find the bounding box of the axis by looking at label - // widths/heights and ticks, make room by diminishing the - // plotOffset; this first phase only looks at one - // dimension per axis, the other dimension depends on the - // other axes so will have to wait - - var lw = axis.labelWidth, - lh = axis.labelHeight, - pos = axis.options.position, - isXAxis = axis.direction === "x", - tickLength = axis.options.tickLength, - axisMargin = options.grid.axisMargin, - padding = options.grid.labelMargin, - innermost = true, - outermost = true, - first = true, - found = false; - - // Determine the axis's position in its direction and on its side - - $.each(isXAxis ? xaxes : yaxes, function(i, a) { - if (a && (a.show || a.reserveSpace)) { - if (a === axis) { - found = true; - } else if (a.options.position === pos) { - if (found) { - outermost = false; - } else { - innermost = false; - } - } - if (!found) { - first = false; - } - } - }); - - // The outermost axis on each side has no margin - - if (outermost) { - axisMargin = 0; - } - - // The ticks for the first axis in each direction stretch across - - if (tickLength == null) { - tickLength = first ? "full" : 5; - } - - if (!isNaN(+tickLength)) - padding += +tickLength; - - if (isXAxis) { - lh += padding; - - if (pos == "bottom") { - plotOffset.bottom += lh + axisMargin; - axis.box = { top: surface.height - plotOffset.bottom, height: lh }; - } - else { - axis.box = { top: plotOffset.top + axisMargin, height: lh }; - plotOffset.top += lh + axisMargin; - } - } - else { - lw += padding; - - if (pos == "left") { - axis.box = { left: plotOffset.left + axisMargin, width: lw }; - plotOffset.left += lw + axisMargin; - } - else { - plotOffset.right += lw + axisMargin; - axis.box = { left: surface.width - plotOffset.right, width: lw }; - } - } - - // save for future reference - axis.position = pos; - axis.tickLength = tickLength; - axis.box.padding = padding; - axis.innermost = innermost; - } - - function allocateAxisBoxSecondPhase(axis) { - // now that all axis boxes have been placed in one - // dimension, we can set the remaining dimension coordinates - if (axis.direction == "x") { - axis.box.left = plotOffset.left - axis.labelWidth / 2; - axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.labelWidth; - } - else { - axis.box.top = plotOffset.top - axis.labelHeight / 2; - axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.labelHeight; - } - } - - function adjustLayoutForThingsStickingOut() { - // possibly adjust plot offset to ensure everything stays - // inside the canvas and isn't clipped off - - var minMargin = options.grid.minBorderMargin, - axis, i; - - // check stuff from the plot (FIXME: this should just read - // a value from the series, otherwise it's impossible to - // customize) - if (minMargin == null) { - minMargin = 0; - for (i = 0; i < series.length; ++i) - minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth/2)); - } - - var margins = { - left: minMargin, - right: minMargin, - top: minMargin, - bottom: minMargin - }; - - // check axis labels, note we don't check the actual - // labels but instead use the overall width/height to not - // jump as much around with replots - $.each(allAxes(), function (_, axis) { - if (axis.reserveSpace && axis.ticks && axis.ticks.length) { - if (axis.direction === "x") { - margins.left = Math.max(margins.left, axis.labelWidth / 2); - margins.right = Math.max(margins.right, axis.labelWidth / 2); - } else { - margins.bottom = Math.max(margins.bottom, axis.labelHeight / 2); - margins.top = Math.max(margins.top, axis.labelHeight / 2); - } - } - }); - - plotOffset.left = Math.ceil(Math.max(margins.left, plotOffset.left)); - plotOffset.right = Math.ceil(Math.max(margins.right, plotOffset.right)); - plotOffset.top = Math.ceil(Math.max(margins.top, plotOffset.top)); - plotOffset.bottom = Math.ceil(Math.max(margins.bottom, plotOffset.bottom)); - } - - function setupGrid() { - var i, axes = allAxes(), showGrid = options.grid.show; - - // Initialize the plot's offset from the edge of the canvas - - for (var a in plotOffset) { - var margin = options.grid.margin || 0; - plotOffset[a] = typeof margin == "number" ? margin : margin[a] || 0; - } - - executeHooks(hooks.processOffset, [plotOffset]); - - // If the grid is visible, add its border width to the offset - - for (var a in plotOffset) { - if(typeof(options.grid.borderWidth) == "object") { - plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0; - } - else { - plotOffset[a] += showGrid ? options.grid.borderWidth : 0; - } - } - - $.each(axes, function (_, axis) { - var axisOpts = axis.options; - axis.show = axisOpts.show == null ? axis.used : axisOpts.show; - axis.reserveSpace = axisOpts.reserveSpace == null ? axis.show : axisOpts.reserveSpace; - setRange(axis); - }); - - if (showGrid) { - - var allocatedAxes = $.grep(axes, function (axis) { - return axis.show || axis.reserveSpace; - }); - - $.each(allocatedAxes, function (_, axis) { - // make the ticks - setupTickGeneration(axis); - setTicks(axis); - snapRangeToTicks(axis, axis.ticks); - // find labelWidth/Height for axis - measureTickLabels(axis); - }); - - // with all dimensions calculated, we can compute the - // axis bounding boxes, start from the outside - // (reverse order) - for (i = allocatedAxes.length - 1; i >= 0; --i) - allocateAxisBoxFirstPhase(allocatedAxes[i]); - - // make sure we've got enough space for things that - // might stick out - adjustLayoutForThingsStickingOut(); - - $.each(allocatedAxes, function (_, axis) { - allocateAxisBoxSecondPhase(axis); - }); - } - - plotWidth = surface.width - plotOffset.left - plotOffset.right; - plotHeight = surface.height - plotOffset.bottom - plotOffset.top; - - // now we got the proper plot dimensions, we can compute the scaling - $.each(axes, function (_, axis) { - setTransformationHelpers(axis); - }); - - if (showGrid) { - drawAxisLabels(); - } - - insertLegend(); - } - - function setRange(axis) { - var opts = axis.options, - min = +(opts.min != null ? opts.min : axis.datamin), - max = +(opts.max != null ? opts.max : axis.datamax), - delta = max - min; - - if (delta == 0.0) { - // degenerate case - var widen = max == 0 ? 1 : 0.01; - - if (opts.min == null) - min -= widen; - // always widen max if we couldn't widen min to ensure we - // don't fall into min == max which doesn't work - if (opts.max == null || opts.min != null) - max += widen; - } - else { - // consider autoscaling - var margin = opts.autoscaleMargin; - if (margin != null) { - if (opts.min == null) { - min -= delta * margin; - // make sure we don't go below zero if all values - // are positive - if (min < 0 && axis.datamin != null && axis.datamin >= 0) - min = 0; - } - if (opts.max == null) { - max += delta * margin; - if (max > 0 && axis.datamax != null && axis.datamax <= 0) - max = 0; - } - } - } - axis.min = min; - axis.max = max; - } - - function setupTickGeneration(axis) { - var opts = axis.options; - - // estimate number of ticks - var noTicks; - if (typeof opts.ticks == "number" && opts.ticks > 0) - noTicks = opts.ticks; - else - // heuristic based on the model a*sqrt(x) fitted to - // some data points that seemed reasonable - noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? surface.width : surface.height); - - var delta = (axis.max - axis.min) / noTicks, - dec = -Math.floor(Math.log(delta) / Math.LN10), - maxDec = opts.tickDecimals; - - if (maxDec != null && dec > maxDec) { - dec = maxDec; - } - - var magn = Math.pow(10, -dec), - norm = delta / magn, // norm is between 1.0 and 10.0 - size; - - if (norm < 1.5) { - size = 1; - } else if (norm < 3) { - size = 2; - // special case for 2.5, requires an extra decimal - if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { - size = 2.5; - ++dec; - } - } else if (norm < 7.5) { - size = 5; - } else { - size = 10; - } - - size *= magn; - - if (opts.minTickSize != null && size < opts.minTickSize) { - size = opts.minTickSize; - } - - axis.delta = delta; - axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); - axis.tickSize = opts.tickSize || size; - - // Time mode was moved to a plug-in in 0.8, and since so many people use it - // we'll add an especially friendly reminder to make sure they included it. - - if (opts.mode == "time" && !axis.tickGenerator) { - throw new Error("Time mode requires the flot.time plugin."); - } - - // Flot supports base-10 axes; any other mode else is handled by a plug-in, - // like flot.time.js. - - if (!axis.tickGenerator) { - - axis.tickGenerator = function (axis) { - - var ticks = [], - start = floorInBase(axis.min, axis.tickSize), - i = 0, - v = Number.NaN, - prev; - - do { - prev = v; - v = start + i * axis.tickSize; - ticks.push(v); - ++i; - } while (v < axis.max && v != prev); - return ticks; - }; - - axis.tickFormatter = function (value, axis) { - - var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1; - var formatted = "" + Math.round(value * factor) / factor; - - // If tickDecimals was specified, ensure that we have exactly that - // much precision; otherwise default to the value's own precision. - - if (axis.tickDecimals != null) { - var decimal = formatted.indexOf("."); - var precision = decimal == -1 ? 0 : formatted.length - decimal - 1; - if (precision < axis.tickDecimals) { - return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision); - } - } - - return formatted; - }; - } - - if ($.isFunction(opts.tickFormatter)) - axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); }; - - if (opts.alignTicksWithAxis != null) { - var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1]; - if (otherAxis && otherAxis.used && otherAxis != axis) { - // consider snapping min/max to outermost nice ticks - var niceTicks = axis.tickGenerator(axis); - if (niceTicks.length > 0) { - if (opts.min == null) - axis.min = Math.min(axis.min, niceTicks[0]); - if (opts.max == null && niceTicks.length > 1) - axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]); - } - - axis.tickGenerator = function (axis) { - // copy ticks, scaled to this axis - var ticks = [], v, i; - for (i = 0; i < otherAxis.ticks.length; ++i) { - v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min); - v = axis.min + v * (axis.max - axis.min); - ticks.push(v); - } - return ticks; - }; - - // we might need an extra decimal since forced - // ticks don't necessarily fit naturally - if (!axis.mode && opts.tickDecimals == null) { - var extraDec = Math.max(0, -Math.floor(Math.log(axis.delta) / Math.LN10) + 1), - ts = axis.tickGenerator(axis); - - // only proceed if the tick interval rounded - // with an extra decimal doesn't give us a - // zero at end - if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec)))) - axis.tickDecimals = extraDec; - } - } - } - } - - function setTicks(axis) { - var oticks = axis.options.ticks, ticks = []; - if (oticks == null || (typeof oticks == "number" && oticks > 0)) - ticks = axis.tickGenerator(axis); - else if (oticks) { - if ($.isFunction(oticks)) - // generate the ticks - ticks = oticks(axis); - else - ticks = oticks; - } - - // clean up/labelify the supplied ticks, copy them over - var i, v; - axis.ticks = []; - for (i = 0; i < ticks.length; ++i) { - var label = null; - var t = ticks[i]; - if (typeof t == "object") { - v = +t[0]; - if (t.length > 1) - label = t[1]; - } - else - v = +t; - if (label == null) - label = axis.tickFormatter(v, axis); - if (!isNaN(v)) - axis.ticks.push({ v: v, label: label }); - } - } - - function snapRangeToTicks(axis, ticks) { - if (axis.options.autoscaleMargin && ticks.length > 0) { - // snap to ticks - if (axis.options.min == null) - axis.min = Math.min(axis.min, ticks[0].v); - if (axis.options.max == null && ticks.length > 1) - axis.max = Math.max(axis.max, ticks[ticks.length - 1].v); - } - } - - function draw() { - - surface.clear(); - - executeHooks(hooks.drawBackground, [ctx]); - - var grid = options.grid; - - // draw background, if any - if (grid.show && grid.backgroundColor) - drawBackground(); - - if (grid.show && !grid.aboveData) { - drawGrid(); - } - - for (var i = 0; i < series.length; ++i) { - executeHooks(hooks.drawSeries, [ctx, series[i]]); - drawSeries(series[i]); - } - - executeHooks(hooks.draw, [ctx]); - - if (grid.show && grid.aboveData) { - drawGrid(); - } - - surface.render(); - - // A draw implies that either the axes or data have changed, so we - // should probably update the overlay highlights as well. - - triggerRedrawOverlay(); - } - - function extractRange(ranges, coord) { - var axis, from, to, key, axes = allAxes(); - - for (var i = 0; i < axes.length; ++i) { - axis = axes[i]; - if (axis.direction == coord) { - key = coord + axis.n + "axis"; - if (!ranges[key] && axis.n == 1) - key = coord + "axis"; // support x1axis as xaxis - if (ranges[key]) { - from = ranges[key].from; - to = ranges[key].to; - break; - } - } - } - - // backwards-compat stuff - to be removed in future - if (!ranges[key]) { - axis = coord == "x" ? xaxes[0] : yaxes[0]; - from = ranges[coord + "1"]; - to = ranges[coord + "2"]; - } - - // auto-reverse as an added bonus - if (from != null && to != null && from > to) { - var tmp = from; - from = to; - to = tmp; - } - - return { from: from, to: to, axis: axis }; - } - - function drawBackground() { - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); - ctx.fillRect(0, 0, plotWidth, plotHeight); - ctx.restore(); - } - - function drawGrid() { - var i, axes, bw, bc; - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - // draw markings - var markings = options.grid.markings; - if (markings) { - if ($.isFunction(markings)) { - axes = plot.getAxes(); - // xmin etc. is backwards compatibility, to be - // removed in the future - axes.xmin = axes.xaxis.min; - axes.xmax = axes.xaxis.max; - axes.ymin = axes.yaxis.min; - axes.ymax = axes.yaxis.max; - - markings = markings(axes); - } - - for (i = 0; i < markings.length; ++i) { - var m = markings[i], - xrange = extractRange(m, "x"), - yrange = extractRange(m, "y"); - - // fill in missing - if (xrange.from == null) - xrange.from = xrange.axis.min; - if (xrange.to == null) - xrange.to = xrange.axis.max; - if (yrange.from == null) - yrange.from = yrange.axis.min; - if (yrange.to == null) - yrange.to = yrange.axis.max; - - // clip - if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max || - yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) - continue; - - xrange.from = Math.max(xrange.from, xrange.axis.min); - xrange.to = Math.min(xrange.to, xrange.axis.max); - yrange.from = Math.max(yrange.from, yrange.axis.min); - yrange.to = Math.min(yrange.to, yrange.axis.max); - - var xequal = xrange.from === xrange.to, - yequal = yrange.from === yrange.to; - - if (xequal && yequal) { - continue; - } - - // then draw - xrange.from = Math.floor(xrange.axis.p2c(xrange.from)); - xrange.to = Math.floor(xrange.axis.p2c(xrange.to)); - yrange.from = Math.floor(yrange.axis.p2c(yrange.from)); - yrange.to = Math.floor(yrange.axis.p2c(yrange.to)); - - if (xequal || yequal) { - var lineWidth = m.lineWidth || options.grid.markingsLineWidth, - subPixel = lineWidth % 2 ? 0.5 : 0; - ctx.beginPath(); - ctx.strokeStyle = m.color || options.grid.markingsColor; - ctx.lineWidth = lineWidth; - if (xequal) { - ctx.moveTo(xrange.to + subPixel, yrange.from); - ctx.lineTo(xrange.to + subPixel, yrange.to); - } else { - ctx.moveTo(xrange.from, yrange.to + subPixel); - ctx.lineTo(xrange.to, yrange.to + subPixel); - } - ctx.stroke(); - } else { - ctx.fillStyle = m.color || options.grid.markingsColor; - ctx.fillRect(xrange.from, yrange.to, - xrange.to - xrange.from, - yrange.from - yrange.to); - } - } - } - - // draw the ticks - axes = allAxes(); - bw = options.grid.borderWidth; - - for (var j = 0; j < axes.length; ++j) { - var axis = axes[j], box = axis.box, - t = axis.tickLength, x, y, xoff, yoff; - if (!axis.show || axis.ticks.length == 0) - continue; - - ctx.lineWidth = 1; - - // find the edges - if (axis.direction == "x") { - x = 0; - if (t == "full") - y = (axis.position == "top" ? 0 : plotHeight); - else - y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0); - } - else { - y = 0; - if (t == "full") - x = (axis.position == "left" ? 0 : plotWidth); - else - x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0); - } - - // draw tick bar - if (!axis.innermost) { - ctx.strokeStyle = axis.options.color; - ctx.beginPath(); - xoff = yoff = 0; - if (axis.direction == "x") - xoff = plotWidth + 1; - else - yoff = plotHeight + 1; - - if (ctx.lineWidth == 1) { - if (axis.direction == "x") { - y = Math.floor(y) + 0.5; - } else { - x = Math.floor(x) + 0.5; - } - } - - ctx.moveTo(x, y); - ctx.lineTo(x + xoff, y + yoff); - ctx.stroke(); - } - - // draw ticks - - ctx.strokeStyle = axis.options.tickColor; - - ctx.beginPath(); - for (i = 0; i < axis.ticks.length; ++i) { - var v = axis.ticks[i].v; - - xoff = yoff = 0; - - if (isNaN(v) || v < axis.min || v > axis.max - // skip those lying on the axes if we got a border - || (t == "full" - && ((typeof bw == "object" && bw[axis.position] > 0) || bw > 0) - && (v == axis.min || v == axis.max))) - continue; - - if (axis.direction == "x") { - x = axis.p2c(v); - yoff = t == "full" ? -plotHeight : t; - - if (axis.position == "top") - yoff = -yoff; - } - else { - y = axis.p2c(v); - xoff = t == "full" ? -plotWidth : t; - - if (axis.position == "left") - xoff = -xoff; - } - - if (ctx.lineWidth == 1) { - if (axis.direction == "x") - x = Math.floor(x) + 0.5; - else - y = Math.floor(y) + 0.5; - } - - ctx.moveTo(x, y); - ctx.lineTo(x + xoff, y + yoff); - } - - ctx.stroke(); - } - - - // draw border - if (bw) { - // If either borderWidth or borderColor is an object, then draw the border - // line by line instead of as one rectangle - bc = options.grid.borderColor; - if(typeof bw == "object" || typeof bc == "object") { - if (typeof bw !== "object") { - bw = {top: bw, right: bw, bottom: bw, left: bw}; - } - if (typeof bc !== "object") { - bc = {top: bc, right: bc, bottom: bc, left: bc}; - } - - if (bw.top > 0) { - ctx.strokeStyle = bc.top; - ctx.lineWidth = bw.top; - ctx.beginPath(); - ctx.moveTo(0 - bw.left, 0 - bw.top/2); - ctx.lineTo(plotWidth, 0 - bw.top/2); - ctx.stroke(); - } - - if (bw.right > 0) { - ctx.strokeStyle = bc.right; - ctx.lineWidth = bw.right; - ctx.beginPath(); - ctx.moveTo(plotWidth + bw.right / 2, 0 - bw.top); - ctx.lineTo(plotWidth + bw.right / 2, plotHeight); - ctx.stroke(); - } - - if (bw.bottom > 0) { - ctx.strokeStyle = bc.bottom; - ctx.lineWidth = bw.bottom; - ctx.beginPath(); - ctx.moveTo(plotWidth + bw.right, plotHeight + bw.bottom / 2); - ctx.lineTo(0, plotHeight + bw.bottom / 2); - ctx.stroke(); - } - - if (bw.left > 0) { - ctx.strokeStyle = bc.left; - ctx.lineWidth = bw.left; - ctx.beginPath(); - ctx.moveTo(0 - bw.left/2, plotHeight + bw.bottom); - ctx.lineTo(0- bw.left/2, 0); - ctx.stroke(); - } - } - else { - ctx.lineWidth = bw; - ctx.strokeStyle = options.grid.borderColor; - ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw); - } - } - - ctx.restore(); - } - - function drawAxisLabels() { - - $.each(allAxes(), function (_, axis) { - var box = axis.box, - legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", - layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, - font = axis.options.font || "flot-tick-label tickLabel", - tick, x, y, halign, valign; - - // Remove text before checking for axis.show and ticks.length; - // otherwise plugins, like flot-tickrotor, that draw their own - // tick labels will end up with both theirs and the defaults. - - surface.removeText(layer); - - if (!axis.show || axis.ticks.length == 0) - return; - - for (var i = 0; i < axis.ticks.length; ++i) { - - tick = axis.ticks[i]; - if (!tick.label || tick.v < axis.min || tick.v > axis.max) - continue; - - if (axis.direction == "x") { - halign = "center"; - x = plotOffset.left + axis.p2c(tick.v); - if (axis.position == "bottom") { - y = box.top + box.padding; - } else { - y = box.top + box.height - box.padding; - valign = "bottom"; - } - } else { - valign = "middle"; - y = plotOffset.top + axis.p2c(tick.v); - if (axis.position == "left") { - x = box.left + box.width - box.padding; - halign = "right"; - } else { - x = box.left + box.padding; - } - } - - surface.addText(layer, x, y, tick.label, font, null, null, halign, valign); - } - }); - } - - function drawSeries(series) { - if (series.lines.show) - drawSeriesLines(series); - if (series.bars.show) - drawSeriesBars(series); - if (series.points.show) - drawSeriesPoints(series); - } - - function drawSeriesLines(series) { - function plotLine(datapoints, xoffset, yoffset, axisx, axisy) { - var points = datapoints.points, - ps = datapoints.pointsize, - prevx = null, prevy = null; - - ctx.beginPath(); - for (var i = ps; i < points.length; i += ps) { - var x1 = points[i - ps], y1 = points[i - ps + 1], - x2 = points[i], y2 = points[i + 1]; - - if (x1 == null || x2 == null) - continue; - - // clip with ymin - if (y1 <= y2 && y1 < axisy.min) { - if (y2 < axisy.min) - continue; // line segment is outside - // compute new intersection point - x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.min; - } - else if (y2 <= y1 && y2 < axisy.min) { - if (y1 < axisy.min) - continue; - x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.min; - } - - // clip with ymax - if (y1 >= y2 && y1 > axisy.max) { - if (y2 > axisy.max) - continue; - x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.max; - } - else if (y2 >= y1 && y2 > axisy.max) { - if (y1 > axisy.max) - continue; - x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.max; - } - - // clip with xmin - if (x1 <= x2 && x1 < axisx.min) { - if (x2 < axisx.min) - continue; - y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.min; - } - else if (x2 <= x1 && x2 < axisx.min) { - if (x1 < axisx.min) - continue; - y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.min; - } - - // clip with xmax - if (x1 >= x2 && x1 > axisx.max) { - if (x2 > axisx.max) - continue; - y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.max; - } - else if (x2 >= x1 && x2 > axisx.max) { - if (x1 > axisx.max) - continue; - y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.max; - } - - if (x1 != prevx || y1 != prevy) - ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); - - prevx = x2; - prevy = y2; - ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset); - } - ctx.stroke(); - } - - function plotLineArea(datapoints, axisx, axisy) { - var points = datapoints.points, - ps = datapoints.pointsize, - bottom = Math.min(Math.max(0, axisy.min), axisy.max), - i = 0, top, areaOpen = false, - ypos = 1, segmentStart = 0, segmentEnd = 0; - - // we process each segment in two turns, first forward - // direction to sketch out top, then once we hit the - // end we go backwards to sketch the bottom - while (true) { - if (ps > 0 && i > points.length + ps) - break; - - i += ps; // ps is negative if going backwards - - var x1 = points[i - ps], - y1 = points[i - ps + ypos], - x2 = points[i], y2 = points[i + ypos]; - - if (areaOpen) { - if (ps > 0 && x1 != null && x2 == null) { - // at turning point - segmentEnd = i; - ps = -ps; - ypos = 2; - continue; - } - - if (ps < 0 && i == segmentStart + ps) { - // done with the reverse sweep - ctx.fill(); - areaOpen = false; - ps = -ps; - ypos = 1; - i = segmentStart = segmentEnd + ps; - continue; - } - } - - if (x1 == null || x2 == null) - continue; - - // clip x values - - // clip with xmin - if (x1 <= x2 && x1 < axisx.min) { - if (x2 < axisx.min) - continue; - y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.min; - } - else if (x2 <= x1 && x2 < axisx.min) { - if (x1 < axisx.min) - continue; - y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.min; - } - - // clip with xmax - if (x1 >= x2 && x1 > axisx.max) { - if (x2 > axisx.max) - continue; - y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.max; - } - else if (x2 >= x1 && x2 > axisx.max) { - if (x1 > axisx.max) - continue; - y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.max; - } - - if (!areaOpen) { - // open area - ctx.beginPath(); - ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom)); - areaOpen = true; - } - - // now first check the case where both is outside - if (y1 >= axisy.max && y2 >= axisy.max) { - ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); - continue; - } - else if (y1 <= axisy.min && y2 <= axisy.min) { - ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); - continue; - } - - // else it's a bit more complicated, there might - // be a flat maxed out rectangle first, then a - // triangular cutout or reverse; to find these - // keep track of the current x values - var x1old = x1, x2old = x2; - - // clip the y values, without shortcutting, we - // go through all cases in turn - - // clip with ymin - if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { - x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.min; - } - else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { - x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.min; - } - - // clip with ymax - if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { - x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.max; - } - else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { - x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.max; - } - - // if the x value was changed we got a rectangle - // to fill - if (x1 != x1old) { - ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1)); - // it goes to (x1, y1), but we fill that below - } - - // fill triangular section, this sometimes result - // in redundant points if (x1, y1) hasn't changed - // from previous line to, but we just ignore that - ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); - - // fill the other rectangle if it's there - if (x2 != x2old) { - ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); - ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2)); - } - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - ctx.lineJoin = "round"; - - var lw = series.lines.lineWidth, - sw = series.shadowSize; - // FIXME: consider another form of shadow when filling is turned on - if (lw > 0 && sw > 0) { - // draw shadow as a thick and thin line with transparency - ctx.lineWidth = sw; - ctx.strokeStyle = "rgba(0,0,0,0.1)"; - // position shadow at angle from the mid of line - var angle = Math.PI/18; - plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis); - ctx.lineWidth = sw/2; - plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis); - } - - ctx.lineWidth = lw; - ctx.strokeStyle = series.color; - var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight); - if (fillStyle) { - ctx.fillStyle = fillStyle; - plotLineArea(series.datapoints, series.xaxis, series.yaxis); - } - - if (lw > 0) - plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis); - ctx.restore(); - } - - function drawSeriesPoints(series) { - function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) { - var points = datapoints.points, ps = datapoints.pointsize; - - for (var i = 0; i < points.length; i += ps) { - var x = points[i], y = points[i + 1]; - if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) - continue; - - ctx.beginPath(); - x = axisx.p2c(x); - y = axisy.p2c(y) + offset; - if (symbol == "circle") - ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false); - else - symbol(ctx, x, y, radius, shadow); - ctx.closePath(); - - if (fillStyle) { - ctx.fillStyle = fillStyle; - ctx.fill(); - } - ctx.stroke(); - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - var lw = series.points.lineWidth, - sw = series.shadowSize, - radius = series.points.radius, - symbol = series.points.symbol; - - // If the user sets the line width to 0, we change it to a very - // small value. A line width of 0 seems to force the default of 1. - // Doing the conditional here allows the shadow setting to still be - // optional even with a lineWidth of 0. - - if( lw == 0 ) - lw = 0.0001; - - if (lw > 0 && sw > 0) { - // draw shadow in two steps - var w = sw / 2; - ctx.lineWidth = w; - ctx.strokeStyle = "rgba(0,0,0,0.1)"; - plotPoints(series.datapoints, radius, null, w + w/2, true, - series.xaxis, series.yaxis, symbol); - - ctx.strokeStyle = "rgba(0,0,0,0.2)"; - plotPoints(series.datapoints, radius, null, w/2, true, - series.xaxis, series.yaxis, symbol); - } - - ctx.lineWidth = lw; - ctx.strokeStyle = series.color; - plotPoints(series.datapoints, radius, - getFillStyle(series.points, series.color), 0, false, - series.xaxis, series.yaxis, symbol); - ctx.restore(); - } - - function drawBar(x, y, b, barLeft, barRight, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) { - var left, right, bottom, top, - drawLeft, drawRight, drawTop, drawBottom, - tmp; - - // in horizontal mode, we start the bar from the left - // instead of from the bottom so it appears to be - // horizontal rather than vertical - if (horizontal) { - drawBottom = drawRight = drawTop = true; - drawLeft = false; - left = b; - right = x; - top = y + barLeft; - bottom = y + barRight; - - // account for negative bars - if (right < left) { - tmp = right; - right = left; - left = tmp; - drawLeft = true; - drawRight = false; - } - } - else { - drawLeft = drawRight = drawTop = true; - drawBottom = false; - left = x + barLeft; - right = x + barRight; - bottom = b; - top = y; - - // account for negative bars - if (top < bottom) { - tmp = top; - top = bottom; - bottom = tmp; - drawBottom = true; - drawTop = false; - } - } - - // clip - if (right < axisx.min || left > axisx.max || - top < axisy.min || bottom > axisy.max) - return; - - if (left < axisx.min) { - left = axisx.min; - drawLeft = false; - } - - if (right > axisx.max) { - right = axisx.max; - drawRight = false; - } - - if (bottom < axisy.min) { - bottom = axisy.min; - drawBottom = false; - } - - if (top > axisy.max) { - top = axisy.max; - drawTop = false; - } - - left = axisx.p2c(left); - bottom = axisy.p2c(bottom); - right = axisx.p2c(right); - top = axisy.p2c(top); - - // fill the bar - if (fillStyleCallback) { - c.fillStyle = fillStyleCallback(bottom, top); - c.fillRect(left, top, right - left, bottom - top) - } - - // draw outline - if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) { - c.beginPath(); - - // FIXME: inline moveTo is buggy with excanvas - c.moveTo(left, bottom); - if (drawLeft) - c.lineTo(left, top); - else - c.moveTo(left, top); - if (drawTop) - c.lineTo(right, top); - else - c.moveTo(right, top); - if (drawRight) - c.lineTo(right, bottom); - else - c.moveTo(right, bottom); - if (drawBottom) - c.lineTo(left, bottom); - else - c.moveTo(left, bottom); - c.stroke(); - } - } - - function drawSeriesBars(series) { - function plotBars(datapoints, barLeft, barRight, fillStyleCallback, axisx, axisy) { - var points = datapoints.points, ps = datapoints.pointsize; - - for (var i = 0; i < points.length; i += ps) { - if (points[i] == null) - continue; - drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth); - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - // FIXME: figure out a way to add shadows (for instance along the right edge) - ctx.lineWidth = series.bars.lineWidth; - ctx.strokeStyle = series.color; - - var barLeft; - - switch (series.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -series.bars.barWidth; - break; - default: - barLeft = -series.bars.barWidth / 2; - } - - var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null; - plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, fillStyleCallback, series.xaxis, series.yaxis); - ctx.restore(); - } - - function getFillStyle(filloptions, seriesColor, bottom, top) { - var fill = filloptions.fill; - if (!fill) - return null; - - if (filloptions.fillColor) - return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor); - - var c = $.color.parse(seriesColor); - c.a = typeof fill == "number" ? fill : 0.4; - c.normalize(); - return c.toString(); - } - - function insertLegend() { - - if (options.legend.container != null) { - $(options.legend.container).html(""); - } else { - placeholder.find(".legend").remove(); - } - - if (!options.legend.show) { - return; - } - - var fragments = [], entries = [], rowStarted = false, - lf = options.legend.labelFormatter, s, label; - - // Build a list of legend entries, with each having a label and a color - - for (var i = 0; i < series.length; ++i) { - s = series[i]; - if (s.label) { - label = lf ? lf(s.label, s) : s.label; - if (label) { - entries.push({ - label: label, - color: s.color - }); - } - } - } - - // Sort the legend using either the default or a custom comparator - - if (options.legend.sorted) { - if ($.isFunction(options.legend.sorted)) { - entries.sort(options.legend.sorted); - } else if (options.legend.sorted == "reverse") { - entries.reverse(); - } else { - var ascending = options.legend.sorted != "descending"; - entries.sort(function(a, b) { - return a.label == b.label ? 0 : ( - (a.label < b.label) != ascending ? 1 : -1 // Logical XOR - ); - }); - } - } - - // Generate markup for the list of entries, in their final order - - for (var i = 0; i < entries.length; ++i) { - - var entry = entries[i]; - - if (i % options.legend.noColumns == 0) { - if (rowStarted) - fragments.push(''); - fragments.push(''); - rowStarted = true; - } - - fragments.push( - '
' + - '' + entry.label + '' - ); - } - - if (rowStarted) - fragments.push(''); - - if (fragments.length == 0) - return; - - var table = '' + fragments.join("") + '
'; - if (options.legend.container != null) - $(options.legend.container).html(table); - else { - var pos = "", - p = options.legend.position, - m = options.legend.margin; - if (m[0] == null) - m = [m, m]; - if (p.charAt(0) == "n") - pos += 'top:' + (m[1] + plotOffset.top) + 'px;'; - else if (p.charAt(0) == "s") - pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;'; - if (p.charAt(1) == "e") - pos += 'right:' + (m[0] + plotOffset.right) + 'px;'; - else if (p.charAt(1) == "w") - pos += 'left:' + (m[0] + plotOffset.left) + 'px;'; - var legend = $('
' + table.replace('style="', 'style="position:absolute;' + pos +';') + '
').appendTo(placeholder); - if (options.legend.backgroundOpacity != 0.0) { - // put in the transparent background - // separately to avoid blended labels and - // label boxes - var c = options.legend.backgroundColor; - if (c == null) { - c = options.grid.backgroundColor; - if (c && typeof c == "string") - c = $.color.parse(c); - else - c = $.color.extract(legend, 'background-color'); - c.a = 1; - c = c.toString(); - } - var div = legend.children(); - $('
').prependTo(legend).css('opacity', options.legend.backgroundOpacity); - } - } - } - - - // interactive features - - var highlights = [], - redrawTimeout = null; - - // returns the data item the mouse is over, or null if none is found - function findNearbyItem(mouseX, mouseY, seriesFilter) { - var maxDistance = options.grid.mouseActiveRadius, - smallestDistance = maxDistance * maxDistance + 1, - item = null, foundPoint = false, i, j, ps; - - for (i = series.length - 1; i >= 0; --i) { - if (!seriesFilter(series[i])) - continue; - - var s = series[i], - axisx = s.xaxis, - axisy = s.yaxis, - points = s.datapoints.points, - mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster - my = axisy.c2p(mouseY), - maxx = maxDistance / axisx.scale, - maxy = maxDistance / axisy.scale; - - ps = s.datapoints.pointsize; - // with inverse transforms, we can't use the maxx/maxy - // optimization, sadly - if (axisx.options.inverseTransform) - maxx = Number.MAX_VALUE; - if (axisy.options.inverseTransform) - maxy = Number.MAX_VALUE; - - if (s.lines.show || s.points.show) { - for (j = 0; j < points.length; j += ps) { - var x = points[j], y = points[j + 1]; - if (x == null) - continue; - - // For points and lines, the cursor must be within a - // certain distance to the data point - if (x - mx > maxx || x - mx < -maxx || - y - my > maxy || y - my < -maxy) - continue; - - // We have to calculate distances in pixels, not in - // data units, because the scales of the axes may be different - var dx = Math.abs(axisx.p2c(x) - mouseX), - dy = Math.abs(axisy.p2c(y) - mouseY), - dist = dx * dx + dy * dy; // we save the sqrt - - // use <= to ensure last point takes precedence - // (last generally means on top of) - if (dist < smallestDistance) { - smallestDistance = dist; - item = [i, j / ps]; - } - } - } - - if (s.bars.show && !item) { // no other point can be nearby - - var barLeft, barRight; - - switch (s.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -s.bars.barWidth; - break; - default: - barLeft = -s.bars.barWidth / 2; - } - - barRight = barLeft + s.bars.barWidth; - - for (j = 0; j < points.length; j += ps) { - var x = points[j], y = points[j + 1], b = points[j + 2]; - if (x == null) - continue; - - // for a bar graph, the cursor must be inside the bar - if (series[i].bars.horizontal ? - (mx <= Math.max(b, x) && mx >= Math.min(b, x) && - my >= y + barLeft && my <= y + barRight) : - (mx >= x + barLeft && mx <= x + barRight && - my >= Math.min(b, y) && my <= Math.max(b, y))) - item = [i, j / ps]; - } - } - } - - if (item) { - i = item[0]; - j = item[1]; - ps = series[i].datapoints.pointsize; - - return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps), - dataIndex: j, - series: series[i], - seriesIndex: i }; - } - - return null; - } - - function onMouseMove(e) { - if (options.grid.hoverable) - triggerClickHoverEvent("plothover", e, - function (s) { return s["hoverable"] != false; }); - } - - function onMouseLeave(e) { - if (options.grid.hoverable) - triggerClickHoverEvent("plothover", e, - function (s) { return false; }); - } - - function onClick(e) { - triggerClickHoverEvent("plotclick", e, - function (s) { return s["clickable"] != false; }); - } - - // trigger click or hover event (they send the same parameters - // so we share their code) - function triggerClickHoverEvent(eventname, event, seriesFilter) { - var offset = eventHolder.offset(), - canvasX = event.pageX - offset.left - plotOffset.left, - canvasY = event.pageY - offset.top - plotOffset.top, - pos = canvasToAxisCoords({ left: canvasX, top: canvasY }); - - pos.pageX = event.pageX; - pos.pageY = event.pageY; - - var item = findNearbyItem(canvasX, canvasY, seriesFilter); - - if (item) { - // fill in mouse pos for any listeners out there - item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left, 10); - item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top, 10); - } - - if (options.grid.autoHighlight) { - // clear auto-highlights - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.auto == eventname && - !(item && h.series == item.series && - h.point[0] == item.datapoint[0] && - h.point[1] == item.datapoint[1])) - unhighlight(h.series, h.point); - } - - if (item) - highlight(item.series, item.datapoint, eventname); - } - - placeholder.trigger(eventname, [ pos, item ]); - } - - function triggerRedrawOverlay() { - var t = options.interaction.redrawOverlayInterval; - if (t == -1) { // skip event queue - drawOverlay(); - return; - } - - if (!redrawTimeout) - redrawTimeout = setTimeout(drawOverlay, t); - } - - function drawOverlay() { - redrawTimeout = null; - - // draw highlights - octx.save(); - overlay.clear(); - octx.translate(plotOffset.left, plotOffset.top); - - var i, hi; - for (i = 0; i < highlights.length; ++i) { - hi = highlights[i]; - - if (hi.series.bars.show) - drawBarHighlight(hi.series, hi.point); - else - drawPointHighlight(hi.series, hi.point); - } - octx.restore(); - - executeHooks(hooks.drawOverlay, [octx]); - } - - function highlight(s, point, auto) { - if (typeof s == "number") - s = series[s]; - - if (typeof point == "number") { - var ps = s.datapoints.pointsize; - point = s.datapoints.points.slice(ps * point, ps * (point + 1)); - } - - var i = indexOfHighlight(s, point); - if (i == -1) { - highlights.push({ series: s, point: point, auto: auto }); - - triggerRedrawOverlay(); - } - else if (!auto) - highlights[i].auto = false; - } - - function unhighlight(s, point) { - if (s == null && point == null) { - highlights = []; - triggerRedrawOverlay(); - return; - } - - if (typeof s == "number") - s = series[s]; - - if (typeof point == "number") { - var ps = s.datapoints.pointsize; - point = s.datapoints.points.slice(ps * point, ps * (point + 1)); - } - - var i = indexOfHighlight(s, point); - if (i != -1) { - highlights.splice(i, 1); - - triggerRedrawOverlay(); - } - } - - function indexOfHighlight(s, p) { - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.series == s && h.point[0] == p[0] - && h.point[1] == p[1]) - return i; - } - return -1; - } - - function drawPointHighlight(series, point) { - var x = point[0], y = point[1], - axisx = series.xaxis, axisy = series.yaxis, - highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(); - - if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) - return; - - var pointRadius = series.points.radius + series.points.lineWidth / 2; - octx.lineWidth = pointRadius; - octx.strokeStyle = highlightColor; - var radius = 1.5 * pointRadius; - x = axisx.p2c(x); - y = axisy.p2c(y); - - octx.beginPath(); - if (series.points.symbol == "circle") - octx.arc(x, y, radius, 0, 2 * Math.PI, false); - else - series.points.symbol(octx, x, y, radius, false); - octx.closePath(); - octx.stroke(); - } - - function drawBarHighlight(series, point) { - var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(), - fillStyle = highlightColor, - barLeft; - - switch (series.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -series.bars.barWidth; - break; - default: - barLeft = -series.bars.barWidth / 2; - } - - octx.lineWidth = series.bars.lineWidth; - octx.strokeStyle = highlightColor; - - drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth, - function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth); - } - - function getColorOrGradient(spec, bottom, top, defaultColor) { - if (typeof spec == "string") - return spec; - else { - // assume this is a gradient spec; IE currently only - // supports a simple vertical gradient properly, so that's - // what we support too - var gradient = ctx.createLinearGradient(0, top, 0, bottom); - - for (var i = 0, l = spec.colors.length; i < l; ++i) { - var c = spec.colors[i]; - if (typeof c != "string") { - var co = $.color.parse(defaultColor); - if (c.brightness != null) - co = co.scale('rgb', c.brightness); - if (c.opacity != null) - co.a *= c.opacity; - c = co.toString(); - } - gradient.addColorStop(i / (l - 1), c); - } - - return gradient; - } - } - } - - // Add the plot function to the top level of the jQuery object - - $.plot = function(placeholder, data, options) { - //var t0 = new Date(); - var plot = new Plot($(placeholder), data, options, $.plot.plugins); - //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime())); - return plot; - }; - - $.plot.version = "0.8.3"; - - $.plot.plugins = []; - - // Also add the plot function as a chainable property - - $.fn.plot = function(data, options) { - return this.each(function() { - $.plot(this, data, options); - }); - }; - - // round to nearby lower multiple of base - function floorInBase(n, base) { - return base * Math.floor(n / base); - } - -})(jQuery); diff --git a/src/plugins/timelion/public/flot/jquery.flot.time.js b/src/plugins/timelion/public/flot/jquery.flot.time.js deleted file mode 100644 index 34c1d121259a2..0000000000000 --- a/src/plugins/timelion/public/flot/jquery.flot.time.js +++ /dev/null @@ -1,432 +0,0 @@ -/* Pretty handling of time axes. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -Set axis.mode to "time" to enable. See the section "Time series data" in -API.txt for details. - -*/ - -(function($) { - - var options = { - xaxis: { - timezone: null, // "browser" for local to the client or timezone for timezone-js - timeformat: null, // format string to use - twelveHourClock: false, // 12 or 24 time in time mode - monthNames: null // list of names of months - } - }; - - // round to nearby lower multiple of base - - function floorInBase(n, base) { - return base * Math.floor(n / base); - } - - // Returns a string with the date d formatted according to fmt. - // A subset of the Open Group's strftime format is supported. - - function formatDate(d, fmt, monthNames, dayNames) { - - if (typeof d.strftime == "function") { - return d.strftime(fmt); - } - - var leftPad = function(n, pad) { - n = "" + n; - pad = "" + (pad == null ? "0" : pad); - return n.length == 1 ? pad + n : n; - }; - - var r = []; - var escape = false; - var hours = d.getHours(); - var isAM = hours < 12; - - if (monthNames == null) { - monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; - } - - if (dayNames == null) { - dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; - } - - var hours12; - - if (hours > 12) { - hours12 = hours - 12; - } else if (hours == 0) { - hours12 = 12; - } else { - hours12 = hours; - } - - for (var i = 0; i < fmt.length; ++i) { - - var c = fmt.charAt(i); - - if (escape) { - switch (c) { - case 'a': c = "" + dayNames[d.getDay()]; break; - case 'b': c = "" + monthNames[d.getMonth()]; break; - case 'd': c = leftPad(d.getDate()); break; - case 'e': c = leftPad(d.getDate(), " "); break; - case 'h': // For back-compat with 0.7; remove in 1.0 - case 'H': c = leftPad(hours); break; - case 'I': c = leftPad(hours12); break; - case 'l': c = leftPad(hours12, " "); break; - case 'm': c = leftPad(d.getMonth() + 1); break; - case 'M': c = leftPad(d.getMinutes()); break; - // quarters not in Open Group's strftime specification - case 'q': - c = "" + (Math.floor(d.getMonth() / 3) + 1); break; - case 'S': c = leftPad(d.getSeconds()); break; - case 'y': c = leftPad(d.getFullYear() % 100); break; - case 'Y': c = "" + d.getFullYear(); break; - case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; - case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; - case 'w': c = "" + d.getDay(); break; - } - r.push(c); - escape = false; - } else { - if (c == "%") { - escape = true; - } else { - r.push(c); - } - } - } - - return r.join(""); - } - - // To have a consistent view of time-based data independent of which time - // zone the client happens to be in we need a date-like object independent - // of time zones. This is done through a wrapper that only calls the UTC - // versions of the accessor methods. - - function makeUtcWrapper(d) { - - function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) { - sourceObj[sourceMethod] = function() { - return targetObj[targetMethod].apply(targetObj, arguments); - }; - }; - - var utc = { - date: d - }; - - // support strftime, if found - - if (d.strftime != undefined) { - addProxyMethod(utc, "strftime", d, "strftime"); - } - - addProxyMethod(utc, "getTime", d, "getTime"); - addProxyMethod(utc, "setTime", d, "setTime"); - - var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"]; - - for (var p = 0; p < props.length; p++) { - addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]); - addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]); - } - - return utc; - }; - - // select time zone strategy. This returns a date-like object tied to the - // desired timezone - - function dateGenerator(ts, opts) { - if (opts.timezone == "browser") { - return new Date(ts); - } else if (!opts.timezone || opts.timezone == "utc") { - return makeUtcWrapper(new Date(ts)); - } else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") { - var d = new timezoneJS.Date(); - // timezone-js is fickle, so be sure to set the time zone before - // setting the time. - d.setTimezone(opts.timezone); - d.setTime(ts); - return d; - } else { - return makeUtcWrapper(new Date(ts)); - } - } - - // map of app. size of time units in milliseconds - - var timeUnitSize = { - "second": 1000, - "minute": 60 * 1000, - "hour": 60 * 60 * 1000, - "day": 24 * 60 * 60 * 1000, - "month": 30 * 24 * 60 * 60 * 1000, - "quarter": 3 * 30 * 24 * 60 * 60 * 1000, - "year": 365.2425 * 24 * 60 * 60 * 1000 - }; - - // the allowed tick sizes, after 1 year we use - // an integer algorithm - - var baseSpec = [ - [1, "second"], [2, "second"], [5, "second"], [10, "second"], - [30, "second"], - [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], - [30, "minute"], - [1, "hour"], [2, "hour"], [4, "hour"], - [8, "hour"], [12, "hour"], - [1, "day"], [2, "day"], [3, "day"], - [0.25, "month"], [0.5, "month"], [1, "month"], - [2, "month"] - ]; - - // we don't know which variant(s) we'll need yet, but generating both is - // cheap - - var specMonths = baseSpec.concat([[3, "month"], [6, "month"], - [1, "year"]]); - var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"], - [1, "year"]]); - - function init(plot) { - plot.hooks.processOptions.push(function (plot, options) { - $.each(plot.getAxes(), function(axisName, axis) { - - var opts = axis.options; - - if (opts.mode == "time") { - axis.tickGenerator = function(axis) { - - var ticks = []; - var d = dateGenerator(axis.min, opts); - var minSize = 0; - - // make quarter use a possibility if quarters are - // mentioned in either of these options - - var spec = (opts.tickSize && opts.tickSize[1] === - "quarter") || - (opts.minTickSize && opts.minTickSize[1] === - "quarter") ? specQuarters : specMonths; - - if (opts.minTickSize != null) { - if (typeof opts.tickSize == "number") { - minSize = opts.tickSize; - } else { - minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; - } - } - - for (var i = 0; i < spec.length - 1; ++i) { - if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]] - + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 - && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) { - break; - } - } - - var size = spec[i][0]; - var unit = spec[i][1]; - - // special-case the possibility of several years - - if (unit == "year") { - - // if given a minTickSize in years, just use it, - // ensuring that it's an integer - - if (opts.minTickSize != null && opts.minTickSize[1] == "year") { - size = Math.floor(opts.minTickSize[0]); - } else { - - var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10)); - var norm = (axis.delta / timeUnitSize.year) / magn; - - if (norm < 1.5) { - size = 1; - } else if (norm < 3) { - size = 2; - } else if (norm < 7.5) { - size = 5; - } else { - size = 10; - } - - size *= magn; - } - - // minimum size for years is 1 - - if (size < 1) { - size = 1; - } - } - - axis.tickSize = opts.tickSize || [size, unit]; - var tickSize = axis.tickSize[0]; - unit = axis.tickSize[1]; - - var step = tickSize * timeUnitSize[unit]; - - if (unit == "second") { - d.setSeconds(floorInBase(d.getSeconds(), tickSize)); - } else if (unit == "minute") { - d.setMinutes(floorInBase(d.getMinutes(), tickSize)); - } else if (unit == "hour") { - d.setHours(floorInBase(d.getHours(), tickSize)); - } else if (unit == "month") { - d.setMonth(floorInBase(d.getMonth(), tickSize)); - } else if (unit == "quarter") { - d.setMonth(3 * floorInBase(d.getMonth() / 3, - tickSize)); - } else if (unit == "year") { - d.setFullYear(floorInBase(d.getFullYear(), tickSize)); - } - - // reset smaller components - - d.setMilliseconds(0); - - if (step >= timeUnitSize.minute) { - d.setSeconds(0); - } - if (step >= timeUnitSize.hour) { - d.setMinutes(0); - } - if (step >= timeUnitSize.day) { - d.setHours(0); - } - if (step >= timeUnitSize.day * 4) { - d.setDate(1); - } - if (step >= timeUnitSize.month * 2) { - d.setMonth(floorInBase(d.getMonth(), 3)); - } - if (step >= timeUnitSize.quarter * 2) { - d.setMonth(floorInBase(d.getMonth(), 6)); - } - if (step >= timeUnitSize.year) { - d.setMonth(0); - } - - var carry = 0; - var v = Number.NaN; - var prev; - - do { - - prev = v; - v = d.getTime(); - ticks.push(v); - - if (unit == "month" || unit == "quarter") { - if (tickSize < 1) { - - // a bit complicated - we'll divide the - // month/quarter up but we need to take - // care of fractions so we don't end up in - // the middle of a day - - d.setDate(1); - var start = d.getTime(); - d.setMonth(d.getMonth() + - (unit == "quarter" ? 3 : 1)); - var end = d.getTime(); - d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); - carry = d.getHours(); - d.setHours(0); - } else { - d.setMonth(d.getMonth() + - tickSize * (unit == "quarter" ? 3 : 1)); - } - } else if (unit == "year") { - d.setFullYear(d.getFullYear() + tickSize); - } else { - d.setTime(v + step); - } - } while (v < axis.max && v != prev); - - return ticks; - }; - - axis.tickFormatter = function (v, axis) { - - var d = dateGenerator(v, axis.options); - - // first check global format - - if (opts.timeformat != null) { - return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames); - } - - // possibly use quarters if quarters are mentioned in - // any of these places - - var useQuarters = (axis.options.tickSize && - axis.options.tickSize[1] == "quarter") || - (axis.options.minTickSize && - axis.options.minTickSize[1] == "quarter"); - - var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; - var span = axis.max - axis.min; - var suffix = (opts.twelveHourClock) ? " %p" : ""; - var hourCode = (opts.twelveHourClock) ? "%I" : "%H"; - var fmt; - - if (t < timeUnitSize.minute) { - fmt = hourCode + ":%M:%S" + suffix; - } else if (t < timeUnitSize.day) { - if (span < 2 * timeUnitSize.day) { - fmt = hourCode + ":%M" + suffix; - } else { - fmt = "%b %d " + hourCode + ":%M" + suffix; - } - } else if (t < timeUnitSize.month) { - fmt = "%b %d"; - } else if ((useQuarters && t < timeUnitSize.quarter) || - (!useQuarters && t < timeUnitSize.year)) { - if (span < timeUnitSize.year) { - fmt = "%b"; - } else { - fmt = "%b %Y"; - } - } else if (useQuarters && t < timeUnitSize.year) { - if (span < timeUnitSize.year) { - fmt = "Q%q"; - } else { - fmt = "Q%q %Y"; - } - } else { - fmt = "%Y"; - } - - var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames); - - return rt; - }; - } - }); - }); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'time', - version: '1.0' - }); - - // Time-axis support used to be in Flot core, which exposed the - // formatDate function on the plot object. Various plugins depend - // on the function, so we need to re-expose it here. - - $.plot.formatDate = formatDate; - $.plot.dateGenerator = dateGenerator; - -})(jQuery); diff --git a/src/plugins/timelion/public/panels/timechart/schema.ts b/src/plugins/timelion/public/panels/timechart/schema.ts index b56d8a66110c2..d874f0d32c9d4 100644 --- a/src/plugins/timelion/public/panels/timechart/schema.ts +++ b/src/plugins/timelion/public/panels/timechart/schema.ts @@ -17,7 +17,6 @@ * under the License. */ -import '../../flot'; import _ from 'lodash'; import $ from 'jquery'; import moment from 'moment-timezone'; diff --git a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx index 1fdddfc272e94..c7efb6dad326d 100644 --- a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx +++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx @@ -201,8 +201,10 @@ export async function buildContextMenuForActions({ for (const panel of Object.values(panels)) { if (panel._level === 0) { - // TODO: Add separator line here once it is available in EUI. - // See https://github.com/elastic/eui/pull/4018 + panels.mainMenu.items.push({ + isSeparator: true, + key: panel.id + '__separator', + }); if (panel.items.length > 3) { panels.mainMenu.items.push({ name: panel.title || panel.id, diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md index 1d706843b8b4e..430241cbe0a05 100644 --- a/src/plugins/usage_collection/README.md +++ b/src/plugins/usage_collection/README.md @@ -64,7 +64,7 @@ All you need to provide is a `type` for organizing your fields, `schema` field t }, fetch: async (collectorFetchContext: CollectorFetchContext) => { - // query ES and get some data + // query ES or saved objects and get some data // summarize the data into a model // return the modeled object that includes whatever you want to track @@ -85,9 +85,11 @@ Some background: - `MY_USAGE_TYPE` can be any string. It usually matches the plugin name. As a safety mechanism, we double check there are no duplicates at the moment of registering the collector. - The `fetch` method needs to support multiple contexts in which it is called. For example, when stats are pulled from a Kibana Metricbeat module, the Beat calls Kibana's stats API to invoke usage collection. -In this case, the `fetch` method is called as a result of an HTTP API request and `callCluster` wraps `callWithRequest` or `esClient` wraps `asCurrentUser`, where the request headers are expected to have read privilege on the entire `.kibana' index. +In this case, the `fetch` method is called as a result of an HTTP API request and `callCluster` wraps `callWithRequest` or `esClient` wraps `asCurrentUser`, where the request headers are expected to have read privilege on the entire `.kibana' index. The `fetch` method also exposes the saved objects client that will have the correct scope when the collectors' `fetch` method is called. -Note: there will be many cases where you won't need to use the `callCluster` (or `esClient`) function that gets passed in to your `fetch` method at all. Your feature might have an accumulating value in server memory, or read something from the OS, or use other clients like a custom SavedObjects client. In that case it's up to the plugin to initialize those clients like the example below: +Note: there will be many cases where you won't need to use the `callCluster`, `esClient` or `soClient` function that gets passed in to your `fetch` method at all. Your feature might have an accumulating value in server memory, or read something from the OS. + +In the case of using a custom SavedObjects client, it is up to the plugin to initialize the client to save the data and it is strongly recommended to scope that client to the `kibana_system` user. ```ts // server/plugin.ts @@ -98,7 +100,7 @@ class Plugin { private savedObjectsRepository?: ISavedObjectsRepository; public setup(core: CoreSetup, plugins: { usageCollection?: UsageCollectionSetup }) { - registerMyPluginUsageCollector(() => this.savedObjectsRepository, plugins.usageCollection); + registerMyPluginUsageCollector(plugins.usageCollection); } public start(core: CoreStart) { diff --git a/src/plugins/usage_collection/server/collector/collector.ts b/src/plugins/usage_collection/server/collector/collector.ts index 6608849ae8b99..11a709c037783 100644 --- a/src/plugins/usage_collection/server/collector/collector.ts +++ b/src/plugins/usage_collection/server/collector/collector.ts @@ -17,7 +17,13 @@ * under the License. */ -import { Logger, LegacyAPICaller, ElasticsearchClient } from 'kibana/server'; +import { + Logger, + LegacyAPICaller, + ElasticsearchClient, + ISavedObjectsRepository, + SavedObjectsClientContract, +} from 'kibana/server'; export type CollectorFormatForBulkUpload = (result: T) => { type: string; payload: U }; @@ -56,7 +62,14 @@ export interface CollectorFetchContext { * - When building the telemetry data payload to report to the remote cluster, the requests are scoped to the `kibana` internal user */ esClient: ElasticsearchClient; + /** + * Request-scoped Saved Objects client: + * - When users are requesting a sample of data, it is scoped to their role to avoid exposing data they should't read + * - When building the telemetry data payload to report to the remote cluster, the requests are scoped to the `kibana` internal user + */ + soClient: SavedObjectsClientContract | ISavedObjectsRepository; } + export interface CollectorOptions { type: string; init?: Function; diff --git a/src/plugins/usage_collection/server/collector/collector_set.test.ts b/src/plugins/usage_collection/server/collector/collector_set.test.ts index c8114269f30e4..45a3437777c5f 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.test.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.test.ts @@ -21,7 +21,11 @@ import { noop } from 'lodash'; import { Collector } from './collector'; import { CollectorSet } from './collector_set'; import { UsageCollector } from './usage_collector'; -import { elasticsearchServiceMock, loggingSystemMock } from '../../../../core/server/mocks'; +import { + elasticsearchServiceMock, + loggingSystemMock, + savedObjectsRepositoryMock, +} from '../../../../core/server/mocks'; const logger = loggingSystemMock.createLogger(); @@ -40,9 +44,9 @@ describe('CollectorSet', () => { loggerSpies.debug.mockRestore(); loggerSpies.warn.mockRestore(); }); - const mockCallCluster = jest.fn().mockResolvedValue({ passTest: 1000 }); const mockEsClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const mockSoClient = savedObjectsRepositoryMock.create(); it('should throw an error if non-Collector type of object is registered', () => { const collectors = new CollectorSet({ logger }); @@ -88,7 +92,7 @@ describe('CollectorSet', () => { }) ); - const result = await collectors.bulkFetch(mockCallCluster, mockEsClient); + const result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient); expect(loggerSpies.debug).toHaveBeenCalledTimes(1); expect(loggerSpies.debug).toHaveBeenCalledWith( 'Fetching data from MY_TEST_COLLECTOR collector' @@ -113,7 +117,7 @@ describe('CollectorSet', () => { let result; try { - result = await collectors.bulkFetch(mockCallCluster, mockEsClient); + result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient); } catch (err) { // Do nothing } @@ -131,7 +135,7 @@ describe('CollectorSet', () => { }) ); - const result = await collectors.bulkFetch(mockCallCluster, mockEsClient); + const result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient); expect(result).toStrictEqual([ { type: 'MY_TEST_COLLECTOR', @@ -149,7 +153,7 @@ describe('CollectorSet', () => { } as any) ); - const result = await collectors.bulkFetch(mockCallCluster, mockEsClient); + const result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient); expect(result).toStrictEqual([ { type: 'MY_TEST_COLLECTOR', @@ -172,7 +176,7 @@ describe('CollectorSet', () => { }) ); - const result = await collectors.bulkFetch(mockCallCluster, mockEsClient); + const result = await collectors.bulkFetch(mockCallCluster, mockEsClient, mockSoClient); expect(result).toStrictEqual([ { type: 'MY_TEST_COLLECTOR', diff --git a/src/plugins/usage_collection/server/collector/collector_set.ts b/src/plugins/usage_collection/server/collector/collector_set.ts index 88204c61bfd8d..4e64cbc1bf30f 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.ts @@ -18,7 +18,13 @@ */ import { snakeCase } from 'lodash'; -import { Logger, LegacyAPICaller, ElasticsearchClient } from 'kibana/server'; +import { + Logger, + LegacyAPICaller, + ElasticsearchClient, + ISavedObjectsRepository, + SavedObjectsClientContract, +} from 'kibana/server'; import { Collector, CollectorOptions } from './collector'; import { UsageCollector } from './usage_collector'; @@ -125,6 +131,7 @@ export class CollectorSet { public bulkFetch = async ( callCluster: LegacyAPICaller, esClient: ElasticsearchClient, + soClient: SavedObjectsClientContract | ISavedObjectsRepository, collectors: Map> = this.collectors ) => { const responses = await Promise.all( @@ -133,7 +140,7 @@ export class CollectorSet { try { return { type: collector.type, - result: await collector.fetch({ callCluster, esClient }), + result: await collector.fetch({ callCluster, esClient, soClient }), }; } catch (err) { this.logger.warn(err); @@ -155,9 +162,18 @@ export class CollectorSet { return this.makeCollectorSetFromArray(filtered); }; - public bulkFetchUsage = async (callCluster: LegacyAPICaller, esClient: ElasticsearchClient) => { + public bulkFetchUsage = async ( + callCluster: LegacyAPICaller, + esClient: ElasticsearchClient, + savedObjectsClient: SavedObjectsClientContract | ISavedObjectsRepository + ) => { const usageCollectors = this.getFilteredCollectorSet((c) => c instanceof UsageCollector); - return await this.bulkFetch(callCluster, esClient, usageCollectors.collectors); + return await this.bulkFetch( + callCluster, + esClient, + savedObjectsClient, + usageCollectors.collectors + ); }; // convert an array of fetched stats results into key/object diff --git a/src/plugins/usage_collection/server/routes/stats/stats.ts b/src/plugins/usage_collection/server/routes/stats/stats.ts index bee25fef669f1..ef64d15fabc2d 100644 --- a/src/plugins/usage_collection/server/routes/stats/stats.ts +++ b/src/plugins/usage_collection/server/routes/stats/stats.ts @@ -26,8 +26,10 @@ import { first } from 'rxjs/operators'; import { ElasticsearchClient, IRouter, + ISavedObjectsRepository, LegacyAPICaller, MetricsServiceSetup, + SavedObjectsClientContract, ServiceStatus, ServiceStatusLevels, } from '../../../../../core/server'; @@ -64,9 +66,10 @@ export function registerStatsRoute({ }) { const getUsage = async ( callCluster: LegacyAPICaller, - esClient: ElasticsearchClient + esClient: ElasticsearchClient, + savedObjectsClient: SavedObjectsClientContract | ISavedObjectsRepository ): Promise => { - const usage = await collectorSet.bulkFetchUsage(callCluster, esClient); + const usage = await collectorSet.bulkFetchUsage(callCluster, esClient, savedObjectsClient); return collectorSet.toObject(usage); }; @@ -101,6 +104,7 @@ export function registerStatsRoute({ if (isExtended) { const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; const esClient = context.core.elasticsearch.client.asCurrentUser; + const savedObjectsClient = context.core.savedObjects.client; if (shouldGetUsage) { const collectorsReady = await collectorSet.areAllCollectorsReady(); @@ -109,7 +113,9 @@ export function registerStatsRoute({ } } - const usagePromise = shouldGetUsage ? getUsage(callCluster, esClient) : Promise.resolve({}); + const usagePromise = shouldGetUsage + ? getUsage(callCluster, esClient, savedObjectsClient) + : Promise.resolve({}); const [usage, clusterUuid] = await Promise.all([usagePromise, getClusterUuid(callCluster)]); let modifiedUsage = usage; diff --git a/src/plugins/usage_collection/server/usage_collection.mock.ts b/src/plugins/usage_collection/server/usage_collection.mock.ts index 4e5e84200d65e..c31756c60e32d 100644 --- a/src/plugins/usage_collection/server/usage_collection.mock.ts +++ b/src/plugins/usage_collection/server/usage_collection.mock.ts @@ -17,7 +17,10 @@ * under the License. */ -import { elasticsearchServiceMock } from '../../../../src/core/server/mocks'; +import { + elasticsearchServiceMock, + savedObjectsRepositoryMock, +} from '../../../../src/core/server/mocks'; import { CollectorOptions } from './collector/collector'; import { UsageCollectionSetup, CollectorFetchContext } from './index'; @@ -52,6 +55,7 @@ export function createCollectorFetchContextMock(): jest.Mocked = { callCluster: elasticsearchServiceMock.createLegacyClusterClient().callAsInternalUser, esClient: elasticsearchServiceMock.createClusterClient().asInternalUser, + soClient: savedObjectsRepositoryMock.create(), }; return collectorFetchClientsMock; } diff --git a/src/plugins/vis_default_editor/public/default_editor.tsx b/src/plugins/vis_default_editor/public/default_editor.tsx index ed94e52ee2399..a7251acfdf75d 100644 --- a/src/plugins/vis_default_editor/public/default_editor.tsx +++ b/src/plugins/vis_default_editor/public/default_editor.tsx @@ -18,6 +18,7 @@ */ import './index.scss'; +import 'brace/mode/json'; import React, { useEffect, useRef, useState, useCallback } from 'react'; import { EventEmitter } from 'events'; diff --git a/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx b/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx index a7b623ac8680c..953ec5e819f44 100644 --- a/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx +++ b/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx @@ -25,7 +25,6 @@ import { useResizeObserver } from '@elastic/eui'; import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; import { useKibana } from '../../../kibana_react/public'; -import '../flot'; import { DEFAULT_TIME_FORMAT } from '../../common/lib'; import { diff --git a/src/plugins/vis_type_timelion/public/flot/jquery.flot.axislabels.js b/src/plugins/vis_type_timelion/public/flot/jquery.flot.axislabels.js deleted file mode 100644 index cda8038953c76..0000000000000 --- a/src/plugins/vis_type_timelion/public/flot/jquery.flot.axislabels.js +++ /dev/null @@ -1,462 +0,0 @@ -/* -Axis Labels Plugin for flot. -http://github.com/markrcote/flot-axislabels -Original code is Copyright (c) 2010 Xuan Luo. -Original code was released under the GPLv3 license by Xuan Luo, September 2010. -Original code was rereleased under the MIT license by Xuan Luo, April 2012. -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. - */ - -(function ($) { - var options = { - axisLabels: { - show: true - } - }; - - function canvasSupported() { - return !!document.createElement('canvas').getContext; - } - - function canvasTextSupported() { - if (!canvasSupported()) { - return false; - } - var dummy_canvas = document.createElement('canvas'); - var context = dummy_canvas.getContext('2d'); - return typeof context.fillText == 'function'; - } - - function css3TransitionSupported() { - var div = document.createElement('div'); - return typeof div.style.MozTransition != 'undefined' // Gecko - || typeof div.style.OTransition != 'undefined' // Opera - || typeof div.style.webkitTransition != 'undefined' // WebKit - || typeof div.style.transition != 'undefined'; - } - - - function AxisLabel(axisName, position, padding, plot, opts) { - this.axisName = axisName; - this.position = position; - this.padding = padding; - this.plot = plot; - this.opts = opts; - this.width = 0; - this.height = 0; - } - - AxisLabel.prototype.cleanup = function() { - }; - - - CanvasAxisLabel.prototype = new AxisLabel(); - CanvasAxisLabel.prototype.constructor = CanvasAxisLabel; - function CanvasAxisLabel(axisName, position, padding, plot, opts) { - AxisLabel.prototype.constructor.call(this, axisName, position, padding, - plot, opts); - } - - CanvasAxisLabel.prototype.calculateSize = function() { - if (!this.opts.axisLabelFontSizePixels) - this.opts.axisLabelFontSizePixels = 14; - if (!this.opts.axisLabelFontFamily) - this.opts.axisLabelFontFamily = 'sans-serif'; - - var textWidth = this.opts.axisLabelFontSizePixels + this.padding; - var textHeight = this.opts.axisLabelFontSizePixels + this.padding; - if (this.position == 'left' || this.position == 'right') { - this.width = this.opts.axisLabelFontSizePixels + this.padding; - this.height = 0; - } else { - this.width = 0; - this.height = this.opts.axisLabelFontSizePixels + this.padding; - } - }; - - CanvasAxisLabel.prototype.draw = function(box) { - if (!this.opts.axisLabelColour) - this.opts.axisLabelColour = 'black'; - var ctx = this.plot.getCanvas().getContext('2d'); - ctx.save(); - ctx.font = this.opts.axisLabelFontSizePixels + 'px ' + - this.opts.axisLabelFontFamily; - ctx.fillStyle = this.opts.axisLabelColour; - var width = ctx.measureText(this.opts.axisLabel).width; - var height = this.opts.axisLabelFontSizePixels; - var x, y, angle = 0; - if (this.position == 'top') { - x = box.left + box.width/2 - width/2; - y = box.top + height*0.72; - } else if (this.position == 'bottom') { - x = box.left + box.width/2 - width/2; - y = box.top + box.height - height*0.72; - } else if (this.position == 'left') { - x = box.left + height*0.72; - y = box.height/2 + box.top + width/2; - angle = -Math.PI/2; - } else if (this.position == 'right') { - x = box.left + box.width - height*0.72; - y = box.height/2 + box.top - width/2; - angle = Math.PI/2; - } - ctx.translate(x, y); - ctx.rotate(angle); - ctx.fillText(this.opts.axisLabel, 0, 0); - ctx.restore(); - }; - - - HtmlAxisLabel.prototype = new AxisLabel(); - HtmlAxisLabel.prototype.constructor = HtmlAxisLabel; - function HtmlAxisLabel(axisName, position, padding, plot, opts) { - AxisLabel.prototype.constructor.call(this, axisName, position, - padding, plot, opts); - this.elem = null; - } - - HtmlAxisLabel.prototype.calculateSize = function() { - var elem = $('
' + - this.opts.axisLabel + '
'); - this.plot.getPlaceholder().append(elem); - // store height and width of label itself, for use in draw() - this.labelWidth = elem.outerWidth(true); - this.labelHeight = elem.outerHeight(true); - elem.remove(); - - this.width = this.height = 0; - if (this.position == 'left' || this.position == 'right') { - this.width = this.labelWidth + this.padding; - } else { - this.height = this.labelHeight + this.padding; - } - }; - - HtmlAxisLabel.prototype.cleanup = function() { - if (this.elem) { - this.elem.remove(); - } - }; - - HtmlAxisLabel.prototype.draw = function(box) { - this.plot.getPlaceholder().find('#' + this.axisName + 'Label').remove(); - this.elem = $('
' - + this.opts.axisLabel + '
'); - this.plot.getPlaceholder().append(this.elem); - if (this.position == 'top') { - this.elem.css('left', box.left + box.width/2 - this.labelWidth/2 + - 'px'); - this.elem.css('top', box.top + 'px'); - } else if (this.position == 'bottom') { - this.elem.css('left', box.left + box.width/2 - this.labelWidth/2 + - 'px'); - this.elem.css('top', box.top + box.height - this.labelHeight + - 'px'); - } else if (this.position == 'left') { - this.elem.css('top', box.top + box.height/2 - this.labelHeight/2 + - 'px'); - this.elem.css('left', box.left + 'px'); - } else if (this.position == 'right') { - this.elem.css('top', box.top + box.height/2 - this.labelHeight/2 + - 'px'); - this.elem.css('left', box.left + box.width - this.labelWidth + - 'px'); - } - }; - - - CssTransformAxisLabel.prototype = new HtmlAxisLabel(); - CssTransformAxisLabel.prototype.constructor = CssTransformAxisLabel; - function CssTransformAxisLabel(axisName, position, padding, plot, opts) { - HtmlAxisLabel.prototype.constructor.call(this, axisName, position, - padding, plot, opts); - } - - CssTransformAxisLabel.prototype.calculateSize = function() { - HtmlAxisLabel.prototype.calculateSize.call(this); - this.width = this.height = 0; - if (this.position == 'left' || this.position == 'right') { - this.width = this.labelHeight + this.padding; - } else { - this.height = this.labelHeight + this.padding; - } - }; - - CssTransformAxisLabel.prototype.transforms = function(degrees, x, y) { - var stransforms = { - '-moz-transform': '', - '-webkit-transform': '', - '-o-transform': '', - '-ms-transform': '' - }; - if (x != 0 || y != 0) { - var stdTranslate = ' translate(' + x + 'px, ' + y + 'px)'; - stransforms['-moz-transform'] += stdTranslate; - stransforms['-webkit-transform'] += stdTranslate; - stransforms['-o-transform'] += stdTranslate; - stransforms['-ms-transform'] += stdTranslate; - } - if (degrees != 0) { - var rotation = degrees / 90; - var stdRotate = ' rotate(' + degrees + 'deg)'; - stransforms['-moz-transform'] += stdRotate; - stransforms['-webkit-transform'] += stdRotate; - stransforms['-o-transform'] += stdRotate; - stransforms['-ms-transform'] += stdRotate; - } - var s = 'top: 0; left: 0; '; - for (var prop in stransforms) { - if (stransforms[prop]) { - s += prop + ':' + stransforms[prop] + ';'; - } - } - s += ';'; - return s; - }; - - CssTransformAxisLabel.prototype.calculateOffsets = function(box) { - var offsets = { x: 0, y: 0, degrees: 0 }; - if (this.position == 'bottom') { - offsets.x = box.left + box.width/2 - this.labelWidth/2; - offsets.y = box.top + box.height - this.labelHeight; - } else if (this.position == 'top') { - offsets.x = box.left + box.width/2 - this.labelWidth/2; - offsets.y = box.top; - } else if (this.position == 'left') { - offsets.degrees = -90; - offsets.x = box.left - this.labelWidth/2 + this.labelHeight/2; - offsets.y = box.height/2 + box.top; - } else if (this.position == 'right') { - offsets.degrees = 90; - offsets.x = box.left + box.width - this.labelWidth/2 - - this.labelHeight/2; - offsets.y = box.height/2 + box.top; - } - offsets.x = Math.round(offsets.x); - offsets.y = Math.round(offsets.y); - - return offsets; - }; - - CssTransformAxisLabel.prototype.draw = function(box) { - this.plot.getPlaceholder().find("." + this.axisName + "Label").remove(); - var offsets = this.calculateOffsets(box); - this.elem = $('
' + this.opts.axisLabel + '
'); - this.plot.getPlaceholder().append(this.elem); - }; - - - IeTransformAxisLabel.prototype = new CssTransformAxisLabel(); - IeTransformAxisLabel.prototype.constructor = IeTransformAxisLabel; - function IeTransformAxisLabel(axisName, position, padding, plot, opts) { - CssTransformAxisLabel.prototype.constructor.call(this, axisName, - position, padding, - plot, opts); - this.requiresResize = false; - } - - IeTransformAxisLabel.prototype.transforms = function(degrees, x, y) { - // I didn't feel like learning the crazy Matrix stuff, so this uses - // a combination of the rotation transform and CSS positioning. - var s = ''; - if (degrees != 0) { - var rotation = degrees/90; - while (rotation < 0) { - rotation += 4; - } - s += ' filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=' + rotation + '); '; - // see below - this.requiresResize = (this.position == 'right'); - } - if (x != 0) { - s += 'left: ' + x + 'px; '; - } - if (y != 0) { - s += 'top: ' + y + 'px; '; - } - return s; - }; - - IeTransformAxisLabel.prototype.calculateOffsets = function(box) { - var offsets = CssTransformAxisLabel.prototype.calculateOffsets.call( - this, box); - // adjust some values to take into account differences between - // CSS and IE rotations. - if (this.position == 'top') { - // FIXME: not sure why, but placing this exactly at the top causes - // the top axis label to flip to the bottom... - offsets.y = box.top + 1; - } else if (this.position == 'left') { - offsets.x = box.left; - offsets.y = box.height/2 + box.top - this.labelWidth/2; - } else if (this.position == 'right') { - offsets.x = box.left + box.width - this.labelHeight; - offsets.y = box.height/2 + box.top - this.labelWidth/2; - } - return offsets; - }; - - IeTransformAxisLabel.prototype.draw = function(box) { - CssTransformAxisLabel.prototype.draw.call(this, box); - if (this.requiresResize) { - this.elem = this.plot.getPlaceholder().find("." + this.axisName + - "Label"); - // Since we used CSS positioning instead of transforms for - // translating the element, and since the positioning is done - // before any rotations, we have to reset the width and height - // in case the browser wrapped the text (specifically for the - // y2axis). - this.elem.css('width', this.labelWidth); - this.elem.css('height', this.labelHeight); - } - }; - - - function init(plot) { - plot.hooks.processOptions.push(function (plot, options) { - - if (!options.axisLabels.show) - return; - - // This is kind of a hack. There are no hooks in Flot between - // the creation and measuring of the ticks (setTicks, measureTickLabels - // in setupGrid() ) and the drawing of the ticks and plot box - // (insertAxisLabels in setupGrid() ). - // - // Therefore, we use a trick where we run the draw routine twice: - // the first time to get the tick measurements, so that we can change - // them, and then have it draw it again. - var secondPass = false; - - var axisLabels = {}; - var axisOffsetCounts = { left: 0, right: 0, top: 0, bottom: 0 }; - - var defaultPadding = 2; // padding between axis and tick labels - plot.hooks.draw.push(function (plot, ctx) { - var hasAxisLabels = false; - if (!secondPass) { - // MEASURE AND SET OPTIONS - $.each(plot.getAxes(), function(axisName, axis) { - var opts = axis.options // Flot 0.7 - || plot.getOptions()[axisName]; // Flot 0.6 - - // Handle redraws initiated outside of this plug-in. - if (axisName in axisLabels) { - axis.labelHeight = axis.labelHeight - - axisLabels[axisName].height; - axis.labelWidth = axis.labelWidth - - axisLabels[axisName].width; - opts.labelHeight = axis.labelHeight; - opts.labelWidth = axis.labelWidth; - axisLabels[axisName].cleanup(); - delete axisLabels[axisName]; - } - - if (!opts || !opts.axisLabel || !axis.show) - return; - - hasAxisLabels = true; - var renderer = null; - - if (!opts.axisLabelUseHtml && - navigator.appName == 'Microsoft Internet Explorer') { - var ua = navigator.userAgent; - var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); - if (re.exec(ua) != null) { - rv = parseFloat(RegExp.$1); - } - if (rv >= 9 && !opts.axisLabelUseCanvas && !opts.axisLabelUseHtml) { - renderer = CssTransformAxisLabel; - } else if (!opts.axisLabelUseCanvas && !opts.axisLabelUseHtml) { - renderer = IeTransformAxisLabel; - } else if (opts.axisLabelUseCanvas) { - renderer = CanvasAxisLabel; - } else { - renderer = HtmlAxisLabel; - } - } else { - if (opts.axisLabelUseHtml || (!css3TransitionSupported() && !canvasTextSupported()) && !opts.axisLabelUseCanvas) { - renderer = HtmlAxisLabel; - } else if (opts.axisLabelUseCanvas || !css3TransitionSupported()) { - renderer = CanvasAxisLabel; - } else { - renderer = CssTransformAxisLabel; - } - } - - var padding = opts.axisLabelPadding === undefined ? - defaultPadding : opts.axisLabelPadding; - - axisLabels[axisName] = new renderer(axisName, - axis.position, padding, - plot, opts); - - // flot interprets axis.labelHeight and .labelWidth as - // the height and width of the tick labels. We increase - // these values to make room for the axis label and - // padding. - - axisLabels[axisName].calculateSize(); - - // AxisLabel.height and .width are the size of the - // axis label and padding. - // Just set opts here because axis will be sorted out on - // the redraw. - - opts.labelHeight = axis.labelHeight + - axisLabels[axisName].height; - opts.labelWidth = axis.labelWidth + - axisLabels[axisName].width; - }); - - // If there are axis labels, re-draw with new label widths and - // heights. - - if (hasAxisLabels) { - secondPass = true; - plot.setupGrid(); - plot.draw(); - } - } else { - secondPass = false; - // DRAW - $.each(plot.getAxes(), function(axisName, axis) { - var opts = axis.options // Flot 0.7 - || plot.getOptions()[axisName]; // Flot 0.6 - if (!opts || !opts.axisLabel || !axis.show) - return; - - axisLabels[axisName].draw(axis.box); - }); - } - }); - }); - } - - - $.plot.plugins.push({ - init: init, - options: options, - name: 'axisLabels', - version: '2.0' - }); -})(jQuery); diff --git a/src/plugins/vis_type_timelion/public/flot/jquery.flot.crosshair.js b/src/plugins/vis_type_timelion/public/flot/jquery.flot.crosshair.js deleted file mode 100644 index 5111695e3d12c..0000000000000 --- a/src/plugins/vis_type_timelion/public/flot/jquery.flot.crosshair.js +++ /dev/null @@ -1,176 +0,0 @@ -/* Flot plugin for showing crosshairs when the mouse hovers over the plot. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin supports these options: - - crosshair: { - mode: null or "x" or "y" or "xy" - color: color - lineWidth: number - } - -Set the mode to one of "x", "y" or "xy". The "x" mode enables a vertical -crosshair that lets you trace the values on the x axis, "y" enables a -horizontal crosshair and "xy" enables them both. "color" is the color of the -crosshair (default is "rgba(170, 0, 0, 0.80)"), "lineWidth" is the width of -the drawn lines (default is 1). - -The plugin also adds four public methods: - - - setCrosshair( pos ) - - Set the position of the crosshair. Note that this is cleared if the user - moves the mouse. "pos" is in coordinates of the plot and should be on the - form { x: xpos, y: ypos } (you can use x2/x3/... if you're using multiple - axes), which is coincidentally the same format as what you get from a - "plothover" event. If "pos" is null, the crosshair is cleared. - - - clearCrosshair() - - Clear the crosshair. - - - lockCrosshair(pos) - - Cause the crosshair to lock to the current location, no longer updating if - the user moves the mouse. Optionally supply a position (passed on to - setCrosshair()) to move it to. - - Example usage: - - var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } }; - $("#graph").bind( "plothover", function ( evt, position, item ) { - if ( item ) { - // Lock the crosshair to the data point being hovered - myFlot.lockCrosshair({ - x: item.datapoint[ 0 ], - y: item.datapoint[ 1 ] - }); - } else { - // Return normal crosshair operation - myFlot.unlockCrosshair(); - } - }); - - - unlockCrosshair() - - Free the crosshair to move again after locking it. -*/ - -(function ($) { - var options = { - crosshair: { - mode: null, // one of null, "x", "y" or "xy", - color: "rgba(170, 0, 0, 0.80)", - lineWidth: 1 - } - }; - - function init(plot) { - // position of crosshair in pixels - var crosshair = { x: -1, y: -1, locked: false }; - - plot.setCrosshair = function setCrosshair(pos) { - if (!pos) - crosshair.x = -1; - else { - var o = plot.p2c(pos); - crosshair.x = Math.max(0, Math.min(o.left, plot.width())); - crosshair.y = Math.max(0, Math.min(o.top, plot.height())); - } - - plot.triggerRedrawOverlay(); - }; - - plot.clearCrosshair = plot.setCrosshair; // passes null for pos - - plot.lockCrosshair = function lockCrosshair(pos) { - if (pos) - plot.setCrosshair(pos); - crosshair.locked = true; - }; - - plot.unlockCrosshair = function unlockCrosshair() { - crosshair.locked = false; - }; - - function onMouseOut(e) { - if (crosshair.locked) - return; - - if (crosshair.x != -1) { - crosshair.x = -1; - plot.triggerRedrawOverlay(); - } - } - - function onMouseMove(e) { - if (crosshair.locked) - return; - - if (plot.getSelection && plot.getSelection()) { - crosshair.x = -1; // hide the crosshair while selecting - return; - } - - var offset = plot.offset(); - crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width())); - crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height())); - plot.triggerRedrawOverlay(); - } - - plot.hooks.bindEvents.push(function (plot, eventHolder) { - if (!plot.getOptions().crosshair.mode) - return; - - eventHolder.mouseout(onMouseOut); - eventHolder.mousemove(onMouseMove); - }); - - plot.hooks.drawOverlay.push(function (plot, ctx) { - var c = plot.getOptions().crosshair; - if (!c.mode) - return; - - var plotOffset = plot.getPlotOffset(); - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - if (crosshair.x != -1) { - var adj = plot.getOptions().crosshair.lineWidth % 2 ? 0.5 : 0; - - ctx.strokeStyle = c.color; - ctx.lineWidth = c.lineWidth; - ctx.lineJoin = "round"; - - ctx.beginPath(); - if (c.mode.indexOf("x") != -1) { - var drawX = Math.floor(crosshair.x) + adj; - ctx.moveTo(drawX, 0); - ctx.lineTo(drawX, plot.height()); - } - if (c.mode.indexOf("y") != -1) { - var drawY = Math.floor(crosshair.y) + adj; - ctx.moveTo(0, drawY); - ctx.lineTo(plot.width(), drawY); - } - ctx.stroke(); - } - ctx.restore(); - }); - - plot.hooks.shutdown.push(function (plot, eventHolder) { - eventHolder.unbind("mouseout", onMouseOut); - eventHolder.unbind("mousemove", onMouseMove); - }); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'crosshair', - version: '1.0' - }); -})(jQuery); diff --git a/src/plugins/vis_type_timelion/public/flot/jquery.flot.js b/src/plugins/vis_type_timelion/public/flot/jquery.flot.js deleted file mode 100644 index 5d613037cf234..0000000000000 --- a/src/plugins/vis_type_timelion/public/flot/jquery.flot.js +++ /dev/null @@ -1,3168 +0,0 @@ -/* JavaScript plotting library for jQuery, version 0.8.3. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -*/ - -// first an inline dependency, jquery.colorhelpers.js, we inline it here -// for convenience - -/* Plugin for jQuery for working with colors. - * - * Version 1.1. - * - * Inspiration from jQuery color animation plugin by John Resig. - * - * Released under the MIT license by Ole Laursen, October 2009. - * - * Examples: - * - * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() - * var c = $.color.extract($("#mydiv"), 'background-color'); - * console.log(c.r, c.g, c.b, c.a); - * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" - * - * Note that .scale() and .add() return the same modified object - * instead of making a new one. - * - * V. 1.1: Fix error handling so e.g. parsing an empty string does - * produce a color rather than just crashing. - */ -(function($){$.color={};$.color.make=function(r,g,b,a){var o={};o.r=r||0;o.g=g||0;o.b=b||0;o.a=a!=null?a:1;o.add=function(c,d){for(var i=0;i=1){return"rgb("+[o.r,o.g,o.b].join(",")+")"}else{return"rgba("+[o.r,o.g,o.b,o.a].join(",")+")"}};o.normalize=function(){function clamp(min,value,max){return valuemax?max:value}o.r=clamp(0,parseInt(o.r),255);o.g=clamp(0,parseInt(o.g),255);o.b=clamp(0,parseInt(o.b),255);o.a=clamp(0,o.a,1);return o};o.clone=function(){return $.color.make(o.r,o.b,o.g,o.a)};return o.normalize()};$.color.extract=function(elem,css){var c;do{c=elem.css(css).toLowerCase();if(c!=""&&c!="transparent")break;elem=elem.parent()}while(elem.length&&!$.nodeName(elem.get(0),"body"));if(c=="rgba(0, 0, 0, 0)")c="transparent";return $.color.parse(c)};$.color.parse=function(str){var res,m=$.color.make;if(res=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10));if(res=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10),parseFloat(res[4]));if(res=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55);if(res=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55,parseFloat(res[4]));if(res=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))return m(parseInt(res[1],16),parseInt(res[2],16),parseInt(res[3],16));if(res=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))return m(parseInt(res[1]+res[1],16),parseInt(res[2]+res[2],16),parseInt(res[3]+res[3],16));var name=$.trim(str).toLowerCase();if(name=="transparent")return m(255,255,255,0);else{res=lookupColors[name]||[0,0,0];return m(res[0],res[1],res[2])}};var lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery); - -// the actual Flot code -(function($) { - - // Cache the prototype hasOwnProperty for faster access - - var hasOwnProperty = Object.prototype.hasOwnProperty; - - // A shim to provide 'detach' to jQuery versions prior to 1.4. Using a DOM - // operation produces the same effect as detach, i.e. removing the element - // without touching its jQuery data. - - // Do not merge this into Flot 0.9, since it requires jQuery 1.4.4+. - - if (!$.fn.detach) { - $.fn.detach = function() { - return this.each(function() { - if (this.parentNode) { - this.parentNode.removeChild( this ); - } - }); - }; - } - - /////////////////////////////////////////////////////////////////////////// - // The Canvas object is a wrapper around an HTML5 tag. - // - // @constructor - // @param {string} cls List of classes to apply to the canvas. - // @param {element} container Element onto which to append the canvas. - // - // Requiring a container is a little iffy, but unfortunately canvas - // operations don't work unless the canvas is attached to the DOM. - - function Canvas(cls, container) { - - var element = container.children("." + cls)[0]; - - if (element == null) { - - element = document.createElement("canvas"); - element.className = cls; - - $(element).css({ direction: "ltr", position: "absolute", left: 0, top: 0 }) - .appendTo(container); - - // If HTML5 Canvas isn't available, fall back to [Ex|Flash]canvas - - if (!element.getContext) { - if (window.G_vmlCanvasManager) { - element = window.G_vmlCanvasManager.initElement(element); - } else { - throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode."); - } - } - } - - this.element = element; - - var context = this.context = element.getContext("2d"); - - // Determine the screen's ratio of physical to device-independent - // pixels. This is the ratio between the canvas width that the browser - // advertises and the number of pixels actually present in that space. - - // The iPhone 4, for example, has a device-independent width of 320px, - // but its screen is actually 640px wide. It therefore has a pixel - // ratio of 2, while most normal devices have a ratio of 1. - - var devicePixelRatio = window.devicePixelRatio || 1, - backingStoreRatio = - context.webkitBackingStorePixelRatio || - context.mozBackingStorePixelRatio || - context.msBackingStorePixelRatio || - context.oBackingStorePixelRatio || - context.backingStorePixelRatio || 1; - - this.pixelRatio = devicePixelRatio / backingStoreRatio; - - // Size the canvas to match the internal dimensions of its container - - this.resize(container.width(), container.height()); - - // Collection of HTML div layers for text overlaid onto the canvas - - this.textContainer = null; - this.text = {}; - - // Cache of text fragments and metrics, so we can avoid expensively - // re-calculating them when the plot is re-rendered in a loop. - - this._textCache = {}; - } - - // Resizes the canvas to the given dimensions. - // - // @param {number} width New width of the canvas, in pixels. - // @param {number} width New height of the canvas, in pixels. - - Canvas.prototype.resize = function(width, height) { - - if (width <= 0 || height <= 0) { - throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + height); - } - - var element = this.element, - context = this.context, - pixelRatio = this.pixelRatio; - - // Resize the canvas, increasing its density based on the display's - // pixel ratio; basically giving it more pixels without increasing the - // size of its element, to take advantage of the fact that retina - // displays have that many more pixels in the same advertised space. - - // Resizing should reset the state (excanvas seems to be buggy though) - - if (this.width != width) { - element.width = width * pixelRatio; - element.style.width = width + "px"; - this.width = width; - } - - if (this.height != height) { - element.height = height * pixelRatio; - element.style.height = height + "px"; - this.height = height; - } - - // Save the context, so we can reset in case we get replotted. The - // restore ensure that we're really back at the initial state, and - // should be safe even if we haven't saved the initial state yet. - - context.restore(); - context.save(); - - // Scale the coordinate space to match the display density; so even though we - // may have twice as many pixels, we still want lines and other drawing to - // appear at the same size; the extra pixels will just make them crisper. - - context.scale(pixelRatio, pixelRatio); - }; - - // Clears the entire canvas area, not including any overlaid HTML text - - Canvas.prototype.clear = function() { - this.context.clearRect(0, 0, this.width, this.height); - }; - - // Finishes rendering the canvas, including managing the text overlay. - - Canvas.prototype.render = function() { - - var cache = this._textCache; - - // For each text layer, add elements marked as active that haven't - // already been rendered, and remove those that are no longer active. - - for (var layerKey in cache) { - if (hasOwnProperty.call(cache, layerKey)) { - - var layer = this.getTextLayer(layerKey), - layerCache = cache[layerKey]; - - layer.hide(); - - for (var styleKey in layerCache) { - if (hasOwnProperty.call(layerCache, styleKey)) { - var styleCache = layerCache[styleKey]; - for (var key in styleCache) { - if (hasOwnProperty.call(styleCache, key)) { - - var positions = styleCache[key].positions; - - for (var i = 0, position; position = positions[i]; i++) { - if (position.active) { - if (!position.rendered) { - layer.append(position.element); - position.rendered = true; - } - } else { - positions.splice(i--, 1); - if (position.rendered) { - position.element.detach(); - } - } - } - - if (positions.length == 0) { - delete styleCache[key]; - } - } - } - } - } - - layer.show(); - } - } - }; - - // Creates (if necessary) and returns the text overlay container. - // - // @param {string} classes String of space-separated CSS classes used to - // uniquely identify the text layer. - // @return {object} The jQuery-wrapped text-layer div. - - Canvas.prototype.getTextLayer = function(classes) { - - var layer = this.text[classes]; - - // Create the text layer if it doesn't exist - - if (layer == null) { - - // Create the text layer container, if it doesn't exist - - if (this.textContainer == null) { - this.textContainer = $("
") - .css({ - position: "absolute", - top: 0, - left: 0, - bottom: 0, - right: 0, - 'font-size': "smaller", - color: "#545454" - }) - .insertAfter(this.element); - } - - layer = this.text[classes] = $("
") - .addClass(classes) - .css({ - position: "absolute", - top: 0, - left: 0, - bottom: 0, - right: 0 - }) - .appendTo(this.textContainer); - } - - return layer; - }; - - // Creates (if necessary) and returns a text info object. - // - // The object looks like this: - // - // { - // width: Width of the text's wrapper div. - // height: Height of the text's wrapper div. - // element: The jQuery-wrapped HTML div containing the text. - // positions: Array of positions at which this text is drawn. - // } - // - // The positions array contains objects that look like this: - // - // { - // active: Flag indicating whether the text should be visible. - // rendered: Flag indicating whether the text is currently visible. - // element: The jQuery-wrapped HTML div containing the text. - // x: X coordinate at which to draw the text. - // y: Y coordinate at which to draw the text. - // } - // - // Each position after the first receives a clone of the original element. - // - // The idea is that that the width, height, and general 'identity' of the - // text is constant no matter where it is placed; the placements are a - // secondary property. - // - // Canvas maintains a cache of recently-used text info objects; getTextInfo - // either returns the cached element or creates a new entry. - // - // @param {string} layer A string of space-separated CSS classes uniquely - // identifying the layer containing this text. - // @param {string} text Text string to retrieve info for. - // @param {(string|object)=} font Either a string of space-separated CSS - // classes or a font-spec object, defining the text's font and style. - // @param {number=} angle Angle at which to rotate the text, in degrees. - // Angle is currently unused, it will be implemented in the future. - // @param {number=} width Maximum width of the text before it wraps. - // @return {object} a text info object. - - Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { - - var textStyle, layerCache, styleCache, info; - - // Cast the value to a string, in case we were given a number or such - - text = "" + text; - - // If the font is a font-spec object, generate a CSS font definition - - if (typeof font === "object") { - textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px/" + font.lineHeight + "px " + font.family; - } else { - textStyle = font; - } - - // Retrieve (or create) the cache for the text's layer and styles - - layerCache = this._textCache[layer]; - - if (layerCache == null) { - layerCache = this._textCache[layer] = {}; - } - - styleCache = layerCache[textStyle]; - - if (styleCache == null) { - styleCache = layerCache[textStyle] = {}; - } - - info = styleCache[text]; - - // If we can't find a matching element in our cache, create a new one - - if (info == null) { - - var element = $("
").html(text) - .css({ - position: "absolute", - 'max-width': width, - top: -9999 - }) - .appendTo(this.getTextLayer(layer)); - - if (typeof font === "object") { - element.css({ - font: textStyle, - color: font.color - }); - } else if (typeof font === "string") { - element.addClass(font); - } - - info = styleCache[text] = { - width: element.outerWidth(true), - height: element.outerHeight(true), - element: element, - positions: [] - }; - - element.detach(); - } - - return info; - }; - - // Adds a text string to the canvas text overlay. - // - // The text isn't drawn immediately; it is marked as rendering, which will - // result in its addition to the canvas on the next render pass. - // - // @param {string} layer A string of space-separated CSS classes uniquely - // identifying the layer containing this text. - // @param {number} x X coordinate at which to draw the text. - // @param {number} y Y coordinate at which to draw the text. - // @param {string} text Text string to draw. - // @param {(string|object)=} font Either a string of space-separated CSS - // classes or a font-spec object, defining the text's font and style. - // @param {number=} angle Angle at which to rotate the text, in degrees. - // Angle is currently unused, it will be implemented in the future. - // @param {number=} width Maximum width of the text before it wraps. - // @param {string=} halign Horizontal alignment of the text; either "left", - // "center" or "right". - // @param {string=} valign Vertical alignment of the text; either "top", - // "middle" or "bottom". - - Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { - - var info = this.getTextInfo(layer, text, font, angle, width), - positions = info.positions; - - // Tweak the div's position to match the text's alignment - - if (halign == "center") { - x -= info.width / 2; - } else if (halign == "right") { - x -= info.width; - } - - if (valign == "middle") { - y -= info.height / 2; - } else if (valign == "bottom") { - y -= info.height; - } - - // Determine whether this text already exists at this position. - // If so, mark it for inclusion in the next render pass. - - for (var i = 0, position; position = positions[i]; i++) { - if (position.x == x && position.y == y) { - position.active = true; - return; - } - } - - // If the text doesn't exist at this position, create a new entry - - // For the very first position we'll re-use the original element, - // while for subsequent ones we'll clone it. - - position = { - active: true, - rendered: false, - element: positions.length ? info.element.clone() : info.element, - x: x, - y: y - }; - - positions.push(position); - - // Move the element to its final position within the container - - position.element.css({ - top: Math.round(y), - left: Math.round(x), - 'text-align': halign // In case the text wraps - }); - }; - - // Removes one or more text strings from the canvas text overlay. - // - // If no parameters are given, all text within the layer is removed. - // - // Note that the text is not immediately removed; it is simply marked as - // inactive, which will result in its removal on the next render pass. - // This avoids the performance penalty for 'clear and redraw' behavior, - // where we potentially get rid of all text on a layer, but will likely - // add back most or all of it later, as when redrawing axes, for example. - // - // @param {string} layer A string of space-separated CSS classes uniquely - // identifying the layer containing this text. - // @param {number=} x X coordinate of the text. - // @param {number=} y Y coordinate of the text. - // @param {string=} text Text string to remove. - // @param {(string|object)=} font Either a string of space-separated CSS - // classes or a font-spec object, defining the text's font and style. - // @param {number=} angle Angle at which the text is rotated, in degrees. - // Angle is currently unused, it will be implemented in the future. - - Canvas.prototype.removeText = function(layer, x, y, text, font, angle) { - if (text == null) { - var layerCache = this._textCache[layer]; - if (layerCache != null) { - for (var styleKey in layerCache) { - if (hasOwnProperty.call(layerCache, styleKey)) { - var styleCache = layerCache[styleKey]; - for (var key in styleCache) { - if (hasOwnProperty.call(styleCache, key)) { - var positions = styleCache[key].positions; - for (var i = 0, position; position = positions[i]; i++) { - position.active = false; - } - } - } - } - } - } - } else { - var positions = this.getTextInfo(layer, text, font, angle).positions; - for (var i = 0, position; position = positions[i]; i++) { - if (position.x == x && position.y == y) { - position.active = false; - } - } - } - }; - - /////////////////////////////////////////////////////////////////////////// - // The top-level container for the entire plot. - - function Plot(placeholder, data_, options_, plugins) { - // data is on the form: - // [ series1, series2 ... ] - // where series is either just the data as [ [x1, y1], [x2, y2], ... ] - // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... } - - var series = [], - options = { - // the color theme used for graphs - colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], - legend: { - show: true, - noColumns: 1, // number of columns in legend table - labelFormatter: null, // fn: string -> string - labelBoxBorderColor: "#ccc", // border color for the little label boxes - container: null, // container (as jQuery object) to put legend in, null means default on top of graph - position: "ne", // position of default legend container within plot - margin: 5, // distance from grid edge to default legend container within plot - backgroundColor: null, // null means auto-detect - backgroundOpacity: 0.85, // set to 0 to avoid background - sorted: null // default to no legend sorting - }, - xaxis: { - show: null, // null = auto-detect, true = always, false = never - position: "bottom", // or "top" - mode: null, // null or "time" - font: null, // null (derived from CSS in placeholder) or object like { size: 11, lineHeight: 13, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" } - color: null, // base color, labels, ticks - tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)" - transform: null, // null or f: number -> number to transform axis - inverseTransform: null, // if transform is set, this should be the inverse function - min: null, // min. value to show, null means set automatically - max: null, // max. value to show, null means set automatically - autoscaleMargin: null, // margin in % to add if auto-setting min/max - ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks - tickFormatter: null, // fn: number -> string - labelWidth: null, // size of tick labels in pixels - labelHeight: null, - reserveSpace: null, // whether to reserve space even if axis isn't shown - tickLength: null, // size in pixels of ticks, or "full" for whole line - alignTicksWithAxis: null, // axis number or null for no sync - tickDecimals: null, // no. of decimals, null means auto - tickSize: null, // number or [number, "unit"] - minTickSize: null // number or [number, "unit"] - }, - yaxis: { - autoscaleMargin: 0.02, - position: "left" // or "right" - }, - xaxes: [], - yaxes: [], - series: { - points: { - show: false, - radius: 3, - lineWidth: 2, // in pixels - fill: true, - fillColor: "#ffffff", - symbol: "circle" // or callback - }, - lines: { - // we don't put in show: false so we can see - // whether lines were actively disabled - lineWidth: 2, // in pixels - fill: false, - fillColor: null, - steps: false - // Omit 'zero', so we can later default its value to - // match that of the 'fill' option. - }, - bars: { - show: false, - lineWidth: 2, // in pixels - barWidth: 1, // in units of the x axis - fill: true, - fillColor: null, - align: "left", // "left", "right", or "center" - horizontal: false, - zero: true - }, - shadowSize: 3, - highlightColor: null - }, - grid: { - show: true, - aboveData: false, - color: "#545454", // primary color used for outline and labels - backgroundColor: null, // null for transparent, else color - borderColor: null, // set if different from the grid color - tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)" - margin: 0, // distance from the canvas edge to the grid - labelMargin: 5, // in pixels - axisMargin: 8, // in pixels - borderWidth: 2, // in pixels - minBorderMargin: null, // in pixels, null means taken from points radius - markings: null, // array of ranges or fn: axes -> array of ranges - markingsColor: "#f4f4f4", - markingsLineWidth: 2, - // interactive stuff - clickable: false, - hoverable: false, - autoHighlight: true, // highlight in case mouse is near - mouseActiveRadius: 10 // how far the mouse can be away to activate an item - }, - interaction: { - redrawOverlayInterval: 1000/60 // time between updates, -1 means in same flow - }, - hooks: {} - }, - surface = null, // the canvas for the plot itself - overlay = null, // canvas for interactive stuff on top of plot - eventHolder = null, // jQuery object that events should be bound to - ctx = null, octx = null, - xaxes = [], yaxes = [], - plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, - plotWidth = 0, plotHeight = 0, - hooks = { - processOptions: [], - processRawData: [], - processDatapoints: [], - processOffset: [], - drawBackground: [], - drawSeries: [], - draw: [], - bindEvents: [], - drawOverlay: [], - shutdown: [] - }, - plot = this; - - // public functions - plot.setData = setData; - plot.setupGrid = setupGrid; - plot.draw = draw; - plot.getPlaceholder = function() { return placeholder; }; - plot.getCanvas = function() { return surface.element; }; - plot.getPlotOffset = function() { return plotOffset; }; - plot.width = function () { return plotWidth; }; - plot.height = function () { return plotHeight; }; - plot.offset = function () { - var o = eventHolder.offset(); - o.left += plotOffset.left; - o.top += plotOffset.top; - return o; - }; - plot.getData = function () { return series; }; - plot.getAxes = function () { - var res = {}, i; - $.each(xaxes.concat(yaxes), function (_, axis) { - if (axis) - res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis; - }); - return res; - }; - plot.getXAxes = function () { return xaxes; }; - plot.getYAxes = function () { return yaxes; }; - plot.c2p = canvasToAxisCoords; - plot.p2c = axisToCanvasCoords; - plot.getOptions = function () { return options; }; - plot.highlight = highlight; - plot.unhighlight = unhighlight; - plot.triggerRedrawOverlay = triggerRedrawOverlay; - plot.pointOffset = function(point) { - return { - left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left, 10), - top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top, 10) - }; - }; - plot.shutdown = shutdown; - plot.destroy = function () { - shutdown(); - placeholder.removeData("plot").empty(); - - series = []; - options = null; - surface = null; - overlay = null; - eventHolder = null; - ctx = null; - octx = null; - xaxes = []; - yaxes = []; - hooks = null; - highlights = []; - plot = null; - }; - plot.resize = function () { - var width = placeholder.width(), - height = placeholder.height(); - surface.resize(width, height); - overlay.resize(width, height); - }; - - // public attributes - plot.hooks = hooks; - - // initialize - initPlugins(plot); - parseOptions(options_); - setupCanvases(); - setData(data_); - setupGrid(); - draw(); - bindEvents(); - - - function executeHooks(hook, args) { - args = [plot].concat(args); - for (var i = 0; i < hook.length; ++i) - hook[i].apply(this, args); - } - - function initPlugins() { - - // References to key classes, allowing plugins to modify them - - var classes = { - Canvas: Canvas - }; - - for (var i = 0; i < plugins.length; ++i) { - var p = plugins[i]; - p.init(plot, classes); - if (p.options) - $.extend(true, options, p.options); - } - } - - function parseOptions(opts) { - - $.extend(true, options, opts); - - // $.extend merges arrays, rather than replacing them. When less - // colors are provided than the size of the default palette, we - // end up with those colors plus the remaining defaults, which is - // not expected behavior; avoid it by replacing them here. - - if (opts && opts.colors) { - options.colors = opts.colors; - } - - if (options.xaxis.color == null) - options.xaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); - if (options.yaxis.color == null) - options.yaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); - - if (options.xaxis.tickColor == null) // grid.tickColor for back-compatibility - options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color; - if (options.yaxis.tickColor == null) // grid.tickColor for back-compatibility - options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color; - - if (options.grid.borderColor == null) - options.grid.borderColor = options.grid.color; - if (options.grid.tickColor == null) - options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString(); - - // Fill in defaults for axis options, including any unspecified - // font-spec fields, if a font-spec was provided. - - // If no x/y axis options were provided, create one of each anyway, - // since the rest of the code assumes that they exist. - - var i, axisOptions, axisCount, - fontSize = placeholder.css("font-size"), - fontSizeDefault = fontSize ? +fontSize.replace("px", "") : 13, - fontDefaults = { - style: placeholder.css("font-style"), - size: Math.round(0.8 * fontSizeDefault), - variant: placeholder.css("font-variant"), - weight: placeholder.css("font-weight"), - family: placeholder.css("font-family") - }; - - axisCount = options.xaxes.length || 1; - for (i = 0; i < axisCount; ++i) { - - axisOptions = options.xaxes[i]; - if (axisOptions && !axisOptions.tickColor) { - axisOptions.tickColor = axisOptions.color; - } - - axisOptions = $.extend(true, {}, options.xaxis, axisOptions); - options.xaxes[i] = axisOptions; - - if (axisOptions.font) { - axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); - if (!axisOptions.font.color) { - axisOptions.font.color = axisOptions.color; - } - if (!axisOptions.font.lineHeight) { - axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); - } - } - } - - axisCount = options.yaxes.length || 1; - for (i = 0; i < axisCount; ++i) { - - axisOptions = options.yaxes[i]; - if (axisOptions && !axisOptions.tickColor) { - axisOptions.tickColor = axisOptions.color; - } - - axisOptions = $.extend(true, {}, options.yaxis, axisOptions); - options.yaxes[i] = axisOptions; - - if (axisOptions.font) { - axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); - if (!axisOptions.font.color) { - axisOptions.font.color = axisOptions.color; - } - if (!axisOptions.font.lineHeight) { - axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); - } - } - } - - // backwards compatibility, to be removed in future - if (options.xaxis.noTicks && options.xaxis.ticks == null) - options.xaxis.ticks = options.xaxis.noTicks; - if (options.yaxis.noTicks && options.yaxis.ticks == null) - options.yaxis.ticks = options.yaxis.noTicks; - if (options.x2axis) { - options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis); - options.xaxes[1].position = "top"; - // Override the inherit to allow the axis to auto-scale - if (options.x2axis.min == null) { - options.xaxes[1].min = null; - } - if (options.x2axis.max == null) { - options.xaxes[1].max = null; - } - } - if (options.y2axis) { - options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis); - options.yaxes[1].position = "right"; - // Override the inherit to allow the axis to auto-scale - if (options.y2axis.min == null) { - options.yaxes[1].min = null; - } - if (options.y2axis.max == null) { - options.yaxes[1].max = null; - } - } - if (options.grid.coloredAreas) - options.grid.markings = options.grid.coloredAreas; - if (options.grid.coloredAreasColor) - options.grid.markingsColor = options.grid.coloredAreasColor; - if (options.lines) - $.extend(true, options.series.lines, options.lines); - if (options.points) - $.extend(true, options.series.points, options.points); - if (options.bars) - $.extend(true, options.series.bars, options.bars); - if (options.shadowSize != null) - options.series.shadowSize = options.shadowSize; - if (options.highlightColor != null) - options.series.highlightColor = options.highlightColor; - - // save options on axes for future reference - for (i = 0; i < options.xaxes.length; ++i) - getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i]; - for (i = 0; i < options.yaxes.length; ++i) - getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i]; - - // add hooks from options - for (var n in hooks) - if (options.hooks[n] && options.hooks[n].length) - hooks[n] = hooks[n].concat(options.hooks[n]); - - executeHooks(hooks.processOptions, [options]); - } - - function setData(d) { - series = parseData(d); - fillInSeriesOptions(); - processData(); - } - - function parseData(d) { - var res = []; - for (var i = 0; i < d.length; ++i) { - var s = $.extend(true, {}, options.series); - - if (d[i].data != null) { - s.data = d[i].data; // move the data instead of deep-copy - delete d[i].data; - - $.extend(true, s, d[i]); - - d[i].data = s.data; - } - else - s.data = d[i]; - res.push(s); - } - - return res; - } - - function axisNumber(obj, coord) { - var a = obj[coord + "axis"]; - if (typeof a == "object") // if we got a real axis, extract number - a = a.n; - if (typeof a != "number") - a = 1; // default to first axis - return a; - } - - function allAxes() { - // return flat array without annoying null entries - return $.grep(xaxes.concat(yaxes), function (a) { return a; }); - } - - function canvasToAxisCoords(pos) { - // return an object with x/y corresponding to all used axes - var res = {}, i, axis; - for (i = 0; i < xaxes.length; ++i) { - axis = xaxes[i]; - if (axis && axis.used) - res["x" + axis.n] = axis.c2p(pos.left); - } - - for (i = 0; i < yaxes.length; ++i) { - axis = yaxes[i]; - if (axis && axis.used) - res["y" + axis.n] = axis.c2p(pos.top); - } - - if (res.x1 !== undefined) - res.x = res.x1; - if (res.y1 !== undefined) - res.y = res.y1; - - return res; - } - - function axisToCanvasCoords(pos) { - // get canvas coords from the first pair of x/y found in pos - var res = {}, i, axis, key; - - for (i = 0; i < xaxes.length; ++i) { - axis = xaxes[i]; - if (axis && axis.used) { - key = "x" + axis.n; - if (pos[key] == null && axis.n == 1) - key = "x"; - - if (pos[key] != null) { - res.left = axis.p2c(pos[key]); - break; - } - } - } - - for (i = 0; i < yaxes.length; ++i) { - axis = yaxes[i]; - if (axis && axis.used) { - key = "y" + axis.n; - if (pos[key] == null && axis.n == 1) - key = "y"; - - if (pos[key] != null) { - res.top = axis.p2c(pos[key]); - break; - } - } - } - - return res; - } - - function getOrCreateAxis(axes, number) { - if (!axes[number - 1]) - axes[number - 1] = { - n: number, // save the number for future reference - direction: axes == xaxes ? "x" : "y", - options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis) - }; - - return axes[number - 1]; - } - - function fillInSeriesOptions() { - - var neededColors = series.length, maxIndex = -1, i; - - // Subtract the number of series that already have fixed colors or - // color indexes from the number that we still need to generate. - - for (i = 0; i < series.length; ++i) { - var sc = series[i].color; - if (sc != null) { - neededColors--; - if (typeof sc == "number" && sc > maxIndex) { - maxIndex = sc; - } - } - } - - // If any of the series have fixed color indexes, then we need to - // generate at least as many colors as the highest index. - - if (neededColors <= maxIndex) { - neededColors = maxIndex + 1; - } - - // Generate all the colors, using first the option colors and then - // variations on those colors once they're exhausted. - - var c, colors = [], colorPool = options.colors, - colorPoolSize = colorPool.length, variation = 0; - - for (i = 0; i < neededColors; i++) { - - c = $.color.parse(colorPool[i % colorPoolSize] || "#666"); - - // Each time we exhaust the colors in the pool we adjust - // a scaling factor used to produce more variations on - // those colors. The factor alternates negative/positive - // to produce lighter/darker colors. - - // Reset the variation after every few cycles, or else - // it will end up producing only white or black colors. - - if (i % colorPoolSize == 0 && i) { - if (variation >= 0) { - if (variation < 0.5) { - variation = -variation - 0.2; - } else variation = 0; - } else variation = -variation; - } - - colors[i] = c.scale('rgb', 1 + variation); - } - - // Finalize the series options, filling in their colors - - var colori = 0, s; - for (i = 0; i < series.length; ++i) { - s = series[i]; - - // assign colors - if (s.color == null) { - s.color = colors[colori].toString(); - ++colori; - } - else if (typeof s.color == "number") - s.color = colors[s.color].toString(); - - // turn on lines automatically in case nothing is set - if (s.lines.show == null) { - var v, show = true; - for (v in s) - if (s[v] && s[v].show) { - show = false; - break; - } - if (show) - s.lines.show = true; - } - - // If nothing was provided for lines.zero, default it to match - // lines.fill, since areas by default should extend to zero. - - if (s.lines.zero == null) { - s.lines.zero = !!s.lines.fill; - } - - // setup axes - s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x")); - s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y")); - } - } - - function processData() { - var topSentry = Number.POSITIVE_INFINITY, - bottomSentry = Number.NEGATIVE_INFINITY, - fakeInfinity = Number.MAX_VALUE, - i, j, k, m, length, - s, points, ps, x, y, axis, val, f, p, - data, format; - - function updateAxis(axis, min, max) { - if (min < axis.datamin && min != -fakeInfinity) - axis.datamin = min; - if (max > axis.datamax && max != fakeInfinity) - axis.datamax = max; - } - - $.each(allAxes(), function (_, axis) { - // init axis - axis.datamin = topSentry; - axis.datamax = bottomSentry; - axis.used = false; - }); - - for (i = 0; i < series.length; ++i) { - s = series[i]; - s.datapoints = { points: [] }; - - executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]); - } - - // first pass: clean and copy data - for (i = 0; i < series.length; ++i) { - s = series[i]; - - data = s.data; - format = s.datapoints.format; - - if (!format) { - format = []; - // find out how to copy - format.push({ x: true, number: true, required: true }); - format.push({ y: true, number: true, required: true }); - - if (s.bars.show || (s.lines.show && s.lines.fill)) { - var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); - format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale }); - if (s.bars.horizontal) { - delete format[format.length - 1].y; - format[format.length - 1].x = true; - } - } - - s.datapoints.format = format; - } - - if (s.datapoints.pointsize != null) - continue; // already filled in - - s.datapoints.pointsize = format.length; - - ps = s.datapoints.pointsize; - points = s.datapoints.points; - - var insertSteps = s.lines.show && s.lines.steps; - s.xaxis.used = s.yaxis.used = true; - - for (j = k = 0; j < data.length; ++j, k += ps) { - p = data[j]; - - var nullify = p == null; - if (!nullify) { - for (m = 0; m < ps; ++m) { - val = p[m]; - f = format[m]; - - if (f) { - if (f.number && val != null) { - val = +val; // convert to number - if (isNaN(val)) - val = null; - else if (val == Infinity) - val = fakeInfinity; - else if (val == -Infinity) - val = -fakeInfinity; - } - - if (val == null) { - if (f.required) - nullify = true; - - if (f.defaultValue != null) - val = f.defaultValue; - } - } - - points[k + m] = val; - } - } - - if (nullify) { - for (m = 0; m < ps; ++m) { - val = points[k + m]; - if (val != null) { - f = format[m]; - // extract min/max info - if (f.autoscale !== false) { - if (f.x) { - updateAxis(s.xaxis, val, val); - } - if (f.y) { - updateAxis(s.yaxis, val, val); - } - } - } - points[k + m] = null; - } - } - else { - // a little bit of line specific stuff that - // perhaps shouldn't be here, but lacking - // better means... - if (insertSteps && k > 0 - && points[k - ps] != null - && points[k - ps] != points[k] - && points[k - ps + 1] != points[k + 1]) { - // copy the point to make room for a middle point - for (m = 0; m < ps; ++m) - points[k + ps + m] = points[k + m]; - - // middle point has same y - points[k + 1] = points[k - ps + 1]; - - // we've added a point, better reflect that - k += ps; - } - } - } - } - - // give the hooks a chance to run - for (i = 0; i < series.length; ++i) { - s = series[i]; - - executeHooks(hooks.processDatapoints, [ s, s.datapoints]); - } - - // second pass: find datamax/datamin for auto-scaling - for (i = 0; i < series.length; ++i) { - s = series[i]; - points = s.datapoints.points; - ps = s.datapoints.pointsize; - format = s.datapoints.format; - - var xmin = topSentry, ymin = topSentry, - xmax = bottomSentry, ymax = bottomSentry; - - for (j = 0; j < points.length; j += ps) { - if (points[j] == null) - continue; - - for (m = 0; m < ps; ++m) { - val = points[j + m]; - f = format[m]; - if (!f || f.autoscale === false || val == fakeInfinity || val == -fakeInfinity) - continue; - - if (f.x) { - if (val < xmin) - xmin = val; - if (val > xmax) - xmax = val; - } - if (f.y) { - if (val < ymin) - ymin = val; - if (val > ymax) - ymax = val; - } - } - } - - if (s.bars.show) { - // make sure we got room for the bar on the dancing floor - var delta; - - switch (s.bars.align) { - case "left": - delta = 0; - break; - case "right": - delta = -s.bars.barWidth; - break; - default: - delta = -s.bars.barWidth / 2; - } - - if (s.bars.horizontal) { - ymin += delta; - ymax += delta + s.bars.barWidth; - } - else { - xmin += delta; - xmax += delta + s.bars.barWidth; - } - } - - updateAxis(s.xaxis, xmin, xmax); - updateAxis(s.yaxis, ymin, ymax); - } - - $.each(allAxes(), function (_, axis) { - if (axis.datamin == topSentry) - axis.datamin = null; - if (axis.datamax == bottomSentry) - axis.datamax = null; - }); - } - - function setupCanvases() { - - // Make sure the placeholder is clear of everything except canvases - // from a previous plot in this container that we'll try to re-use. - - placeholder.css("padding", 0) // padding messes up the positioning - .children().filter(function(){ - return !$(this).hasClass("flot-overlay") && !$(this).hasClass('flot-base'); - }).remove(); - - if (placeholder.css("position") == 'static') - placeholder.css("position", "relative"); // for positioning labels and overlay - - surface = new Canvas("flot-base", placeholder); - overlay = new Canvas("flot-overlay", placeholder); // overlay canvas for interactive features - - ctx = surface.context; - octx = overlay.context; - - // define which element we're listening for events on - eventHolder = $(overlay.element).unbind(); - - // If we're re-using a plot object, shut down the old one - - var existing = placeholder.data("plot"); - - if (existing) { - existing.shutdown(); - overlay.clear(); - } - - // save in case we get replotted - placeholder.data("plot", plot); - } - - function bindEvents() { - // bind events - if (options.grid.hoverable) { - eventHolder.mousemove(onMouseMove); - - // Use bind, rather than .mouseleave, because we officially - // still support jQuery 1.2.6, which doesn't define a shortcut - // for mouseenter or mouseleave. This was a bug/oversight that - // was fixed somewhere around 1.3.x. We can return to using - // .mouseleave when we drop support for 1.2.6. - - eventHolder.bind("mouseleave", onMouseLeave); - } - - if (options.grid.clickable) - eventHolder.click(onClick); - - executeHooks(hooks.bindEvents, [eventHolder]); - } - - function shutdown() { - if (redrawTimeout) - clearTimeout(redrawTimeout); - - eventHolder.unbind("mousemove", onMouseMove); - eventHolder.unbind("mouseleave", onMouseLeave); - eventHolder.unbind("click", onClick); - - executeHooks(hooks.shutdown, [eventHolder]); - } - - function setTransformationHelpers(axis) { - // set helper functions on the axis, assumes plot area - // has been computed already - - function identity(x) { return x; } - - var s, m, t = axis.options.transform || identity, - it = axis.options.inverseTransform; - - // precompute how much the axis is scaling a point - // in canvas space - if (axis.direction == "x") { - s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min)); - m = Math.min(t(axis.max), t(axis.min)); - } - else { - s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min)); - s = -s; - m = Math.max(t(axis.max), t(axis.min)); - } - - // data point to canvas coordinate - if (t == identity) // slight optimization - axis.p2c = function (p) { return (p - m) * s; }; - else - axis.p2c = function (p) { return (t(p) - m) * s; }; - // canvas coordinate to data point - if (!it) - axis.c2p = function (c) { return m + c / s; }; - else - axis.c2p = function (c) { return it(m + c / s); }; - } - - function measureTickLabels(axis) { - - var opts = axis.options, - ticks = axis.ticks || [], - labelWidth = opts.labelWidth || 0, - labelHeight = opts.labelHeight || 0, - maxWidth = labelWidth || (axis.direction == "x" ? Math.floor(surface.width / (ticks.length || 1)) : null), - legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", - layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, - font = opts.font || "flot-tick-label tickLabel"; - - for (var i = 0; i < ticks.length; ++i) { - - var t = ticks[i]; - - if (!t.label) - continue; - - var info = surface.getTextInfo(layer, t.label, font, null, maxWidth); - - labelWidth = Math.max(labelWidth, info.width); - labelHeight = Math.max(labelHeight, info.height); - } - - axis.labelWidth = opts.labelWidth || labelWidth; - axis.labelHeight = opts.labelHeight || labelHeight; - } - - function allocateAxisBoxFirstPhase(axis) { - // find the bounding box of the axis by looking at label - // widths/heights and ticks, make room by diminishing the - // plotOffset; this first phase only looks at one - // dimension per axis, the other dimension depends on the - // other axes so will have to wait - - var lw = axis.labelWidth, - lh = axis.labelHeight, - pos = axis.options.position, - isXAxis = axis.direction === "x", - tickLength = axis.options.tickLength, - axisMargin = options.grid.axisMargin, - padding = options.grid.labelMargin, - innermost = true, - outermost = true, - first = true, - found = false; - - // Determine the axis's position in its direction and on its side - - $.each(isXAxis ? xaxes : yaxes, function(i, a) { - if (a && (a.show || a.reserveSpace)) { - if (a === axis) { - found = true; - } else if (a.options.position === pos) { - if (found) { - outermost = false; - } else { - innermost = false; - } - } - if (!found) { - first = false; - } - } - }); - - // The outermost axis on each side has no margin - - if (outermost) { - axisMargin = 0; - } - - // The ticks for the first axis in each direction stretch across - - if (tickLength == null) { - tickLength = first ? "full" : 5; - } - - if (!isNaN(+tickLength)) - padding += +tickLength; - - if (isXAxis) { - lh += padding; - - if (pos == "bottom") { - plotOffset.bottom += lh + axisMargin; - axis.box = { top: surface.height - plotOffset.bottom, height: lh }; - } - else { - axis.box = { top: plotOffset.top + axisMargin, height: lh }; - plotOffset.top += lh + axisMargin; - } - } - else { - lw += padding; - - if (pos == "left") { - axis.box = { left: plotOffset.left + axisMargin, width: lw }; - plotOffset.left += lw + axisMargin; - } - else { - plotOffset.right += lw + axisMargin; - axis.box = { left: surface.width - plotOffset.right, width: lw }; - } - } - - // save for future reference - axis.position = pos; - axis.tickLength = tickLength; - axis.box.padding = padding; - axis.innermost = innermost; - } - - function allocateAxisBoxSecondPhase(axis) { - // now that all axis boxes have been placed in one - // dimension, we can set the remaining dimension coordinates - if (axis.direction == "x") { - axis.box.left = plotOffset.left - axis.labelWidth / 2; - axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.labelWidth; - } - else { - axis.box.top = plotOffset.top - axis.labelHeight / 2; - axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.labelHeight; - } - } - - function adjustLayoutForThingsStickingOut() { - // possibly adjust plot offset to ensure everything stays - // inside the canvas and isn't clipped off - - var minMargin = options.grid.minBorderMargin, - axis, i; - - // check stuff from the plot (FIXME: this should just read - // a value from the series, otherwise it's impossible to - // customize) - if (minMargin == null) { - minMargin = 0; - for (i = 0; i < series.length; ++i) - minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth/2)); - } - - var margins = { - left: minMargin, - right: minMargin, - top: minMargin, - bottom: minMargin - }; - - // check axis labels, note we don't check the actual - // labels but instead use the overall width/height to not - // jump as much around with replots - $.each(allAxes(), function (_, axis) { - if (axis.reserveSpace && axis.ticks && axis.ticks.length) { - if (axis.direction === "x") { - margins.left = Math.max(margins.left, axis.labelWidth / 2); - margins.right = Math.max(margins.right, axis.labelWidth / 2); - } else { - margins.bottom = Math.max(margins.bottom, axis.labelHeight / 2); - margins.top = Math.max(margins.top, axis.labelHeight / 2); - } - } - }); - - plotOffset.left = Math.ceil(Math.max(margins.left, plotOffset.left)); - plotOffset.right = Math.ceil(Math.max(margins.right, plotOffset.right)); - plotOffset.top = Math.ceil(Math.max(margins.top, plotOffset.top)); - plotOffset.bottom = Math.ceil(Math.max(margins.bottom, plotOffset.bottom)); - } - - function setupGrid() { - var i, axes = allAxes(), showGrid = options.grid.show; - - // Initialize the plot's offset from the edge of the canvas - - for (var a in plotOffset) { - var margin = options.grid.margin || 0; - plotOffset[a] = typeof margin == "number" ? margin : margin[a] || 0; - } - - executeHooks(hooks.processOffset, [plotOffset]); - - // If the grid is visible, add its border width to the offset - - for (var a in plotOffset) { - if(typeof(options.grid.borderWidth) == "object") { - plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0; - } - else { - plotOffset[a] += showGrid ? options.grid.borderWidth : 0; - } - } - - $.each(axes, function (_, axis) { - var axisOpts = axis.options; - axis.show = axisOpts.show == null ? axis.used : axisOpts.show; - axis.reserveSpace = axisOpts.reserveSpace == null ? axis.show : axisOpts.reserveSpace; - setRange(axis); - }); - - if (showGrid) { - - var allocatedAxes = $.grep(axes, function (axis) { - return axis.show || axis.reserveSpace; - }); - - $.each(allocatedAxes, function (_, axis) { - // make the ticks - setupTickGeneration(axis); - setTicks(axis); - snapRangeToTicks(axis, axis.ticks); - // find labelWidth/Height for axis - measureTickLabels(axis); - }); - - // with all dimensions calculated, we can compute the - // axis bounding boxes, start from the outside - // (reverse order) - for (i = allocatedAxes.length - 1; i >= 0; --i) - allocateAxisBoxFirstPhase(allocatedAxes[i]); - - // make sure we've got enough space for things that - // might stick out - adjustLayoutForThingsStickingOut(); - - $.each(allocatedAxes, function (_, axis) { - allocateAxisBoxSecondPhase(axis); - }); - } - - plotWidth = surface.width - plotOffset.left - plotOffset.right; - plotHeight = surface.height - plotOffset.bottom - plotOffset.top; - - // now we got the proper plot dimensions, we can compute the scaling - $.each(axes, function (_, axis) { - setTransformationHelpers(axis); - }); - - if (showGrid) { - drawAxisLabels(); - } - - insertLegend(); - } - - function setRange(axis) { - var opts = axis.options, - min = +(opts.min != null ? opts.min : axis.datamin), - max = +(opts.max != null ? opts.max : axis.datamax), - delta = max - min; - - if (delta == 0.0) { - // degenerate case - var widen = max == 0 ? 1 : 0.01; - - if (opts.min == null) - min -= widen; - // always widen max if we couldn't widen min to ensure we - // don't fall into min == max which doesn't work - if (opts.max == null || opts.min != null) - max += widen; - } - else { - // consider autoscaling - var margin = opts.autoscaleMargin; - if (margin != null) { - if (opts.min == null) { - min -= delta * margin; - // make sure we don't go below zero if all values - // are positive - if (min < 0 && axis.datamin != null && axis.datamin >= 0) - min = 0; - } - if (opts.max == null) { - max += delta * margin; - if (max > 0 && axis.datamax != null && axis.datamax <= 0) - max = 0; - } - } - } - axis.min = min; - axis.max = max; - } - - function setupTickGeneration(axis) { - var opts = axis.options; - - // estimate number of ticks - var noTicks; - if (typeof opts.ticks == "number" && opts.ticks > 0) - noTicks = opts.ticks; - else - // heuristic based on the model a*sqrt(x) fitted to - // some data points that seemed reasonable - noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? surface.width : surface.height); - - var delta = (axis.max - axis.min) / noTicks, - dec = -Math.floor(Math.log(delta) / Math.LN10), - maxDec = opts.tickDecimals; - - if (maxDec != null && dec > maxDec) { - dec = maxDec; - } - - var magn = Math.pow(10, -dec), - norm = delta / magn, // norm is between 1.0 and 10.0 - size; - - if (norm < 1.5) { - size = 1; - } else if (norm < 3) { - size = 2; - // special case for 2.5, requires an extra decimal - if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { - size = 2.5; - ++dec; - } - } else if (norm < 7.5) { - size = 5; - } else { - size = 10; - } - - size *= magn; - - if (opts.minTickSize != null && size < opts.minTickSize) { - size = opts.minTickSize; - } - - axis.delta = delta; - axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); - axis.tickSize = opts.tickSize || size; - - // Time mode was moved to a plug-in in 0.8, and since so many people use it - // we'll add an especially friendly reminder to make sure they included it. - - if (opts.mode == "time" && !axis.tickGenerator) { - throw new Error("Time mode requires the flot.time plugin."); - } - - // Flot supports base-10 axes; any other mode else is handled by a plug-in, - // like flot.time.js. - - if (!axis.tickGenerator) { - - axis.tickGenerator = function (axis) { - - var ticks = [], - start = floorInBase(axis.min, axis.tickSize), - i = 0, - v = Number.NaN, - prev; - - do { - prev = v; - v = start + i * axis.tickSize; - ticks.push(v); - ++i; - } while (v < axis.max && v != prev); - return ticks; - }; - - axis.tickFormatter = function (value, axis) { - - var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1; - var formatted = "" + Math.round(value * factor) / factor; - - // If tickDecimals was specified, ensure that we have exactly that - // much precision; otherwise default to the value's own precision. - - if (axis.tickDecimals != null) { - var decimal = formatted.indexOf("."); - var precision = decimal == -1 ? 0 : formatted.length - decimal - 1; - if (precision < axis.tickDecimals) { - return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision); - } - } - - return formatted; - }; - } - - if ($.isFunction(opts.tickFormatter)) - axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); }; - - if (opts.alignTicksWithAxis != null) { - var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1]; - if (otherAxis && otherAxis.used && otherAxis != axis) { - // consider snapping min/max to outermost nice ticks - var niceTicks = axis.tickGenerator(axis); - if (niceTicks.length > 0) { - if (opts.min == null) - axis.min = Math.min(axis.min, niceTicks[0]); - if (opts.max == null && niceTicks.length > 1) - axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]); - } - - axis.tickGenerator = function (axis) { - // copy ticks, scaled to this axis - var ticks = [], v, i; - for (i = 0; i < otherAxis.ticks.length; ++i) { - v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min); - v = axis.min + v * (axis.max - axis.min); - ticks.push(v); - } - return ticks; - }; - - // we might need an extra decimal since forced - // ticks don't necessarily fit naturally - if (!axis.mode && opts.tickDecimals == null) { - var extraDec = Math.max(0, -Math.floor(Math.log(axis.delta) / Math.LN10) + 1), - ts = axis.tickGenerator(axis); - - // only proceed if the tick interval rounded - // with an extra decimal doesn't give us a - // zero at end - if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec)))) - axis.tickDecimals = extraDec; - } - } - } - } - - function setTicks(axis) { - var oticks = axis.options.ticks, ticks = []; - if (oticks == null || (typeof oticks == "number" && oticks > 0)) - ticks = axis.tickGenerator(axis); - else if (oticks) { - if ($.isFunction(oticks)) - // generate the ticks - ticks = oticks(axis); - else - ticks = oticks; - } - - // clean up/labelify the supplied ticks, copy them over - var i, v; - axis.ticks = []; - for (i = 0; i < ticks.length; ++i) { - var label = null; - var t = ticks[i]; - if (typeof t == "object") { - v = +t[0]; - if (t.length > 1) - label = t[1]; - } - else - v = +t; - if (label == null) - label = axis.tickFormatter(v, axis); - if (!isNaN(v)) - axis.ticks.push({ v: v, label: label }); - } - } - - function snapRangeToTicks(axis, ticks) { - if (axis.options.autoscaleMargin && ticks.length > 0) { - // snap to ticks - if (axis.options.min == null) - axis.min = Math.min(axis.min, ticks[0].v); - if (axis.options.max == null && ticks.length > 1) - axis.max = Math.max(axis.max, ticks[ticks.length - 1].v); - } - } - - function draw() { - - surface.clear(); - - executeHooks(hooks.drawBackground, [ctx]); - - var grid = options.grid; - - // draw background, if any - if (grid.show && grid.backgroundColor) - drawBackground(); - - if (grid.show && !grid.aboveData) { - drawGrid(); - } - - for (var i = 0; i < series.length; ++i) { - executeHooks(hooks.drawSeries, [ctx, series[i]]); - drawSeries(series[i]); - } - - executeHooks(hooks.draw, [ctx]); - - if (grid.show && grid.aboveData) { - drawGrid(); - } - - surface.render(); - - // A draw implies that either the axes or data have changed, so we - // should probably update the overlay highlights as well. - - triggerRedrawOverlay(); - } - - function extractRange(ranges, coord) { - var axis, from, to, key, axes = allAxes(); - - for (var i = 0; i < axes.length; ++i) { - axis = axes[i]; - if (axis.direction == coord) { - key = coord + axis.n + "axis"; - if (!ranges[key] && axis.n == 1) - key = coord + "axis"; // support x1axis as xaxis - if (ranges[key]) { - from = ranges[key].from; - to = ranges[key].to; - break; - } - } - } - - // backwards-compat stuff - to be removed in future - if (!ranges[key]) { - axis = coord == "x" ? xaxes[0] : yaxes[0]; - from = ranges[coord + "1"]; - to = ranges[coord + "2"]; - } - - // auto-reverse as an added bonus - if (from != null && to != null && from > to) { - var tmp = from; - from = to; - to = tmp; - } - - return { from: from, to: to, axis: axis }; - } - - function drawBackground() { - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); - ctx.fillRect(0, 0, plotWidth, plotHeight); - ctx.restore(); - } - - function drawGrid() { - var i, axes, bw, bc; - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - // draw markings - var markings = options.grid.markings; - if (markings) { - if ($.isFunction(markings)) { - axes = plot.getAxes(); - // xmin etc. is backwards compatibility, to be - // removed in the future - axes.xmin = axes.xaxis.min; - axes.xmax = axes.xaxis.max; - axes.ymin = axes.yaxis.min; - axes.ymax = axes.yaxis.max; - - markings = markings(axes); - } - - for (i = 0; i < markings.length; ++i) { - var m = markings[i], - xrange = extractRange(m, "x"), - yrange = extractRange(m, "y"); - - // fill in missing - if (xrange.from == null) - xrange.from = xrange.axis.min; - if (xrange.to == null) - xrange.to = xrange.axis.max; - if (yrange.from == null) - yrange.from = yrange.axis.min; - if (yrange.to == null) - yrange.to = yrange.axis.max; - - // clip - if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max || - yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) - continue; - - xrange.from = Math.max(xrange.from, xrange.axis.min); - xrange.to = Math.min(xrange.to, xrange.axis.max); - yrange.from = Math.max(yrange.from, yrange.axis.min); - yrange.to = Math.min(yrange.to, yrange.axis.max); - - var xequal = xrange.from === xrange.to, - yequal = yrange.from === yrange.to; - - if (xequal && yequal) { - continue; - } - - // then draw - xrange.from = Math.floor(xrange.axis.p2c(xrange.from)); - xrange.to = Math.floor(xrange.axis.p2c(xrange.to)); - yrange.from = Math.floor(yrange.axis.p2c(yrange.from)); - yrange.to = Math.floor(yrange.axis.p2c(yrange.to)); - - if (xequal || yequal) { - var lineWidth = m.lineWidth || options.grid.markingsLineWidth, - subPixel = lineWidth % 2 ? 0.5 : 0; - ctx.beginPath(); - ctx.strokeStyle = m.color || options.grid.markingsColor; - ctx.lineWidth = lineWidth; - if (xequal) { - ctx.moveTo(xrange.to + subPixel, yrange.from); - ctx.lineTo(xrange.to + subPixel, yrange.to); - } else { - ctx.moveTo(xrange.from, yrange.to + subPixel); - ctx.lineTo(xrange.to, yrange.to + subPixel); - } - ctx.stroke(); - } else { - ctx.fillStyle = m.color || options.grid.markingsColor; - ctx.fillRect(xrange.from, yrange.to, - xrange.to - xrange.from, - yrange.from - yrange.to); - } - } - } - - // draw the ticks - axes = allAxes(); - bw = options.grid.borderWidth; - - for (var j = 0; j < axes.length; ++j) { - var axis = axes[j], box = axis.box, - t = axis.tickLength, x, y, xoff, yoff; - if (!axis.show || axis.ticks.length == 0) - continue; - - ctx.lineWidth = 1; - - // find the edges - if (axis.direction == "x") { - x = 0; - if (t == "full") - y = (axis.position == "top" ? 0 : plotHeight); - else - y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0); - } - else { - y = 0; - if (t == "full") - x = (axis.position == "left" ? 0 : plotWidth); - else - x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0); - } - - // draw tick bar - if (!axis.innermost) { - ctx.strokeStyle = axis.options.color; - ctx.beginPath(); - xoff = yoff = 0; - if (axis.direction == "x") - xoff = plotWidth + 1; - else - yoff = plotHeight + 1; - - if (ctx.lineWidth == 1) { - if (axis.direction == "x") { - y = Math.floor(y) + 0.5; - } else { - x = Math.floor(x) + 0.5; - } - } - - ctx.moveTo(x, y); - ctx.lineTo(x + xoff, y + yoff); - ctx.stroke(); - } - - // draw ticks - - ctx.strokeStyle = axis.options.tickColor; - - ctx.beginPath(); - for (i = 0; i < axis.ticks.length; ++i) { - var v = axis.ticks[i].v; - - xoff = yoff = 0; - - if (isNaN(v) || v < axis.min || v > axis.max - // skip those lying on the axes if we got a border - || (t == "full" - && ((typeof bw == "object" && bw[axis.position] > 0) || bw > 0) - && (v == axis.min || v == axis.max))) - continue; - - if (axis.direction == "x") { - x = axis.p2c(v); - yoff = t == "full" ? -plotHeight : t; - - if (axis.position == "top") - yoff = -yoff; - } - else { - y = axis.p2c(v); - xoff = t == "full" ? -plotWidth : t; - - if (axis.position == "left") - xoff = -xoff; - } - - if (ctx.lineWidth == 1) { - if (axis.direction == "x") - x = Math.floor(x) + 0.5; - else - y = Math.floor(y) + 0.5; - } - - ctx.moveTo(x, y); - ctx.lineTo(x + xoff, y + yoff); - } - - ctx.stroke(); - } - - - // draw border - if (bw) { - // If either borderWidth or borderColor is an object, then draw the border - // line by line instead of as one rectangle - bc = options.grid.borderColor; - if(typeof bw == "object" || typeof bc == "object") { - if (typeof bw !== "object") { - bw = {top: bw, right: bw, bottom: bw, left: bw}; - } - if (typeof bc !== "object") { - bc = {top: bc, right: bc, bottom: bc, left: bc}; - } - - if (bw.top > 0) { - ctx.strokeStyle = bc.top; - ctx.lineWidth = bw.top; - ctx.beginPath(); - ctx.moveTo(0 - bw.left, 0 - bw.top/2); - ctx.lineTo(plotWidth, 0 - bw.top/2); - ctx.stroke(); - } - - if (bw.right > 0) { - ctx.strokeStyle = bc.right; - ctx.lineWidth = bw.right; - ctx.beginPath(); - ctx.moveTo(plotWidth + bw.right / 2, 0 - bw.top); - ctx.lineTo(plotWidth + bw.right / 2, plotHeight); - ctx.stroke(); - } - - if (bw.bottom > 0) { - ctx.strokeStyle = bc.bottom; - ctx.lineWidth = bw.bottom; - ctx.beginPath(); - ctx.moveTo(plotWidth + bw.right, plotHeight + bw.bottom / 2); - ctx.lineTo(0, plotHeight + bw.bottom / 2); - ctx.stroke(); - } - - if (bw.left > 0) { - ctx.strokeStyle = bc.left; - ctx.lineWidth = bw.left; - ctx.beginPath(); - ctx.moveTo(0 - bw.left/2, plotHeight + bw.bottom); - ctx.lineTo(0- bw.left/2, 0); - ctx.stroke(); - } - } - else { - ctx.lineWidth = bw; - ctx.strokeStyle = options.grid.borderColor; - ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw); - } - } - - ctx.restore(); - } - - function drawAxisLabels() { - - $.each(allAxes(), function (_, axis) { - var box = axis.box, - legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", - layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, - font = axis.options.font || "flot-tick-label tickLabel", - tick, x, y, halign, valign; - - // Remove text before checking for axis.show and ticks.length; - // otherwise plugins, like flot-tickrotor, that draw their own - // tick labels will end up with both theirs and the defaults. - - surface.removeText(layer); - - if (!axis.show || axis.ticks.length == 0) - return; - - for (var i = 0; i < axis.ticks.length; ++i) { - - tick = axis.ticks[i]; - if (!tick.label || tick.v < axis.min || tick.v > axis.max) - continue; - - if (axis.direction == "x") { - halign = "center"; - x = plotOffset.left + axis.p2c(tick.v); - if (axis.position == "bottom") { - y = box.top + box.padding; - } else { - y = box.top + box.height - box.padding; - valign = "bottom"; - } - } else { - valign = "middle"; - y = plotOffset.top + axis.p2c(tick.v); - if (axis.position == "left") { - x = box.left + box.width - box.padding; - halign = "right"; - } else { - x = box.left + box.padding; - } - } - - surface.addText(layer, x, y, tick.label, font, null, null, halign, valign); - } - }); - } - - function drawSeries(series) { - if (series.lines.show) - drawSeriesLines(series); - if (series.bars.show) - drawSeriesBars(series); - if (series.points.show) - drawSeriesPoints(series); - } - - function drawSeriesLines(series) { - function plotLine(datapoints, xoffset, yoffset, axisx, axisy) { - var points = datapoints.points, - ps = datapoints.pointsize, - prevx = null, prevy = null; - - ctx.beginPath(); - for (var i = ps; i < points.length; i += ps) { - var x1 = points[i - ps], y1 = points[i - ps + 1], - x2 = points[i], y2 = points[i + 1]; - - if (x1 == null || x2 == null) - continue; - - // clip with ymin - if (y1 <= y2 && y1 < axisy.min) { - if (y2 < axisy.min) - continue; // line segment is outside - // compute new intersection point - x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.min; - } - else if (y2 <= y1 && y2 < axisy.min) { - if (y1 < axisy.min) - continue; - x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.min; - } - - // clip with ymax - if (y1 >= y2 && y1 > axisy.max) { - if (y2 > axisy.max) - continue; - x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.max; - } - else if (y2 >= y1 && y2 > axisy.max) { - if (y1 > axisy.max) - continue; - x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.max; - } - - // clip with xmin - if (x1 <= x2 && x1 < axisx.min) { - if (x2 < axisx.min) - continue; - y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.min; - } - else if (x2 <= x1 && x2 < axisx.min) { - if (x1 < axisx.min) - continue; - y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.min; - } - - // clip with xmax - if (x1 >= x2 && x1 > axisx.max) { - if (x2 > axisx.max) - continue; - y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.max; - } - else if (x2 >= x1 && x2 > axisx.max) { - if (x1 > axisx.max) - continue; - y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.max; - } - - if (x1 != prevx || y1 != prevy) - ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); - - prevx = x2; - prevy = y2; - ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset); - } - ctx.stroke(); - } - - function plotLineArea(datapoints, axisx, axisy) { - var points = datapoints.points, - ps = datapoints.pointsize, - bottom = Math.min(Math.max(0, axisy.min), axisy.max), - i = 0, top, areaOpen = false, - ypos = 1, segmentStart = 0, segmentEnd = 0; - - // we process each segment in two turns, first forward - // direction to sketch out top, then once we hit the - // end we go backwards to sketch the bottom - while (true) { - if (ps > 0 && i > points.length + ps) - break; - - i += ps; // ps is negative if going backwards - - var x1 = points[i - ps], - y1 = points[i - ps + ypos], - x2 = points[i], y2 = points[i + ypos]; - - if (areaOpen) { - if (ps > 0 && x1 != null && x2 == null) { - // at turning point - segmentEnd = i; - ps = -ps; - ypos = 2; - continue; - } - - if (ps < 0 && i == segmentStart + ps) { - // done with the reverse sweep - ctx.fill(); - areaOpen = false; - ps = -ps; - ypos = 1; - i = segmentStart = segmentEnd + ps; - continue; - } - } - - if (x1 == null || x2 == null) - continue; - - // clip x values - - // clip with xmin - if (x1 <= x2 && x1 < axisx.min) { - if (x2 < axisx.min) - continue; - y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.min; - } - else if (x2 <= x1 && x2 < axisx.min) { - if (x1 < axisx.min) - continue; - y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.min; - } - - // clip with xmax - if (x1 >= x2 && x1 > axisx.max) { - if (x2 > axisx.max) - continue; - y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.max; - } - else if (x2 >= x1 && x2 > axisx.max) { - if (x1 > axisx.max) - continue; - y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.max; - } - - if (!areaOpen) { - // open area - ctx.beginPath(); - ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom)); - areaOpen = true; - } - - // now first check the case where both is outside - if (y1 >= axisy.max && y2 >= axisy.max) { - ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); - continue; - } - else if (y1 <= axisy.min && y2 <= axisy.min) { - ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); - continue; - } - - // else it's a bit more complicated, there might - // be a flat maxed out rectangle first, then a - // triangular cutout or reverse; to find these - // keep track of the current x values - var x1old = x1, x2old = x2; - - // clip the y values, without shortcutting, we - // go through all cases in turn - - // clip with ymin - if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { - x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.min; - } - else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { - x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.min; - } - - // clip with ymax - if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { - x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.max; - } - else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { - x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.max; - } - - // if the x value was changed we got a rectangle - // to fill - if (x1 != x1old) { - ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1)); - // it goes to (x1, y1), but we fill that below - } - - // fill triangular section, this sometimes result - // in redundant points if (x1, y1) hasn't changed - // from previous line to, but we just ignore that - ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); - - // fill the other rectangle if it's there - if (x2 != x2old) { - ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); - ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2)); - } - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - ctx.lineJoin = "round"; - - var lw = series.lines.lineWidth, - sw = series.shadowSize; - // FIXME: consider another form of shadow when filling is turned on - if (lw > 0 && sw > 0) { - // draw shadow as a thick and thin line with transparency - ctx.lineWidth = sw; - ctx.strokeStyle = "rgba(0,0,0,0.1)"; - // position shadow at angle from the mid of line - var angle = Math.PI/18; - plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis); - ctx.lineWidth = sw/2; - plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis); - } - - ctx.lineWidth = lw; - ctx.strokeStyle = series.color; - var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight); - if (fillStyle) { - ctx.fillStyle = fillStyle; - plotLineArea(series.datapoints, series.xaxis, series.yaxis); - } - - if (lw > 0) - plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis); - ctx.restore(); - } - - function drawSeriesPoints(series) { - function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) { - var points = datapoints.points, ps = datapoints.pointsize; - - for (var i = 0; i < points.length; i += ps) { - var x = points[i], y = points[i + 1]; - if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) - continue; - - ctx.beginPath(); - x = axisx.p2c(x); - y = axisy.p2c(y) + offset; - if (symbol == "circle") - ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false); - else - symbol(ctx, x, y, radius, shadow); - ctx.closePath(); - - if (fillStyle) { - ctx.fillStyle = fillStyle; - ctx.fill(); - } - ctx.stroke(); - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - var lw = series.points.lineWidth, - sw = series.shadowSize, - radius = series.points.radius, - symbol = series.points.symbol; - - // If the user sets the line width to 0, we change it to a very - // small value. A line width of 0 seems to force the default of 1. - // Doing the conditional here allows the shadow setting to still be - // optional even with a lineWidth of 0. - - if( lw == 0 ) - lw = 0.0001; - - if (lw > 0 && sw > 0) { - // draw shadow in two steps - var w = sw / 2; - ctx.lineWidth = w; - ctx.strokeStyle = "rgba(0,0,0,0.1)"; - plotPoints(series.datapoints, radius, null, w + w/2, true, - series.xaxis, series.yaxis, symbol); - - ctx.strokeStyle = "rgba(0,0,0,0.2)"; - plotPoints(series.datapoints, radius, null, w/2, true, - series.xaxis, series.yaxis, symbol); - } - - ctx.lineWidth = lw; - ctx.strokeStyle = series.color; - plotPoints(series.datapoints, radius, - getFillStyle(series.points, series.color), 0, false, - series.xaxis, series.yaxis, symbol); - ctx.restore(); - } - - function drawBar(x, y, b, barLeft, barRight, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) { - var left, right, bottom, top, - drawLeft, drawRight, drawTop, drawBottom, - tmp; - - // in horizontal mode, we start the bar from the left - // instead of from the bottom so it appears to be - // horizontal rather than vertical - if (horizontal) { - drawBottom = drawRight = drawTop = true; - drawLeft = false; - left = b; - right = x; - top = y + barLeft; - bottom = y + barRight; - - // account for negative bars - if (right < left) { - tmp = right; - right = left; - left = tmp; - drawLeft = true; - drawRight = false; - } - } - else { - drawLeft = drawRight = drawTop = true; - drawBottom = false; - left = x + barLeft; - right = x + barRight; - bottom = b; - top = y; - - // account for negative bars - if (top < bottom) { - tmp = top; - top = bottom; - bottom = tmp; - drawBottom = true; - drawTop = false; - } - } - - // clip - if (right < axisx.min || left > axisx.max || - top < axisy.min || bottom > axisy.max) - return; - - if (left < axisx.min) { - left = axisx.min; - drawLeft = false; - } - - if (right > axisx.max) { - right = axisx.max; - drawRight = false; - } - - if (bottom < axisy.min) { - bottom = axisy.min; - drawBottom = false; - } - - if (top > axisy.max) { - top = axisy.max; - drawTop = false; - } - - left = axisx.p2c(left); - bottom = axisy.p2c(bottom); - right = axisx.p2c(right); - top = axisy.p2c(top); - - // fill the bar - if (fillStyleCallback) { - c.fillStyle = fillStyleCallback(bottom, top); - c.fillRect(left, top, right - left, bottom - top) - } - - // draw outline - if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) { - c.beginPath(); - - // FIXME: inline moveTo is buggy with excanvas - c.moveTo(left, bottom); - if (drawLeft) - c.lineTo(left, top); - else - c.moveTo(left, top); - if (drawTop) - c.lineTo(right, top); - else - c.moveTo(right, top); - if (drawRight) - c.lineTo(right, bottom); - else - c.moveTo(right, bottom); - if (drawBottom) - c.lineTo(left, bottom); - else - c.moveTo(left, bottom); - c.stroke(); - } - } - - function drawSeriesBars(series) { - function plotBars(datapoints, barLeft, barRight, fillStyleCallback, axisx, axisy) { - var points = datapoints.points, ps = datapoints.pointsize; - - for (var i = 0; i < points.length; i += ps) { - if (points[i] == null) - continue; - drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth); - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - // FIXME: figure out a way to add shadows (for instance along the right edge) - ctx.lineWidth = series.bars.lineWidth; - ctx.strokeStyle = series.color; - - var barLeft; - - switch (series.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -series.bars.barWidth; - break; - default: - barLeft = -series.bars.barWidth / 2; - } - - var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null; - plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, fillStyleCallback, series.xaxis, series.yaxis); - ctx.restore(); - } - - function getFillStyle(filloptions, seriesColor, bottom, top) { - var fill = filloptions.fill; - if (!fill) - return null; - - if (filloptions.fillColor) - return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor); - - var c = $.color.parse(seriesColor); - c.a = typeof fill == "number" ? fill : 0.4; - c.normalize(); - return c.toString(); - } - - function insertLegend() { - - if (options.legend.container != null) { - $(options.legend.container).html(""); - } else { - placeholder.find(".legend").remove(); - } - - if (!options.legend.show) { - return; - } - - var fragments = [], entries = [], rowStarted = false, - lf = options.legend.labelFormatter, s, label; - - // Build a list of legend entries, with each having a label and a color - - for (var i = 0; i < series.length; ++i) { - s = series[i]; - if (s.label) { - label = lf ? lf(s.label, s) : s.label; - if (label) { - entries.push({ - label: label, - color: s.color - }); - } - } - } - - // Sort the legend using either the default or a custom comparator - - if (options.legend.sorted) { - if ($.isFunction(options.legend.sorted)) { - entries.sort(options.legend.sorted); - } else if (options.legend.sorted == "reverse") { - entries.reverse(); - } else { - var ascending = options.legend.sorted != "descending"; - entries.sort(function(a, b) { - return a.label == b.label ? 0 : ( - (a.label < b.label) != ascending ? 1 : -1 // Logical XOR - ); - }); - } - } - - // Generate markup for the list of entries, in their final order - - for (var i = 0; i < entries.length; ++i) { - - var entry = entries[i]; - - if (i % options.legend.noColumns == 0) { - if (rowStarted) - fragments.push(''); - fragments.push(''); - rowStarted = true; - } - - fragments.push( - '
' + - '' + entry.label + '' - ); - } - - if (rowStarted) - fragments.push(''); - - if (fragments.length == 0) - return; - - var table = '' + fragments.join("") + '
'; - if (options.legend.container != null) - $(options.legend.container).html(table); - else { - var pos = "", - p = options.legend.position, - m = options.legend.margin; - if (m[0] == null) - m = [m, m]; - if (p.charAt(0) == "n") - pos += 'top:' + (m[1] + plotOffset.top) + 'px;'; - else if (p.charAt(0) == "s") - pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;'; - if (p.charAt(1) == "e") - pos += 'right:' + (m[0] + plotOffset.right) + 'px;'; - else if (p.charAt(1) == "w") - pos += 'left:' + (m[0] + plotOffset.left) + 'px;'; - var legend = $('
' + table.replace('style="', 'style="position:absolute;' + pos +';') + '
').appendTo(placeholder); - if (options.legend.backgroundOpacity != 0.0) { - // put in the transparent background - // separately to avoid blended labels and - // label boxes - var c = options.legend.backgroundColor; - if (c == null) { - c = options.grid.backgroundColor; - if (c && typeof c == "string") - c = $.color.parse(c); - else - c = $.color.extract(legend, 'background-color'); - c.a = 1; - c = c.toString(); - } - var div = legend.children(); - $('
').prependTo(legend).css('opacity', options.legend.backgroundOpacity); - } - } - } - - - // interactive features - - var highlights = [], - redrawTimeout = null; - - // returns the data item the mouse is over, or null if none is found - function findNearbyItem(mouseX, mouseY, seriesFilter) { - var maxDistance = options.grid.mouseActiveRadius, - smallestDistance = maxDistance * maxDistance + 1, - item = null, foundPoint = false, i, j, ps; - - for (i = series.length - 1; i >= 0; --i) { - if (!seriesFilter(series[i])) - continue; - - var s = series[i], - axisx = s.xaxis, - axisy = s.yaxis, - points = s.datapoints.points, - mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster - my = axisy.c2p(mouseY), - maxx = maxDistance / axisx.scale, - maxy = maxDistance / axisy.scale; - - ps = s.datapoints.pointsize; - // with inverse transforms, we can't use the maxx/maxy - // optimization, sadly - if (axisx.options.inverseTransform) - maxx = Number.MAX_VALUE; - if (axisy.options.inverseTransform) - maxy = Number.MAX_VALUE; - - if (s.lines.show || s.points.show) { - for (j = 0; j < points.length; j += ps) { - var x = points[j], y = points[j + 1]; - if (x == null) - continue; - - // For points and lines, the cursor must be within a - // certain distance to the data point - if (x - mx > maxx || x - mx < -maxx || - y - my > maxy || y - my < -maxy) - continue; - - // We have to calculate distances in pixels, not in - // data units, because the scales of the axes may be different - var dx = Math.abs(axisx.p2c(x) - mouseX), - dy = Math.abs(axisy.p2c(y) - mouseY), - dist = dx * dx + dy * dy; // we save the sqrt - - // use <= to ensure last point takes precedence - // (last generally means on top of) - if (dist < smallestDistance) { - smallestDistance = dist; - item = [i, j / ps]; - } - } - } - - if (s.bars.show && !item) { // no other point can be nearby - - var barLeft, barRight; - - switch (s.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -s.bars.barWidth; - break; - default: - barLeft = -s.bars.barWidth / 2; - } - - barRight = barLeft + s.bars.barWidth; - - for (j = 0; j < points.length; j += ps) { - var x = points[j], y = points[j + 1], b = points[j + 2]; - if (x == null) - continue; - - // for a bar graph, the cursor must be inside the bar - if (series[i].bars.horizontal ? - (mx <= Math.max(b, x) && mx >= Math.min(b, x) && - my >= y + barLeft && my <= y + barRight) : - (mx >= x + barLeft && mx <= x + barRight && - my >= Math.min(b, y) && my <= Math.max(b, y))) - item = [i, j / ps]; - } - } - } - - if (item) { - i = item[0]; - j = item[1]; - ps = series[i].datapoints.pointsize; - - return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps), - dataIndex: j, - series: series[i], - seriesIndex: i }; - } - - return null; - } - - function onMouseMove(e) { - if (options.grid.hoverable) - triggerClickHoverEvent("plothover", e, - function (s) { return s["hoverable"] != false; }); - } - - function onMouseLeave(e) { - if (options.grid.hoverable) - triggerClickHoverEvent("plothover", e, - function (s) { return false; }); - } - - function onClick(e) { - triggerClickHoverEvent("plotclick", e, - function (s) { return s["clickable"] != false; }); - } - - // trigger click or hover event (they send the same parameters - // so we share their code) - function triggerClickHoverEvent(eventname, event, seriesFilter) { - var offset = eventHolder.offset(), - canvasX = event.pageX - offset.left - plotOffset.left, - canvasY = event.pageY - offset.top - plotOffset.top, - pos = canvasToAxisCoords({ left: canvasX, top: canvasY }); - - pos.pageX = event.pageX; - pos.pageY = event.pageY; - - var item = findNearbyItem(canvasX, canvasY, seriesFilter); - - if (item) { - // fill in mouse pos for any listeners out there - item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left, 10); - item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top, 10); - } - - if (options.grid.autoHighlight) { - // clear auto-highlights - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.auto == eventname && - !(item && h.series == item.series && - h.point[0] == item.datapoint[0] && - h.point[1] == item.datapoint[1])) - unhighlight(h.series, h.point); - } - - if (item) - highlight(item.series, item.datapoint, eventname); - } - - placeholder.trigger(eventname, [ pos, item ]); - } - - function triggerRedrawOverlay() { - var t = options.interaction.redrawOverlayInterval; - if (t == -1) { // skip event queue - drawOverlay(); - return; - } - - if (!redrawTimeout) - redrawTimeout = setTimeout(drawOverlay, t); - } - - function drawOverlay() { - redrawTimeout = null; - - // draw highlights - octx.save(); - overlay.clear(); - octx.translate(plotOffset.left, plotOffset.top); - - var i, hi; - for (i = 0; i < highlights.length; ++i) { - hi = highlights[i]; - - if (hi.series.bars.show) - drawBarHighlight(hi.series, hi.point); - else - drawPointHighlight(hi.series, hi.point); - } - octx.restore(); - - executeHooks(hooks.drawOverlay, [octx]); - } - - function highlight(s, point, auto) { - if (typeof s == "number") - s = series[s]; - - if (typeof point == "number") { - var ps = s.datapoints.pointsize; - point = s.datapoints.points.slice(ps * point, ps * (point + 1)); - } - - var i = indexOfHighlight(s, point); - if (i == -1) { - highlights.push({ series: s, point: point, auto: auto }); - - triggerRedrawOverlay(); - } - else if (!auto) - highlights[i].auto = false; - } - - function unhighlight(s, point) { - if (s == null && point == null) { - highlights = []; - triggerRedrawOverlay(); - return; - } - - if (typeof s == "number") - s = series[s]; - - if (typeof point == "number") { - var ps = s.datapoints.pointsize; - point = s.datapoints.points.slice(ps * point, ps * (point + 1)); - } - - var i = indexOfHighlight(s, point); - if (i != -1) { - highlights.splice(i, 1); - - triggerRedrawOverlay(); - } - } - - function indexOfHighlight(s, p) { - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.series == s && h.point[0] == p[0] - && h.point[1] == p[1]) - return i; - } - return -1; - } - - function drawPointHighlight(series, point) { - var x = point[0], y = point[1], - axisx = series.xaxis, axisy = series.yaxis, - highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(); - - if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) - return; - - var pointRadius = series.points.radius + series.points.lineWidth / 2; - octx.lineWidth = pointRadius; - octx.strokeStyle = highlightColor; - var radius = 1.5 * pointRadius; - x = axisx.p2c(x); - y = axisy.p2c(y); - - octx.beginPath(); - if (series.points.symbol == "circle") - octx.arc(x, y, radius, 0, 2 * Math.PI, false); - else - series.points.symbol(octx, x, y, radius, false); - octx.closePath(); - octx.stroke(); - } - - function drawBarHighlight(series, point) { - var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(), - fillStyle = highlightColor, - barLeft; - - switch (series.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -series.bars.barWidth; - break; - default: - barLeft = -series.bars.barWidth / 2; - } - - octx.lineWidth = series.bars.lineWidth; - octx.strokeStyle = highlightColor; - - drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth, - function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth); - } - - function getColorOrGradient(spec, bottom, top, defaultColor) { - if (typeof spec == "string") - return spec; - else { - // assume this is a gradient spec; IE currently only - // supports a simple vertical gradient properly, so that's - // what we support too - var gradient = ctx.createLinearGradient(0, top, 0, bottom); - - for (var i = 0, l = spec.colors.length; i < l; ++i) { - var c = spec.colors[i]; - if (typeof c != "string") { - var co = $.color.parse(defaultColor); - if (c.brightness != null) - co = co.scale('rgb', c.brightness); - if (c.opacity != null) - co.a *= c.opacity; - c = co.toString(); - } - gradient.addColorStop(i / (l - 1), c); - } - - return gradient; - } - } - } - - // Add the plot function to the top level of the jQuery object - - $.plot = function(placeholder, data, options) { - //var t0 = new Date(); - var plot = new Plot($(placeholder), data, options, $.plot.plugins); - //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime())); - return plot; - }; - - $.plot.version = "0.8.3"; - - $.plot.plugins = []; - - // Also add the plot function as a chainable property - - $.fn.plot = function(data, options) { - return this.each(function() { - $.plot(this, data, options); - }); - }; - - // round to nearby lower multiple of base - function floorInBase(n, base) { - return base * Math.floor(n / base); - } - -})(jQuery); diff --git a/src/plugins/vis_type_timelion/public/flot/jquery.flot.selection.js b/src/plugins/vis_type_timelion/public/flot/jquery.flot.selection.js deleted file mode 100644 index c8707b30f4e6f..0000000000000 --- a/src/plugins/vis_type_timelion/public/flot/jquery.flot.selection.js +++ /dev/null @@ -1,360 +0,0 @@ -/* Flot plugin for selecting regions of a plot. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin supports these options: - -selection: { - mode: null or "x" or "y" or "xy", - color: color, - shape: "round" or "miter" or "bevel", - minSize: number of pixels -} - -Selection support is enabled by setting the mode to one of "x", "y" or "xy". -In "x" mode, the user will only be able to specify the x range, similarly for -"y" mode. For "xy", the selection becomes a rectangle where both ranges can be -specified. "color" is color of the selection (if you need to change the color -later on, you can get to it with plot.getOptions().selection.color). "shape" -is the shape of the corners of the selection. - -"minSize" is the minimum size a selection can be in pixels. This value can -be customized to determine the smallest size a selection can be and still -have the selection rectangle be displayed. When customizing this value, the -fact that it refers to pixels, not axis units must be taken into account. -Thus, for example, if there is a bar graph in time mode with BarWidth set to 1 -minute, setting "minSize" to 1 will not make the minimum selection size 1 -minute, but rather 1 pixel. Note also that setting "minSize" to 0 will prevent -"plotunselected" events from being fired when the user clicks the mouse without -dragging. - -When selection support is enabled, a "plotselected" event will be emitted on -the DOM element you passed into the plot function. The event handler gets a -parameter with the ranges selected on the axes, like this: - - placeholder.bind( "plotselected", function( event, ranges ) { - alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to) - // similar for yaxis - with multiple axes, the extra ones are in - // x2axis, x3axis, ... - }); - -The "plotselected" event is only fired when the user has finished making the -selection. A "plotselecting" event is fired during the process with the same -parameters as the "plotselected" event, in case you want to know what's -happening while it's happening, - -A "plotunselected" event with no arguments is emitted when the user clicks the -mouse to remove the selection. As stated above, setting "minSize" to 0 will -destroy this behavior. - -The plugin also adds the following methods to the plot object: - -- setSelection( ranges, preventEvent ) - - Set the selection rectangle. The passed in ranges is on the same form as - returned in the "plotselected" event. If the selection mode is "x", you - should put in either an xaxis range, if the mode is "y" you need to put in - an yaxis range and both xaxis and yaxis if the selection mode is "xy", like - this: - - setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } }); - - setSelection will trigger the "plotselected" event when called. If you don't - want that to happen, e.g. if you're inside a "plotselected" handler, pass - true as the second parameter. If you are using multiple axes, you can - specify the ranges on any of those, e.g. as x2axis/x3axis/... instead of - xaxis, the plugin picks the first one it sees. - -- clearSelection( preventEvent ) - - Clear the selection rectangle. Pass in true to avoid getting a - "plotunselected" event. - -- getSelection() - - Returns the current selection in the same format as the "plotselected" - event. If there's currently no selection, the function returns null. - -*/ - -(function ($) { - function init(plot) { - var selection = { - first: { x: -1, y: -1}, second: { x: -1, y: -1}, - show: false, - active: false - }; - - // FIXME: The drag handling implemented here should be - // abstracted out, there's some similar code from a library in - // the navigation plugin, this should be massaged a bit to fit - // the Flot cases here better and reused. Doing this would - // make this plugin much slimmer. - var savedhandlers = {}; - - var mouseUpHandler = null; - - function onMouseMove(e) { - if (selection.active) { - updateSelection(e); - - plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]); - } - } - - function onMouseDown(e) { - if (e.which != 1) // only accept left-click - return; - - // cancel out any text selections - document.body.focus(); - - // prevent text selection and drag in old-school browsers - if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) { - savedhandlers.onselectstart = document.onselectstart; - document.onselectstart = function () { return false; }; - } - if (document.ondrag !== undefined && savedhandlers.ondrag == null) { - savedhandlers.ondrag = document.ondrag; - document.ondrag = function () { return false; }; - } - - setSelectionPos(selection.first, e); - - selection.active = true; - - // this is a bit silly, but we have to use a closure to be - // able to whack the same handler again - mouseUpHandler = function (e) { onMouseUp(e); }; - - $(document).one("mouseup", mouseUpHandler); - } - - function onMouseUp(e) { - mouseUpHandler = null; - - // revert drag stuff for old-school browsers - if (document.onselectstart !== undefined) - document.onselectstart = savedhandlers.onselectstart; - if (document.ondrag !== undefined) - document.ondrag = savedhandlers.ondrag; - - // no more dragging - selection.active = false; - updateSelection(e); - - if (selectionIsSane()) - triggerSelectedEvent(); - else { - // this counts as a clear - plot.getPlaceholder().trigger("plotunselected", [ ]); - plot.getPlaceholder().trigger("plotselecting", [ null ]); - } - - return false; - } - - function getSelection() { - if (!selectionIsSane()) - return null; - - if (!selection.show) return null; - - var r = {}, c1 = selection.first, c2 = selection.second; - $.each(plot.getAxes(), function (name, axis) { - if (axis.used) { - var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]); - r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) }; - } - }); - return r; - } - - function triggerSelectedEvent() { - var r = getSelection(); - - plot.getPlaceholder().trigger("plotselected", [ r ]); - - // backwards-compat stuff, to be removed in future - if (r.xaxis && r.yaxis) - plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]); - } - - function clamp(min, value, max) { - return value < min ? min: (value > max ? max: value); - } - - function setSelectionPos(pos, e) { - var o = plot.getOptions(); - var offset = plot.getPlaceholder().offset(); - var plotOffset = plot.getPlotOffset(); - pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width()); - pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height()); - - if (o.selection.mode == "y") - pos.x = pos == selection.first ? 0 : plot.width(); - - if (o.selection.mode == "x") - pos.y = pos == selection.first ? 0 : plot.height(); - } - - function updateSelection(pos) { - if (pos.pageX == null) - return; - - setSelectionPos(selection.second, pos); - if (selectionIsSane()) { - selection.show = true; - plot.triggerRedrawOverlay(); - } - else - clearSelection(true); - } - - function clearSelection(preventEvent) { - if (selection.show) { - selection.show = false; - plot.triggerRedrawOverlay(); - if (!preventEvent) - plot.getPlaceholder().trigger("plotunselected", [ ]); - } - } - - // function taken from markings support in Flot - function extractRange(ranges, coord) { - var axis, from, to, key, axes = plot.getAxes(); - - for (var k in axes) { - axis = axes[k]; - if (axis.direction == coord) { - key = coord + axis.n + "axis"; - if (!ranges[key] && axis.n == 1) - key = coord + "axis"; // support x1axis as xaxis - if (ranges[key]) { - from = ranges[key].from; - to = ranges[key].to; - break; - } - } - } - - // backwards-compat stuff - to be removed in future - if (!ranges[key]) { - axis = coord == "x" ? plot.getXAxes()[0] : plot.getYAxes()[0]; - from = ranges[coord + "1"]; - to = ranges[coord + "2"]; - } - - // auto-reverse as an added bonus - if (from != null && to != null && from > to) { - var tmp = from; - from = to; - to = tmp; - } - - return { from: from, to: to, axis: axis }; - } - - function setSelection(ranges, preventEvent) { - var axis, range, o = plot.getOptions(); - - if (o.selection.mode == "y") { - selection.first.x = 0; - selection.second.x = plot.width(); - } - else { - range = extractRange(ranges, "x"); - - selection.first.x = range.axis.p2c(range.from); - selection.second.x = range.axis.p2c(range.to); - } - - if (o.selection.mode == "x") { - selection.first.y = 0; - selection.second.y = plot.height(); - } - else { - range = extractRange(ranges, "y"); - - selection.first.y = range.axis.p2c(range.from); - selection.second.y = range.axis.p2c(range.to); - } - - selection.show = true; - plot.triggerRedrawOverlay(); - if (!preventEvent && selectionIsSane()) - triggerSelectedEvent(); - } - - function selectionIsSane() { - var minSize = plot.getOptions().selection.minSize; - return Math.abs(selection.second.x - selection.first.x) >= minSize && - Math.abs(selection.second.y - selection.first.y) >= minSize; - } - - plot.clearSelection = clearSelection; - plot.setSelection = setSelection; - plot.getSelection = getSelection; - - plot.hooks.bindEvents.push(function(plot, eventHolder) { - var o = plot.getOptions(); - if (o.selection.mode != null) { - eventHolder.mousemove(onMouseMove); - eventHolder.mousedown(onMouseDown); - } - }); - - - plot.hooks.drawOverlay.push(function (plot, ctx) { - // draw selection - if (selection.show && selectionIsSane()) { - var plotOffset = plot.getPlotOffset(); - var o = plot.getOptions(); - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - var c = $.color.parse(o.selection.color); - - ctx.strokeStyle = c.scale('a', 0.8).toString(); - ctx.lineWidth = 1; - ctx.lineJoin = o.selection.shape; - ctx.fillStyle = c.scale('a', 0.4).toString(); - - var x = Math.min(selection.first.x, selection.second.x) + 0.5, - y = Math.min(selection.first.y, selection.second.y) + 0.5, - w = Math.abs(selection.second.x - selection.first.x) - 1, - h = Math.abs(selection.second.y - selection.first.y) - 1; - - ctx.fillRect(x, y, w, h); - ctx.strokeRect(x, y, w, h); - - ctx.restore(); - } - }); - - plot.hooks.shutdown.push(function (plot, eventHolder) { - eventHolder.unbind("mousemove", onMouseMove); - eventHolder.unbind("mousedown", onMouseDown); - - if (mouseUpHandler) - $(document).unbind("mouseup", mouseUpHandler); - }); - - } - - $.plot.plugins.push({ - init: init, - options: { - selection: { - mode: null, // one of null, "x", "y" or "xy" - color: "#e8cfac", - shape: "round", // one of "round", "miter", or "bevel" - minSize: 5 // minimum number of pixels - } - }, - name: 'selection', - version: '1.1' - }); -})(jQuery); diff --git a/src/plugins/vis_type_timelion/public/flot/jquery.flot.stack.js b/src/plugins/vis_type_timelion/public/flot/jquery.flot.stack.js deleted file mode 100644 index 0d91c0f3c0160..0000000000000 --- a/src/plugins/vis_type_timelion/public/flot/jquery.flot.stack.js +++ /dev/null @@ -1,188 +0,0 @@ -/* Flot plugin for stacking data sets rather than overlaying them. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin assumes the data is sorted on x (or y if stacking horizontally). -For line charts, it is assumed that if a line has an undefined gap (from a -null point), then the line above it should have the same gap - insert zeros -instead of "null" if you want another behaviour. This also holds for the start -and end of the chart. Note that stacking a mix of positive and negative values -in most instances doesn't make sense (so it looks weird). - -Two or more series are stacked when their "stack" attribute is set to the same -key (which can be any number or string or just "true"). To specify the default -stack, you can set the stack option like this: - - series: { - stack: null/false, true, or a key (number/string) - } - -You can also specify it for a single series, like this: - - $.plot( $("#placeholder"), [{ - data: [ ... ], - stack: true - }]) - -The stacking order is determined by the order of the data series in the array -(later series end up on top of the previous). - -Internally, the plugin modifies the datapoints in each series, adding an -offset to the y value. For line series, extra data points are inserted through -interpolation. If there's a second y value, it's also adjusted (e.g for bar -charts or filled areas). - -*/ - -(function ($) { - var options = { - series: { stack: null } // or number/string - }; - - function init(plot) { - function findMatchingSeries(s, allseries) { - var res = null; - for (var i = 0; i < allseries.length; ++i) { - if (s == allseries[i]) - break; - - if (allseries[i].stack == s.stack) - res = allseries[i]; - } - - return res; - } - - function stackData(plot, s, datapoints) { - if (s.stack == null || s.stack === false) - return; - - var other = findMatchingSeries(s, plot.getData()); - if (!other) - return; - - var ps = datapoints.pointsize, - points = datapoints.points, - otherps = other.datapoints.pointsize, - otherpoints = other.datapoints.points, - newpoints = [], - px, py, intery, qx, qy, bottom, - withlines = s.lines.show, - horizontal = s.bars.horizontal, - withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y), - withsteps = withlines && s.lines.steps, - fromgap = true, - keyOffset = horizontal ? 1 : 0, - accumulateOffset = horizontal ? 0 : 1, - i = 0, j = 0, l, m; - - while (true) { - if (i >= points.length) - break; - - l = newpoints.length; - - if (points[i] == null) { - // copy gaps - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - i += ps; - } - else if (j >= otherpoints.length) { - // for lines, we can't use the rest of the points - if (!withlines) { - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - } - i += ps; - } - else if (otherpoints[j] == null) { - // oops, got a gap - for (m = 0; m < ps; ++m) - newpoints.push(null); - fromgap = true; - j += otherps; - } - else { - // cases where we actually got two points - px = points[i + keyOffset]; - py = points[i + accumulateOffset]; - qx = otherpoints[j + keyOffset]; - qy = otherpoints[j + accumulateOffset]; - bottom = 0; - - if (px == qx) { - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - - newpoints[l + accumulateOffset] += qy; - bottom = qy; - - i += ps; - j += otherps; - } - else if (px > qx) { - // we got past point below, might need to - // insert interpolated extra point - if (withlines && i > 0 && points[i - ps] != null) { - intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); - newpoints.push(qx); - newpoints.push(intery + qy); - for (m = 2; m < ps; ++m) - newpoints.push(points[i + m]); - bottom = qy; - } - - j += otherps; - } - else { // px < qx - if (fromgap && withlines) { - // if we come from a gap, we just skip this point - i += ps; - continue; - } - - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - - // we might be able to interpolate a point below, - // this can give us a better y - if (withlines && j > 0 && otherpoints[j - otherps] != null) - bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx); - - newpoints[l + accumulateOffset] += bottom; - - i += ps; - } - - fromgap = false; - - if (l != newpoints.length && withbottom) - newpoints[l + 2] += bottom; - } - - // maintain the line steps invariant - if (withsteps && l != newpoints.length && l > 0 - && newpoints[l] != null - && newpoints[l] != newpoints[l - ps] - && newpoints[l + 1] != newpoints[l - ps + 1]) { - for (m = 0; m < ps; ++m) - newpoints[l + ps + m] = newpoints[l + m]; - newpoints[l + 1] = newpoints[l - ps + 1]; - } - } - - datapoints.points = newpoints; - } - - plot.hooks.processDatapoints.push(stackData); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'stack', - version: '1.2' - }); -})(jQuery); diff --git a/src/plugins/vis_type_timelion/public/flot/jquery.flot.symbol.js b/src/plugins/vis_type_timelion/public/flot/jquery.flot.symbol.js deleted file mode 100644 index 79f634971b6fa..0000000000000 --- a/src/plugins/vis_type_timelion/public/flot/jquery.flot.symbol.js +++ /dev/null @@ -1,71 +0,0 @@ -/* Flot plugin that adds some extra symbols for plotting points. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The symbols are accessed as strings through the standard symbol options: - - series: { - points: { - symbol: "square" // or "diamond", "triangle", "cross" - } - } - -*/ - -(function ($) { - function processRawData(plot, series, datapoints) { - // we normalize the area of each symbol so it is approximately the - // same as a circle of the given radius - - var handlers = { - square: function (ctx, x, y, radius, shadow) { - // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 - var size = radius * Math.sqrt(Math.PI) / 2; - ctx.rect(x - size, y - size, size + size, size + size); - }, - diamond: function (ctx, x, y, radius, shadow) { - // pi * r^2 = 2s^2 => s = r * sqrt(pi/2) - var size = radius * Math.sqrt(Math.PI / 2); - ctx.moveTo(x - size, y); - ctx.lineTo(x, y - size); - ctx.lineTo(x + size, y); - ctx.lineTo(x, y + size); - ctx.lineTo(x - size, y); - }, - triangle: function (ctx, x, y, radius, shadow) { - // pi * r^2 = 1/2 * s^2 * sin (pi / 3) => s = r * sqrt(2 * pi / sin(pi / 3)) - var size = radius * Math.sqrt(2 * Math.PI / Math.sin(Math.PI / 3)); - var height = size * Math.sin(Math.PI / 3); - ctx.moveTo(x - size/2, y + height/2); - ctx.lineTo(x + size/2, y + height/2); - if (!shadow) { - ctx.lineTo(x, y - height/2); - ctx.lineTo(x - size/2, y + height/2); - } - }, - cross: function (ctx, x, y, radius, shadow) { - // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 - var size = radius * Math.sqrt(Math.PI) / 2; - ctx.moveTo(x - size, y - size); - ctx.lineTo(x + size, y + size); - ctx.moveTo(x - size, y + size); - ctx.lineTo(x + size, y - size); - } - }; - - var s = series.points.symbol; - if (handlers[s]) - series.points.symbol = handlers[s]; - } - - function init(plot) { - plot.hooks.processDatapoints.push(processRawData); - } - - $.plot.plugins.push({ - init: init, - name: 'symbols', - version: '1.0' - }); -})(jQuery); diff --git a/src/plugins/vis_type_timelion/public/flot/jquery.flot.time.js b/src/plugins/vis_type_timelion/public/flot/jquery.flot.time.js deleted file mode 100644 index 34c1d121259a2..0000000000000 --- a/src/plugins/vis_type_timelion/public/flot/jquery.flot.time.js +++ /dev/null @@ -1,432 +0,0 @@ -/* Pretty handling of time axes. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -Set axis.mode to "time" to enable. See the section "Time series data" in -API.txt for details. - -*/ - -(function($) { - - var options = { - xaxis: { - timezone: null, // "browser" for local to the client or timezone for timezone-js - timeformat: null, // format string to use - twelveHourClock: false, // 12 or 24 time in time mode - monthNames: null // list of names of months - } - }; - - // round to nearby lower multiple of base - - function floorInBase(n, base) { - return base * Math.floor(n / base); - } - - // Returns a string with the date d formatted according to fmt. - // A subset of the Open Group's strftime format is supported. - - function formatDate(d, fmt, monthNames, dayNames) { - - if (typeof d.strftime == "function") { - return d.strftime(fmt); - } - - var leftPad = function(n, pad) { - n = "" + n; - pad = "" + (pad == null ? "0" : pad); - return n.length == 1 ? pad + n : n; - }; - - var r = []; - var escape = false; - var hours = d.getHours(); - var isAM = hours < 12; - - if (monthNames == null) { - monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; - } - - if (dayNames == null) { - dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; - } - - var hours12; - - if (hours > 12) { - hours12 = hours - 12; - } else if (hours == 0) { - hours12 = 12; - } else { - hours12 = hours; - } - - for (var i = 0; i < fmt.length; ++i) { - - var c = fmt.charAt(i); - - if (escape) { - switch (c) { - case 'a': c = "" + dayNames[d.getDay()]; break; - case 'b': c = "" + monthNames[d.getMonth()]; break; - case 'd': c = leftPad(d.getDate()); break; - case 'e': c = leftPad(d.getDate(), " "); break; - case 'h': // For back-compat with 0.7; remove in 1.0 - case 'H': c = leftPad(hours); break; - case 'I': c = leftPad(hours12); break; - case 'l': c = leftPad(hours12, " "); break; - case 'm': c = leftPad(d.getMonth() + 1); break; - case 'M': c = leftPad(d.getMinutes()); break; - // quarters not in Open Group's strftime specification - case 'q': - c = "" + (Math.floor(d.getMonth() / 3) + 1); break; - case 'S': c = leftPad(d.getSeconds()); break; - case 'y': c = leftPad(d.getFullYear() % 100); break; - case 'Y': c = "" + d.getFullYear(); break; - case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; - case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; - case 'w': c = "" + d.getDay(); break; - } - r.push(c); - escape = false; - } else { - if (c == "%") { - escape = true; - } else { - r.push(c); - } - } - } - - return r.join(""); - } - - // To have a consistent view of time-based data independent of which time - // zone the client happens to be in we need a date-like object independent - // of time zones. This is done through a wrapper that only calls the UTC - // versions of the accessor methods. - - function makeUtcWrapper(d) { - - function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) { - sourceObj[sourceMethod] = function() { - return targetObj[targetMethod].apply(targetObj, arguments); - }; - }; - - var utc = { - date: d - }; - - // support strftime, if found - - if (d.strftime != undefined) { - addProxyMethod(utc, "strftime", d, "strftime"); - } - - addProxyMethod(utc, "getTime", d, "getTime"); - addProxyMethod(utc, "setTime", d, "setTime"); - - var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"]; - - for (var p = 0; p < props.length; p++) { - addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]); - addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]); - } - - return utc; - }; - - // select time zone strategy. This returns a date-like object tied to the - // desired timezone - - function dateGenerator(ts, opts) { - if (opts.timezone == "browser") { - return new Date(ts); - } else if (!opts.timezone || opts.timezone == "utc") { - return makeUtcWrapper(new Date(ts)); - } else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") { - var d = new timezoneJS.Date(); - // timezone-js is fickle, so be sure to set the time zone before - // setting the time. - d.setTimezone(opts.timezone); - d.setTime(ts); - return d; - } else { - return makeUtcWrapper(new Date(ts)); - } - } - - // map of app. size of time units in milliseconds - - var timeUnitSize = { - "second": 1000, - "minute": 60 * 1000, - "hour": 60 * 60 * 1000, - "day": 24 * 60 * 60 * 1000, - "month": 30 * 24 * 60 * 60 * 1000, - "quarter": 3 * 30 * 24 * 60 * 60 * 1000, - "year": 365.2425 * 24 * 60 * 60 * 1000 - }; - - // the allowed tick sizes, after 1 year we use - // an integer algorithm - - var baseSpec = [ - [1, "second"], [2, "second"], [5, "second"], [10, "second"], - [30, "second"], - [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], - [30, "minute"], - [1, "hour"], [2, "hour"], [4, "hour"], - [8, "hour"], [12, "hour"], - [1, "day"], [2, "day"], [3, "day"], - [0.25, "month"], [0.5, "month"], [1, "month"], - [2, "month"] - ]; - - // we don't know which variant(s) we'll need yet, but generating both is - // cheap - - var specMonths = baseSpec.concat([[3, "month"], [6, "month"], - [1, "year"]]); - var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"], - [1, "year"]]); - - function init(plot) { - plot.hooks.processOptions.push(function (plot, options) { - $.each(plot.getAxes(), function(axisName, axis) { - - var opts = axis.options; - - if (opts.mode == "time") { - axis.tickGenerator = function(axis) { - - var ticks = []; - var d = dateGenerator(axis.min, opts); - var minSize = 0; - - // make quarter use a possibility if quarters are - // mentioned in either of these options - - var spec = (opts.tickSize && opts.tickSize[1] === - "quarter") || - (opts.minTickSize && opts.minTickSize[1] === - "quarter") ? specQuarters : specMonths; - - if (opts.minTickSize != null) { - if (typeof opts.tickSize == "number") { - minSize = opts.tickSize; - } else { - minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; - } - } - - for (var i = 0; i < spec.length - 1; ++i) { - if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]] - + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 - && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) { - break; - } - } - - var size = spec[i][0]; - var unit = spec[i][1]; - - // special-case the possibility of several years - - if (unit == "year") { - - // if given a minTickSize in years, just use it, - // ensuring that it's an integer - - if (opts.minTickSize != null && opts.minTickSize[1] == "year") { - size = Math.floor(opts.minTickSize[0]); - } else { - - var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10)); - var norm = (axis.delta / timeUnitSize.year) / magn; - - if (norm < 1.5) { - size = 1; - } else if (norm < 3) { - size = 2; - } else if (norm < 7.5) { - size = 5; - } else { - size = 10; - } - - size *= magn; - } - - // minimum size for years is 1 - - if (size < 1) { - size = 1; - } - } - - axis.tickSize = opts.tickSize || [size, unit]; - var tickSize = axis.tickSize[0]; - unit = axis.tickSize[1]; - - var step = tickSize * timeUnitSize[unit]; - - if (unit == "second") { - d.setSeconds(floorInBase(d.getSeconds(), tickSize)); - } else if (unit == "minute") { - d.setMinutes(floorInBase(d.getMinutes(), tickSize)); - } else if (unit == "hour") { - d.setHours(floorInBase(d.getHours(), tickSize)); - } else if (unit == "month") { - d.setMonth(floorInBase(d.getMonth(), tickSize)); - } else if (unit == "quarter") { - d.setMonth(3 * floorInBase(d.getMonth() / 3, - tickSize)); - } else if (unit == "year") { - d.setFullYear(floorInBase(d.getFullYear(), tickSize)); - } - - // reset smaller components - - d.setMilliseconds(0); - - if (step >= timeUnitSize.minute) { - d.setSeconds(0); - } - if (step >= timeUnitSize.hour) { - d.setMinutes(0); - } - if (step >= timeUnitSize.day) { - d.setHours(0); - } - if (step >= timeUnitSize.day * 4) { - d.setDate(1); - } - if (step >= timeUnitSize.month * 2) { - d.setMonth(floorInBase(d.getMonth(), 3)); - } - if (step >= timeUnitSize.quarter * 2) { - d.setMonth(floorInBase(d.getMonth(), 6)); - } - if (step >= timeUnitSize.year) { - d.setMonth(0); - } - - var carry = 0; - var v = Number.NaN; - var prev; - - do { - - prev = v; - v = d.getTime(); - ticks.push(v); - - if (unit == "month" || unit == "quarter") { - if (tickSize < 1) { - - // a bit complicated - we'll divide the - // month/quarter up but we need to take - // care of fractions so we don't end up in - // the middle of a day - - d.setDate(1); - var start = d.getTime(); - d.setMonth(d.getMonth() + - (unit == "quarter" ? 3 : 1)); - var end = d.getTime(); - d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); - carry = d.getHours(); - d.setHours(0); - } else { - d.setMonth(d.getMonth() + - tickSize * (unit == "quarter" ? 3 : 1)); - } - } else if (unit == "year") { - d.setFullYear(d.getFullYear() + tickSize); - } else { - d.setTime(v + step); - } - } while (v < axis.max && v != prev); - - return ticks; - }; - - axis.tickFormatter = function (v, axis) { - - var d = dateGenerator(v, axis.options); - - // first check global format - - if (opts.timeformat != null) { - return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames); - } - - // possibly use quarters if quarters are mentioned in - // any of these places - - var useQuarters = (axis.options.tickSize && - axis.options.tickSize[1] == "quarter") || - (axis.options.minTickSize && - axis.options.minTickSize[1] == "quarter"); - - var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; - var span = axis.max - axis.min; - var suffix = (opts.twelveHourClock) ? " %p" : ""; - var hourCode = (opts.twelveHourClock) ? "%I" : "%H"; - var fmt; - - if (t < timeUnitSize.minute) { - fmt = hourCode + ":%M:%S" + suffix; - } else if (t < timeUnitSize.day) { - if (span < 2 * timeUnitSize.day) { - fmt = hourCode + ":%M" + suffix; - } else { - fmt = "%b %d " + hourCode + ":%M" + suffix; - } - } else if (t < timeUnitSize.month) { - fmt = "%b %d"; - } else if ((useQuarters && t < timeUnitSize.quarter) || - (!useQuarters && t < timeUnitSize.year)) { - if (span < timeUnitSize.year) { - fmt = "%b"; - } else { - fmt = "%b %Y"; - } - } else if (useQuarters && t < timeUnitSize.year) { - if (span < timeUnitSize.year) { - fmt = "Q%q"; - } else { - fmt = "Q%q %Y"; - } - } else { - fmt = "%Y"; - } - - var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames); - - return rt; - }; - } - }); - }); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'time', - version: '1.0' - }); - - // Time-axis support used to be in Flot core, which exposed the - // formatDate function on the plot object. Various plugins depend - // on the function, so we need to re-expose it here. - - $.plot.formatDate = formatDate; - $.plot.dateGenerator = dateGenerator; - -})(jQuery); diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 86159a13379a1..ef7d8ea189024 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -49,7 +49,7 @@ import { DEFAULT_APP_CATEGORIES } from '../../../core/public'; import { SavedObjectsStart } from '../../saved_objects/public'; import { EmbeddableStart } from '../../embeddable/public'; import { DashboardStart } from '../../dashboard/public'; -import { UiActionsStart, VISUALIZE_FIELD_TRIGGER } from '../../ui_actions/public'; +import { UiActionsSetup, VISUALIZE_FIELD_TRIGGER } from '../../ui_actions/public'; import { setUISettings, setApplication, @@ -69,7 +69,6 @@ export interface VisualizePluginStartDependencies { urlForwarding: UrlForwardingStart; savedObjects: SavedObjectsStart; dashboard: DashboardStart; - uiActions: UiActionsStart; } export interface VisualizePluginSetupDependencies { @@ -77,6 +76,7 @@ export interface VisualizePluginSetupDependencies { urlForwarding: UrlForwardingSetup; data: DataPublicPluginSetup; share?: SharePluginSetup; + uiActions: UiActionsSetup; } export class VisualizePlugin @@ -90,7 +90,7 @@ export class VisualizePlugin public async setup( core: CoreSetup, - { home, urlForwarding, data, share }: VisualizePluginSetupDependencies + { home, urlForwarding, data, share, uiActions }: VisualizePluginSetupDependencies ) { const { appMounted, @@ -135,6 +135,7 @@ export class VisualizePlugin ); } setUISettings(core.uiSettings); + uiActions.addTriggerAction(VISUALIZE_FIELD_TRIGGER, visualizeFieldAction); core.application.register({ id: 'visualize', @@ -236,7 +237,6 @@ export class VisualizePlugin if (plugins.share) { setShareService(plugins.share); } - plugins.uiActions.addTriggerAction(VISUALIZE_FIELD_TRIGGER, visualizeFieldAction); } stop() { diff --git a/test/common/services/security/role_mappings.ts b/test/common/services/security/role_mappings.ts index 7951d4b5b47b2..267294991f30e 100644 --- a/test/common/services/security/role_mappings.ts +++ b/test/common/services/security/role_mappings.ts @@ -23,10 +23,24 @@ import { KbnClient, ToolingLog } from '@kbn/dev-utils'; export class RoleMappings { constructor(private log: ToolingLog, private kbnClient: KbnClient) {} + public async getAll() { + this.log.debug(`Getting role mappings`); + const { data, status, statusText } = await this.kbnClient.request>({ + path: `/internal/security/role_mapping`, + method: 'GET', + }); + if (status !== 200) { + throw new Error( + `Expected status code of 200, received ${status} ${statusText}: ${util.inspect(data)}` + ); + } + return data; + } + public async create(name: string, roleMapping: Record) { this.log.debug(`creating role mapping ${name}`); const { data, status, statusText } = await this.kbnClient.request({ - path: `/internal/security/role_mapping/${name}`, + path: `/internal/security/role_mapping/${encodeURIComponent(name)}`, method: 'POST', body: roleMapping, }); @@ -41,7 +55,7 @@ export class RoleMappings { public async delete(name: string) { this.log.debug(`deleting role mapping ${name}`); const { data, status, statusText } = await this.kbnClient.request({ - path: `/internal/security/role_mapping/${name}`, + path: `/internal/security/role_mapping/${encodeURIComponent(name)}`, method: 'DELETE', }); if (status !== 200 && status !== 404) { diff --git a/test/functional/apps/discover/_field_visualize.ts b/test/functional/apps/discover/_field_visualize.ts index c95211e98cdba..1a1631b9db48b 100644 --- a/test/functional/apps/discover/_field_visualize.ts +++ b/test/functional/apps/discover/_field_visualize.ts @@ -32,7 +32,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { defaultIndex: 'logstash-*', }; - describe('discover field visualize button', () => { + describe('discover field visualize button', function () { + // unskipped on cloud as these tests test the navigation + // from Discover to Visualize which happens only on OSS + this.tags(['skipCloud']); before(async function () { log.debug('load kibana index with default index pattern'); await esArchiver.load('discover'); diff --git a/test/functional/apps/discover/_shared_links.js b/test/functional/apps/discover/_shared_links.js index 94409a94e9257..56c6485624043 100644 --- a/test/functional/apps/discover/_shared_links.js +++ b/test/functional/apps/discover/_shared_links.js @@ -28,7 +28,8 @@ export default function ({ getService, getPageObjects }) { const browser = getService('browser'); const toasts = getService('toasts'); - describe('shared links', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/80104 + describe.skip('shared links', function describeIndexTests() { let baseUrl; async function setup({ storeStateInSessionStorage }) { diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index cf75d5ad7c103..e29f75b806574 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -282,6 +282,10 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider await PageObjects.header.waitUntilLoadingHasFinished(); } + async hasIndexPattern(name: string) { + return await find.existsByLinkText(name); + } + async clickIndexPatternByName(name: string) { const indexLink = await find.byXPath(`//a[descendant::*[text()='${name}']]`); await indexLink.click(); @@ -324,6 +328,13 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider await retry.try(async () => { await PageObjects.header.waitUntilLoadingHasFinished(); await this.clickKibanaIndexPatterns(); + const exists = await this.hasIndexPattern(indexPatternName); + + if (exists) { + await this.clickIndexPatternByName(indexPatternName); + return; + } + await PageObjects.header.waitUntilLoadingHasFinished(); await this.clickAddNewIndexPatternButton(); if (!isStandardIndexPattern) { diff --git a/test/functional/services/filter_bar.ts b/test/functional/services/filter_bar.ts index 98ab1babd60fe..de895918efbba 100644 --- a/test/functional/services/filter_bar.ts +++ b/test/functional/services/filter_bar.ts @@ -124,9 +124,10 @@ export function FilterBarProvider({ getService, getPageObjects }: FtrProviderCon await comboBox.set('filterOperatorList', operator); const params = await testSubjects.find('filterParams'); const paramsComboBoxes = await params.findAllByCssSelector( - '[data-test-subj~="filterParamsComboBox"]' + '[data-test-subj~="filterParamsComboBox"]', + 1000 ); - const paramFields = await params.findAllByTagName('input'); + const paramFields = await params.findAllByTagName('input', 1000); for (let i = 0; i < values.length; i++) { let fieldValues = values[i]; if (!Array.isArray(fieldValues)) { diff --git a/test/functional/services/toasts.ts b/test/functional/services/toasts.ts index f5416a44e3b5a..1148da14556e2 100644 --- a/test/functional/services/toasts.ts +++ b/test/functional/services/toasts.ts @@ -71,6 +71,12 @@ export function ToastsProvider({ getService }: FtrProviderContext) { private async getGlobalToastList() { return await testSubjects.find('globalToastList'); } + + public async getToastCount() { + const list = await this.getGlobalToastList(); + const toasts = await list.findAllByCssSelector(`.euiToast`); + return toasts.length; + } } return new Toasts(); diff --git a/test/plugin_functional/plugins/session_notifications/kibana.json b/test/plugin_functional/plugins/session_notifications/kibana.json new file mode 100644 index 0000000000000..0b80b531d2f84 --- /dev/null +++ b/test/plugin_functional/plugins/session_notifications/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "session_notifications", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["session_notifications"], + "server": false, + "ui": true, + "requiredPlugins": ["data", "navigation"] +} \ No newline at end of file diff --git a/test/plugin_functional/plugins/session_notifications/package.json b/test/plugin_functional/plugins/session_notifications/package.json new file mode 100644 index 0000000000000..7a61867db2b58 --- /dev/null +++ b/test/plugin_functional/plugins/session_notifications/package.json @@ -0,0 +1,18 @@ +{ + "name": "session_notifications", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/session_notifications", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "4.0.2" + } +} + diff --git a/test/plugin_functional/plugins/session_notifications/public/index.ts b/test/plugin_functional/plugins/session_notifications/public/index.ts new file mode 100644 index 0000000000000..fbc573e8dc6fe --- /dev/null +++ b/test/plugin_functional/plugins/session_notifications/public/index.ts @@ -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. + */ + +import { PluginInitializer } from 'kibana/public'; +import { SessionNotificationsPlugin } from './plugin'; + +export const plugin: PluginInitializer = () => new SessionNotificationsPlugin(); diff --git a/test/plugin_functional/plugins/session_notifications/public/plugin.tsx b/test/plugin_functional/plugins/session_notifications/public/plugin.tsx new file mode 100644 index 0000000000000..a41ecb3edebd2 --- /dev/null +++ b/test/plugin_functional/plugins/session_notifications/public/plugin.tsx @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { AppPluginDependenciesStart, AppPluginDependenciesSetup } from './types'; + +export class SessionNotificationsPlugin implements Plugin { + private sessionIds: Array = []; + public setup(core: CoreSetup, { navigation }: AppPluginDependenciesSetup) { + const showSessions = { + id: 'showSessionsButton', + label: 'Show Sessions', + description: 'Sessions', + run: () => { + core.notifications.toasts.addInfo(this.sessionIds.join(','), { + toastLifeTimeMs: 50000, + }); + }, + tooltip: () => { + return this.sessionIds.join(','); + }, + testId: 'showSessionsButton', + }; + + navigation.registerMenuItem(showSessions); + + const clearSessions = { + id: 'clearSessionsButton', + label: 'Clear Sessions', + description: 'Sessions', + run: () => { + this.sessionIds.length = 0; + }, + testId: 'clearSessionsButton', + }; + + navigation.registerMenuItem(clearSessions); + } + + public start(core: CoreStart, { data }: AppPluginDependenciesStart) { + core.application.currentAppId$.subscribe(() => { + this.sessionIds.length = 0; + }); + + data.search.session.getSession$().subscribe((sessionId?: string) => { + this.sessionIds.push(sessionId); + }); + } + public stop() {} +} diff --git a/test/plugin_functional/plugins/session_notifications/public/types.ts b/test/plugin_functional/plugins/session_notifications/public/types.ts new file mode 100644 index 0000000000000..de9055d03d21e --- /dev/null +++ b/test/plugin_functional/plugins/session_notifications/public/types.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 { NavigationPublicPluginSetup } from '../../../../../src/plugins/navigation/public'; +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; + +export interface AppPluginDependenciesSetup { + navigation: NavigationPublicPluginSetup; +} +export interface AppPluginDependenciesStart { + data: DataPublicPluginStart; +} diff --git a/test/plugin_functional/plugins/session_notifications/tsconfig.json b/test/plugin_functional/plugins/session_notifications/tsconfig.json new file mode 100644 index 0000000000000..3d9d8ca9451d4 --- /dev/null +++ b/test/plugin_functional/plugins/session_notifications/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../../../typings/**/*", + ], + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] +} diff --git a/test/plugin_functional/test_suites/data_plugin/index.ts b/test/plugin_functional/test_suites/data_plugin/index.ts index bbf9d823e357e..212a75b9cf441 100644 --- a/test/plugin_functional/test_suites/data_plugin/index.ts +++ b/test/plugin_functional/test_suites/data_plugin/index.ts @@ -35,7 +35,9 @@ export default function ({ await PageObjects.common.navigateToApp('settings'); await PageObjects.settings.createIndexPattern('shakespeare', ''); }); - loadTestFile(require.resolve('./index_patterns')); + loadTestFile(require.resolve('./search')); + loadTestFile(require.resolve('./session')); + loadTestFile(require.resolve('./index_patterns')); }); } diff --git a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts index 4359816efb958..2c846dc780311 100644 --- a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts +++ b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts @@ -23,7 +23,9 @@ import '../../plugins/core_provider_plugin/types'; export default function ({ getService }: PluginFunctionalProviderContext) { const supertest = getService('supertest'); - describe('index patterns', function () { + // skipping the tests as it deletes index patterns created by other test causing unexpected failures + // https://github.com/elastic/kibana/issues/79886 + describe.skip('index patterns', function () { let indexPatternId = ''; it('can get all ids', async () => { diff --git a/test/plugin_functional/test_suites/data_plugin/session.ts b/test/plugin_functional/test_suites/data_plugin/session.ts new file mode 100644 index 0000000000000..88241fffae904 --- /dev/null +++ b/test/plugin_functional/test_suites/data_plugin/session.ts @@ -0,0 +1,83 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { PluginFunctionalProviderContext } from '../../services'; + +export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) { + const PageObjects = getPageObjects(['common', 'header', 'dashboard', 'discover', 'timePicker']); + const filterBar = getService('filterBar'); + const testSubjects = getService('testSubjects'); + const toasts = getService('toasts'); + + const getSessionIds = async () => { + const sessionsBtn = await testSubjects.find('showSessionsButton'); + await sessionsBtn.click(); + const toast = await toasts.getToastElement(1); + const sessionIds = await toast.getVisibleText(); + return sessionIds.split(','); + }; + + describe('Session management', function describeIndexTests() { + describe('Discover', () => { + before(async () => { + await PageObjects.common.navigateToApp('discover'); + await testSubjects.click('clearSessionsButton'); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + afterEach(async () => { + await testSubjects.click('clearSessionsButton'); + await toasts.dismissAllToasts(); + }); + + it('Starts on index pattern select', async () => { + await PageObjects.discover.selectIndexPattern('shakespeare'); + await PageObjects.header.waitUntilLoadingHasFinished(); + const sessionIds = await getSessionIds(); + + // Discover calls destroy on index pattern change, which explicitly closes a session + expect(sessionIds.length).to.be(2); + expect(sessionIds[0].length).to.be(0); + expect(sessionIds[1].length).not.to.be(0); + }); + + it('Starts on a refresh', async () => { + await testSubjects.click('querySubmitButton'); + await PageObjects.header.waitUntilLoadingHasFinished(); + const sessionIds = await getSessionIds(); + expect(sessionIds.length).to.be(1); + }); + + it('Starts a new session on sort', async () => { + await PageObjects.discover.clickFieldListItemAdd('speaker'); + await PageObjects.discover.clickFieldSort('speaker'); + await PageObjects.header.waitUntilLoadingHasFinished(); + const sessionIds = await getSessionIds(); + expect(sessionIds.length).to.be(1); + }); + + it('Starts a new session on filter change', async () => { + await filterBar.addFilter('line_number', 'is', '4.3.108'); + await PageObjects.header.waitUntilLoadingHasFinished(); + const sessionIds = await getSessionIds(); + expect(sessionIds.length).to.be(1); + }); + }); + }); +} diff --git a/x-pack/package.json b/x-pack/package.json index 484a64fdc2628..91fecff094110 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -44,12 +44,6 @@ "@mapbox/mapbox-gl-draw": "^1.2.0", "@mapbox/mapbox-gl-rtl-text": "^0.2.3", "@scant/router": "^0.1.0", - "@storybook/addon-actions": "^6.0.16", - "@storybook/addon-essentials": "^6.0.16", - "@storybook/addon-knobs": "^6.0.16", - "@storybook/addon-storyshots": "^6.0.16", - "@storybook/react": "^6.0.16", - "@storybook/theming": "^6.0.16", "@testing-library/dom": "^7.24.2", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.0.4", diff --git a/x-pack/plugins/actions/server/action_type_registry.test.ts b/x-pack/plugins/actions/server/action_type_registry.test.ts index b25e33400df5d..e641b81189b93 100644 --- a/x-pack/plugins/actions/server/action_type_registry.test.ts +++ b/x-pack/plugins/actions/server/action_type_registry.test.ts @@ -11,6 +11,7 @@ import { ActionExecutor, ExecutorError, ILicenseState, TaskRunnerFactory } from import { actionsConfigMock } from './actions_config.mock'; import { licenseStateMock } from './lib/license_state.mock'; import { ActionsConfigurationUtilities } from './actions_config'; +import { licensingMock } from '../../licensing/server/mocks'; const mockTaskManager = taskManagerMock.setup(); let mockedLicenseState: jest.Mocked; @@ -22,6 +23,7 @@ beforeEach(() => { mockedLicenseState = licenseStateMock.create(); mockedActionsConfig = actionsConfigMock.create(); actionTypeRegistryParams = { + licensing: licensingMock.createSetup(), taskManager: mockTaskManager, taskRunnerFactory: new TaskRunnerFactory( new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) @@ -51,7 +53,7 @@ describe('register()', () => { actionTypeRegistry.register({ id: 'my-action-type', name: 'My action type', - minimumLicenseRequired: 'basic', + minimumLicenseRequired: 'gold', executor, }); expect(actionTypeRegistry.has('my-action-type')).toEqual(true); @@ -69,6 +71,10 @@ describe('register()', () => { }, ] `); + expect(actionTypeRegistryParams.licensing.featureUsage.register).toHaveBeenCalledWith( + 'Connector: My action type', + 'gold' + ); }); test('shallow clones the given action type', () => { @@ -123,6 +129,31 @@ describe('register()', () => { expect(getRetry(0, new ExecutorError('my message', {}, undefined))).toEqual(false); expect(getRetry(0, new ExecutorError('my message', {}, retryTime))).toEqual(retryTime); }); + + test('registers gold+ action types to the licensing feature usage API', () => { + const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); + actionTypeRegistry.register({ + id: 'my-action-type', + name: 'My action type', + minimumLicenseRequired: 'gold', + executor, + }); + expect(actionTypeRegistryParams.licensing.featureUsage.register).toHaveBeenCalledWith( + 'Connector: My action type', + 'gold' + ); + }); + + test(`doesn't register basic action types to the licensing feature usage API`, () => { + const actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); + actionTypeRegistry.register({ + id: 'my-action-type', + name: 'My action type', + minimumLicenseRequired: 'basic', + executor, + }); + expect(actionTypeRegistryParams.licensing.featureUsage.register).not.toHaveBeenCalled(); + }); }); describe('get()', () => { @@ -232,10 +263,20 @@ describe('isActionTypeEnabled', () => { expect(actionTypeRegistry.isActionExecutable('my-slack1', 'foo')).toEqual(true); }); - test('should call isLicenseValidForActionType of the license state', async () => { + test('should call isLicenseValidForActionType of the license state with notifyUsage false by default', async () => { mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); actionTypeRegistry.isActionTypeEnabled('foo'); - expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType); + expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType, { + notifyUsage: false, + }); + }); + + test('should call isLicenseValidForActionType of the license state with notifyUsage true when specified', async () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + actionTypeRegistry.isActionTypeEnabled('foo', { notifyUsage: true }); + expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType, { + notifyUsage: true, + }); }); test('should return false when isActionTypeEnabled is false and isLicenseValidForActionType is true', async () => { @@ -298,3 +339,36 @@ describe('ensureActionTypeEnabled', () => { ).toThrowErrorMatchingInlineSnapshot(`"Fail"`); }); }); + +describe('isActionExecutable()', () => { + let actionTypeRegistry: ActionTypeRegistry; + const fooActionType: ActionType = { + id: 'foo', + name: 'Foo', + minimumLicenseRequired: 'basic', + executor: async (options) => { + return { status: 'ok', actionId: options.actionId }; + }, + }; + + beforeEach(() => { + actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); + actionTypeRegistry.register(fooActionType); + }); + + test('should call isLicenseValidForActionType of the license state with notifyUsage false by default', async () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + actionTypeRegistry.isActionExecutable('123', 'foo'); + expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType, { + notifyUsage: false, + }); + }); + + test('should call isLicenseValidForActionType of the license state with notifyUsage true when specified', async () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + actionTypeRegistry.isActionExecutable('123', 'foo', { notifyUsage: true }); + expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType, { + notifyUsage: true, + }); + }); +}); diff --git a/x-pack/plugins/actions/server/action_type_registry.ts b/x-pack/plugins/actions/server/action_type_registry.ts index 4015381ff9502..b93d4a6e78ac6 100644 --- a/x-pack/plugins/actions/server/action_type_registry.ts +++ b/x-pack/plugins/actions/server/action_type_registry.ts @@ -7,9 +7,15 @@ import Boom from 'boom'; import { i18n } from '@kbn/i18n'; import { RunContext, TaskManagerSetupContract } from '../../task_manager/server'; -import { ExecutorError, TaskRunnerFactory, ILicenseState } from './lib'; import { ActionType as CommonActionType } from '../common'; import { ActionsConfigurationUtilities } from './actions_config'; +import { LicensingPluginSetup } from '../../licensing/server'; +import { + ExecutorError, + getActionTypeFeatureUsageName, + TaskRunnerFactory, + ILicenseState, +} from './lib'; import { ActionType, PreConfiguredAction, @@ -19,6 +25,7 @@ import { } from './types'; export interface ActionTypeRegistryOpts { + licensing: LicensingPluginSetup; taskManager: TaskManagerSetupContract; taskRunnerFactory: TaskRunnerFactory; actionsConfigUtils: ActionsConfigurationUtilities; @@ -33,6 +40,7 @@ export class ActionTypeRegistry { private readonly actionsConfigUtils: ActionsConfigurationUtilities; private readonly licenseState: ILicenseState; private readonly preconfiguredActions: PreConfiguredAction[]; + private readonly licensing: LicensingPluginSetup; constructor(constructorParams: ActionTypeRegistryOpts) { this.taskManager = constructorParams.taskManager; @@ -40,6 +48,7 @@ export class ActionTypeRegistry { this.actionsConfigUtils = constructorParams.actionsConfigUtils; this.licenseState = constructorParams.licenseState; this.preconfiguredActions = constructorParams.preconfiguredActions; + this.licensing = constructorParams.licensing; } /** @@ -54,26 +63,36 @@ export class ActionTypeRegistry { */ public ensureActionTypeEnabled(id: string) { this.actionsConfigUtils.ensureActionTypeEnabled(id); + // Important to happen last because the function will notify of feature usage at the + // same time and it shouldn't notify when the action type isn't enabled this.licenseState.ensureLicenseForActionType(this.get(id)); } /** * Returns true if action type is enabled in the config and a valid license is used. */ - public isActionTypeEnabled(id: string) { + public isActionTypeEnabled( + id: string, + options: { notifyUsage: boolean } = { notifyUsage: false } + ) { return ( this.actionsConfigUtils.isActionTypeEnabled(id) && - this.licenseState.isLicenseValidForActionType(this.get(id)).isValid === true + this.licenseState.isLicenseValidForActionType(this.get(id), options).isValid === true ); } /** * Returns true if action type is enabled or it is a preconfigured action type. */ - public isActionExecutable(actionId: string, actionTypeId: string) { + public isActionExecutable( + actionId: string, + actionTypeId: string, + options: { notifyUsage: boolean } = { notifyUsage: false } + ) { + const actionTypeEnabled = this.isActionTypeEnabled(actionTypeId, options); return ( - this.isActionTypeEnabled(actionTypeId) || - (!this.isActionTypeEnabled(actionTypeId) && + actionTypeEnabled || + (!actionTypeEnabled && this.preconfiguredActions.find( (preconfiguredAction) => preconfiguredAction.id === actionId ) !== undefined) @@ -118,6 +137,13 @@ export class ActionTypeRegistry { createTaskRunner: (context: RunContext) => this.taskRunnerFactory.create(context), }, }); + // No need to notify usage on basic action types + if (actionType.minimumLicenseRequired !== 'basic') { + this.licensing.featureUsage.register( + getActionTypeFeatureUsageName(actionType as ActionType), + actionType.minimumLicenseRequired + ); + } } /** diff --git a/x-pack/plugins/actions/server/actions_client.mock.ts b/x-pack/plugins/actions/server/actions_client.mock.ts index 48122a5ce4e0f..0c16c88ad7a89 100644 --- a/x-pack/plugins/actions/server/actions_client.mock.ts +++ b/x-pack/plugins/actions/server/actions_client.mock.ts @@ -20,6 +20,7 @@ const createActionsClientMock = () => { execute: jest.fn(), enqueueExecution: jest.fn(), listTypes: jest.fn(), + isActionTypeEnabled: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index adef12454f2d5..2b6aec42e0d21 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -8,12 +8,13 @@ import { schema } from '@kbn/config-schema'; import { ActionTypeRegistry, ActionTypeRegistryOpts } from './action_type_registry'; import { ActionsClient } from './actions_client'; -import { ExecutorType } from './types'; +import { ExecutorType, ActionType } from './types'; import { ActionExecutor, TaskRunnerFactory, ILicenseState } from './lib'; import { taskManagerMock } from '../../task_manager/server/task_manager.mock'; import { actionsConfigMock } from './actions_config.mock'; import { getActionsConfigurationUtilities } from './actions_config'; import { licenseStateMock } from './lib/license_state.mock'; +import { licensingMock } from '../../licensing/server/mocks'; import { elasticsearchServiceMock, @@ -47,6 +48,7 @@ beforeEach(() => { jest.resetAllMocks(); mockedLicenseState = licenseStateMock.create(); actionTypeRegistryParams = { + licensing: licensingMock.createSetup(), taskManager: mockTaskManager, taskRunnerFactory: new TaskRunnerFactory( new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) @@ -299,6 +301,7 @@ describe('create()', () => { }); const localActionTypeRegistryParams = { + licensing: licensingMock.createSetup(), taskManager: mockTaskManager, taskRunnerFactory: new TaskRunnerFactory( new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) @@ -1244,3 +1247,31 @@ describe('enqueueExecution()', () => { expect(executionEnqueuer).toHaveBeenCalledWith(unsecuredSavedObjectsClient, opts); }); }); + +describe('isActionTypeEnabled()', () => { + const fooActionType: ActionType = { + id: 'foo', + name: 'Foo', + minimumLicenseRequired: 'gold', + executor: jest.fn(), + }; + beforeEach(() => { + actionTypeRegistry.register(fooActionType); + }); + + test('should call isLicenseValidForActionType of the license state with notifyUsage false by default', () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + actionsClient.isActionTypeEnabled('foo'); + expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType, { + notifyUsage: false, + }); + }); + + test('should call isLicenseValidForActionType of the license state with notifyUsage true when specified', () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + actionsClient.isActionTypeEnabled('foo', { notifyUsage: true }); + expect(mockedLicenseState.isLicenseValidForActionType).toHaveBeenCalledWith(fooActionType, { + notifyUsage: true, + }); + }); +}); diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 4079a6ddeeb8a..e565d420d772e 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -343,6 +343,13 @@ export class ActionsClient { public async listTypes(): Promise { return this.actionTypeRegistry.list(); } + + public isActionTypeEnabled( + actionTypeId: string, + options: { notifyUsage: boolean } = { notifyUsage: false } + ) { + return this.actionTypeRegistry.isActionTypeEnabled(actionTypeId, options); + } } function actionFromSavedObject(savedObject: SavedObject): ActionResult { diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts index acab6dd41b4b3..f7882849708e5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts @@ -12,6 +12,7 @@ import { Logger } from '../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../src/core/server/mocks'; import { actionsConfigMock } from '../actions_config.mock'; import { licenseStateMock } from '../lib/license_state.mock'; +import { licensingMock } from '../../../licensing/server/mocks'; const ACTION_TYPE_IDS = ['.index', '.email', '.pagerduty', '.server-log', '.slack', '.webhook']; @@ -21,6 +22,7 @@ export function createActionTypeRegistry(): { } { const logger = loggingSystemMock.create().get() as jest.Mocked; const actionTypeRegistry = new ActionTypeRegistry({ + licensing: licensingMock.createSetup(), taskManager: taskManagerMock.setup(), taskRunnerFactory: new TaskRunnerFactory( new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }) diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts index 7682f01ed769d..33e78ee444cd0 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -23,9 +23,10 @@ beforeEach(() => jest.resetAllMocks()); describe('execute()', () => { test('schedules the action with all given parameters', async () => { + const actionTypeRegistry = actionTypeRegistryMock.create(); const executeFn = createExecutionEnqueuerFunction({ taskManager: mockTaskManager, - actionTypeRegistry: actionTypeRegistryMock.create(), + actionTypeRegistry, isESOUsingEphemeralEncryptionKey: false, preconfiguredActions: [], }); @@ -76,6 +77,9 @@ describe('execute()', () => { }, {} ); + expect(actionTypeRegistry.isActionExecutable).toHaveBeenCalledWith('123', 'mock-action', { + notifyUsage: true, + }); }); test('schedules the action with all given parameters with a preconfigured action', async () => { diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index b226583fade52..f0a22c642cf61 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -51,7 +51,7 @@ export function createExecutionEnqueuerFunction({ id ); - if (!actionTypeRegistry.isActionExecutable(id, actionTypeId)) { + if (!actionTypeRegistry.isActionExecutable(id, actionTypeId, { notifyUsage: true })) { actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); } diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index 692d14e859b34..4ff56536e3867 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -90,6 +90,9 @@ test('successfully executes', async () => { ); expect(actionTypeRegistry.get).toHaveBeenCalledWith('test'); + expect(actionTypeRegistry.isActionExecutable).toHaveBeenCalledWith('1', 'test', { + notifyUsage: true, + }); expect(actionType.executor).toHaveBeenCalledWith({ actionId: '1', diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index 0d4d6de3be1f9..0015b417d72ce 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -102,7 +102,7 @@ export class ActionExecutor { namespace.namespace ); - if (!actionTypeRegistry.isActionExecutable(actionId, actionTypeId)) { + if (!actionTypeRegistry.isActionExecutable(actionId, actionTypeId, { notifyUsage: true })) { actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); } const actionType = actionTypeRegistry.get(actionTypeId); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/index.js b/x-pack/plugins/actions/server/lib/get_action_type_feature_usage_name.ts similarity index 60% rename from x-pack/plugins/monitoring/public/lib/jquery_flot/index.js rename to x-pack/plugins/actions/server/lib/get_action_type_feature_usage_name.ts index abf060aca8c08..75919442b2ce6 100644 --- a/x-pack/plugins/monitoring/public/lib/jquery_flot/index.js +++ b/x-pack/plugins/actions/server/lib/get_action_type_feature_usage_name.ts @@ -4,4 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export { default } from './jquery_flot'; +import { ActionType } from '../types'; + +export function getActionTypeFeatureUsageName(actionType: ActionType) { + return `Connector: ${actionType.name}`; +} diff --git a/x-pack/plugins/actions/server/lib/index.ts b/x-pack/plugins/actions/server/lib/index.ts index e97875b91cf33..4c8e7ab17db68 100644 --- a/x-pack/plugins/actions/server/lib/index.ts +++ b/x-pack/plugins/actions/server/lib/index.ts @@ -10,6 +10,7 @@ export { TaskRunnerFactory } from './task_runner_factory'; export { ActionExecutor, ActionExecutorContract } from './action_executor'; export { ILicenseState, LicenseState } from './license_state'; export { verifyApiAccess } from './verify_api_access'; +export { getActionTypeFeatureUsageName } from './get_action_type_feature_usage_name'; export { ActionTypeDisabledError, ActionTypeDisabledReason, diff --git a/x-pack/plugins/actions/server/lib/license_state.mock.ts b/x-pack/plugins/actions/server/lib/license_state.mock.ts index d59e9dbdc540f..e5bd9fc9d16cd 100644 --- a/x-pack/plugins/actions/server/lib/license_state.mock.ts +++ b/x-pack/plugins/actions/server/lib/license_state.mock.ts @@ -12,6 +12,7 @@ export const createLicenseStateMock = () => { getLicenseInformation: jest.fn(), ensureLicenseForActionType: jest.fn(), isLicenseValidForActionType: jest.fn(), + setNotifyUsage: jest.fn(), checkLicense: jest.fn().mockResolvedValue({ state: 'valid', }), diff --git a/x-pack/plugins/actions/server/lib/license_state.test.ts b/x-pack/plugins/actions/server/lib/license_state.test.ts index 32c3c54faf007..06148b1825e73 100644 --- a/x-pack/plugins/actions/server/lib/license_state.test.ts +++ b/x-pack/plugins/actions/server/lib/license_state.test.ts @@ -55,6 +55,7 @@ describe('checkLicense()', () => { describe('isLicenseValidForActionType', () => { let license: Subject; let licenseState: ILicenseState; + const mockNotifyUsage = jest.fn(); const fooActionType: ActionType = { id: 'foo', name: 'Foo', @@ -67,6 +68,7 @@ describe('isLicenseValidForActionType', () => { beforeEach(() => { license = new Subject(); licenseState = new LicenseState(license); + licenseState.setNotifyUsage(mockNotifyUsage); }); test('should return false when license not defined', () => { @@ -113,11 +115,42 @@ describe('isLicenseValidForActionType', () => { isValid: true, }); }); + + test('should not call notifyUsage by default', () => { + const goldLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'gold' }, + }); + license.next(goldLicense); + licenseState.isLicenseValidForActionType(fooActionType); + expect(mockNotifyUsage).not.toHaveBeenCalled(); + }); + + test('should not call notifyUsage on basic action types', () => { + const basicLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'basic' }, + }); + license.next(basicLicense); + licenseState.isLicenseValidForActionType({ + ...fooActionType, + minimumLicenseRequired: 'basic', + }); + expect(mockNotifyUsage).not.toHaveBeenCalled(); + }); + + test('should call notifyUsage when specified', () => { + const goldLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'gold' }, + }); + license.next(goldLicense); + licenseState.isLicenseValidForActionType(fooActionType, { notifyUsage: true }); + expect(mockNotifyUsage).toHaveBeenCalledWith('Connector: Foo'); + }); }); describe('ensureLicenseForActionType()', () => { let license: Subject; let licenseState: ILicenseState; + const mockNotifyUsage = jest.fn(); const fooActionType: ActionType = { id: 'foo', name: 'Foo', @@ -130,6 +163,7 @@ describe('ensureLicenseForActionType()', () => { beforeEach(() => { license = new Subject(); licenseState = new LicenseState(license); + licenseState.setNotifyUsage(mockNotifyUsage); }); test('should throw when license not defined', () => { @@ -178,6 +212,15 @@ describe('ensureLicenseForActionType()', () => { license.next(goldLicense); licenseState.ensureLicenseForActionType(fooActionType); }); + + test('should call notifyUsage', () => { + const goldLicense = licensingMock.createLicense({ + license: { status: 'active', type: 'gold' }, + }); + license.next(goldLicense); + licenseState.ensureLicenseForActionType(fooActionType); + expect(mockNotifyUsage).toHaveBeenCalledWith('Connector: Foo'); + }); }); function createUnavailableLicense() { diff --git a/x-pack/plugins/actions/server/lib/license_state.ts b/x-pack/plugins/actions/server/lib/license_state.ts index 1686d0201e96c..902fadb3da170 100644 --- a/x-pack/plugins/actions/server/lib/license_state.ts +++ b/x-pack/plugins/actions/server/lib/license_state.ts @@ -11,6 +11,8 @@ import { ILicense } from '../../../licensing/common/types'; import { PLUGIN } from '../constants/plugin'; import { ActionType } from '../types'; import { ActionTypeDisabledError } from './errors'; +import { LicensingPluginStart } from '../../../licensing/server'; +import { getActionTypeFeatureUsageName } from './get_action_type_feature_usage_name'; export type ILicenseState = PublicMethodsOf; @@ -24,6 +26,7 @@ export class LicenseState { private licenseInformation: ActionsLicenseInformation = this.checkLicense(undefined); private subscription: Subscription; private license?: ILicense; + private _notifyUsage: LicensingPluginStart['featureUsage']['notifyUsage'] | null = null; constructor(license$: Observable) { this.subscription = license$.subscribe(this.updateInformation.bind(this)); @@ -34,6 +37,10 @@ export class LicenseState { this.licenseInformation = this.checkLicense(license); } + public setNotifyUsage(notifyUsage: LicensingPluginStart['featureUsage']['notifyUsage']) { + this._notifyUsage = notifyUsage; + } + public clean() { this.subscription.unsubscribe(); } @@ -43,8 +50,13 @@ export class LicenseState { } public isLicenseValidForActionType( - actionType: ActionType + actionType: ActionType, + { notifyUsage }: { notifyUsage: boolean } = { notifyUsage: false } ): { isValid: true } | { isValid: false; reason: 'unavailable' | 'expired' | 'invalid' } { + if (notifyUsage) { + this.notifyUsage(actionType); + } + if (!this.license?.isAvailable) { return { isValid: false, reason: 'unavailable' }; } @@ -65,7 +77,16 @@ export class LicenseState { } } + private notifyUsage(actionType: ActionType) { + // No need to notify usage on basic action types + if (this._notifyUsage && actionType.minimumLicenseRequired !== 'basic') { + this._notifyUsage(getActionTypeFeatureUsageName(actionType)); + } + } + public ensureLicenseForActionType(actionType: ActionType) { + this.notifyUsage(actionType); + const check = this.isLicenseValidForActionType(actionType); if (check.isValid) { diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index 9d545600e61ee..7f7f9e196da07 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -211,6 +211,7 @@ describe('Actions Plugin', () => { features: featuresPluginMock.createSetup(), }; pluginsStart = { + licensing: licensingMock.createStart(), taskManager: taskManagerMock.createStart(), encryptedSavedObjects: encryptedSavedObjectsMock.createStart(), }; @@ -255,5 +256,49 @@ describe('Actions Plugin', () => { ); }); }); + + describe('isActionTypeEnabled()', () => { + const actionType: ActionType = { + id: 'my-action-type', + name: 'My action type', + minimumLicenseRequired: 'gold', + executor: jest.fn(), + }; + + it('passes through the notifyUsage option when set to true', async () => { + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const pluginSetup = await plugin.setup(coreSetup as any, pluginsSetup); + pluginSetup.registerType(actionType); + const pluginStart = plugin.start(coreStart, pluginsStart); + + pluginStart.isActionTypeEnabled('my-action-type', { notifyUsage: true }); + expect(pluginsStart.licensing.featureUsage.notifyUsage).toHaveBeenCalledWith( + 'Connector: My action type' + ); + }); + }); + + describe('isActionExecutable()', () => { + const actionType: ActionType = { + id: 'my-action-type', + name: 'My action type', + minimumLicenseRequired: 'gold', + executor: jest.fn(), + }; + + it('passes through the notifyUsage option when set to true', async () => { + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const pluginSetup = await plugin.setup(coreSetup as any, pluginsSetup); + pluginSetup.registerType(actionType); + const pluginStart = plugin.start(coreStart, pluginsStart); + + pluginStart.isActionExecutable('123', 'my-action-type', { notifyUsage: true }); + expect(pluginsStart.licensing.featureUsage.notifyUsage).toHaveBeenCalledWith( + 'Connector: My action type' + ); + }); + }); }); }); diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 1a15a5a815195..ef20ffbb9ee68 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -26,7 +26,7 @@ import { EncryptedSavedObjectsPluginStart, } from '../../encrypted_saved_objects/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; -import { LicensingPluginSetup } from '../../licensing/server'; +import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server'; import { LICENSE_TYPE } from '../../licensing/common/types'; import { SpacesPluginSetup, SpacesServiceSetup } from '../../spaces/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; @@ -93,8 +93,12 @@ export interface PluginSetupContract { } export interface PluginStartContract { - isActionTypeEnabled(id: string): boolean; - isActionExecutable(actionId: string, actionTypeId: string): boolean; + isActionTypeEnabled(id: string, options?: { notifyUsage: boolean }): boolean; + isActionExecutable( + actionId: string, + actionTypeId: string, + options?: { notifyUsage: boolean } + ): boolean; getActionsClientWithRequest(request: KibanaRequest): Promise>; getActionsAuthorizationWithRequest(request: KibanaRequest): PublicMethodsOf; preconfiguredActions: PreConfiguredAction[]; @@ -113,6 +117,7 @@ export interface ActionsPluginsSetup { export interface ActionsPluginsStart { encryptedSavedObjects: EncryptedSavedObjectsPluginStart; taskManager: TaskManagerStartContract; + licensing: LicensingPluginStart; } const includedHiddenTypes = [ @@ -196,6 +201,7 @@ export class ActionsPlugin implements Plugin, Plugi } const actionTypeRegistry = new ActionTypeRegistry({ + licensing: plugins.licensing, taskRunnerFactory, taskManager: plugins.taskManager, actionsConfigUtils, @@ -268,6 +274,7 @@ export class ActionsPlugin implements Plugin, Plugi public start(core: CoreStart, plugins: ActionsPluginsStart): PluginStartContract { const { logger, + licenseState, actionExecutor, actionTypeRegistry, taskRunnerFactory, @@ -278,6 +285,8 @@ export class ActionsPlugin implements Plugin, Plugi getUnsecuredSavedObjectsClient, } = this; + licenseState?.setNotifyUsage(plugins.licensing.featureUsage.notifyUsage); + const encryptedSavedObjectsClient = plugins.encryptedSavedObjects.getClient({ includedHiddenTypes, }); @@ -368,11 +377,15 @@ export class ActionsPlugin implements Plugin, Plugi scheduleActionsTelemetry(this.telemetryLogger, plugins.taskManager); return { - isActionTypeEnabled: (id) => { - return this.actionTypeRegistry!.isActionTypeEnabled(id); + isActionTypeEnabled: (id, options = { notifyUsage: false }) => { + return this.actionTypeRegistry!.isActionTypeEnabled(id, options); }, - isActionExecutable: (actionId: string, actionTypeId: string) => { - return this.actionTypeRegistry!.isActionExecutable(actionId, actionTypeId); + isActionExecutable: ( + actionId: string, + actionTypeId: string, + options = { notifyUsage: false } + ) => { + return this.actionTypeRegistry!.isActionExecutable(actionId, actionTypeId, options); }, getActionsAuthorizationWithRequest(request: KibanaRequest) { return instantiateAuthorization(request); diff --git a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts index ef3a9e42b983f..88abce7298622 100644 --- a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts @@ -1038,6 +1038,11 @@ export class AlertsClient { const actionsClient = await this.getActionsClient(); const actionIds = [...new Set(alertActions.map((alertAction) => alertAction.id))]; const actionResults = await actionsClient.getBulk(actionIds); + const actionTypeIds = [...new Set(actionResults.map((action) => action.actionTypeId))]; + actionTypeIds.forEach((id) => { + // Notify action type usage via "isActionTypeEnabled" function + actionsClient.isActionTypeEnabled(id, { notifyUsage: true }); + }); alertActions.forEach(({ id, ...alertAction }, i) => { const actionResultValue = actionResults.find((action) => action.id === id); if (actionResultValue) { diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts index 65a30d1750149..56e868732e3fb 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts @@ -10,9 +10,9 @@ import { taskManagerMock } from '../../../../task_manager/server/task_manager.mo import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; -import { actionsClientMock, actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; -import { ActionsAuthorization } from '../../../../actions/server'; +import { ActionsAuthorization, ActionsClient } from '../../../../actions/server'; import { TaskStatus } from '../../../../task_manager/server'; import { getBeforeSetup, setGlobalDate } from './lib'; @@ -374,6 +374,10 @@ describe('create()', () => { "scheduledTaskId": "task-123", } `); + const actionsClient = (await alertsClientParams.getActionsClient()) as jest.Mocked< + ActionsClient + >; + expect(actionsClient.isActionTypeEnabled).toHaveBeenCalledWith('test', { notifyUsage: true }); }); test('creates an alert with multiple actions', async () => { @@ -690,7 +694,11 @@ describe('create()', () => { test('throws error if loading actions fails', async () => { const data = getMockData(); - const actionsClient = actionsClientMock.create(); + // Reset from default behaviour + const actionsClient = (await alertsClientParams.getActionsClient()) as jest.Mocked< + ActionsClient + >; + actionsClient.getBulk.mockReset(); actionsClient.getBulk.mockRejectedValueOnce(new Error('Test Error')); alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts index 14275575f75f4..60b5b62954f05 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts @@ -15,7 +15,7 @@ import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/s import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; import { AlertsAuthorization } from '../../authorization/alerts_authorization'; import { resolvable } from '../../test_utils'; -import { ActionsAuthorization } from '../../../../actions/server'; +import { ActionsAuthorization, ActionsClient } from '../../../../actions/server'; import { TaskStatus } from '../../../../task_manager/server'; import { getBeforeSetup, setGlobalDate } from './lib'; @@ -319,6 +319,11 @@ describe('update()', () => { "version": "123", } `); + const actionsClient = (await alertsClientParams.getActionsClient()) as jest.Mocked< + ActionsClient + >; + expect(actionsClient.isActionTypeEnabled).toHaveBeenCalledWith('test', { notifyUsage: true }); + expect(actionsClient.isActionTypeEnabled).toHaveBeenCalledWith('test2', { notifyUsage: true }); }); it('calls the createApiKey function', async () => { diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts index aca447b6adedd..21e642d228b4d 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts @@ -86,7 +86,9 @@ export function createExecutionHandler({ const alertLabel = `${alertType.id}:${alertId}: '${alertName}'`; for (const action of actions) { - if (!actionsPlugin.isActionExecutable(action.id, action.actionTypeId)) { + if ( + !actionsPlugin.isActionExecutable(action.id, action.actionTypeId, { notifyUsage: true }) + ) { logger.warn( `Alert "${alertId}" skipped scheduling action "${action.id}" because it is disabled` ); diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx index f910f34d258fd..4f87e13104371 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx @@ -17,7 +17,7 @@ import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression'; import { AnomalySeverity, SelectAnomalySeverity, -} from './SelectAnomalySeverity'; +} from './select_anomaly_severity'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { EnvironmentField, diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.test.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.test.tsx new file mode 100644 index 0000000000000..0db8fa6c9d0d4 --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.test.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { render } from '@testing-library/react'; +import React, { ReactNode } from 'react'; +import { IntlProvider } from 'react-intl'; +import { ANOMALY_SEVERITY } from '../../../../../ml/common'; +import { SelectAnomalySeverity } from './select_anomaly_severity'; + +function Wrapper({ children }: { children?: ReactNode }) { + return {children}; +} + +describe('SelectAnomalySeverity', () => { + it('shows the correct text for each item', async () => { + const result = render( + {}} + value={ANOMALY_SEVERITY.CRITICAL} + />, + { wrapper: Wrapper } + ); + const button = (await result.findAllByText('critical'))[1]; + + button.click(); + + const options = await result.findAllByTestId( + 'SelectAnomalySeverity option text' + ); + + expect( + options.map((option) => (option.firstChild as HTMLElement)?.innerHTML) + ).toEqual([ + 'score critical ', // Trailing space is intentional here, to keep the i18n simple + 'score major and above', + 'score minor and above', + 'score warning and above', + ]); + }); +}); diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.tsx similarity index 86% rename from x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx rename to x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.tsx index 468d08339431c..b0513c3b59579 100644 --- a/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/select_anomaly_severity.tsx @@ -45,11 +45,14 @@ export function SelectAnomalySeverity({ onChange, value }: Props) { -

+

diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__fixtures__/props.json similarity index 89% rename from x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json rename to x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__fixtures__/props.json index 7f24ad8b0d308..2e213c44bccf0 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__fixtures__/props.json @@ -11,10 +11,7 @@ "value": 46.06666666666667, "timeseries": [] }, - "avgResponseTime": null, - "environments": [ - "test" - ] + "environments": ["test"] }, { "serviceName": "opbeans-python", diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js deleted file mode 100644 index 7c306c16cca1f..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { shallow } from 'enzyme'; -import { ServiceList, SERVICE_COLUMNS } from '../index'; -import props from './props.json'; -import { mockMoment } from '../../../../../utils/testHelpers'; -import { ServiceHealthStatus } from '../../../../../../common/service_health_status'; - -describe('ServiceOverview -> List', () => { - beforeAll(() => { - mockMoment(); - }); - - it('renders empty state', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - it('renders with data', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - it('renders columns correctly', () => { - const service = { - serviceName: 'opbeans-python', - agentName: 'python', - transactionsPerMinute: { - value: 86.93333333333334, - timeseries: [], - }, - errorsPerMinute: { - value: 12.6, - timeseries: [], - }, - avgResponseTime: { - value: 91535.42944785276, - timeseries: [], - }, - environments: ['test'], - }; - const renderedColumns = SERVICE_COLUMNS.map((c) => - c.render(service[c.field], service) - ); - - expect(renderedColumns[0]).toMatchSnapshot(); - }); - - describe('without ML data', () => { - it('does not render health column', () => { - const wrapper = shallow(); - - const columns = wrapper.props().columns; - - expect(columns[0].field).not.toBe('healthStatus'); - }); - }); - - describe('with ML data', () => { - it('renders health column', () => { - const wrapper = shallow( - ({ - ...item, - healthStatus: ServiceHealthStatus.warning, - }))} - /> - ); - - const columns = wrapper.props().columns; - - expect(columns[0].field).toBe('healthStatus'); - }); - }); -}); diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap deleted file mode 100644 index e6a9823f3ee28..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap +++ /dev/null @@ -1,153 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ServiceOverview -> List renders columns correctly 1`] = ` - -`; - -exports[`ServiceOverview -> List renders empty state 1`] = ` - -`; - -exports[`ServiceOverview -> List renders with data 1`] = ` - -`; diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx index aa0222582b891..0ce07a3c0ad27 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx @@ -153,7 +153,7 @@ export const SERVICE_COLUMNS: Array> = [ width: px(unit * 10), }, { - field: 'errorsPerMinute', + field: 'transactionErrorRate', name: i18n.translate('xpack.apm.servicesTable.transactionErrorRate', { defaultMessage: 'Error rate %', }), @@ -191,18 +191,20 @@ export function ServiceList({ items, noItemsMessage }: Props) { const columns = displayHealthStatus ? SERVICE_COLUMNS : SERVICE_COLUMNS.filter((column) => column.field !== 'healthStatus'); + const initialSortField = displayHealthStatus + ? 'healthStatus' + : 'transactionsPerMinute'; return ( { // For healthStatus, sort items by healthStatus first, then by TPM - return sortField === 'healthStatus' ? orderBy( itemsToSort, @@ -220,13 +222,15 @@ export function ServiceList({ items, noItemsMessage }: Props) { itemsToSort, (item) => { switch (sortField) { + // Use `?? -1` here so `undefined` will appear after/before `0`. + // In the table this will make the "N/A" items always at the + // bottom/top. case 'avgResponseTime': - return item.avgResponseTime?.value ?? 0; + return item.avgResponseTime?.value ?? -1; case 'transactionsPerMinute': - return item.transactionsPerMinute?.value ?? 0; + return item.transactionsPerMinute?.value ?? -1; case 'transactionErrorRate': - return item.transactionErrorRate?.value ?? 0; - + return item.transactionErrorRate?.value ?? -1; default: return item[sortField as keyof typeof item]; } diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/service_list.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/service_list.test.tsx new file mode 100644 index 0000000000000..73777c2221a5b --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/service_list.test.tsx @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { ReactNode } from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { ServiceHealthStatus } from '../../../../../common/service_health_status'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ServiceListAPIResponse } from '../../../../../server/lib/services/get_services'; +import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; +import { mockMoment, renderWithTheme } from '../../../../utils/testHelpers'; +import { ServiceList, SERVICE_COLUMNS } from './'; +import props from './__fixtures__/props.json'; + +function Wrapper({ children }: { children?: ReactNode }) { + return ( + + {children} + + ); +} + +describe('ServiceList', () => { + beforeAll(() => { + mockMoment(); + }); + + it('renders empty state', () => { + expect(() => + renderWithTheme(, { wrapper: Wrapper }) + ).not.toThrowError(); + }); + + it('renders with data', () => { + expect(() => + renderWithTheme( + , + { wrapper: Wrapper } + ) + ).not.toThrowError(); + }); + + it('renders columns correctly', () => { + const service: any = { + serviceName: 'opbeans-python', + agentName: 'python', + transactionsPerMinute: { + value: 86.93333333333334, + timeseries: [], + }, + errorsPerMinute: { + value: 12.6, + timeseries: [], + }, + avgResponseTime: { + value: 91535.42944785276, + timeseries: [], + }, + environments: ['test'], + }; + const renderedColumns = SERVICE_COLUMNS.map((c) => + c.render!(service[c.field!], service) + ); + + expect(renderedColumns[0]).toMatchInlineSnapshot(` + + `); + }); + + describe('without ML data', () => { + it('does not render the health column', () => { + const { queryByText } = renderWithTheme( + , + { + wrapper: Wrapper, + } + ); + const healthHeading = queryByText('Health'); + + expect(healthHeading).toBeNull(); + }); + + it('sorts by transactions per minute', async () => { + const { findByTitle } = renderWithTheme( + , + { + wrapper: Wrapper, + } + ); + + expect( + await findByTitle('Trans. per minute; Sorted in descending order') + ).toBeInTheDocument(); + }); + }); + + describe('with ML data', () => { + it('renders the health column', async () => { + const { findByTitle } = renderWithTheme( + ({ + ...item, + healthStatus: ServiceHealthStatus.warning, + }) + )} + />, + { wrapper: Wrapper } + ); + + expect( + await findByTitle('Health; Sorted in descending order') + ).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx index 7c887da6dc5e6..8c7d088d36eb2 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -36,7 +36,7 @@ import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTy import { TransactionList } from './TransactionList'; import { useRedirect } from './useRedirect'; import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types'; -import { ClientSideMonitoringCallout } from './ClientSideMonitoringCallout'; +import { UserExperienceCallout } from './user_experience_callout'; function getRedirectLocation({ urlParams, @@ -129,7 +129,7 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) { {transactionType === TRANSACTION_PAGE_LOAD && ( <> - + )} diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/ClientSideMonitoringCallout.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/user_experience_callout.tsx similarity index 75% rename from x-pack/plugins/apm/public/components/app/TransactionOverview/ClientSideMonitoringCallout.tsx rename to x-pack/plugins/apm/public/components/app/TransactionOverview/user_experience_callout.tsx index becae4d7eb5d7..41e84d4acfba5 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/ClientSideMonitoringCallout.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/user_experience_callout.tsx @@ -9,21 +9,21 @@ import { EuiButton, EuiCallOut, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; -export function ClientSideMonitoringCallout() { +export function UserExperienceCallout() { const { core } = useApmPluginContext(); - const clientSideMonitoringHref = core.http.basePath.prepend(`/app/ux`); + const userExperienceHref = core.http.basePath.prepend(`/app/ux`); return ( {i18n.translate( - 'xpack.apm.transactionOverview.clientSideMonitoring.calloutText', + 'xpack.apm.transactionOverview.userExperience.calloutText', { defaultMessage: 'We are beyond excited to introduce a new experience for analyzing the user experience metrics specifically for your RUM services. It provides insights into the core vitals and visitor breakdown by browser and location. The app is always available in the left sidebar among the other Observability views.', @@ -31,9 +31,9 @@ export function ClientSideMonitoringCallout() { )} - + {i18n.translate( - 'xpack.apm.transactionOverview.clientSideMonitoring.linkLabel', + 'xpack.apm.transactionOverview.userExperience.linkLabel', { defaultMessage: 'Take me there' } )} diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index 75d8842d4843b..d597b65040ce6 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -55,7 +55,7 @@ export const APM_FEATURE = { read: [], }, alerting: { - all: Object.values(AlertType), + read: Object.values(AlertType), }, management: { insightsAndAlerting: ['triggersActions'], diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index 4cb0c4c750dd1..5ea3714e81b6f 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -58,14 +58,27 @@ export async function getServicesItems({ }), ]); - const allMetrics = [ - ...transactionDurationAverages, - ...agentNames, - ...transactionRates, - ...transactionErrorRates, - ...environments, - ...healthStatuses, - ]; + const apmServiceMetrics = joinByKey( + [ + ...transactionDurationAverages, + ...agentNames, + ...transactionRates, + ...transactionErrorRates, + ...environments, + ], + 'serviceName' + ); + + const apmServices = apmServiceMetrics.map(({ serviceName }) => serviceName); + + // make sure to exclude health statuses from services + // that are not found in APM data + + const matchedHealthStatuses = healthStatuses.filter(({ serviceName }) => + apmServices.includes(serviceName) + ); + + const allMetrics = [...apmServiceMetrics, ...matchedHealthStatuses]; return joinByKey(allMetrics, 'serviceName'); } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/API.md b/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/API.md deleted file mode 100644 index cd3927b4b9df0..0000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/API.md +++ /dev/null @@ -1,1498 +0,0 @@ -# Flot Reference # - -**Table of Contents** - -[Introduction](#introduction) -| [Data Format](#data-format) -| [Plot Options](#plot-options) -| [Customizing the legend](#customizing-the-legend) -| [Customizing the axes](#customizing-the-axes) -| [Multiple axes](#multiple-axes) -| [Time series data](#time-series-data) -| [Customizing the data series](#customizing-the-data-series) -| [Customizing the grid](#customizing-the-grid) -| [Specifying gradients](#specifying-gradients) -| [Plot Methods](#plot-methods) -| [Hooks](#hooks) -| [Plugins](#plugins) -| [Version number](#version-number) - ---- - -## Introduction ## - -Consider a call to the plot function: - -```js -var plot = $.plot(placeholder, data, options) -``` - -The placeholder is a jQuery object or DOM element or jQuery expression -that the plot will be put into. This placeholder needs to have its -width and height set as explained in the [README](README.md) (go read that now if -you haven't, it's short). The plot will modify some properties of the -placeholder so it's recommended you simply pass in a div that you -don't use for anything else. Make sure you check any fancy styling -you apply to the div, e.g. background images have been reported to be a -problem on IE 7. - -The plot function can also be used as a jQuery chainable property. This form -naturally can't return the plot object directly, but you can still access it -via the 'plot' data key, like this: - -```js -var plot = $("#placeholder").plot(data, options).data("plot"); -``` - -The format of the data is documented below, as is the available -options. The plot object returned from the call has some methods you -can call. These are documented separately below. - -Note that in general Flot gives no guarantees if you change any of the -objects you pass in to the plot function or get out of it since -they're not necessarily deep-copied. - - -## Data Format ## - -The data is an array of data series: - -```js -[ series1, series2, ... ] -``` - -A series can either be raw data or an object with properties. The raw -data format is an array of points: - -```js -[ [x1, y1], [x2, y2], ... ] -``` - -E.g. - -```js -[ [1, 3], [2, 14.01], [3.5, 3.14] ] -``` - -Note that to simplify the internal logic in Flot both the x and y -values must be numbers (even if specifying time series, see below for -how to do this). This is a common problem because you might retrieve -data from the database and serialize them directly to JSON without -noticing the wrong type. If you're getting mysterious errors, double -check that you're inputting numbers and not strings. - -If a null is specified as a point or if one of the coordinates is null -or couldn't be converted to a number, the point is ignored when -drawing. As a special case, a null value for lines is interpreted as a -line segment end, i.e. the points before and after the null value are -not connected. - -Lines and points take two coordinates. For filled lines and bars, you -can specify a third coordinate which is the bottom of the filled -area/bar (defaults to 0). - -The format of a single series object is as follows: - -```js -{ - color: color or number - data: rawdata - label: string - lines: specific lines options - bars: specific bars options - points: specific points options - xaxis: number - yaxis: number - clickable: boolean - hoverable: boolean - shadowSize: number - highlightColor: color or number -} -``` - -You don't have to specify any of them except the data, the rest are -options that will get default values. Typically you'd only specify -label and data, like this: - -```js -{ - label: "y = 3", - data: [[0, 3], [10, 3]] -} -``` - -The label is used for the legend, if you don't specify one, the series -will not show up in the legend. - -If you don't specify color, the series will get a color from the -auto-generated colors. The color is either a CSS color specification -(like "rgb(255, 100, 123)") or an integer that specifies which of -auto-generated colors to select, e.g. 0 will get color no. 0, etc. - -The latter is mostly useful if you let the user add and remove series, -in which case you can hard-code the color index to prevent the colors -from jumping around between the series. - -The "xaxis" and "yaxis" options specify which axis to use. The axes -are numbered from 1 (default), so { yaxis: 2} means that the series -should be plotted against the second y axis. - -"clickable" and "hoverable" can be set to false to disable -interactivity for specific series if interactivity is turned on in -the plot, see below. - -The rest of the options are all documented below as they are the same -as the default options passed in via the options parameter in the plot -command. When you specify them for a specific data series, they will -override the default options for the plot for that data series. - -Here's a complete example of a simple data specification: - -```js -[ { label: "Foo", data: [ [10, 1], [17, -14], [30, 5] ] }, - { label: "Bar", data: [ [11, 13], [19, 11], [30, -7] ] } -] -``` - - -## Plot Options ## - -All options are completely optional. They are documented individually -below, to change them you just specify them in an object, e.g. - -```js -var options = { - series: { - lines: { show: true }, - points: { show: true } - } -}; - -$.plot(placeholder, data, options); -``` - - -## Customizing the legend ## - -```js -legend: { - show: boolean - labelFormatter: null or (fn: string, series object -> string) - labelBoxBorderColor: color - noColumns: number - position: "ne" or "nw" or "se" or "sw" - margin: number of pixels or [x margin, y margin] - backgroundColor: null or color - backgroundOpacity: number between 0 and 1 - container: null or jQuery object/DOM element/jQuery expression - sorted: null/false, true, "ascending", "descending", "reverse", or a comparator -} -``` - -The legend is generated as a table with the data series labels and -small label boxes with the color of the series. If you want to format -the labels in some way, e.g. make them to links, you can pass in a -function for "labelFormatter". Here's an example that makes them -clickable: - -```js -labelFormatter: function(label, series) { - // series is the series object for the label - return '' + label + ''; -} -``` - -To prevent a series from showing up in the legend, simply have the function -return null. - -"noColumns" is the number of columns to divide the legend table into. -"position" specifies the overall placement of the legend within the -plot (top-right, top-left, etc.) and margin the distance to the plot -edge (this can be either a number or an array of two numbers like [x, -y]). "backgroundColor" and "backgroundOpacity" specifies the -background. The default is a partly transparent auto-detected -background. - -If you want the legend to appear somewhere else in the DOM, you can -specify "container" as a jQuery object/expression to put the legend -table into. The "position" and "margin" etc. options will then be -ignored. Note that Flot will overwrite the contents of the container. - -Legend entries appear in the same order as their series by default. If "sorted" -is "reverse" then they appear in the opposite order from their series. To sort -them alphabetically, you can specify true, "ascending" or "descending", where -true and "ascending" are equivalent. - -You can also provide your own comparator function that accepts two -objects with "label" and "color" properties, and returns zero if they -are equal, a positive value if the first is greater than the second, -and a negative value if the first is less than the second. - -```js -sorted: function(a, b) { - // sort alphabetically in ascending order - return a.label == b.label ? 0 : ( - a.label > b.label ? 1 : -1 - ) -} -``` - - -## Customizing the axes ## - -```js -xaxis, yaxis: { - show: null or true/false - position: "bottom" or "top" or "left" or "right" - mode: null or "time" ("time" requires jquery.flot.time.js plugin) - timezone: null, "browser" or timezone (only makes sense for mode: "time") - - color: null or color spec - tickColor: null or color spec - font: null or font spec object - - min: null or number - max: null or number - autoscaleMargin: null or number - - transform: null or fn: number -> number - inverseTransform: null or fn: number -> number - - ticks: null or number or ticks array or (fn: axis -> ticks array) - tickSize: number or array - minTickSize: number or array - tickFormatter: (fn: number, object -> string) or string - tickDecimals: null or number - - labelWidth: null or number - labelHeight: null or number - reserveSpace: null or true - - tickLength: null or number - - alignTicksWithAxis: null or number -} -``` - -All axes have the same kind of options. The following describes how to -configure one axis, see below for what to do if you've got more than -one x axis or y axis. - -If you don't set the "show" option (i.e. it is null), visibility is -auto-detected, i.e. the axis will show up if there's data associated -with it. You can override this by setting the "show" option to true or -false. - -The "position" option specifies where the axis is placed, bottom or -top for x axes, left or right for y axes. The "mode" option determines -how the data is interpreted, the default of null means as decimal -numbers. Use "time" for time series data; see the time series data -section. The time plugin (jquery.flot.time.js) is required for time -series support. - -The "color" option determines the color of the line and ticks for the axis, and -defaults to the grid color with transparency. For more fine-grained control you -can also set the color of the ticks separately with "tickColor". - -You can customize the font and color used to draw the axis tick labels with CSS -or directly via the "font" option. When "font" is null - the default - each -tick label is given the 'flot-tick-label' class. For compatibility with Flot -0.7 and earlier the labels are also given the 'tickLabel' class, but this is -deprecated and scheduled to be removed with the release of version 1.0.0. - -To enable more granular control over styles, labels are divided between a set -of text containers, with each holding the labels for one axis. These containers -are given the classes 'flot-[x|y]-axis', and 'flot-[x|y]#-axis', where '#' is -the number of the axis when there are multiple axes. For example, the x-axis -labels for a simple plot with only a single x-axis might look like this: - -```html -
-
January 2013
- ... -
-``` - -For direct control over label styles you can also provide "font" as an object -with this format: - -```js -{ - size: 11, - lineHeight: 13, - style: "italic", - weight: "bold", - family: "sans-serif", - variant: "small-caps", - color: "#545454" -} -``` - -The size and lineHeight must be expressed in pixels; CSS units such as 'em' -or 'smaller' are not allowed. - -The options "min"/"max" are the precise minimum/maximum value on the -scale. If you don't specify either of them, a value will automatically -be chosen based on the minimum/maximum data values. Note that Flot -always examines all the data values you feed to it, even if a -restriction on another axis may make some of them invisible (this -makes interactive use more stable). - -The "autoscaleMargin" is a bit esoteric: it's the fraction of margin -that the scaling algorithm will add to avoid that the outermost points -ends up on the grid border. Note that this margin is only applied when -a min or max value is not explicitly set. If a margin is specified, -the plot will furthermore extend the axis end-point to the nearest -whole tick. The default value is "null" for the x axes and 0.02 for y -axes which seems appropriate for most cases. - -"transform" and "inverseTransform" are callbacks you can put in to -change the way the data is drawn. You can design a function to -compress or expand certain parts of the axis non-linearly, e.g. -suppress weekends or compress far away points with a logarithm or some -other means. When Flot draws the plot, each value is first put through -the transform function. Here's an example, the x axis can be turned -into a natural logarithm axis with the following code: - -```js -xaxis: { - transform: function (v) { return Math.log(v); }, - inverseTransform: function (v) { return Math.exp(v); } -} -``` - -Similarly, for reversing the y axis so the values appear in inverse -order: - -```js -yaxis: { - transform: function (v) { return -v; }, - inverseTransform: function (v) { return -v; } -} -``` - -Note that for finding extrema, Flot assumes that the transform -function does not reorder values (it should be monotone). - -The inverseTransform is simply the inverse of the transform function -(so v == inverseTransform(transform(v)) for all relevant v). It is -required for converting from canvas coordinates to data coordinates, -e.g. for a mouse interaction where a certain pixel is clicked. If you -don't use any interactive features of Flot, you may not need it. - - -The rest of the options deal with the ticks. - -If you don't specify any ticks, a tick generator algorithm will make -some for you. The algorithm has two passes. It first estimates how -many ticks would be reasonable and uses this number to compute a nice -round tick interval size. Then it generates the ticks. - -You can specify how many ticks the algorithm aims for by setting -"ticks" to a number. The algorithm always tries to generate reasonably -round tick values so even if you ask for three ticks, you might get -five if that fits better with the rounding. If you don't want any -ticks at all, set "ticks" to 0 or an empty array. - -Another option is to skip the rounding part and directly set the tick -interval size with "tickSize". If you set it to 2, you'll get ticks at -2, 4, 6, etc. Alternatively, you can specify that you just don't want -ticks at a size less than a specific tick size with "minTickSize". -Note that for time series, the format is an array like [2, "month"], -see the next section. - -If you want to completely override the tick algorithm, you can specify -an array for "ticks", either like this: - -```js -ticks: [0, 1.2, 2.4] -``` - -Or like this where the labels are also customized: - -```js -ticks: [[0, "zero"], [1.2, "one mark"], [2.4, "two marks"]] -``` - -You can mix the two if you like. - -For extra flexibility you can specify a function as the "ticks" -parameter. The function will be called with an object with the axis -min and max and should return a ticks array. Here's a simplistic tick -generator that spits out intervals of pi, suitable for use on the x -axis for trigonometric functions: - -```js -function piTickGenerator(axis) { - var res = [], i = Math.floor(axis.min / Math.PI); - do { - var v = i * Math.PI; - res.push([v, i + "\u03c0"]); - ++i; - } while (v < axis.max); - return res; -} -``` - -You can control how the ticks look like with "tickDecimals", the -number of decimals to display (default is auto-detected). - -Alternatively, for ultimate control over how ticks are formatted you can -provide a function to "tickFormatter". The function is passed two -parameters, the tick value and an axis object with information, and -should return a string. The default formatter looks like this: - -```js -function formatter(val, axis) { - return val.toFixed(axis.tickDecimals); -} -``` - -The axis object has "min" and "max" with the range of the axis, -"tickDecimals" with the number of decimals to round the value to and -"tickSize" with the size of the interval between ticks as calculated -by the automatic axis scaling algorithm (or specified by you). Here's -an example of a custom formatter: - -```js -function suffixFormatter(val, axis) { - if (val > 1000000) - return (val / 1000000).toFixed(axis.tickDecimals) + " MB"; - else if (val > 1000) - return (val / 1000).toFixed(axis.tickDecimals) + " kB"; - else - return val.toFixed(axis.tickDecimals) + " B"; -} -``` - -"labelWidth" and "labelHeight" specifies a fixed size of the tick -labels in pixels. They're useful in case you need to align several -plots. "reserveSpace" means that even if an axis isn't shown, Flot -should reserve space for it - it is useful in combination with -labelWidth and labelHeight for aligning multi-axis charts. - -"tickLength" is the length of the tick lines in pixels. By default, the -innermost axes will have ticks that extend all across the plot, while -any extra axes use small ticks. A value of null means use the default, -while a number means small ticks of that length - set it to 0 to hide -the lines completely. - -If you set "alignTicksWithAxis" to the number of another axis, e.g. -alignTicksWithAxis: 1, Flot will ensure that the autogenerated ticks -of this axis are aligned with the ticks of the other axis. This may -improve the looks, e.g. if you have one y axis to the left and one to -the right, because the grid lines will then match the ticks in both -ends. The trade-off is that the forced ticks won't necessarily be at -natural places. - - -## Multiple axes ## - -If you need more than one x axis or y axis, you need to specify for -each data series which axis they are to use, as described under the -format of the data series, e.g. { data: [...], yaxis: 2 } specifies -that a series should be plotted against the second y axis. - -To actually configure that axis, you can't use the xaxis/yaxis options -directly - instead there are two arrays in the options: - -```js -xaxes: [] -yaxes: [] -``` - -Here's an example of configuring a single x axis and two y axes (we -can leave options of the first y axis empty as the defaults are fine): - -```js -{ - xaxes: [ { position: "top" } ], - yaxes: [ { }, { position: "right", min: 20 } ] -} -``` - -The arrays get their default values from the xaxis/yaxis settings, so -say you want to have all y axes start at zero, you can simply specify -yaxis: { min: 0 } instead of adding a min parameter to all the axes. - -Generally, the various interfaces in Flot dealing with data points -either accept an xaxis/yaxis parameter to specify which axis number to -use (starting from 1), or lets you specify the coordinate directly as -x2/x3/... or x2axis/x3axis/... instead of "x" or "xaxis". - - -## Time series data ## - -Please note that it is now required to include the time plugin, -jquery.flot.time.js, for time series support. - -Time series are a bit more difficult than scalar data because -calendars don't follow a simple base 10 system. For many cases, Flot -abstracts most of this away, but it can still be a bit difficult to -get the data into Flot. So we'll first discuss the data format. - -The time series support in Flot is based on Javascript timestamps, -i.e. everywhere a time value is expected or handed over, a Javascript -timestamp number is used. This is a number, not a Date object. A -Javascript timestamp is the number of milliseconds since January 1, -1970 00:00:00 UTC. This is almost the same as Unix timestamps, except it's -in milliseconds, so remember to multiply by 1000! - -You can see a timestamp like this - -```js -alert((new Date()).getTime()) -``` - -There are different schools of thought when it comes to display of -timestamps. Many will want the timestamps to be displayed according to -a certain time zone, usually the time zone in which the data has been -produced. Some want the localized experience, where the timestamps are -displayed according to the local time of the visitor. Flot supports -both. Optionally you can include a third-party library to get -additional timezone support. - -Default behavior is that Flot always displays timestamps according to -UTC. The reason being that the core Javascript Date object does not -support other fixed time zones. Often your data is at another time -zone, so it may take a little bit of tweaking to work around this -limitation. - -The easiest way to think about it is to pretend that the data -production time zone is UTC, even if it isn't. So if you have a -datapoint at 2002-02-20 08:00, you can generate a timestamp for eight -o'clock UTC even if it really happened eight o'clock UTC+0200. - -In PHP you can get an appropriate timestamp with: - -```php -strtotime("2002-02-20 UTC") * 1000 -``` - -In Python you can get it with something like: - -```python -calendar.timegm(datetime_object.timetuple()) * 1000 -``` -In Ruby you can get it using the `#to_i` method on the -[`Time`](http://apidock.com/ruby/Time/to_i) object. If you're using the -`active_support` gem (default for Ruby on Rails applications) `#to_i` is also -available on the `DateTime` and `ActiveSupport::TimeWithZone` objects. You -simply need to multiply the result by 1000: - -```ruby -Time.now.to_i * 1000 # => 1383582043000 -# ActiveSupport examples: -DateTime.now.to_i * 1000 # => 1383582043000 -ActiveSupport::TimeZone.new('Asia/Shanghai').now.to_i * 1000 -# => 1383582043000 -``` - -In .NET you can get it with something like: - -```aspx -public static int GetJavascriptTimestamp(System.DateTime input) -{ - System.TimeSpan span = new System.TimeSpan(System.DateTime.Parse("1/1/1970").Ticks); - System.DateTime time = input.Subtract(span); - return (long)(time.Ticks / 10000); -} -``` - -Javascript also has some support for parsing date strings, so it is -possible to generate the timestamps manually client-side. - -If you've already got the real UTC timestamp, it's too late to use the -pretend trick described above. But you can fix up the timestamps by -adding the time zone offset, e.g. for UTC+0200 you would add 2 hours -to the UTC timestamp you got. Then it'll look right on the plot. Most -programming environments have some means of getting the timezone -offset for a specific date (note that you need to get the offset for -each individual timestamp to account for daylight savings). - -The alternative with core Javascript is to interpret the timestamps -according to the time zone that the visitor is in, which means that -the ticks will shift with the time zone and daylight savings of each -visitor. This behavior is enabled by setting the axis option -"timezone" to the value "browser". - -If you need more time zone functionality than this, there is still -another option. If you include the "timezone-js" library - in the page and set axis.timezone -to a value recognized by said library, Flot will use timezone-js to -interpret the timestamps according to that time zone. - -Once you've gotten the timestamps into the data and specified "time" -as the axis mode, Flot will automatically generate relevant ticks and -format them. As always, you can tweak the ticks via the "ticks" option -- just remember that the values should be timestamps (numbers), not -Date objects. - -Tick generation and formatting can also be controlled separately -through the following axis options: - -```js -minTickSize: array -timeformat: null or format string -monthNames: null or array of size 12 of strings -dayNames: null or array of size 7 of strings -twelveHourClock: boolean -``` - -Here "timeformat" is a format string to use. You might use it like -this: - -```js -xaxis: { - mode: "time", - timeformat: "%Y/%m/%d" -} -``` - -This will result in tick labels like "2000/12/24". A subset of the -standard strftime specifiers are supported (plus the nonstandard %q): - -```js -%a: weekday name (customizable) -%b: month name (customizable) -%d: day of month, zero-padded (01-31) -%e: day of month, space-padded ( 1-31) -%H: hours, 24-hour time, zero-padded (00-23) -%I: hours, 12-hour time, zero-padded (01-12) -%m: month, zero-padded (01-12) -%M: minutes, zero-padded (00-59) -%q: quarter (1-4) -%S: seconds, zero-padded (00-59) -%y: year (two digits) -%Y: year (four digits) -%p: am/pm -%P: AM/PM (uppercase version of %p) -%w: weekday as number (0-6, 0 being Sunday) -``` - -Flot 0.8 switched from %h to the standard %H hours specifier. The %h specifier -is still available, for backwards-compatibility, but is deprecated and -scheduled to be removed permanently with the release of version 1.0. - -You can customize the month names with the "monthNames" option. For -instance, for Danish you might specify: - -```js -monthNames: ["jan", "feb", "mar", "apr", "maj", "jun", "jul", "aug", "sep", "okt", "nov", "dec"] -``` - -Similarly you can customize the weekday names with the "dayNames" -option. An example in French: - -```js -dayNames: ["dim", "lun", "mar", "mer", "jeu", "ven", "sam"] -``` - -If you set "twelveHourClock" to true, the autogenerated timestamps -will use 12 hour AM/PM timestamps instead of 24 hour. This only -applies if you have not set "timeformat". Use the "%I" and "%p" or -"%P" options if you want to build your own format string with 12-hour -times. - -If the Date object has a strftime property (and it is a function), it -will be used instead of the built-in formatter. Thus you can include -a strftime library such as http://hacks.bluesmoon.info/strftime/ for -more powerful date/time formatting. - -If everything else fails, you can control the formatting by specifying -a custom tick formatter function as usual. Here's a simple example -which will format December 24 as 24/12: - -```js -tickFormatter: function (val, axis) { - var d = new Date(val); - return d.getUTCDate() + "/" + (d.getUTCMonth() + 1); -} -``` - -Note that for the time mode "tickSize" and "minTickSize" are a bit -special in that they are arrays on the form "[value, unit]" where unit -is one of "second", "minute", "hour", "day", "month" and "year". So -you can specify - -```js -minTickSize: [1, "month"] -``` - -to get a tick interval size of at least 1 month and correspondingly, -if axis.tickSize is [2, "day"] in the tick formatter, the ticks have -been produced with two days in-between. - - -## Customizing the data series ## - -```js -series: { - lines, points, bars: { - show: boolean - lineWidth: number - fill: boolean or number - fillColor: null or color/gradient - } - - lines, bars: { - zero: boolean - } - - points: { - radius: number - symbol: "circle" or function - } - - bars: { - barWidth: number - align: "left", "right" or "center" - horizontal: boolean - } - - lines: { - steps: boolean - } - - shadowSize: number - highlightColor: color or number -} - -colors: [ color1, color2, ... ] -``` - -The options inside "series: {}" are copied to each of the series. So -you can specify that all series should have bars by putting it in the -global options, or override it for individual series by specifying -bars in a particular the series object in the array of data. - -The most important options are "lines", "points" and "bars" that -specify whether and how lines, points and bars should be shown for -each data series. In case you don't specify anything at all, Flot will -default to showing lines (you can turn this off with -lines: { show: false }). You can specify the various types -independently of each other, and Flot will happily draw each of them -in turn (this is probably only useful for lines and points), e.g. - -```js -var options = { - series: { - lines: { show: true, fill: true, fillColor: "rgba(255, 255, 255, 0.8)" }, - points: { show: true, fill: false } - } -}; -``` - -"lineWidth" is the thickness of the line or outline in pixels. You can -set it to 0 to prevent a line or outline from being drawn; this will -also hide the shadow. - -"fill" is whether the shape should be filled. For lines, this produces -area graphs. You can use "fillColor" to specify the color of the fill. -If "fillColor" evaluates to false (default for everything except -points which are filled with white), the fill color is auto-set to the -color of the data series. You can adjust the opacity of the fill by -setting fill to a number between 0 (fully transparent) and 1 (fully -opaque). - -For bars, fillColor can be a gradient, see the gradient documentation -below. "barWidth" is the width of the bars in units of the x axis (or -the y axis if "horizontal" is true), contrary to most other measures -that are specified in pixels. For instance, for time series the unit -is milliseconds so 24 * 60 * 60 * 1000 produces bars with the width of -a day. "align" specifies whether a bar should be left-aligned -(default), right-aligned or centered on top of the value it represents. -When "horizontal" is on, the bars are drawn horizontally, i.e. from the -y axis instead of the x axis; note that the bar end points are still -defined in the same way so you'll probably want to swap the -coordinates if you've been plotting vertical bars first. - -Area and bar charts normally start from zero, regardless of the data's range. -This is because they convey information through size, and starting from a -different value would distort their meaning. In cases where the fill is purely -for decorative purposes, however, "zero" allows you to override this behavior. -It defaults to true for filled lines and bars; setting it to false tells the -series to use the same automatic scaling as an un-filled line. - -For lines, "steps" specifies whether two adjacent data points are -connected with a straight (possibly diagonal) line or with first a -horizontal and then a vertical line. Note that this transforms the -data by adding extra points. - -For points, you can specify the radius and the symbol. The only -built-in symbol type is circles, for other types you can use a plugin -or define them yourself by specifying a callback: - -```js -function cross(ctx, x, y, radius, shadow) { - var size = radius * Math.sqrt(Math.PI) / 2; - ctx.moveTo(x - size, y - size); - ctx.lineTo(x + size, y + size); - ctx.moveTo(x - size, y + size); - ctx.lineTo(x + size, y - size); -} -``` - -The parameters are the drawing context, x and y coordinates of the -center of the point, a radius which corresponds to what the circle -would have used and whether the call is to draw a shadow (due to -limited canvas support, shadows are currently faked through extra -draws). It's good practice to ensure that the area covered by the -symbol is the same as for the circle with the given radius, this -ensures that all symbols have approximately the same visual weight. - -"shadowSize" is the default size of shadows in pixels. Set it to 0 to -remove shadows. - -"highlightColor" is the default color of the translucent overlay used -to highlight the series when the mouse hovers over it. - -The "colors" array specifies a default color theme to get colors for -the data series from. You can specify as many colors as you like, like -this: - -```js -colors: ["#d18b2c", "#dba255", "#919733"] -``` - -If there are more data series than colors, Flot will try to generate -extra colors by lightening and darkening colors in the theme. - - -## Customizing the grid ## - -```js -grid: { - show: boolean - aboveData: boolean - color: color - backgroundColor: color/gradient or null - margin: number or margin object - labelMargin: number - axisMargin: number - markings: array of markings or (fn: axes -> array of markings) - borderWidth: number or object with "top", "right", "bottom" and "left" properties with different widths - borderColor: color or null or object with "top", "right", "bottom" and "left" properties with different colors - minBorderMargin: number or null - clickable: boolean - hoverable: boolean - autoHighlight: boolean - mouseActiveRadius: number -} - -interaction: { - redrawOverlayInterval: number or -1 -} -``` - -The grid is the thing with the axes and a number of ticks. Many of the -things in the grid are configured under the individual axes, but not -all. "color" is the color of the grid itself whereas "backgroundColor" -specifies the background color inside the grid area, here null means -that the background is transparent. You can also set a gradient, see -the gradient documentation below. - -You can turn off the whole grid including tick labels by setting -"show" to false. "aboveData" determines whether the grid is drawn -above the data or below (below is default). - -"margin" is the space in pixels between the canvas edge and the grid, -which can be either a number or an object with individual margins for -each side, in the form: - -```js -margin: { - top: top margin in pixels - left: left margin in pixels - bottom: bottom margin in pixels - right: right margin in pixels -} -``` - -"labelMargin" is the space in pixels between tick labels and axis -line, and "axisMargin" is the space in pixels between axes when there -are two next to each other. - -"borderWidth" is the width of the border around the plot. Set it to 0 -to disable the border. Set it to an object with "top", "right", -"bottom" and "left" properties to use different widths. You can -also set "borderColor" if you want the border to have a different color -than the grid lines. Set it to an object with "top", "right", "bottom" -and "left" properties to use different colors. "minBorderMargin" controls -the default minimum margin around the border - it's used to make sure -that points aren't accidentally clipped by the canvas edge so by default -the value is computed from the point radius. - -"markings" is used to draw simple lines and rectangular areas in the -background of the plot. You can either specify an array of ranges on -the form { xaxis: { from, to }, yaxis: { from, to } } (with multiple -axes, you can specify coordinates for other axes instead, e.g. as -x2axis/x3axis/...) or with a function that returns such an array given -the axes for the plot in an object as the first parameter. - -You can set the color of markings by specifying "color" in the ranges -object. Here's an example array: - -```js -markings: [ { xaxis: { from: 0, to: 2 }, yaxis: { from: 10, to: 10 }, color: "#bb0000" }, ... ] -``` - -If you leave out one of the values, that value is assumed to go to the -border of the plot. So for example if you only specify { xaxis: { -from: 0, to: 2 } } it means an area that extends from the top to the -bottom of the plot in the x range 0-2. - -A line is drawn if from and to are the same, e.g. - -```js -markings: [ { yaxis: { from: 1, to: 1 } }, ... ] -``` - -would draw a line parallel to the x axis at y = 1. You can control the -line width with "lineWidth" in the range object. - -An example function that makes vertical stripes might look like this: - -```js -markings: function (axes) { - var markings = []; - for (var x = Math.floor(axes.xaxis.min); x < axes.xaxis.max; x += 2) - markings.push({ xaxis: { from: x, to: x + 1 } }); - return markings; -} -``` - -If you set "clickable" to true, the plot will listen for click events -on the plot area and fire a "plotclick" event on the placeholder with -a position and a nearby data item object as parameters. The coordinates -are available both in the unit of the axes (not in pixels) and in -global screen coordinates. - -Likewise, if you set "hoverable" to true, the plot will listen for -mouse move events on the plot area and fire a "plothover" event with -the same parameters as the "plotclick" event. If "autoHighlight" is -true (the default), nearby data items are highlighted automatically. -If needed, you can disable highlighting and control it yourself with -the highlight/unhighlight plot methods described elsewhere. - -You can use "plotclick" and "plothover" events like this: - -```js -$.plot($("#placeholder"), [ d ], { grid: { clickable: true } }); - -$("#placeholder").bind("plotclick", function (event, pos, item) { - alert("You clicked at " + pos.x + ", " + pos.y); - // axis coordinates for other axes, if present, are in pos.x2, pos.x3, ... - // if you need global screen coordinates, they are pos.pageX, pos.pageY - - if (item) { - highlight(item.series, item.datapoint); - alert("You clicked a point!"); - } -}); -``` - -The item object in this example is either null or a nearby object on the form: - -```js -item: { - datapoint: the point, e.g. [0, 2] - dataIndex: the index of the point in the data array - series: the series object - seriesIndex: the index of the series - pageX, pageY: the global screen coordinates of the point -} -``` - -For instance, if you have specified the data like this - -```js -$.plot($("#placeholder"), [ { label: "Foo", data: [[0, 10], [7, 3]] } ], ...); -``` - -and the mouse is near the point (7, 3), "datapoint" is [7, 3], -"dataIndex" will be 1, "series" is a normalized series object with -among other things the "Foo" label in series.label and the color in -series.color, and "seriesIndex" is 0. Note that plugins and options -that transform the data can shift the indexes from what you specified -in the original data array. - -If you use the above events to update some other information and want -to clear out that info in case the mouse goes away, you'll probably -also need to listen to "mouseout" events on the placeholder div. - -"mouseActiveRadius" specifies how far the mouse can be from an item -and still activate it. If there are two or more points within this -radius, Flot chooses the closest item. For bars, the top-most bar -(from the latest specified data series) is chosen. - -If you want to disable interactivity for a specific data series, you -can set "hoverable" and "clickable" to false in the options for that -series, like this: - -```js -{ data: [...], label: "Foo", clickable: false } -``` - -"redrawOverlayInterval" specifies the maximum time to delay a redraw -of interactive things (this works as a rate limiting device). The -default is capped to 60 frames per second. You can set it to -1 to -disable the rate limiting. - - -## Specifying gradients ## - -A gradient is specified like this: - -```js -{ colors: [ color1, color2, ... ] } -``` - -For instance, you might specify a background on the grid going from -black to gray like this: - -```js -grid: { - backgroundColor: { colors: ["#000", "#999"] } -} -``` - -For the series you can specify the gradient as an object that -specifies the scaling of the brightness and the opacity of the series -color, e.g. - -```js -{ colors: [{ opacity: 0.8 }, { brightness: 0.6, opacity: 0.8 } ] } -``` - -where the first color simply has its alpha scaled, whereas the second -is also darkened. For instance, for bars the following makes the bars -gradually disappear, without outline: - -```js -bars: { - show: true, - lineWidth: 0, - fill: true, - fillColor: { colors: [ { opacity: 0.8 }, { opacity: 0.1 } ] } -} -``` - -Flot currently only supports vertical gradients drawn from top to -bottom because that's what works with IE. - - -## Plot Methods ## - -The Plot object returned from the plot function has some methods you -can call: - - - highlight(series, datapoint) - - Highlight a specific datapoint in the data series. You can either - specify the actual objects, e.g. if you got them from a - "plotclick" event, or you can specify the indices, e.g. - highlight(1, 3) to highlight the fourth point in the second series - (remember, zero-based indexing). - - - unhighlight(series, datapoint) or unhighlight() - - Remove the highlighting of the point, same parameters as - highlight. - - If you call unhighlight with no parameters, e.g. as - plot.unhighlight(), all current highlights are removed. - - - setData(data) - - You can use this to reset the data used. Note that axis scaling, - ticks, legend etc. will not be recomputed (use setupGrid() to do - that). You'll probably want to call draw() afterwards. - - You can use this function to speed up redrawing a small plot if - you know that the axes won't change. Put in the new data with - setData(newdata), call draw(), and you're good to go. Note that - for large datasets, almost all the time is consumed in draw() - plotting the data so in this case don't bother. - - - setupGrid() - - Recalculate and set axis scaling, ticks, legend etc. - - Note that because of the drawing model of the canvas, this - function will immediately redraw (actually reinsert in the DOM) - the labels and the legend, but not the actual tick lines because - they're drawn on the canvas. You need to call draw() to get the - canvas redrawn. - - - draw() - - Redraws the plot canvas. - - - triggerRedrawOverlay() - - Schedules an update of an overlay canvas used for drawing - interactive things like a selection and point highlights. This - is mostly useful for writing plugins. The redraw doesn't happen - immediately, instead a timer is set to catch multiple successive - redraws (e.g. from a mousemove). You can get to the overlay by - setting up a drawOverlay hook. - - - width()/height() - - Gets the width and height of the plotting area inside the grid. - This is smaller than the canvas or placeholder dimensions as some - extra space is needed (e.g. for labels). - - - offset() - - Returns the offset of the plotting area inside the grid relative - to the document, useful for instance for calculating mouse - positions (event.pageX/Y minus this offset is the pixel position - inside the plot). - - - pointOffset({ x: xpos, y: ypos }) - - Returns the calculated offset of the data point at (x, y) in data - space within the placeholder div. If you are working with multiple - axes, you can specify the x and y axis references, e.g. - - ```js - o = pointOffset({ x: xpos, y: ypos, xaxis: 2, yaxis: 3 }) - // o.left and o.top now contains the offset within the div - ```` - - - resize() - - Tells Flot to resize the drawing canvas to the size of the - placeholder. You need to run setupGrid() and draw() afterwards as - canvas resizing is a destructive operation. This is used - internally by the resize plugin. - - - shutdown() - - Cleans up any event handlers Flot has currently registered. This - is used internally. - -There are also some members that let you peek inside the internal -workings of Flot which is useful in some cases. Note that if you change -something in the objects returned, you're changing the objects used by -Flot to keep track of its state, so be careful. - - - getData() - - Returns an array of the data series currently used in normalized - form with missing settings filled in according to the global - options. So for instance to find out what color Flot has assigned - to the data series, you could do this: - - ```js - var series = plot.getData(); - for (var i = 0; i < series.length; ++i) - alert(series[i].color); - ``` - - A notable other interesting field besides color is datapoints - which has a field "points" with the normalized data points in a - flat array (the field "pointsize" is the increment in the flat - array to get to the next point so for a dataset consisting only of - (x,y) pairs it would be 2). - - - getAxes() - - Gets an object with the axes. The axes are returned as the - attributes of the object, so for instance getAxes().xaxis is the - x axis. - - Various things are stuffed inside an axis object, e.g. you could - use getAxes().xaxis.ticks to find out what the ticks are for the - xaxis. Two other useful attributes are p2c and c2p, functions for - transforming from data point space to the canvas plot space and - back. Both returns values that are offset with the plot offset. - Check the Flot source code for the complete set of attributes (or - output an axis with console.log() and inspect it). - - With multiple axes, the extra axes are returned as x2axis, x3axis, - etc., e.g. getAxes().y2axis is the second y axis. You can check - y2axis.used to see whether the axis is associated with any data - points and y2axis.show to see if it is currently shown. - - - getPlaceholder() - - Returns placeholder that the plot was put into. This can be useful - for plugins for adding DOM elements or firing events. - - - getCanvas() - - Returns the canvas used for drawing in case you need to hack on it - yourself. You'll probably need to get the plot offset too. - - - getPlotOffset() - - Gets the offset that the grid has within the canvas as an object - with distances from the canvas edges as "left", "right", "top", - "bottom". I.e., if you draw a circle on the canvas with the center - placed at (left, top), its center will be at the top-most, left - corner of the grid. - - - getOptions() - - Gets the options for the plot, normalized, with default values - filled in. You get a reference to actual values used by Flot, so - if you modify the values in here, Flot will use the new values. - If you change something, you probably have to call draw() or - setupGrid() or triggerRedrawOverlay() to see the change. - - -## Hooks ## - -In addition to the public methods, the Plot object also has some hooks -that can be used to modify the plotting process. You can install a -callback function at various points in the process, the function then -gets access to the internal data structures in Flot. - -Here's an overview of the phases Flot goes through: - - 1. Plugin initialization, parsing options - - 2. Constructing the canvases used for drawing - - 3. Set data: parsing data specification, calculating colors, - copying raw data points into internal format, - normalizing them, finding max/min for axis auto-scaling - - 4. Grid setup: calculating axis spacing, ticks, inserting tick - labels, the legend - - 5. Draw: drawing the grid, drawing each of the series in turn - - 6. Setting up event handling for interactive features - - 7. Responding to events, if any - - 8. Shutdown: this mostly happens in case a plot is overwritten - -Each hook is simply a function which is put in the appropriate array. -You can add them through the "hooks" option, and they are also available -after the plot is constructed as the "hooks" attribute on the returned -plot object, e.g. - -```js - // define a simple draw hook - function hellohook(plot, canvascontext) { alert("hello!"); }; - - // pass it in, in an array since we might want to specify several - var plot = $.plot(placeholder, data, { hooks: { draw: [hellohook] } }); - - // we can now find it again in plot.hooks.draw[0] unless a plugin - // has added other hooks -``` - -The available hooks are described below. All hook callbacks get the -plot object as first parameter. You can find some examples of defined -hooks in the plugins bundled with Flot. - - - processOptions [phase 1] - - ```function(plot, options)``` - - Called after Flot has parsed and merged options. Useful in the - instance where customizations beyond simple merging of default - values is needed. A plugin might use it to detect that it has been - enabled and then turn on or off other options. - - - - processRawData [phase 3] - - ```function(plot, series, data, datapoints)``` - - Called before Flot copies and normalizes the raw data for the given - series. If the function fills in datapoints.points with normalized - points and sets datapoints.pointsize to the size of the points, - Flot will skip the copying/normalization step for this series. - - In any case, you might be interested in setting datapoints.format, - an array of objects for specifying how a point is normalized and - how it interferes with axis scaling. It accepts the following options: - - ```js - { - x, y: boolean, - number: boolean, - required: boolean, - defaultValue: value, - autoscale: boolean - } - ``` - - "x" and "y" specify whether the value is plotted against the x or y axis, - and is currently used only to calculate axis min-max ranges. The default - format array, for example, looks like this: - - ```js - [ - { x: true, number: true, required: true }, - { y: true, number: true, required: true } - ] - ``` - - This indicates that a point, i.e. [0, 25], consists of two values, with the - first being plotted on the x axis and the second on the y axis. - - If "number" is true, then the value must be numeric, and is set to null if - it cannot be converted to a number. - - "defaultValue" provides a fallback in case the original value is null. This - is for instance handy for bars, where one can omit the third coordinate - (the bottom of the bar), which then defaults to zero. - - If "required" is true, then the value must exist (be non-null) for the - point as a whole to be valid. If no value is provided, then the entire - point is cleared out with nulls, turning it into a gap in the series. - - "autoscale" determines whether the value is considered when calculating an - automatic min-max range for the axes that the value is plotted against. - - - processDatapoints [phase 3] - - ```function(plot, series, datapoints)``` - - Called after normalization of the given series but before finding - min/max of the data points. This hook is useful for implementing data - transformations. "datapoints" contains the normalized data points in - a flat array as datapoints.points with the size of a single point - given in datapoints.pointsize. Here's a simple transform that - multiplies all y coordinates by 2: - - ```js - function multiply(plot, series, datapoints) { - var points = datapoints.points, ps = datapoints.pointsize; - for (var i = 0; i < points.length; i += ps) - points[i + 1] *= 2; - } - ``` - - Note that you must leave datapoints in a good condition as Flot - doesn't check it or do any normalization on it afterwards. - - - processOffset [phase 4] - - ```function(plot, offset)``` - - Called after Flot has initialized the plot's offset, but before it - draws any axes or plot elements. This hook is useful for customizing - the margins between the grid and the edge of the canvas. "offset" is - an object with attributes "top", "bottom", "left" and "right", - corresponding to the margins on the four sides of the plot. - - - drawBackground [phase 5] - - ```function(plot, canvascontext)``` - - Called before all other drawing operations. Used to draw backgrounds - or other custom elements before the plot or axes have been drawn. - - - drawSeries [phase 5] - - ```function(plot, canvascontext, series)``` - - Hook for custom drawing of a single series. Called just before the - standard drawing routine has been called in the loop that draws - each series. - - - draw [phase 5] - - ```function(plot, canvascontext)``` - - Hook for drawing on the canvas. Called after the grid is drawn - (unless it's disabled or grid.aboveData is set) and the series have - been plotted (in case any points, lines or bars have been turned - on). For examples of how to draw things, look at the source code. - - - bindEvents [phase 6] - - ```function(plot, eventHolder)``` - - Called after Flot has setup its event handlers. Should set any - necessary event handlers on eventHolder, a jQuery object with the - canvas, e.g. - - ```js - function (plot, eventHolder) { - eventHolder.mousedown(function (e) { - alert("You pressed the mouse at " + e.pageX + " " + e.pageY); - }); - } - ``` - - Interesting events include click, mousemove, mouseup/down. You can - use all jQuery events. Usually, the event handlers will update the - state by drawing something (add a drawOverlay hook and call - triggerRedrawOverlay) or firing an externally visible event for - user code. See the crosshair plugin for an example. - - Currently, eventHolder actually contains both the static canvas - used for the plot itself and the overlay canvas used for - interactive features because some versions of IE get the stacking - order wrong. The hook only gets one event, though (either for the - overlay or for the static canvas). - - Note that custom plot events generated by Flot are not generated on - eventHolder, but on the div placeholder supplied as the first - argument to the plot call. You can get that with - plot.getPlaceholder() - that's probably also the one you should use - if you need to fire a custom event. - - - drawOverlay [phase 7] - - ```function (plot, canvascontext)``` - - The drawOverlay hook is used for interactive things that need a - canvas to draw on. The model currently used by Flot works the way - that an extra overlay canvas is positioned on top of the static - canvas. This overlay is cleared and then completely redrawn - whenever something interesting happens. This hook is called when - the overlay canvas is to be redrawn. - - "canvascontext" is the 2D context of the overlay canvas. You can - use this to draw things. You'll most likely need some of the - metrics computed by Flot, e.g. plot.width()/plot.height(). See the - crosshair plugin for an example. - - - shutdown [phase 8] - - ```function (plot, eventHolder)``` - - Run when plot.shutdown() is called, which usually only happens in - case a plot is overwritten by a new plot. If you're writing a - plugin that adds extra DOM elements or event handlers, you should - add a callback to clean up after you. Take a look at the section in - the [PLUGINS](PLUGINS.md) document for more info. - - -## Plugins ## - -Plugins extend the functionality of Flot. To use a plugin, simply -include its Javascript file after Flot in the HTML page. - -If you're worried about download size/latency, you can concatenate all -the plugins you use, and Flot itself for that matter, into one big file -(make sure you get the order right), then optionally run it through a -Javascript minifier such as YUI Compressor. - -Here's a brief explanation of how the plugin plumbings work: - -Each plugin registers itself in the global array $.plot.plugins. When -you make a new plot object with $.plot, Flot goes through this array -calling the "init" function of each plugin and merging default options -from the "option" attribute of the plugin. The init function gets a -reference to the plot object created and uses this to register hooks -and add new public methods if needed. - -See the [PLUGINS](PLUGINS.md) document for details on how to write a plugin. As the -above description hints, it's actually pretty easy. - - -## Version number ## - -The version number of Flot is available in ```$.plot.version```. diff --git a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/index.js b/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/index.js deleted file mode 100644 index ff3de33b017a7..0000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/index.js +++ /dev/null @@ -1,15 +0,0 @@ -// TODO: This is bad. We aren't loading jQuery again, because Kibana already has, but we aren't really assured of that. -// That could change at any moment. - -//import $ from 'jquery'; -//if (window) window.jQuery = $; -require('./jquery.flot'); -require('./jquery.flot.time'); -require('./jquery.flot.canvas'); -require('./jquery.flot.symbol'); -require('./jquery.flot.crosshair'); -require('./jquery.flot.selection'); -require('./jquery.flot.stack'); -require('./jquery.flot.threshold'); -require('./jquery.flot.fillbetween'); -//module.exports = $; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.crosshair.js b/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.crosshair.js deleted file mode 100644 index 5111695e3d12c..0000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.crosshair.js +++ /dev/null @@ -1,176 +0,0 @@ -/* Flot plugin for showing crosshairs when the mouse hovers over the plot. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin supports these options: - - crosshair: { - mode: null or "x" or "y" or "xy" - color: color - lineWidth: number - } - -Set the mode to one of "x", "y" or "xy". The "x" mode enables a vertical -crosshair that lets you trace the values on the x axis, "y" enables a -horizontal crosshair and "xy" enables them both. "color" is the color of the -crosshair (default is "rgba(170, 0, 0, 0.80)"), "lineWidth" is the width of -the drawn lines (default is 1). - -The plugin also adds four public methods: - - - setCrosshair( pos ) - - Set the position of the crosshair. Note that this is cleared if the user - moves the mouse. "pos" is in coordinates of the plot and should be on the - form { x: xpos, y: ypos } (you can use x2/x3/... if you're using multiple - axes), which is coincidentally the same format as what you get from a - "plothover" event. If "pos" is null, the crosshair is cleared. - - - clearCrosshair() - - Clear the crosshair. - - - lockCrosshair(pos) - - Cause the crosshair to lock to the current location, no longer updating if - the user moves the mouse. Optionally supply a position (passed on to - setCrosshair()) to move it to. - - Example usage: - - var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } }; - $("#graph").bind( "plothover", function ( evt, position, item ) { - if ( item ) { - // Lock the crosshair to the data point being hovered - myFlot.lockCrosshair({ - x: item.datapoint[ 0 ], - y: item.datapoint[ 1 ] - }); - } else { - // Return normal crosshair operation - myFlot.unlockCrosshair(); - } - }); - - - unlockCrosshair() - - Free the crosshair to move again after locking it. -*/ - -(function ($) { - var options = { - crosshair: { - mode: null, // one of null, "x", "y" or "xy", - color: "rgba(170, 0, 0, 0.80)", - lineWidth: 1 - } - }; - - function init(plot) { - // position of crosshair in pixels - var crosshair = { x: -1, y: -1, locked: false }; - - plot.setCrosshair = function setCrosshair(pos) { - if (!pos) - crosshair.x = -1; - else { - var o = plot.p2c(pos); - crosshair.x = Math.max(0, Math.min(o.left, plot.width())); - crosshair.y = Math.max(0, Math.min(o.top, plot.height())); - } - - plot.triggerRedrawOverlay(); - }; - - plot.clearCrosshair = plot.setCrosshair; // passes null for pos - - plot.lockCrosshair = function lockCrosshair(pos) { - if (pos) - plot.setCrosshair(pos); - crosshair.locked = true; - }; - - plot.unlockCrosshair = function unlockCrosshair() { - crosshair.locked = false; - }; - - function onMouseOut(e) { - if (crosshair.locked) - return; - - if (crosshair.x != -1) { - crosshair.x = -1; - plot.triggerRedrawOverlay(); - } - } - - function onMouseMove(e) { - if (crosshair.locked) - return; - - if (plot.getSelection && plot.getSelection()) { - crosshair.x = -1; // hide the crosshair while selecting - return; - } - - var offset = plot.offset(); - crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width())); - crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height())); - plot.triggerRedrawOverlay(); - } - - plot.hooks.bindEvents.push(function (plot, eventHolder) { - if (!plot.getOptions().crosshair.mode) - return; - - eventHolder.mouseout(onMouseOut); - eventHolder.mousemove(onMouseMove); - }); - - plot.hooks.drawOverlay.push(function (plot, ctx) { - var c = plot.getOptions().crosshair; - if (!c.mode) - return; - - var plotOffset = plot.getPlotOffset(); - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - if (crosshair.x != -1) { - var adj = plot.getOptions().crosshair.lineWidth % 2 ? 0.5 : 0; - - ctx.strokeStyle = c.color; - ctx.lineWidth = c.lineWidth; - ctx.lineJoin = "round"; - - ctx.beginPath(); - if (c.mode.indexOf("x") != -1) { - var drawX = Math.floor(crosshair.x) + adj; - ctx.moveTo(drawX, 0); - ctx.lineTo(drawX, plot.height()); - } - if (c.mode.indexOf("y") != -1) { - var drawY = Math.floor(crosshair.y) + adj; - ctx.moveTo(0, drawY); - ctx.lineTo(plot.width(), drawY); - } - ctx.stroke(); - } - ctx.restore(); - }); - - plot.hooks.shutdown.push(function (plot, eventHolder) { - eventHolder.unbind("mouseout", onMouseOut); - eventHolder.unbind("mousemove", onMouseMove); - }); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'crosshair', - version: '1.0' - }); -})(jQuery); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.errorbars.js b/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.errorbars.js deleted file mode 100644 index 2583d5c20c321..0000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.errorbars.js +++ /dev/null @@ -1,353 +0,0 @@ -/* Flot plugin for plotting error bars. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -Error bars are used to show standard deviation and other statistical -properties in a plot. - -* Created by Rui Pereira - rui (dot) pereira (at) gmail (dot) com - -This plugin allows you to plot error-bars over points. Set "errorbars" inside -the points series to the axis name over which there will be error values in -your data array (*even* if you do not intend to plot them later, by setting -"show: null" on xerr/yerr). - -The plugin supports these options: - - series: { - points: { - errorbars: "x" or "y" or "xy", - xerr: { - show: null/false or true, - asymmetric: null/false or true, - upperCap: null or "-" or function, - lowerCap: null or "-" or function, - color: null or color, - radius: null or number - }, - yerr: { same options as xerr } - } - } - -Each data point array is expected to be of the type: - - "x" [ x, y, xerr ] - "y" [ x, y, yerr ] - "xy" [ x, y, xerr, yerr ] - -Where xerr becomes xerr_lower,xerr_upper for the asymmetric error case, and -equivalently for yerr. Eg., a datapoint for the "xy" case with symmetric -error-bars on X and asymmetric on Y would be: - - [ x, y, xerr, yerr_lower, yerr_upper ] - -By default no end caps are drawn. Setting upperCap and/or lowerCap to "-" will -draw a small cap perpendicular to the error bar. They can also be set to a -user-defined drawing function, with (ctx, x, y, radius) as parameters, as eg. - - function drawSemiCircle( ctx, x, y, radius ) { - ctx.beginPath(); - ctx.arc( x, y, radius, 0, Math.PI, false ); - ctx.moveTo( x - radius, y ); - ctx.lineTo( x + radius, y ); - ctx.stroke(); - } - -Color and radius both default to the same ones of the points series if not -set. The independent radius parameter on xerr/yerr is useful for the case when -we may want to add error-bars to a line, without showing the interconnecting -points (with radius: 0), and still showing end caps on the error-bars. -shadowSize and lineWidth are derived as well from the points series. - -*/ - -(function ($) { - var options = { - series: { - points: { - errorbars: null, //should be 'x', 'y' or 'xy' - xerr: { err: 'x', show: null, asymmetric: null, upperCap: null, lowerCap: null, color: null, radius: null}, - yerr: { err: 'y', show: null, asymmetric: null, upperCap: null, lowerCap: null, color: null, radius: null} - } - } - }; - - function processRawData(plot, series, data, datapoints){ - if (!series.points.errorbars) - return; - - // x,y values - var format = [ - { x: true, number: true, required: true }, - { y: true, number: true, required: true } - ]; - - var errors = series.points.errorbars; - // error bars - first X then Y - if (errors == 'x' || errors == 'xy') { - // lower / upper error - if (series.points.xerr.asymmetric) { - format.push({ x: true, number: true, required: true }); - format.push({ x: true, number: true, required: true }); - } else - format.push({ x: true, number: true, required: true }); - } - if (errors == 'y' || errors == 'xy') { - // lower / upper error - if (series.points.yerr.asymmetric) { - format.push({ y: true, number: true, required: true }); - format.push({ y: true, number: true, required: true }); - } else - format.push({ y: true, number: true, required: true }); - } - datapoints.format = format; - } - - function parseErrors(series, i){ - - var points = series.datapoints.points; - - // read errors from points array - var exl = null, - exu = null, - eyl = null, - eyu = null; - var xerr = series.points.xerr, - yerr = series.points.yerr; - - var eb = series.points.errorbars; - // error bars - first X - if (eb == 'x' || eb == 'xy') { - if (xerr.asymmetric) { - exl = points[i + 2]; - exu = points[i + 3]; - if (eb == 'xy') - if (yerr.asymmetric){ - eyl = points[i + 4]; - eyu = points[i + 5]; - } else eyl = points[i + 4]; - } else { - exl = points[i + 2]; - if (eb == 'xy') - if (yerr.asymmetric) { - eyl = points[i + 3]; - eyu = points[i + 4]; - } else eyl = points[i + 3]; - } - // only Y - } else if (eb == 'y') - if (yerr.asymmetric) { - eyl = points[i + 2]; - eyu = points[i + 3]; - } else eyl = points[i + 2]; - - // symmetric errors? - if (exu == null) exu = exl; - if (eyu == null) eyu = eyl; - - var errRanges = [exl, exu, eyl, eyu]; - // nullify if not showing - if (!xerr.show){ - errRanges[0] = null; - errRanges[1] = null; - } - if (!yerr.show){ - errRanges[2] = null; - errRanges[3] = null; - } - return errRanges; - } - - function drawSeriesErrors(plot, ctx, s){ - - var points = s.datapoints.points, - ps = s.datapoints.pointsize, - ax = [s.xaxis, s.yaxis], - radius = s.points.radius, - err = [s.points.xerr, s.points.yerr]; - - //sanity check, in case some inverted axis hack is applied to flot - var invertX = false; - if (ax[0].p2c(ax[0].max) < ax[0].p2c(ax[0].min)) { - invertX = true; - var tmp = err[0].lowerCap; - err[0].lowerCap = err[0].upperCap; - err[0].upperCap = tmp; - } - - var invertY = false; - if (ax[1].p2c(ax[1].min) < ax[1].p2c(ax[1].max)) { - invertY = true; - var tmp = err[1].lowerCap; - err[1].lowerCap = err[1].upperCap; - err[1].upperCap = tmp; - } - - for (var i = 0; i < s.datapoints.points.length; i += ps) { - - //parse - var errRanges = parseErrors(s, i); - - //cycle xerr & yerr - for (var e = 0; e < err.length; e++){ - - var minmax = [ax[e].min, ax[e].max]; - - //draw this error? - if (errRanges[e * err.length]){ - - //data coordinates - var x = points[i], - y = points[i + 1]; - - //errorbar ranges - var upper = [x, y][e] + errRanges[e * err.length + 1], - lower = [x, y][e] - errRanges[e * err.length]; - - //points outside of the canvas - if (err[e].err == 'x') - if (y > ax[1].max || y < ax[1].min || upper < ax[0].min || lower > ax[0].max) - continue; - if (err[e].err == 'y') - if (x > ax[0].max || x < ax[0].min || upper < ax[1].min || lower > ax[1].max) - continue; - - // prevent errorbars getting out of the canvas - var drawUpper = true, - drawLower = true; - - if (upper > minmax[1]) { - drawUpper = false; - upper = minmax[1]; - } - if (lower < minmax[0]) { - drawLower = false; - lower = minmax[0]; - } - - //sanity check, in case some inverted axis hack is applied to flot - if ((err[e].err == 'x' && invertX) || (err[e].err == 'y' && invertY)) { - //swap coordinates - var tmp = lower; - lower = upper; - upper = tmp; - tmp = drawLower; - drawLower = drawUpper; - drawUpper = tmp; - tmp = minmax[0]; - minmax[0] = minmax[1]; - minmax[1] = tmp; - } - - // convert to pixels - x = ax[0].p2c(x), - y = ax[1].p2c(y), - upper = ax[e].p2c(upper); - lower = ax[e].p2c(lower); - minmax[0] = ax[e].p2c(minmax[0]); - minmax[1] = ax[e].p2c(minmax[1]); - - //same style as points by default - var lw = err[e].lineWidth ? err[e].lineWidth : s.points.lineWidth, - sw = s.points.shadowSize != null ? s.points.shadowSize : s.shadowSize; - - //shadow as for points - if (lw > 0 && sw > 0) { - var w = sw / 2; - ctx.lineWidth = w; - ctx.strokeStyle = "rgba(0,0,0,0.1)"; - drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, w + w/2, minmax); - - ctx.strokeStyle = "rgba(0,0,0,0.2)"; - drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, w/2, minmax); - } - - ctx.strokeStyle = err[e].color? err[e].color: s.color; - ctx.lineWidth = lw; - //draw it - drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, 0, minmax); - } - } - } - } - - function drawError(ctx,err,x,y,upper,lower,drawUpper,drawLower,radius,offset,minmax){ - - //shadow offset - y += offset; - upper += offset; - lower += offset; - - // error bar - avoid plotting over circles - if (err.err == 'x'){ - if (upper > x + radius) drawPath(ctx, [[upper,y],[Math.max(x + radius,minmax[0]),y]]); - else drawUpper = false; - if (lower < x - radius) drawPath(ctx, [[Math.min(x - radius,minmax[1]),y],[lower,y]] ); - else drawLower = false; - } - else { - if (upper < y - radius) drawPath(ctx, [[x,upper],[x,Math.min(y - radius,minmax[0])]] ); - else drawUpper = false; - if (lower > y + radius) drawPath(ctx, [[x,Math.max(y + radius,minmax[1])],[x,lower]] ); - else drawLower = false; - } - - //internal radius value in errorbar, allows to plot radius 0 points and still keep proper sized caps - //this is a way to get errorbars on lines without visible connecting dots - radius = err.radius != null? err.radius: radius; - - // upper cap - if (drawUpper) { - if (err.upperCap == '-'){ - if (err.err=='x') drawPath(ctx, [[upper,y - radius],[upper,y + radius]] ); - else drawPath(ctx, [[x - radius,upper],[x + radius,upper]] ); - } else if ($.isFunction(err.upperCap)){ - if (err.err=='x') err.upperCap(ctx, upper, y, radius); - else err.upperCap(ctx, x, upper, radius); - } - } - // lower cap - if (drawLower) { - if (err.lowerCap == '-'){ - if (err.err=='x') drawPath(ctx, [[lower,y - radius],[lower,y + radius]] ); - else drawPath(ctx, [[x - radius,lower],[x + radius,lower]] ); - } else if ($.isFunction(err.lowerCap)){ - if (err.err=='x') err.lowerCap(ctx, lower, y, radius); - else err.lowerCap(ctx, x, lower, radius); - } - } - } - - function drawPath(ctx, pts){ - ctx.beginPath(); - ctx.moveTo(pts[0][0], pts[0][1]); - for (var p=1; p < pts.length; p++) - ctx.lineTo(pts[p][0], pts[p][1]); - ctx.stroke(); - } - - function draw(plot, ctx){ - var plotOffset = plot.getPlotOffset(); - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - $.each(plot.getData(), function (i, s) { - if (s.points.errorbars && (s.points.xerr.show || s.points.yerr.show)) - drawSeriesErrors(plot, ctx, s); - }); - ctx.restore(); - } - - function init(plot) { - plot.hooks.processRawData.push(processRawData); - plot.hooks.draw.push(draw); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'errorbars', - version: '1.0' - }); -})(jQuery); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.image.js b/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.image.js deleted file mode 100644 index 625a03571d270..0000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.image.js +++ /dev/null @@ -1,241 +0,0 @@ -/* Flot plugin for plotting images. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The data syntax is [ [ image, x1, y1, x2, y2 ], ... ] where (x1, y1) and -(x2, y2) are where you intend the two opposite corners of the image to end up -in the plot. Image must be a fully loaded Javascript image (you can make one -with new Image()). If the image is not complete, it's skipped when plotting. - -There are two helpers included for retrieving images. The easiest work the way -that you put in URLs instead of images in the data, like this: - - [ "myimage.png", 0, 0, 10, 10 ] - -Then call $.plot.image.loadData( data, options, callback ) where data and -options are the same as you pass in to $.plot. This loads the images, replaces -the URLs in the data with the corresponding images and calls "callback" when -all images are loaded (or failed loading). In the callback, you can then call -$.plot with the data set. See the included example. - -A more low-level helper, $.plot.image.load(urls, callback) is also included. -Given a list of URLs, it calls callback with an object mapping from URL to -Image object when all images are loaded or have failed loading. - -The plugin supports these options: - - series: { - images: { - show: boolean - anchor: "corner" or "center" - alpha: [ 0, 1 ] - } - } - -They can be specified for a specific series: - - $.plot( $("#placeholder"), [{ - data: [ ... ], - images: { ... } - ]) - -Note that because the data format is different from usual data points, you -can't use images with anything else in a specific data series. - -Setting "anchor" to "center" causes the pixels in the image to be anchored at -the corner pixel centers inside of at the pixel corners, effectively letting -half a pixel stick out to each side in the plot. - -A possible future direction could be support for tiling for large images (like -Google Maps). - -*/ - -(function ($) { - var options = { - series: { - images: { - show: false, - alpha: 1, - anchor: "corner" // or "center" - } - } - }; - - $.plot.image = {}; - - $.plot.image.loadDataImages = function (series, options, callback) { - var urls = [], points = []; - - var defaultShow = options.series.images.show; - - $.each(series, function (i, s) { - if (!(defaultShow || s.images.show)) - return; - - if (s.data) - s = s.data; - - $.each(s, function (i, p) { - if (typeof p[0] == "string") { - urls.push(p[0]); - points.push(p); - } - }); - }); - - $.plot.image.load(urls, function (loadedImages) { - $.each(points, function (i, p) { - var url = p[0]; - if (loadedImages[url]) - p[0] = loadedImages[url]; - }); - - callback(); - }); - } - - $.plot.image.load = function (urls, callback) { - var missing = urls.length, loaded = {}; - if (missing == 0) - callback({}); - - $.each(urls, function (i, url) { - var handler = function () { - --missing; - - loaded[url] = this; - - if (missing == 0) - callback(loaded); - }; - - $('').load(handler).error(handler).attr('src', url); - }); - }; - - function drawSeries(plot, ctx, series) { - var plotOffset = plot.getPlotOffset(); - - if (!series.images || !series.images.show) - return; - - var points = series.datapoints.points, - ps = series.datapoints.pointsize; - - for (var i = 0; i < points.length; i += ps) { - var img = points[i], - x1 = points[i + 1], y1 = points[i + 2], - x2 = points[i + 3], y2 = points[i + 4], - xaxis = series.xaxis, yaxis = series.yaxis, - tmp; - - // actually we should check img.complete, but it - // appears to be a somewhat unreliable indicator in - // IE6 (false even after load event) - if (!img || img.width <= 0 || img.height <= 0) - continue; - - if (x1 > x2) { - tmp = x2; - x2 = x1; - x1 = tmp; - } - if (y1 > y2) { - tmp = y2; - y2 = y1; - y1 = tmp; - } - - // if the anchor is at the center of the pixel, expand the - // image by 1/2 pixel in each direction - if (series.images.anchor == "center") { - tmp = 0.5 * (x2-x1) / (img.width - 1); - x1 -= tmp; - x2 += tmp; - tmp = 0.5 * (y2-y1) / (img.height - 1); - y1 -= tmp; - y2 += tmp; - } - - // clip - if (x1 == x2 || y1 == y2 || - x1 >= xaxis.max || x2 <= xaxis.min || - y1 >= yaxis.max || y2 <= yaxis.min) - continue; - - var sx1 = 0, sy1 = 0, sx2 = img.width, sy2 = img.height; - if (x1 < xaxis.min) { - sx1 += (sx2 - sx1) * (xaxis.min - x1) / (x2 - x1); - x1 = xaxis.min; - } - - if (x2 > xaxis.max) { - sx2 += (sx2 - sx1) * (xaxis.max - x2) / (x2 - x1); - x2 = xaxis.max; - } - - if (y1 < yaxis.min) { - sy2 += (sy1 - sy2) * (yaxis.min - y1) / (y2 - y1); - y1 = yaxis.min; - } - - if (y2 > yaxis.max) { - sy1 += (sy1 - sy2) * (yaxis.max - y2) / (y2 - y1); - y2 = yaxis.max; - } - - x1 = xaxis.p2c(x1); - x2 = xaxis.p2c(x2); - y1 = yaxis.p2c(y1); - y2 = yaxis.p2c(y2); - - // the transformation may have swapped us - if (x1 > x2) { - tmp = x2; - x2 = x1; - x1 = tmp; - } - if (y1 > y2) { - tmp = y2; - y2 = y1; - y1 = tmp; - } - - tmp = ctx.globalAlpha; - ctx.globalAlpha *= series.images.alpha; - ctx.drawImage(img, - sx1, sy1, sx2 - sx1, sy2 - sy1, - x1 + plotOffset.left, y1 + plotOffset.top, - x2 - x1, y2 - y1); - ctx.globalAlpha = tmp; - } - } - - function processRawData(plot, series, data, datapoints) { - if (!series.images.show) - return; - - // format is Image, x1, y1, x2, y2 (opposite corners) - datapoints.format = [ - { required: true }, - { x: true, number: true, required: true }, - { y: true, number: true, required: true }, - { x: true, number: true, required: true }, - { y: true, number: true, required: true } - ]; - } - - function init(plot) { - plot.hooks.processRawData.push(processRawData); - plot.hooks.drawSeries.push(drawSeries); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'image', - version: '1.1' - }); -})(jQuery); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.js b/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.js deleted file mode 100644 index 39f3e4cf3efe5..0000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.js +++ /dev/null @@ -1,3168 +0,0 @@ -/* Javascript plotting library for jQuery, version 0.8.3. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -*/ - -// first an inline dependency, jquery.colorhelpers.js, we inline it here -// for convenience - -/* Plugin for jQuery for working with colors. - * - * Version 1.1. - * - * Inspiration from jQuery color animation plugin by John Resig. - * - * Released under the MIT license by Ole Laursen, October 2009. - * - * Examples: - * - * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() - * var c = $.color.extract($("#mydiv"), 'background-color'); - * console.log(c.r, c.g, c.b, c.a); - * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" - * - * Note that .scale() and .add() return the same modified object - * instead of making a new one. - * - * V. 1.1: Fix error handling so e.g. parsing an empty string does - * produce a color rather than just crashing. - */ -(function($){$.color={};$.color.make=function(r,g,b,a){var o={};o.r=r||0;o.g=g||0;o.b=b||0;o.a=a!=null?a:1;o.add=function(c,d){for(var i=0;i=1){return"rgb("+[o.r,o.g,o.b].join(",")+")"}else{return"rgba("+[o.r,o.g,o.b,o.a].join(",")+")"}};o.normalize=function(){function clamp(min,value,max){return valuemax?max:value}o.r=clamp(0,parseInt(o.r),255);o.g=clamp(0,parseInt(o.g),255);o.b=clamp(0,parseInt(o.b),255);o.a=clamp(0,o.a,1);return o};o.clone=function(){return $.color.make(o.r,o.b,o.g,o.a)};return o.normalize()};$.color.extract=function(elem,css){var c;do{c=elem.css(css).toLowerCase();if(c!=""&&c!="transparent")break;elem=elem.parent()}while(elem.length&&!$.nodeName(elem.get(0),"body"));if(c=="rgba(0, 0, 0, 0)")c="transparent";return $.color.parse(c)};$.color.parse=function(str){var res,m=$.color.make;if(res=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10));if(res=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10),parseFloat(res[4]));if(res=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55);if(res=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55,parseFloat(res[4]));if(res=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))return m(parseInt(res[1],16),parseInt(res[2],16),parseInt(res[3],16));if(res=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))return m(parseInt(res[1]+res[1],16),parseInt(res[2]+res[2],16),parseInt(res[3]+res[3],16));var name=$.trim(str).toLowerCase();if(name=="transparent")return m(255,255,255,0);else{res=lookupColors[name]||[0,0,0];return m(res[0],res[1],res[2])}};var lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery); - -// the actual Flot code -(function($) { - - // Cache the prototype hasOwnProperty for faster access - - var hasOwnProperty = Object.prototype.hasOwnProperty; - - // A shim to provide 'detach' to jQuery versions prior to 1.4. Using a DOM - // operation produces the same effect as detach, i.e. removing the element - // without touching its jQuery data. - - // Do not merge this into Flot 0.9, since it requires jQuery 1.4.4+. - - if (!$.fn.detach) { - $.fn.detach = function() { - return this.each(function() { - if (this.parentNode) { - this.parentNode.removeChild( this ); - } - }); - }; - } - - /////////////////////////////////////////////////////////////////////////// - // The Canvas object is a wrapper around an HTML5 tag. - // - // @constructor - // @param {string} cls List of classes to apply to the canvas. - // @param {element} container Element onto which to append the canvas. - // - // Requiring a container is a little iffy, but unfortunately canvas - // operations don't work unless the canvas is attached to the DOM. - - function Canvas(cls, container) { - - var element = container.children("." + cls)[0]; - - if (element == null) { - - element = document.createElement("canvas"); - element.className = cls; - - $(element).css({ direction: "ltr", position: "absolute", left: 0, top: 0 }) - .appendTo(container); - - // If HTML5 Canvas isn't available, fall back to [Ex|Flash]canvas - - if (!element.getContext) { - if (window.G_vmlCanvasManager) { - element = window.G_vmlCanvasManager.initElement(element); - } else { - throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode."); - } - } - } - - this.element = element; - - var context = this.context = element.getContext("2d"); - - // Determine the screen's ratio of physical to device-independent - // pixels. This is the ratio between the canvas width that the browser - // advertises and the number of pixels actually present in that space. - - // The iPhone 4, for example, has a device-independent width of 320px, - // but its screen is actually 640px wide. It therefore has a pixel - // ratio of 2, while most normal devices have a ratio of 1. - - var devicePixelRatio = window.devicePixelRatio || 1, - backingStoreRatio = - context.webkitBackingStorePixelRatio || - context.mozBackingStorePixelRatio || - context.msBackingStorePixelRatio || - context.oBackingStorePixelRatio || - context.backingStorePixelRatio || 1; - - this.pixelRatio = devicePixelRatio / backingStoreRatio; - - // Size the canvas to match the internal dimensions of its container - - this.resize(container.width(), container.height()); - - // Collection of HTML div layers for text overlaid onto the canvas - - this.textContainer = null; - this.text = {}; - - // Cache of text fragments and metrics, so we can avoid expensively - // re-calculating them when the plot is re-rendered in a loop. - - this._textCache = {}; - } - - // Resizes the canvas to the given dimensions. - // - // @param {number} width New width of the canvas, in pixels. - // @param {number} width New height of the canvas, in pixels. - - Canvas.prototype.resize = function(width, height) { - - if (width <= 0 || height <= 0) { - throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + height); - } - - var element = this.element, - context = this.context, - pixelRatio = this.pixelRatio; - - // Resize the canvas, increasing its density based on the display's - // pixel ratio; basically giving it more pixels without increasing the - // size of its element, to take advantage of the fact that retina - // displays have that many more pixels in the same advertised space. - - // Resizing should reset the state (excanvas seems to be buggy though) - - if (this.width != width) { - element.width = width * pixelRatio; - element.style.width = width + "px"; - this.width = width; - } - - if (this.height != height) { - element.height = height * pixelRatio; - element.style.height = height + "px"; - this.height = height; - } - - // Save the context, so we can reset in case we get replotted. The - // restore ensure that we're really back at the initial state, and - // should be safe even if we haven't saved the initial state yet. - - context.restore(); - context.save(); - - // Scale the coordinate space to match the display density; so even though we - // may have twice as many pixels, we still want lines and other drawing to - // appear at the same size; the extra pixels will just make them crisper. - - context.scale(pixelRatio, pixelRatio); - }; - - // Clears the entire canvas area, not including any overlaid HTML text - - Canvas.prototype.clear = function() { - this.context.clearRect(0, 0, this.width, this.height); - }; - - // Finishes rendering the canvas, including managing the text overlay. - - Canvas.prototype.render = function() { - - var cache = this._textCache; - - // For each text layer, add elements marked as active that haven't - // already been rendered, and remove those that are no longer active. - - for (var layerKey in cache) { - if (hasOwnProperty.call(cache, layerKey)) { - - var layer = this.getTextLayer(layerKey), - layerCache = cache[layerKey]; - - layer.hide(); - - for (var styleKey in layerCache) { - if (hasOwnProperty.call(layerCache, styleKey)) { - var styleCache = layerCache[styleKey]; - for (var key in styleCache) { - if (hasOwnProperty.call(styleCache, key)) { - - var positions = styleCache[key].positions; - - for (var i = 0, position; position = positions[i]; i++) { - if (position.active) { - if (!position.rendered) { - layer.append(position.element); - position.rendered = true; - } - } else { - positions.splice(i--, 1); - if (position.rendered) { - position.element.detach(); - } - } - } - - if (positions.length == 0) { - delete styleCache[key]; - } - } - } - } - } - - layer.show(); - } - } - }; - - // Creates (if necessary) and returns the text overlay container. - // - // @param {string} classes String of space-separated CSS classes used to - // uniquely identify the text layer. - // @return {object} The jQuery-wrapped text-layer div. - - Canvas.prototype.getTextLayer = function(classes) { - - var layer = this.text[classes]; - - // Create the text layer if it doesn't exist - - if (layer == null) { - - // Create the text layer container, if it doesn't exist - - if (this.textContainer == null) { - this.textContainer = $("
") - .css({ - position: "absolute", - top: 0, - left: 0, - bottom: 0, - right: 0, - 'font-size': "smaller", - color: "#545454" - }) - .insertAfter(this.element); - } - - layer = this.text[classes] = $("
") - .addClass(classes) - .css({ - position: "absolute", - top: 0, - left: 0, - bottom: 0, - right: 0 - }) - .appendTo(this.textContainer); - } - - return layer; - }; - - // Creates (if necessary) and returns a text info object. - // - // The object looks like this: - // - // { - // width: Width of the text's wrapper div. - // height: Height of the text's wrapper div. - // element: The jQuery-wrapped HTML div containing the text. - // positions: Array of positions at which this text is drawn. - // } - // - // The positions array contains objects that look like this: - // - // { - // active: Flag indicating whether the text should be visible. - // rendered: Flag indicating whether the text is currently visible. - // element: The jQuery-wrapped HTML div containing the text. - // x: X coordinate at which to draw the text. - // y: Y coordinate at which to draw the text. - // } - // - // Each position after the first receives a clone of the original element. - // - // The idea is that that the width, height, and general 'identity' of the - // text is constant no matter where it is placed; the placements are a - // secondary property. - // - // Canvas maintains a cache of recently-used text info objects; getTextInfo - // either returns the cached element or creates a new entry. - // - // @param {string} layer A string of space-separated CSS classes uniquely - // identifying the layer containing this text. - // @param {string} text Text string to retrieve info for. - // @param {(string|object)=} font Either a string of space-separated CSS - // classes or a font-spec object, defining the text's font and style. - // @param {number=} angle Angle at which to rotate the text, in degrees. - // Angle is currently unused, it will be implemented in the future. - // @param {number=} width Maximum width of the text before it wraps. - // @return {object} a text info object. - - Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { - - var textStyle, layerCache, styleCache, info; - - // Cast the value to a string, in case we were given a number or such - - text = "" + text; - - // If the font is a font-spec object, generate a CSS font definition - - if (typeof font === "object") { - textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px/" + font.lineHeight + "px " + font.family; - } else { - textStyle = font; - } - - // Retrieve (or create) the cache for the text's layer and styles - - layerCache = this._textCache[layer]; - - if (layerCache == null) { - layerCache = this._textCache[layer] = {}; - } - - styleCache = layerCache[textStyle]; - - if (styleCache == null) { - styleCache = layerCache[textStyle] = {}; - } - - info = styleCache[text]; - - // If we can't find a matching element in our cache, create a new one - - if (info == null) { - - var element = $("
").html(text) - .css({ - position: "absolute", - 'max-width': width, - top: -9999 - }) - .appendTo(this.getTextLayer(layer)); - - if (typeof font === "object") { - element.css({ - font: textStyle, - color: font.color - }); - } else if (typeof font === "string") { - element.addClass(font); - } - - info = styleCache[text] = { - width: element.outerWidth(true), - height: element.outerHeight(true), - element: element, - positions: [] - }; - - element.detach(); - } - - return info; - }; - - // Adds a text string to the canvas text overlay. - // - // The text isn't drawn immediately; it is marked as rendering, which will - // result in its addition to the canvas on the next render pass. - // - // @param {string} layer A string of space-separated CSS classes uniquely - // identifying the layer containing this text. - // @param {number} x X coordinate at which to draw the text. - // @param {number} y Y coordinate at which to draw the text. - // @param {string} text Text string to draw. - // @param {(string|object)=} font Either a string of space-separated CSS - // classes or a font-spec object, defining the text's font and style. - // @param {number=} angle Angle at which to rotate the text, in degrees. - // Angle is currently unused, it will be implemented in the future. - // @param {number=} width Maximum width of the text before it wraps. - // @param {string=} halign Horizontal alignment of the text; either "left", - // "center" or "right". - // @param {string=} valign Vertical alignment of the text; either "top", - // "middle" or "bottom". - - Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { - - var info = this.getTextInfo(layer, text, font, angle, width), - positions = info.positions; - - // Tweak the div's position to match the text's alignment - - if (halign == "center") { - x -= info.width / 2; - } else if (halign == "right") { - x -= info.width; - } - - if (valign == "middle") { - y -= info.height / 2; - } else if (valign == "bottom") { - y -= info.height; - } - - // Determine whether this text already exists at this position. - // If so, mark it for inclusion in the next render pass. - - for (var i = 0, position; position = positions[i]; i++) { - if (position.x == x && position.y == y) { - position.active = true; - return; - } - } - - // If the text doesn't exist at this position, create a new entry - - // For the very first position we'll re-use the original element, - // while for subsequent ones we'll clone it. - - position = { - active: true, - rendered: false, - element: positions.length ? info.element.clone() : info.element, - x: x, - y: y - }; - - positions.push(position); - - // Move the element to its final position within the container - - position.element.css({ - top: Math.round(y), - left: Math.round(x), - 'text-align': halign // In case the text wraps - }); - }; - - // Removes one or more text strings from the canvas text overlay. - // - // If no parameters are given, all text within the layer is removed. - // - // Note that the text is not immediately removed; it is simply marked as - // inactive, which will result in its removal on the next render pass. - // This avoids the performance penalty for 'clear and redraw' behavior, - // where we potentially get rid of all text on a layer, but will likely - // add back most or all of it later, as when redrawing axes, for example. - // - // @param {string} layer A string of space-separated CSS classes uniquely - // identifying the layer containing this text. - // @param {number=} x X coordinate of the text. - // @param {number=} y Y coordinate of the text. - // @param {string=} text Text string to remove. - // @param {(string|object)=} font Either a string of space-separated CSS - // classes or a font-spec object, defining the text's font and style. - // @param {number=} angle Angle at which the text is rotated, in degrees. - // Angle is currently unused, it will be implemented in the future. - - Canvas.prototype.removeText = function(layer, x, y, text, font, angle) { - if (text == null) { - var layerCache = this._textCache[layer]; - if (layerCache != null) { - for (var styleKey in layerCache) { - if (hasOwnProperty.call(layerCache, styleKey)) { - var styleCache = layerCache[styleKey]; - for (var key in styleCache) { - if (hasOwnProperty.call(styleCache, key)) { - var positions = styleCache[key].positions; - for (var i = 0, position; position = positions[i]; i++) { - position.active = false; - } - } - } - } - } - } - } else { - var positions = this.getTextInfo(layer, text, font, angle).positions; - for (var i = 0, position; position = positions[i]; i++) { - if (position.x == x && position.y == y) { - position.active = false; - } - } - } - }; - - /////////////////////////////////////////////////////////////////////////// - // The top-level container for the entire plot. - - function Plot(placeholder, data_, options_, plugins) { - // data is on the form: - // [ series1, series2 ... ] - // where series is either just the data as [ [x1, y1], [x2, y2], ... ] - // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... } - - var series = [], - options = { - // the color theme used for graphs - colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], - legend: { - show: true, - noColumns: 1, // number of colums in legend table - labelFormatter: null, // fn: string -> string - labelBoxBorderColor: "#ccc", // border color for the little label boxes - container: null, // container (as jQuery object) to put legend in, null means default on top of graph - position: "ne", // position of default legend container within plot - margin: 5, // distance from grid edge to default legend container within plot - backgroundColor: null, // null means auto-detect - backgroundOpacity: 0.85, // set to 0 to avoid background - sorted: null // default to no legend sorting - }, - xaxis: { - show: null, // null = auto-detect, true = always, false = never - position: "bottom", // or "top" - mode: null, // null or "time" - font: null, // null (derived from CSS in placeholder) or object like { size: 11, lineHeight: 13, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" } - color: null, // base color, labels, ticks - tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)" - transform: null, // null or f: number -> number to transform axis - inverseTransform: null, // if transform is set, this should be the inverse function - min: null, // min. value to show, null means set automatically - max: null, // max. value to show, null means set automatically - autoscaleMargin: null, // margin in % to add if auto-setting min/max - ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks - tickFormatter: null, // fn: number -> string - labelWidth: null, // size of tick labels in pixels - labelHeight: null, - reserveSpace: null, // whether to reserve space even if axis isn't shown - tickLength: null, // size in pixels of ticks, or "full" for whole line - alignTicksWithAxis: null, // axis number or null for no sync - tickDecimals: null, // no. of decimals, null means auto - tickSize: null, // number or [number, "unit"] - minTickSize: null // number or [number, "unit"] - }, - yaxis: { - autoscaleMargin: 0.02, - position: "left" // or "right" - }, - xaxes: [], - yaxes: [], - series: { - points: { - show: false, - radius: 3, - lineWidth: 2, // in pixels - fill: true, - fillColor: "#ffffff", - symbol: "circle" // or callback - }, - lines: { - // we don't put in show: false so we can see - // whether lines were actively disabled - lineWidth: 2, // in pixels - fill: false, - fillColor: null, - steps: false - // Omit 'zero', so we can later default its value to - // match that of the 'fill' option. - }, - bars: { - show: false, - lineWidth: 2, // in pixels - barWidth: 1, // in units of the x axis - fill: true, - fillColor: null, - align: "left", // "left", "right", or "center" - horizontal: false, - zero: true - }, - shadowSize: 3, - highlightColor: null - }, - grid: { - show: true, - aboveData: false, - color: "#545454", // primary color used for outline and labels - backgroundColor: null, // null for transparent, else color - borderColor: null, // set if different from the grid color - tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)" - margin: 0, // distance from the canvas edge to the grid - labelMargin: 5, // in pixels - axisMargin: 8, // in pixels - borderWidth: 2, // in pixels - minBorderMargin: null, // in pixels, null means taken from points radius - markings: null, // array of ranges or fn: axes -> array of ranges - markingsColor: "#f4f4f4", - markingsLineWidth: 2, - // interactive stuff - clickable: false, - hoverable: false, - autoHighlight: true, // highlight in case mouse is near - mouseActiveRadius: 10 // how far the mouse can be away to activate an item - }, - interaction: { - redrawOverlayInterval: 1000/60 // time between updates, -1 means in same flow - }, - hooks: {} - }, - surface = null, // the canvas for the plot itself - overlay = null, // canvas for interactive stuff on top of plot - eventHolder = null, // jQuery object that events should be bound to - ctx = null, octx = null, - xaxes = [], yaxes = [], - plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, - plotWidth = 0, plotHeight = 0, - hooks = { - processOptions: [], - processRawData: [], - processDatapoints: [], - processOffset: [], - drawBackground: [], - drawSeries: [], - draw: [], - bindEvents: [], - drawOverlay: [], - shutdown: [] - }, - plot = this; - - // public functions - plot.setData = setData; - plot.setupGrid = setupGrid; - plot.draw = draw; - plot.getPlaceholder = function() { return placeholder; }; - plot.getCanvas = function() { return surface.element; }; - plot.getPlotOffset = function() { return plotOffset; }; - plot.width = function () { return plotWidth; }; - plot.height = function () { return plotHeight; }; - plot.offset = function () { - var o = eventHolder.offset(); - o.left += plotOffset.left; - o.top += plotOffset.top; - return o; - }; - plot.getData = function () { return series; }; - plot.getAxes = function () { - var res = {}, i; - $.each(xaxes.concat(yaxes), function (_, axis) { - if (axis) - res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis; - }); - return res; - }; - plot.getXAxes = function () { return xaxes; }; - plot.getYAxes = function () { return yaxes; }; - plot.c2p = canvasToAxisCoords; - plot.p2c = axisToCanvasCoords; - plot.getOptions = function () { return options; }; - plot.highlight = highlight; - plot.unhighlight = unhighlight; - plot.triggerRedrawOverlay = triggerRedrawOverlay; - plot.pointOffset = function(point) { - return { - left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left, 10), - top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top, 10) - }; - }; - plot.shutdown = shutdown; - plot.destroy = function () { - shutdown(); - placeholder.removeData("plot").empty(); - - series = []; - options = null; - surface = null; - overlay = null; - eventHolder = null; - ctx = null; - octx = null; - xaxes = []; - yaxes = []; - hooks = null; - highlights = []; - plot = null; - }; - plot.resize = function () { - var width = placeholder.width(), - height = placeholder.height(); - surface.resize(width, height); - overlay.resize(width, height); - }; - - // public attributes - plot.hooks = hooks; - - // initialize - initPlugins(plot); - parseOptions(options_); - setupCanvases(); - setData(data_); - setupGrid(); - draw(); - bindEvents(); - - - function executeHooks(hook, args) { - args = [plot].concat(args); - for (var i = 0; i < hook.length; ++i) - hook[i].apply(this, args); - } - - function initPlugins() { - - // References to key classes, allowing plugins to modify them - - var classes = { - Canvas: Canvas - }; - - for (var i = 0; i < plugins.length; ++i) { - var p = plugins[i]; - p.init(plot, classes); - if (p.options) - $.extend(true, options, p.options); - } - } - - function parseOptions(opts) { - - $.extend(true, options, opts); - - // $.extend merges arrays, rather than replacing them. When less - // colors are provided than the size of the default palette, we - // end up with those colors plus the remaining defaults, which is - // not expected behavior; avoid it by replacing them here. - - if (opts && opts.colors) { - options.colors = opts.colors; - } - - if (options.xaxis.color == null) - options.xaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); - if (options.yaxis.color == null) - options.yaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); - - if (options.xaxis.tickColor == null) // grid.tickColor for back-compatibility - options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color; - if (options.yaxis.tickColor == null) // grid.tickColor for back-compatibility - options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color; - - if (options.grid.borderColor == null) - options.grid.borderColor = options.grid.color; - if (options.grid.tickColor == null) - options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString(); - - // Fill in defaults for axis options, including any unspecified - // font-spec fields, if a font-spec was provided. - - // If no x/y axis options were provided, create one of each anyway, - // since the rest of the code assumes that they exist. - - var i, axisOptions, axisCount, - fontSize = placeholder.css("font-size"), - fontSizeDefault = fontSize ? +fontSize.replace("px", "") : 13, - fontDefaults = { - style: placeholder.css("font-style"), - size: Math.round(0.8 * fontSizeDefault), - variant: placeholder.css("font-variant"), - weight: placeholder.css("font-weight"), - family: placeholder.css("font-family") - }; - - axisCount = options.xaxes.length || 1; - for (i = 0; i < axisCount; ++i) { - - axisOptions = options.xaxes[i]; - if (axisOptions && !axisOptions.tickColor) { - axisOptions.tickColor = axisOptions.color; - } - - axisOptions = $.extend(true, {}, options.xaxis, axisOptions); - options.xaxes[i] = axisOptions; - - if (axisOptions.font) { - axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); - if (!axisOptions.font.color) { - axisOptions.font.color = axisOptions.color; - } - if (!axisOptions.font.lineHeight) { - axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); - } - } - } - - axisCount = options.yaxes.length || 1; - for (i = 0; i < axisCount; ++i) { - - axisOptions = options.yaxes[i]; - if (axisOptions && !axisOptions.tickColor) { - axisOptions.tickColor = axisOptions.color; - } - - axisOptions = $.extend(true, {}, options.yaxis, axisOptions); - options.yaxes[i] = axisOptions; - - if (axisOptions.font) { - axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); - if (!axisOptions.font.color) { - axisOptions.font.color = axisOptions.color; - } - if (!axisOptions.font.lineHeight) { - axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); - } - } - } - - // backwards compatibility, to be removed in future - if (options.xaxis.noTicks && options.xaxis.ticks == null) - options.xaxis.ticks = options.xaxis.noTicks; - if (options.yaxis.noTicks && options.yaxis.ticks == null) - options.yaxis.ticks = options.yaxis.noTicks; - if (options.x2axis) { - options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis); - options.xaxes[1].position = "top"; - // Override the inherit to allow the axis to auto-scale - if (options.x2axis.min == null) { - options.xaxes[1].min = null; - } - if (options.x2axis.max == null) { - options.xaxes[1].max = null; - } - } - if (options.y2axis) { - options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis); - options.yaxes[1].position = "right"; - // Override the inherit to allow the axis to auto-scale - if (options.y2axis.min == null) { - options.yaxes[1].min = null; - } - if (options.y2axis.max == null) { - options.yaxes[1].max = null; - } - } - if (options.grid.coloredAreas) - options.grid.markings = options.grid.coloredAreas; - if (options.grid.coloredAreasColor) - options.grid.markingsColor = options.grid.coloredAreasColor; - if (options.lines) - $.extend(true, options.series.lines, options.lines); - if (options.points) - $.extend(true, options.series.points, options.points); - if (options.bars) - $.extend(true, options.series.bars, options.bars); - if (options.shadowSize != null) - options.series.shadowSize = options.shadowSize; - if (options.highlightColor != null) - options.series.highlightColor = options.highlightColor; - - // save options on axes for future reference - for (i = 0; i < options.xaxes.length; ++i) - getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i]; - for (i = 0; i < options.yaxes.length; ++i) - getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i]; - - // add hooks from options - for (var n in hooks) - if (options.hooks[n] && options.hooks[n].length) - hooks[n] = hooks[n].concat(options.hooks[n]); - - executeHooks(hooks.processOptions, [options]); - } - - function setData(d) { - series = parseData(d); - fillInSeriesOptions(); - processData(); - } - - function parseData(d) { - var res = []; - for (var i = 0; i < d.length; ++i) { - var s = $.extend(true, {}, options.series); - - if (d[i].data != null) { - s.data = d[i].data; // move the data instead of deep-copy - delete d[i].data; - - $.extend(true, s, d[i]); - - d[i].data = s.data; - } - else - s.data = d[i]; - res.push(s); - } - - return res; - } - - function axisNumber(obj, coord) { - var a = obj[coord + "axis"]; - if (typeof a == "object") // if we got a real axis, extract number - a = a.n; - if (typeof a != "number") - a = 1; // default to first axis - return a; - } - - function allAxes() { - // return flat array without annoying null entries - return $.grep(xaxes.concat(yaxes), function (a) { return a; }); - } - - function canvasToAxisCoords(pos) { - // return an object with x/y corresponding to all used axes - var res = {}, i, axis; - for (i = 0; i < xaxes.length; ++i) { - axis = xaxes[i]; - if (axis && axis.used) - res["x" + axis.n] = axis.c2p(pos.left); - } - - for (i = 0; i < yaxes.length; ++i) { - axis = yaxes[i]; - if (axis && axis.used) - res["y" + axis.n] = axis.c2p(pos.top); - } - - if (res.x1 !== undefined) - res.x = res.x1; - if (res.y1 !== undefined) - res.y = res.y1; - - return res; - } - - function axisToCanvasCoords(pos) { - // get canvas coords from the first pair of x/y found in pos - var res = {}, i, axis, key; - - for (i = 0; i < xaxes.length; ++i) { - axis = xaxes[i]; - if (axis && axis.used) { - key = "x" + axis.n; - if (pos[key] == null && axis.n == 1) - key = "x"; - - if (pos[key] != null) { - res.left = axis.p2c(pos[key]); - break; - } - } - } - - for (i = 0; i < yaxes.length; ++i) { - axis = yaxes[i]; - if (axis && axis.used) { - key = "y" + axis.n; - if (pos[key] == null && axis.n == 1) - key = "y"; - - if (pos[key] != null) { - res.top = axis.p2c(pos[key]); - break; - } - } - } - - return res; - } - - function getOrCreateAxis(axes, number) { - if (!axes[number - 1]) - axes[number - 1] = { - n: number, // save the number for future reference - direction: axes == xaxes ? "x" : "y", - options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis) - }; - - return axes[number - 1]; - } - - function fillInSeriesOptions() { - - var neededColors = series.length, maxIndex = -1, i; - - // Subtract the number of series that already have fixed colors or - // color indexes from the number that we still need to generate. - - for (i = 0; i < series.length; ++i) { - var sc = series[i].color; - if (sc != null) { - neededColors--; - if (typeof sc == "number" && sc > maxIndex) { - maxIndex = sc; - } - } - } - - // If any of the series have fixed color indexes, then we need to - // generate at least as many colors as the highest index. - - if (neededColors <= maxIndex) { - neededColors = maxIndex + 1; - } - - // Generate all the colors, using first the option colors and then - // variations on those colors once they're exhausted. - - var c, colors = [], colorPool = options.colors, - colorPoolSize = colorPool.length, variation = 0; - - for (i = 0; i < neededColors; i++) { - - c = $.color.parse(colorPool[i % colorPoolSize] || "#666"); - - // Each time we exhaust the colors in the pool we adjust - // a scaling factor used to produce more variations on - // those colors. The factor alternates negative/positive - // to produce lighter/darker colors. - - // Reset the variation after every few cycles, or else - // it will end up producing only white or black colors. - - if (i % colorPoolSize == 0 && i) { - if (variation >= 0) { - if (variation < 0.5) { - variation = -variation - 0.2; - } else variation = 0; - } else variation = -variation; - } - - colors[i] = c.scale('rgb', 1 + variation); - } - - // Finalize the series options, filling in their colors - - var colori = 0, s; - for (i = 0; i < series.length; ++i) { - s = series[i]; - - // assign colors - if (s.color == null) { - s.color = colors[colori].toString(); - ++colori; - } - else if (typeof s.color == "number") - s.color = colors[s.color].toString(); - - // turn on lines automatically in case nothing is set - if (s.lines.show == null) { - var v, show = true; - for (v in s) - if (s[v] && s[v].show) { - show = false; - break; - } - if (show) - s.lines.show = true; - } - - // If nothing was provided for lines.zero, default it to match - // lines.fill, since areas by default should extend to zero. - - if (s.lines.zero == null) { - s.lines.zero = !!s.lines.fill; - } - - // setup axes - s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x")); - s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y")); - } - } - - function processData() { - var topSentry = Number.POSITIVE_INFINITY, - bottomSentry = Number.NEGATIVE_INFINITY, - fakeInfinity = Number.MAX_VALUE, - i, j, k, m, length, - s, points, ps, x, y, axis, val, f, p, - data, format; - - function updateAxis(axis, min, max) { - if (min < axis.datamin && min != -fakeInfinity) - axis.datamin = min; - if (max > axis.datamax && max != fakeInfinity) - axis.datamax = max; - } - - $.each(allAxes(), function (_, axis) { - // init axis - axis.datamin = topSentry; - axis.datamax = bottomSentry; - axis.used = false; - }); - - for (i = 0; i < series.length; ++i) { - s = series[i]; - s.datapoints = { points: [] }; - - executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]); - } - - // first pass: clean and copy data - for (i = 0; i < series.length; ++i) { - s = series[i]; - - data = s.data; - format = s.datapoints.format; - - if (!format) { - format = []; - // find out how to copy - format.push({ x: true, number: true, required: true }); - format.push({ y: true, number: true, required: true }); - - if (s.bars.show || (s.lines.show && s.lines.fill)) { - var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); - format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale }); - if (s.bars.horizontal) { - delete format[format.length - 1].y; - format[format.length - 1].x = true; - } - } - - s.datapoints.format = format; - } - - if (s.datapoints.pointsize != null) - continue; // already filled in - - s.datapoints.pointsize = format.length; - - ps = s.datapoints.pointsize; - points = s.datapoints.points; - - var insertSteps = s.lines.show && s.lines.steps; - s.xaxis.used = s.yaxis.used = true; - - for (j = k = 0; j < data.length; ++j, k += ps) { - p = data[j]; - - var nullify = p == null; - if (!nullify) { - for (m = 0; m < ps; ++m) { - val = p[m]; - f = format[m]; - - if (f) { - if (f.number && val != null) { - val = +val; // convert to number - if (isNaN(val)) - val = null; - else if (val == Infinity) - val = fakeInfinity; - else if (val == -Infinity) - val = -fakeInfinity; - } - - if (val == null) { - if (f.required) - nullify = true; - - if (f.defaultValue != null) - val = f.defaultValue; - } - } - - points[k + m] = val; - } - } - - if (nullify) { - for (m = 0; m < ps; ++m) { - val = points[k + m]; - if (val != null) { - f = format[m]; - // extract min/max info - if (f.autoscale !== false) { - if (f.x) { - updateAxis(s.xaxis, val, val); - } - if (f.y) { - updateAxis(s.yaxis, val, val); - } - } - } - points[k + m] = null; - } - } - else { - // a little bit of line specific stuff that - // perhaps shouldn't be here, but lacking - // better means... - if (insertSteps && k > 0 - && points[k - ps] != null - && points[k - ps] != points[k] - && points[k - ps + 1] != points[k + 1]) { - // copy the point to make room for a middle point - for (m = 0; m < ps; ++m) - points[k + ps + m] = points[k + m]; - - // middle point has same y - points[k + 1] = points[k - ps + 1]; - - // we've added a point, better reflect that - k += ps; - } - } - } - } - - // give the hooks a chance to run - for (i = 0; i < series.length; ++i) { - s = series[i]; - - executeHooks(hooks.processDatapoints, [ s, s.datapoints]); - } - - // second pass: find datamax/datamin for auto-scaling - for (i = 0; i < series.length; ++i) { - s = series[i]; - points = s.datapoints.points; - ps = s.datapoints.pointsize; - format = s.datapoints.format; - - var xmin = topSentry, ymin = topSentry, - xmax = bottomSentry, ymax = bottomSentry; - - for (j = 0; j < points.length; j += ps) { - if (points[j] == null) - continue; - - for (m = 0; m < ps; ++m) { - val = points[j + m]; - f = format[m]; - if (!f || f.autoscale === false || val == fakeInfinity || val == -fakeInfinity) - continue; - - if (f.x) { - if (val < xmin) - xmin = val; - if (val > xmax) - xmax = val; - } - if (f.y) { - if (val < ymin) - ymin = val; - if (val > ymax) - ymax = val; - } - } - } - - if (s.bars.show) { - // make sure we got room for the bar on the dancing floor - var delta; - - switch (s.bars.align) { - case "left": - delta = 0; - break; - case "right": - delta = -s.bars.barWidth; - break; - default: - delta = -s.bars.barWidth / 2; - } - - if (s.bars.horizontal) { - ymin += delta; - ymax += delta + s.bars.barWidth; - } - else { - xmin += delta; - xmax += delta + s.bars.barWidth; - } - } - - updateAxis(s.xaxis, xmin, xmax); - updateAxis(s.yaxis, ymin, ymax); - } - - $.each(allAxes(), function (_, axis) { - if (axis.datamin == topSentry) - axis.datamin = null; - if (axis.datamax == bottomSentry) - axis.datamax = null; - }); - } - - function setupCanvases() { - - // Make sure the placeholder is clear of everything except canvases - // from a previous plot in this container that we'll try to re-use. - - placeholder.css("padding", 0) // padding messes up the positioning - .children().filter(function(){ - return !$(this).hasClass("flot-overlay") && !$(this).hasClass('flot-base'); - }).remove(); - - if (placeholder.css("position") == 'static') - placeholder.css("position", "relative"); // for positioning labels and overlay - - surface = new Canvas("flot-base", placeholder); - overlay = new Canvas("flot-overlay", placeholder); // overlay canvas for interactive features - - ctx = surface.context; - octx = overlay.context; - - // define which element we're listening for events on - eventHolder = $(overlay.element).unbind(); - - // If we're re-using a plot object, shut down the old one - - var existing = placeholder.data("plot"); - - if (existing) { - existing.shutdown(); - overlay.clear(); - } - - // save in case we get replotted - placeholder.data("plot", plot); - } - - function bindEvents() { - // bind events - if (options.grid.hoverable) { - eventHolder.mousemove(onMouseMove); - - // Use bind, rather than .mouseleave, because we officially - // still support jQuery 1.2.6, which doesn't define a shortcut - // for mouseenter or mouseleave. This was a bug/oversight that - // was fixed somewhere around 1.3.x. We can return to using - // .mouseleave when we drop support for 1.2.6. - - eventHolder.bind("mouseleave", onMouseLeave); - } - - if (options.grid.clickable) - eventHolder.click(onClick); - - executeHooks(hooks.bindEvents, [eventHolder]); - } - - function shutdown() { - if (redrawTimeout) - clearTimeout(redrawTimeout); - - eventHolder.unbind("mousemove", onMouseMove); - eventHolder.unbind("mouseleave", onMouseLeave); - eventHolder.unbind("click", onClick); - - executeHooks(hooks.shutdown, [eventHolder]); - } - - function setTransformationHelpers(axis) { - // set helper functions on the axis, assumes plot area - // has been computed already - - function identity(x) { return x; } - - var s, m, t = axis.options.transform || identity, - it = axis.options.inverseTransform; - - // precompute how much the axis is scaling a point - // in canvas space - if (axis.direction == "x") { - s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min)); - m = Math.min(t(axis.max), t(axis.min)); - } - else { - s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min)); - s = -s; - m = Math.max(t(axis.max), t(axis.min)); - } - - // data point to canvas coordinate - if (t == identity) // slight optimization - axis.p2c = function (p) { return (p - m) * s; }; - else - axis.p2c = function (p) { return (t(p) - m) * s; }; - // canvas coordinate to data point - if (!it) - axis.c2p = function (c) { return m + c / s; }; - else - axis.c2p = function (c) { return it(m + c / s); }; - } - - function measureTickLabels(axis) { - - var opts = axis.options, - ticks = axis.ticks || [], - labelWidth = opts.labelWidth || 0, - labelHeight = opts.labelHeight || 0, - maxWidth = labelWidth || (axis.direction == "x" ? Math.floor(surface.width / (ticks.length || 1)) : null), - legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", - layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, - font = opts.font || "flot-tick-label tickLabel"; - - for (var i = 0; i < ticks.length; ++i) { - - var t = ticks[i]; - - if (!t.label) - continue; - - var info = surface.getTextInfo(layer, t.label, font, null, maxWidth); - - labelWidth = Math.max(labelWidth, info.width); - labelHeight = Math.max(labelHeight, info.height); - } - - axis.labelWidth = opts.labelWidth || labelWidth; - axis.labelHeight = opts.labelHeight || labelHeight; - } - - function allocateAxisBoxFirstPhase(axis) { - // find the bounding box of the axis by looking at label - // widths/heights and ticks, make room by diminishing the - // plotOffset; this first phase only looks at one - // dimension per axis, the other dimension depends on the - // other axes so will have to wait - - var lw = axis.labelWidth, - lh = axis.labelHeight, - pos = axis.options.position, - isXAxis = axis.direction === "x", - tickLength = axis.options.tickLength, - axisMargin = options.grid.axisMargin, - padding = options.grid.labelMargin, - innermost = true, - outermost = true, - first = true, - found = false; - - // Determine the axis's position in its direction and on its side - - $.each(isXAxis ? xaxes : yaxes, function(i, a) { - if (a && (a.show || a.reserveSpace)) { - if (a === axis) { - found = true; - } else if (a.options.position === pos) { - if (found) { - outermost = false; - } else { - innermost = false; - } - } - if (!found) { - first = false; - } - } - }); - - // The outermost axis on each side has no margin - - if (outermost) { - axisMargin = 0; - } - - // The ticks for the first axis in each direction stretch across - - if (tickLength == null) { - tickLength = first ? "full" : 5; - } - - if (!isNaN(+tickLength)) - padding += +tickLength; - - if (isXAxis) { - lh += padding; - - if (pos == "bottom") { - plotOffset.bottom += lh + axisMargin; - axis.box = { top: surface.height - plotOffset.bottom, height: lh }; - } - else { - axis.box = { top: plotOffset.top + axisMargin, height: lh }; - plotOffset.top += lh + axisMargin; - } - } - else { - lw += padding; - - if (pos == "left") { - axis.box = { left: plotOffset.left + axisMargin, width: lw }; - plotOffset.left += lw + axisMargin; - } - else { - plotOffset.right += lw + axisMargin; - axis.box = { left: surface.width - plotOffset.right, width: lw }; - } - } - - // save for future reference - axis.position = pos; - axis.tickLength = tickLength; - axis.box.padding = padding; - axis.innermost = innermost; - } - - function allocateAxisBoxSecondPhase(axis) { - // now that all axis boxes have been placed in one - // dimension, we can set the remaining dimension coordinates - if (axis.direction == "x") { - axis.box.left = plotOffset.left - axis.labelWidth / 2; - axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.labelWidth; - } - else { - axis.box.top = plotOffset.top - axis.labelHeight / 2; - axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.labelHeight; - } - } - - function adjustLayoutForThingsStickingOut() { - // possibly adjust plot offset to ensure everything stays - // inside the canvas and isn't clipped off - - var minMargin = options.grid.minBorderMargin, - axis, i; - - // check stuff from the plot (FIXME: this should just read - // a value from the series, otherwise it's impossible to - // customize) - if (minMargin == null) { - minMargin = 0; - for (i = 0; i < series.length; ++i) - minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth/2)); - } - - var margins = { - left: minMargin, - right: minMargin, - top: minMargin, - bottom: minMargin - }; - - // check axis labels, note we don't check the actual - // labels but instead use the overall width/height to not - // jump as much around with replots - $.each(allAxes(), function (_, axis) { - if (axis.reserveSpace && axis.ticks && axis.ticks.length) { - if (axis.direction === "x") { - margins.left = Math.max(margins.left, axis.labelWidth / 2); - margins.right = Math.max(margins.right, axis.labelWidth / 2); - } else { - margins.bottom = Math.max(margins.bottom, axis.labelHeight / 2); - margins.top = Math.max(margins.top, axis.labelHeight / 2); - } - } - }); - - plotOffset.left = Math.ceil(Math.max(margins.left, plotOffset.left)); - plotOffset.right = Math.ceil(Math.max(margins.right, plotOffset.right)); - plotOffset.top = Math.ceil(Math.max(margins.top, plotOffset.top)); - plotOffset.bottom = Math.ceil(Math.max(margins.bottom, plotOffset.bottom)); - } - - function setupGrid() { - var i, axes = allAxes(), showGrid = options.grid.show; - - // Initialize the plot's offset from the edge of the canvas - - for (var a in plotOffset) { - var margin = options.grid.margin || 0; - plotOffset[a] = typeof margin == "number" ? margin : margin[a] || 0; - } - - executeHooks(hooks.processOffset, [plotOffset]); - - // If the grid is visible, add its border width to the offset - - for (var a in plotOffset) { - if(typeof(options.grid.borderWidth) == "object") { - plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0; - } - else { - plotOffset[a] += showGrid ? options.grid.borderWidth : 0; - } - } - - $.each(axes, function (_, axis) { - var axisOpts = axis.options; - axis.show = axisOpts.show == null ? axis.used : axisOpts.show; - axis.reserveSpace = axisOpts.reserveSpace == null ? axis.show : axisOpts.reserveSpace; - setRange(axis); - }); - - if (showGrid) { - - var allocatedAxes = $.grep(axes, function (axis) { - return axis.show || axis.reserveSpace; - }); - - $.each(allocatedAxes, function (_, axis) { - // make the ticks - setupTickGeneration(axis); - setTicks(axis); - snapRangeToTicks(axis, axis.ticks); - // find labelWidth/Height for axis - measureTickLabels(axis); - }); - - // with all dimensions calculated, we can compute the - // axis bounding boxes, start from the outside - // (reverse order) - for (i = allocatedAxes.length - 1; i >= 0; --i) - allocateAxisBoxFirstPhase(allocatedAxes[i]); - - // make sure we've got enough space for things that - // might stick out - adjustLayoutForThingsStickingOut(); - - $.each(allocatedAxes, function (_, axis) { - allocateAxisBoxSecondPhase(axis); - }); - } - - plotWidth = surface.width - plotOffset.left - plotOffset.right; - plotHeight = surface.height - plotOffset.bottom - plotOffset.top; - - // now we got the proper plot dimensions, we can compute the scaling - $.each(axes, function (_, axis) { - setTransformationHelpers(axis); - }); - - if (showGrid) { - drawAxisLabels(); - } - - insertLegend(); - } - - function setRange(axis) { - var opts = axis.options, - min = +(opts.min != null ? opts.min : axis.datamin), - max = +(opts.max != null ? opts.max : axis.datamax), - delta = max - min; - - if (delta == 0.0) { - // degenerate case - var widen = max == 0 ? 1 : 0.01; - - if (opts.min == null) - min -= widen; - // always widen max if we couldn't widen min to ensure we - // don't fall into min == max which doesn't work - if (opts.max == null || opts.min != null) - max += widen; - } - else { - // consider autoscaling - var margin = opts.autoscaleMargin; - if (margin != null) { - if (opts.min == null) { - min -= delta * margin; - // make sure we don't go below zero if all values - // are positive - if (min < 0 && axis.datamin != null && axis.datamin >= 0) - min = 0; - } - if (opts.max == null) { - max += delta * margin; - if (max > 0 && axis.datamax != null && axis.datamax <= 0) - max = 0; - } - } - } - axis.min = min; - axis.max = max; - } - - function setupTickGeneration(axis) { - var opts = axis.options; - - // estimate number of ticks - var noTicks; - if (typeof opts.ticks == "number" && opts.ticks > 0) - noTicks = opts.ticks; - else - // heuristic based on the model a*sqrt(x) fitted to - // some data points that seemed reasonable - noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? surface.width : surface.height); - - var delta = (axis.max - axis.min) / noTicks, - dec = -Math.floor(Math.log(delta) / Math.LN10), - maxDec = opts.tickDecimals; - - if (maxDec != null && dec > maxDec) { - dec = maxDec; - } - - var magn = Math.pow(10, -dec), - norm = delta / magn, // norm is between 1.0 and 10.0 - size; - - if (norm < 1.5) { - size = 1; - } else if (norm < 3) { - size = 2; - // special case for 2.5, requires an extra decimal - if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { - size = 2.5; - ++dec; - } - } else if (norm < 7.5) { - size = 5; - } else { - size = 10; - } - - size *= magn; - - if (opts.minTickSize != null && size < opts.minTickSize) { - size = opts.minTickSize; - } - - axis.delta = delta; - axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); - axis.tickSize = opts.tickSize || size; - - // Time mode was moved to a plug-in in 0.8, and since so many people use it - // we'll add an especially friendly reminder to make sure they included it. - - if (opts.mode == "time" && !axis.tickGenerator) { - throw new Error("Time mode requires the flot.time plugin."); - } - - // Flot supports base-10 axes; any other mode else is handled by a plug-in, - // like flot.time.js. - - if (!axis.tickGenerator) { - - axis.tickGenerator = function (axis) { - - var ticks = [], - start = floorInBase(axis.min, axis.tickSize), - i = 0, - v = Number.NaN, - prev; - - do { - prev = v; - v = start + i * axis.tickSize; - ticks.push(v); - ++i; - } while (v < axis.max && v != prev); - return ticks; - }; - - axis.tickFormatter = function (value, axis) { - - var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1; - var formatted = "" + Math.round(value * factor) / factor; - - // If tickDecimals was specified, ensure that we have exactly that - // much precision; otherwise default to the value's own precision. - - if (axis.tickDecimals != null) { - var decimal = formatted.indexOf("."); - var precision = decimal == -1 ? 0 : formatted.length - decimal - 1; - if (precision < axis.tickDecimals) { - return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision); - } - } - - return formatted; - }; - } - - if ($.isFunction(opts.tickFormatter)) - axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); }; - - if (opts.alignTicksWithAxis != null) { - var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1]; - if (otherAxis && otherAxis.used && otherAxis != axis) { - // consider snapping min/max to outermost nice ticks - var niceTicks = axis.tickGenerator(axis); - if (niceTicks.length > 0) { - if (opts.min == null) - axis.min = Math.min(axis.min, niceTicks[0]); - if (opts.max == null && niceTicks.length > 1) - axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]); - } - - axis.tickGenerator = function (axis) { - // copy ticks, scaled to this axis - var ticks = [], v, i; - for (i = 0; i < otherAxis.ticks.length; ++i) { - v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min); - v = axis.min + v * (axis.max - axis.min); - ticks.push(v); - } - return ticks; - }; - - // we might need an extra decimal since forced - // ticks don't necessarily fit naturally - if (!axis.mode && opts.tickDecimals == null) { - var extraDec = Math.max(0, -Math.floor(Math.log(axis.delta) / Math.LN10) + 1), - ts = axis.tickGenerator(axis); - - // only proceed if the tick interval rounded - // with an extra decimal doesn't give us a - // zero at end - if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec)))) - axis.tickDecimals = extraDec; - } - } - } - } - - function setTicks(axis) { - var oticks = axis.options.ticks, ticks = []; - if (oticks == null || (typeof oticks == "number" && oticks > 0)) - ticks = axis.tickGenerator(axis); - else if (oticks) { - if ($.isFunction(oticks)) - // generate the ticks - ticks = oticks(axis); - else - ticks = oticks; - } - - // clean up/labelify the supplied ticks, copy them over - var i, v; - axis.ticks = []; - for (i = 0; i < ticks.length; ++i) { - var label = null; - var t = ticks[i]; - if (typeof t == "object") { - v = +t[0]; - if (t.length > 1) - label = t[1]; - } - else - v = +t; - if (label == null) - label = axis.tickFormatter(v, axis); - if (!isNaN(v)) - axis.ticks.push({ v: v, label: label }); - } - } - - function snapRangeToTicks(axis, ticks) { - if (axis.options.autoscaleMargin && ticks.length > 0) { - // snap to ticks - if (axis.options.min == null) - axis.min = Math.min(axis.min, ticks[0].v); - if (axis.options.max == null && ticks.length > 1) - axis.max = Math.max(axis.max, ticks[ticks.length - 1].v); - } - } - - function draw() { - - surface.clear(); - - executeHooks(hooks.drawBackground, [ctx]); - - var grid = options.grid; - - // draw background, if any - if (grid.show && grid.backgroundColor) - drawBackground(); - - if (grid.show && !grid.aboveData) { - drawGrid(); - } - - for (var i = 0; i < series.length; ++i) { - executeHooks(hooks.drawSeries, [ctx, series[i]]); - drawSeries(series[i]); - } - - executeHooks(hooks.draw, [ctx]); - - if (grid.show && grid.aboveData) { - drawGrid(); - } - - surface.render(); - - // A draw implies that either the axes or data have changed, so we - // should probably update the overlay highlights as well. - - triggerRedrawOverlay(); - } - - function extractRange(ranges, coord) { - var axis, from, to, key, axes = allAxes(); - - for (var i = 0; i < axes.length; ++i) { - axis = axes[i]; - if (axis.direction == coord) { - key = coord + axis.n + "axis"; - if (!ranges[key] && axis.n == 1) - key = coord + "axis"; // support x1axis as xaxis - if (ranges[key]) { - from = ranges[key].from; - to = ranges[key].to; - break; - } - } - } - - // backwards-compat stuff - to be removed in future - if (!ranges[key]) { - axis = coord == "x" ? xaxes[0] : yaxes[0]; - from = ranges[coord + "1"]; - to = ranges[coord + "2"]; - } - - // auto-reverse as an added bonus - if (from != null && to != null && from > to) { - var tmp = from; - from = to; - to = tmp; - } - - return { from: from, to: to, axis: axis }; - } - - function drawBackground() { - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); - ctx.fillRect(0, 0, plotWidth, plotHeight); - ctx.restore(); - } - - function drawGrid() { - var i, axes, bw, bc; - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - // draw markings - var markings = options.grid.markings; - if (markings) { - if ($.isFunction(markings)) { - axes = plot.getAxes(); - // xmin etc. is backwards compatibility, to be - // removed in the future - axes.xmin = axes.xaxis.min; - axes.xmax = axes.xaxis.max; - axes.ymin = axes.yaxis.min; - axes.ymax = axes.yaxis.max; - - markings = markings(axes); - } - - for (i = 0; i < markings.length; ++i) { - var m = markings[i], - xrange = extractRange(m, "x"), - yrange = extractRange(m, "y"); - - // fill in missing - if (xrange.from == null) - xrange.from = xrange.axis.min; - if (xrange.to == null) - xrange.to = xrange.axis.max; - if (yrange.from == null) - yrange.from = yrange.axis.min; - if (yrange.to == null) - yrange.to = yrange.axis.max; - - // clip - if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max || - yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) - continue; - - xrange.from = Math.max(xrange.from, xrange.axis.min); - xrange.to = Math.min(xrange.to, xrange.axis.max); - yrange.from = Math.max(yrange.from, yrange.axis.min); - yrange.to = Math.min(yrange.to, yrange.axis.max); - - var xequal = xrange.from === xrange.to, - yequal = yrange.from === yrange.to; - - if (xequal && yequal) { - continue; - } - - // then draw - xrange.from = Math.floor(xrange.axis.p2c(xrange.from)); - xrange.to = Math.floor(xrange.axis.p2c(xrange.to)); - yrange.from = Math.floor(yrange.axis.p2c(yrange.from)); - yrange.to = Math.floor(yrange.axis.p2c(yrange.to)); - - if (xequal || yequal) { - var lineWidth = m.lineWidth || options.grid.markingsLineWidth, - subPixel = lineWidth % 2 ? 0.5 : 0; - ctx.beginPath(); - ctx.strokeStyle = m.color || options.grid.markingsColor; - ctx.lineWidth = lineWidth; - if (xequal) { - ctx.moveTo(xrange.to + subPixel, yrange.from); - ctx.lineTo(xrange.to + subPixel, yrange.to); - } else { - ctx.moveTo(xrange.from, yrange.to + subPixel); - ctx.lineTo(xrange.to, yrange.to + subPixel); - } - ctx.stroke(); - } else { - ctx.fillStyle = m.color || options.grid.markingsColor; - ctx.fillRect(xrange.from, yrange.to, - xrange.to - xrange.from, - yrange.from - yrange.to); - } - } - } - - // draw the ticks - axes = allAxes(); - bw = options.grid.borderWidth; - - for (var j = 0; j < axes.length; ++j) { - var axis = axes[j], box = axis.box, - t = axis.tickLength, x, y, xoff, yoff; - if (!axis.show || axis.ticks.length == 0) - continue; - - ctx.lineWidth = 1; - - // find the edges - if (axis.direction == "x") { - x = 0; - if (t == "full") - y = (axis.position == "top" ? 0 : plotHeight); - else - y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0); - } - else { - y = 0; - if (t == "full") - x = (axis.position == "left" ? 0 : plotWidth); - else - x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0); - } - - // draw tick bar - if (!axis.innermost) { - ctx.strokeStyle = axis.options.color; - ctx.beginPath(); - xoff = yoff = 0; - if (axis.direction == "x") - xoff = plotWidth + 1; - else - yoff = plotHeight + 1; - - if (ctx.lineWidth == 1) { - if (axis.direction == "x") { - y = Math.floor(y) + 0.5; - } else { - x = Math.floor(x) + 0.5; - } - } - - ctx.moveTo(x, y); - ctx.lineTo(x + xoff, y + yoff); - ctx.stroke(); - } - - // draw ticks - - ctx.strokeStyle = axis.options.tickColor; - - ctx.beginPath(); - for (i = 0; i < axis.ticks.length; ++i) { - var v = axis.ticks[i].v; - - xoff = yoff = 0; - - if (isNaN(v) || v < axis.min || v > axis.max - // skip those lying on the axes if we got a border - || (t == "full" - && ((typeof bw == "object" && bw[axis.position] > 0) || bw > 0) - && (v == axis.min || v == axis.max))) - continue; - - if (axis.direction == "x") { - x = axis.p2c(v); - yoff = t == "full" ? -plotHeight : t; - - if (axis.position == "top") - yoff = -yoff; - } - else { - y = axis.p2c(v); - xoff = t == "full" ? -plotWidth : t; - - if (axis.position == "left") - xoff = -xoff; - } - - if (ctx.lineWidth == 1) { - if (axis.direction == "x") - x = Math.floor(x) + 0.5; - else - y = Math.floor(y) + 0.5; - } - - ctx.moveTo(x, y); - ctx.lineTo(x + xoff, y + yoff); - } - - ctx.stroke(); - } - - - // draw border - if (bw) { - // If either borderWidth or borderColor is an object, then draw the border - // line by line instead of as one rectangle - bc = options.grid.borderColor; - if(typeof bw == "object" || typeof bc == "object") { - if (typeof bw !== "object") { - bw = {top: bw, right: bw, bottom: bw, left: bw}; - } - if (typeof bc !== "object") { - bc = {top: bc, right: bc, bottom: bc, left: bc}; - } - - if (bw.top > 0) { - ctx.strokeStyle = bc.top; - ctx.lineWidth = bw.top; - ctx.beginPath(); - ctx.moveTo(0 - bw.left, 0 - bw.top/2); - ctx.lineTo(plotWidth, 0 - bw.top/2); - ctx.stroke(); - } - - if (bw.right > 0) { - ctx.strokeStyle = bc.right; - ctx.lineWidth = bw.right; - ctx.beginPath(); - ctx.moveTo(plotWidth + bw.right / 2, 0 - bw.top); - ctx.lineTo(plotWidth + bw.right / 2, plotHeight); - ctx.stroke(); - } - - if (bw.bottom > 0) { - ctx.strokeStyle = bc.bottom; - ctx.lineWidth = bw.bottom; - ctx.beginPath(); - ctx.moveTo(plotWidth + bw.right, plotHeight + bw.bottom / 2); - ctx.lineTo(0, plotHeight + bw.bottom / 2); - ctx.stroke(); - } - - if (bw.left > 0) { - ctx.strokeStyle = bc.left; - ctx.lineWidth = bw.left; - ctx.beginPath(); - ctx.moveTo(0 - bw.left/2, plotHeight + bw.bottom); - ctx.lineTo(0- bw.left/2, 0); - ctx.stroke(); - } - } - else { - ctx.lineWidth = bw; - ctx.strokeStyle = options.grid.borderColor; - ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw); - } - } - - ctx.restore(); - } - - function drawAxisLabels() { - - $.each(allAxes(), function (_, axis) { - var box = axis.box, - legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", - layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, - font = axis.options.font || "flot-tick-label tickLabel", - tick, x, y, halign, valign; - - // Remove text before checking for axis.show and ticks.length; - // otherwise plugins, like flot-tickrotor, that draw their own - // tick labels will end up with both theirs and the defaults. - - surface.removeText(layer); - - if (!axis.show || axis.ticks.length == 0) - return; - - for (var i = 0; i < axis.ticks.length; ++i) { - - tick = axis.ticks[i]; - if (!tick.label || tick.v < axis.min || tick.v > axis.max) - continue; - - if (axis.direction == "x") { - halign = "center"; - x = plotOffset.left + axis.p2c(tick.v); - if (axis.position == "bottom") { - y = box.top + box.padding; - } else { - y = box.top + box.height - box.padding; - valign = "bottom"; - } - } else { - valign = "middle"; - y = plotOffset.top + axis.p2c(tick.v); - if (axis.position == "left") { - x = box.left + box.width - box.padding; - halign = "right"; - } else { - x = box.left + box.padding; - } - } - - surface.addText(layer, x, y, tick.label, font, null, null, halign, valign); - } - }); - } - - function drawSeries(series) { - if (series.lines.show) - drawSeriesLines(series); - if (series.bars.show) - drawSeriesBars(series); - if (series.points.show) - drawSeriesPoints(series); - } - - function drawSeriesLines(series) { - function plotLine(datapoints, xoffset, yoffset, axisx, axisy) { - var points = datapoints.points, - ps = datapoints.pointsize, - prevx = null, prevy = null; - - ctx.beginPath(); - for (var i = ps; i < points.length; i += ps) { - var x1 = points[i - ps], y1 = points[i - ps + 1], - x2 = points[i], y2 = points[i + 1]; - - if (x1 == null || x2 == null) - continue; - - // clip with ymin - if (y1 <= y2 && y1 < axisy.min) { - if (y2 < axisy.min) - continue; // line segment is outside - // compute new intersection point - x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.min; - } - else if (y2 <= y1 && y2 < axisy.min) { - if (y1 < axisy.min) - continue; - x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.min; - } - - // clip with ymax - if (y1 >= y2 && y1 > axisy.max) { - if (y2 > axisy.max) - continue; - x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.max; - } - else if (y2 >= y1 && y2 > axisy.max) { - if (y1 > axisy.max) - continue; - x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.max; - } - - // clip with xmin - if (x1 <= x2 && x1 < axisx.min) { - if (x2 < axisx.min) - continue; - y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.min; - } - else if (x2 <= x1 && x2 < axisx.min) { - if (x1 < axisx.min) - continue; - y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.min; - } - - // clip with xmax - if (x1 >= x2 && x1 > axisx.max) { - if (x2 > axisx.max) - continue; - y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.max; - } - else if (x2 >= x1 && x2 > axisx.max) { - if (x1 > axisx.max) - continue; - y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.max; - } - - if (x1 != prevx || y1 != prevy) - ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); - - prevx = x2; - prevy = y2; - ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset); - } - ctx.stroke(); - } - - function plotLineArea(datapoints, axisx, axisy) { - var points = datapoints.points, - ps = datapoints.pointsize, - bottom = Math.min(Math.max(0, axisy.min), axisy.max), - i = 0, top, areaOpen = false, - ypos = 1, segmentStart = 0, segmentEnd = 0; - - // we process each segment in two turns, first forward - // direction to sketch out top, then once we hit the - // end we go backwards to sketch the bottom - while (true) { - if (ps > 0 && i > points.length + ps) - break; - - i += ps; // ps is negative if going backwards - - var x1 = points[i - ps], - y1 = points[i - ps + ypos], - x2 = points[i], y2 = points[i + ypos]; - - if (areaOpen) { - if (ps > 0 && x1 != null && x2 == null) { - // at turning point - segmentEnd = i; - ps = -ps; - ypos = 2; - continue; - } - - if (ps < 0 && i == segmentStart + ps) { - // done with the reverse sweep - ctx.fill(); - areaOpen = false; - ps = -ps; - ypos = 1; - i = segmentStart = segmentEnd + ps; - continue; - } - } - - if (x1 == null || x2 == null) - continue; - - // clip x values - - // clip with xmin - if (x1 <= x2 && x1 < axisx.min) { - if (x2 < axisx.min) - continue; - y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.min; - } - else if (x2 <= x1 && x2 < axisx.min) { - if (x1 < axisx.min) - continue; - y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.min; - } - - // clip with xmax - if (x1 >= x2 && x1 > axisx.max) { - if (x2 > axisx.max) - continue; - y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.max; - } - else if (x2 >= x1 && x2 > axisx.max) { - if (x1 > axisx.max) - continue; - y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.max; - } - - if (!areaOpen) { - // open area - ctx.beginPath(); - ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom)); - areaOpen = true; - } - - // now first check the case where both is outside - if (y1 >= axisy.max && y2 >= axisy.max) { - ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); - continue; - } - else if (y1 <= axisy.min && y2 <= axisy.min) { - ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); - continue; - } - - // else it's a bit more complicated, there might - // be a flat maxed out rectangle first, then a - // triangular cutout or reverse; to find these - // keep track of the current x values - var x1old = x1, x2old = x2; - - // clip the y values, without shortcutting, we - // go through all cases in turn - - // clip with ymin - if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { - x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.min; - } - else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { - x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.min; - } - - // clip with ymax - if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { - x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.max; - } - else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { - x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.max; - } - - // if the x value was changed we got a rectangle - // to fill - if (x1 != x1old) { - ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1)); - // it goes to (x1, y1), but we fill that below - } - - // fill triangular section, this sometimes result - // in redundant points if (x1, y1) hasn't changed - // from previous line to, but we just ignore that - ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); - - // fill the other rectangle if it's there - if (x2 != x2old) { - ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); - ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2)); - } - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - ctx.lineJoin = "round"; - - var lw = series.lines.lineWidth, - sw = series.shadowSize; - // FIXME: consider another form of shadow when filling is turned on - if (lw > 0 && sw > 0) { - // draw shadow as a thick and thin line with transparency - ctx.lineWidth = sw; - ctx.strokeStyle = "rgba(0,0,0,0.1)"; - // position shadow at angle from the mid of line - var angle = Math.PI/18; - plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis); - ctx.lineWidth = sw/2; - plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis); - } - - ctx.lineWidth = lw; - ctx.strokeStyle = series.color; - var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight); - if (fillStyle) { - ctx.fillStyle = fillStyle; - plotLineArea(series.datapoints, series.xaxis, series.yaxis); - } - - if (lw > 0) - plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis); - ctx.restore(); - } - - function drawSeriesPoints(series) { - function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) { - var points = datapoints.points, ps = datapoints.pointsize; - - for (var i = 0; i < points.length; i += ps) { - var x = points[i], y = points[i + 1]; - if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) - continue; - - ctx.beginPath(); - x = axisx.p2c(x); - y = axisy.p2c(y) + offset; - if (symbol == "circle") - ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false); - else - symbol(ctx, x, y, radius, shadow); - ctx.closePath(); - - if (fillStyle) { - ctx.fillStyle = fillStyle; - ctx.fill(); - } - ctx.stroke(); - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - var lw = series.points.lineWidth, - sw = series.shadowSize, - radius = series.points.radius, - symbol = series.points.symbol; - - // If the user sets the line width to 0, we change it to a very - // small value. A line width of 0 seems to force the default of 1. - // Doing the conditional here allows the shadow setting to still be - // optional even with a lineWidth of 0. - - if( lw == 0 ) - lw = 0.0001; - - if (lw > 0 && sw > 0) { - // draw shadow in two steps - var w = sw / 2; - ctx.lineWidth = w; - ctx.strokeStyle = "rgba(0,0,0,0.1)"; - plotPoints(series.datapoints, radius, null, w + w/2, true, - series.xaxis, series.yaxis, symbol); - - ctx.strokeStyle = "rgba(0,0,0,0.2)"; - plotPoints(series.datapoints, radius, null, w/2, true, - series.xaxis, series.yaxis, symbol); - } - - ctx.lineWidth = lw; - ctx.strokeStyle = series.color; - plotPoints(series.datapoints, radius, - getFillStyle(series.points, series.color), 0, false, - series.xaxis, series.yaxis, symbol); - ctx.restore(); - } - - function drawBar(x, y, b, barLeft, barRight, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) { - var left, right, bottom, top, - drawLeft, drawRight, drawTop, drawBottom, - tmp; - - // in horizontal mode, we start the bar from the left - // instead of from the bottom so it appears to be - // horizontal rather than vertical - if (horizontal) { - drawBottom = drawRight = drawTop = true; - drawLeft = false; - left = b; - right = x; - top = y + barLeft; - bottom = y + barRight; - - // account for negative bars - if (right < left) { - tmp = right; - right = left; - left = tmp; - drawLeft = true; - drawRight = false; - } - } - else { - drawLeft = drawRight = drawTop = true; - drawBottom = false; - left = x + barLeft; - right = x + barRight; - bottom = b; - top = y; - - // account for negative bars - if (top < bottom) { - tmp = top; - top = bottom; - bottom = tmp; - drawBottom = true; - drawTop = false; - } - } - - // clip - if (right < axisx.min || left > axisx.max || - top < axisy.min || bottom > axisy.max) - return; - - if (left < axisx.min) { - left = axisx.min; - drawLeft = false; - } - - if (right > axisx.max) { - right = axisx.max; - drawRight = false; - } - - if (bottom < axisy.min) { - bottom = axisy.min; - drawBottom = false; - } - - if (top > axisy.max) { - top = axisy.max; - drawTop = false; - } - - left = axisx.p2c(left); - bottom = axisy.p2c(bottom); - right = axisx.p2c(right); - top = axisy.p2c(top); - - // fill the bar - if (fillStyleCallback) { - c.fillStyle = fillStyleCallback(bottom, top); - c.fillRect(left, top, right - left, bottom - top) - } - - // draw outline - if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) { - c.beginPath(); - - // FIXME: inline moveTo is buggy with excanvas - c.moveTo(left, bottom); - if (drawLeft) - c.lineTo(left, top); - else - c.moveTo(left, top); - if (drawTop) - c.lineTo(right, top); - else - c.moveTo(right, top); - if (drawRight) - c.lineTo(right, bottom); - else - c.moveTo(right, bottom); - if (drawBottom) - c.lineTo(left, bottom); - else - c.moveTo(left, bottom); - c.stroke(); - } - } - - function drawSeriesBars(series) { - function plotBars(datapoints, barLeft, barRight, fillStyleCallback, axisx, axisy) { - var points = datapoints.points, ps = datapoints.pointsize; - - for (var i = 0; i < points.length; i += ps) { - if (points[i] == null) - continue; - drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth); - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - // FIXME: figure out a way to add shadows (for instance along the right edge) - ctx.lineWidth = series.bars.lineWidth; - ctx.strokeStyle = series.color; - - var barLeft; - - switch (series.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -series.bars.barWidth; - break; - default: - barLeft = -series.bars.barWidth / 2; - } - - var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null; - plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, fillStyleCallback, series.xaxis, series.yaxis); - ctx.restore(); - } - - function getFillStyle(filloptions, seriesColor, bottom, top) { - var fill = filloptions.fill; - if (!fill) - return null; - - if (filloptions.fillColor) - return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor); - - var c = $.color.parse(seriesColor); - c.a = typeof fill == "number" ? fill : 0.4; - c.normalize(); - return c.toString(); - } - - function insertLegend() { - - if (options.legend.container != null) { - $(options.legend.container).html(""); - } else { - placeholder.find(".legend").remove(); - } - - if (!options.legend.show) { - return; - } - - var fragments = [], entries = [], rowStarted = false, - lf = options.legend.labelFormatter, s, label; - - // Build a list of legend entries, with each having a label and a color - - for (var i = 0; i < series.length; ++i) { - s = series[i]; - if (s.label) { - label = lf ? lf(s.label, s) : s.label; - if (label) { - entries.push({ - label: label, - color: s.color - }); - } - } - } - - // Sort the legend using either the default or a custom comparator - - if (options.legend.sorted) { - if ($.isFunction(options.legend.sorted)) { - entries.sort(options.legend.sorted); - } else if (options.legend.sorted == "reverse") { - entries.reverse(); - } else { - var ascending = options.legend.sorted != "descending"; - entries.sort(function(a, b) { - return a.label == b.label ? 0 : ( - (a.label < b.label) != ascending ? 1 : -1 // Logical XOR - ); - }); - } - } - - // Generate markup for the list of entries, in their final order - - for (var i = 0; i < entries.length; ++i) { - - var entry = entries[i]; - - if (i % options.legend.noColumns == 0) { - if (rowStarted) - fragments.push(''); - fragments.push(''); - rowStarted = true; - } - - fragments.push( - '
' + - '' + entry.label + '' - ); - } - - if (rowStarted) - fragments.push(''); - - if (fragments.length == 0) - return; - - var table = '' + fragments.join("") + '
'; - if (options.legend.container != null) - $(options.legend.container).html(table); - else { - var pos = "", - p = options.legend.position, - m = options.legend.margin; - if (m[0] == null) - m = [m, m]; - if (p.charAt(0) == "n") - pos += 'top:' + (m[1] + plotOffset.top) + 'px;'; - else if (p.charAt(0) == "s") - pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;'; - if (p.charAt(1) == "e") - pos += 'right:' + (m[0] + plotOffset.right) + 'px;'; - else if (p.charAt(1) == "w") - pos += 'left:' + (m[0] + plotOffset.left) + 'px;'; - var legend = $('
' + table.replace('style="', 'style="position:absolute;' + pos +';') + '
').appendTo(placeholder); - if (options.legend.backgroundOpacity != 0.0) { - // put in the transparent background - // separately to avoid blended labels and - // label boxes - var c = options.legend.backgroundColor; - if (c == null) { - c = options.grid.backgroundColor; - if (c && typeof c == "string") - c = $.color.parse(c); - else - c = $.color.extract(legend, 'background-color'); - c.a = 1; - c = c.toString(); - } - var div = legend.children(); - $('
').prependTo(legend).css('opacity', options.legend.backgroundOpacity); - } - } - } - - - // interactive features - - var highlights = [], - redrawTimeout = null; - - // returns the data item the mouse is over, or null if none is found - function findNearbyItem(mouseX, mouseY, seriesFilter) { - var maxDistance = options.grid.mouseActiveRadius, - smallestDistance = maxDistance * maxDistance + 1, - item = null, foundPoint = false, i, j, ps; - - for (i = series.length - 1; i >= 0; --i) { - if (!seriesFilter(series[i])) - continue; - - var s = series[i], - axisx = s.xaxis, - axisy = s.yaxis, - points = s.datapoints.points, - mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster - my = axisy.c2p(mouseY), - maxx = maxDistance / axisx.scale, - maxy = maxDistance / axisy.scale; - - ps = s.datapoints.pointsize; - // with inverse transforms, we can't use the maxx/maxy - // optimization, sadly - if (axisx.options.inverseTransform) - maxx = Number.MAX_VALUE; - if (axisy.options.inverseTransform) - maxy = Number.MAX_VALUE; - - if (s.lines.show || s.points.show) { - for (j = 0; j < points.length; j += ps) { - var x = points[j], y = points[j + 1]; - if (x == null) - continue; - - // For points and lines, the cursor must be within a - // certain distance to the data point - if (x - mx > maxx || x - mx < -maxx || - y - my > maxy || y - my < -maxy) - continue; - - // We have to calculate distances in pixels, not in - // data units, because the scales of the axes may be different - var dx = Math.abs(axisx.p2c(x) - mouseX), - dy = Math.abs(axisy.p2c(y) - mouseY), - dist = dx * dx + dy * dy; // we save the sqrt - - // use <= to ensure last point takes precedence - // (last generally means on top of) - if (dist < smallestDistance) { - smallestDistance = dist; - item = [i, j / ps]; - } - } - } - - if (s.bars.show && !item) { // no other point can be nearby - - var barLeft, barRight; - - switch (s.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -s.bars.barWidth; - break; - default: - barLeft = -s.bars.barWidth / 2; - } - - barRight = barLeft + s.bars.barWidth; - - for (j = 0; j < points.length; j += ps) { - var x = points[j], y = points[j + 1], b = points[j + 2]; - if (x == null) - continue; - - // for a bar graph, the cursor must be inside the bar - if (series[i].bars.horizontal ? - (mx <= Math.max(b, x) && mx >= Math.min(b, x) && - my >= y + barLeft && my <= y + barRight) : - (mx >= x + barLeft && mx <= x + barRight && - my >= Math.min(b, y) && my <= Math.max(b, y))) - item = [i, j / ps]; - } - } - } - - if (item) { - i = item[0]; - j = item[1]; - ps = series[i].datapoints.pointsize; - - return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps), - dataIndex: j, - series: series[i], - seriesIndex: i }; - } - - return null; - } - - function onMouseMove(e) { - if (options.grid.hoverable) - triggerClickHoverEvent("plothover", e, - function (s) { return s["hoverable"] != false; }); - } - - function onMouseLeave(e) { - if (options.grid.hoverable) - triggerClickHoverEvent("plothover", e, - function (s) { return false; }); - } - - function onClick(e) { - triggerClickHoverEvent("plotclick", e, - function (s) { return s["clickable"] != false; }); - } - - // trigger click or hover event (they send the same parameters - // so we share their code) - function triggerClickHoverEvent(eventname, event, seriesFilter) { - var offset = eventHolder.offset(), - canvasX = event.pageX - offset.left - plotOffset.left, - canvasY = event.pageY - offset.top - plotOffset.top, - pos = canvasToAxisCoords({ left: canvasX, top: canvasY }); - - pos.pageX = event.pageX; - pos.pageY = event.pageY; - - var item = findNearbyItem(canvasX, canvasY, seriesFilter); - - if (item) { - // fill in mouse pos for any listeners out there - item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left, 10); - item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top, 10); - } - - if (options.grid.autoHighlight) { - // clear auto-highlights - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.auto == eventname && - !(item && h.series == item.series && - h.point[0] == item.datapoint[0] && - h.point[1] == item.datapoint[1])) - unhighlight(h.series, h.point); - } - - if (item) - highlight(item.series, item.datapoint, eventname); - } - - placeholder.trigger(eventname, [ pos, item ]); - } - - function triggerRedrawOverlay() { - var t = options.interaction.redrawOverlayInterval; - if (t == -1) { // skip event queue - drawOverlay(); - return; - } - - if (!redrawTimeout) - redrawTimeout = setTimeout(drawOverlay, t); - } - - function drawOverlay() { - redrawTimeout = null; - - // draw highlights - octx.save(); - overlay.clear(); - octx.translate(plotOffset.left, plotOffset.top); - - var i, hi; - for (i = 0; i < highlights.length; ++i) { - hi = highlights[i]; - - if (hi.series.bars.show) - drawBarHighlight(hi.series, hi.point); - else - drawPointHighlight(hi.series, hi.point); - } - octx.restore(); - - executeHooks(hooks.drawOverlay, [octx]); - } - - function highlight(s, point, auto) { - if (typeof s == "number") - s = series[s]; - - if (typeof point == "number") { - var ps = s.datapoints.pointsize; - point = s.datapoints.points.slice(ps * point, ps * (point + 1)); - } - - var i = indexOfHighlight(s, point); - if (i == -1) { - highlights.push({ series: s, point: point, auto: auto }); - - triggerRedrawOverlay(); - } - else if (!auto) - highlights[i].auto = false; - } - - function unhighlight(s, point) { - if (s == null && point == null) { - highlights = []; - triggerRedrawOverlay(); - return; - } - - if (typeof s == "number") - s = series[s]; - - if (typeof point == "number") { - var ps = s.datapoints.pointsize; - point = s.datapoints.points.slice(ps * point, ps * (point + 1)); - } - - var i = indexOfHighlight(s, point); - if (i != -1) { - highlights.splice(i, 1); - - triggerRedrawOverlay(); - } - } - - function indexOfHighlight(s, p) { - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.series == s && h.point[0] == p[0] - && h.point[1] == p[1]) - return i; - } - return -1; - } - - function drawPointHighlight(series, point) { - var x = point[0], y = point[1], - axisx = series.xaxis, axisy = series.yaxis, - highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(); - - if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) - return; - - var pointRadius = series.points.radius + series.points.lineWidth / 2; - octx.lineWidth = pointRadius; - octx.strokeStyle = highlightColor; - var radius = 1.5 * pointRadius; - x = axisx.p2c(x); - y = axisy.p2c(y); - - octx.beginPath(); - if (series.points.symbol == "circle") - octx.arc(x, y, radius, 0, 2 * Math.PI, false); - else - series.points.symbol(octx, x, y, radius, false); - octx.closePath(); - octx.stroke(); - } - - function drawBarHighlight(series, point) { - var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(), - fillStyle = highlightColor, - barLeft; - - switch (series.bars.align) { - case "left": - barLeft = 0; - break; - case "right": - barLeft = -series.bars.barWidth; - break; - default: - barLeft = -series.bars.barWidth / 2; - } - - octx.lineWidth = series.bars.lineWidth; - octx.strokeStyle = highlightColor; - - drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth, - function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth); - } - - function getColorOrGradient(spec, bottom, top, defaultColor) { - if (typeof spec == "string") - return spec; - else { - // assume this is a gradient spec; IE currently only - // supports a simple vertical gradient properly, so that's - // what we support too - var gradient = ctx.createLinearGradient(0, top, 0, bottom); - - for (var i = 0, l = spec.colors.length; i < l; ++i) { - var c = spec.colors[i]; - if (typeof c != "string") { - var co = $.color.parse(defaultColor); - if (c.brightness != null) - co = co.scale('rgb', c.brightness); - if (c.opacity != null) - co.a *= c.opacity; - c = co.toString(); - } - gradient.addColorStop(i / (l - 1), c); - } - - return gradient; - } - } - } - - // Add the plot function to the top level of the jQuery object - - $.plot = function(placeholder, data, options) { - //var t0 = new Date(); - var plot = new Plot($(placeholder), data, options, $.plot.plugins); - //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime())); - return plot; - }; - - $.plot.version = "0.8.3"; - - $.plot.plugins = []; - - // Also add the plot function as a chainable property - - $.fn.plot = function(data, options) { - return this.each(function() { - $.plot(this, data, options); - }); - }; - - // round to nearby lower multiple of base - function floorInBase(n, base) { - return base * Math.floor(n / base); - } - -})(jQuery); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.selection.js b/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.selection.js deleted file mode 100644 index d3c20fa4e12f2..0000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.selection.js +++ /dev/null @@ -1,360 +0,0 @@ -/* Flot plugin for selecting regions of a plot. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin supports these options: - -selection: { - mode: null or "x" or "y" or "xy", - color: color, - shape: "round" or "miter" or "bevel", - minSize: number of pixels -} - -Selection support is enabled by setting the mode to one of "x", "y" or "xy". -In "x" mode, the user will only be able to specify the x range, similarly for -"y" mode. For "xy", the selection becomes a rectangle where both ranges can be -specified. "color" is color of the selection (if you need to change the color -later on, you can get to it with plot.getOptions().selection.color). "shape" -is the shape of the corners of the selection. - -"minSize" is the minimum size a selection can be in pixels. This value can -be customized to determine the smallest size a selection can be and still -have the selection rectangle be displayed. When customizing this value, the -fact that it refers to pixels, not axis units must be taken into account. -Thus, for example, if there is a bar graph in time mode with BarWidth set to 1 -minute, setting "minSize" to 1 will not make the minimum selection size 1 -minute, but rather 1 pixel. Note also that setting "minSize" to 0 will prevent -"plotunselected" events from being fired when the user clicks the mouse without -dragging. - -When selection support is enabled, a "plotselected" event will be emitted on -the DOM element you passed into the plot function. The event handler gets a -parameter with the ranges selected on the axes, like this: - - placeholder.bind( "plotselected", function( event, ranges ) { - alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to) - // similar for yaxis - with multiple axes, the extra ones are in - // x2axis, x3axis, ... - }); - -The "plotselected" event is only fired when the user has finished making the -selection. A "plotselecting" event is fired during the process with the same -parameters as the "plotselected" event, in case you want to know what's -happening while it's happening, - -A "plotunselected" event with no arguments is emitted when the user clicks the -mouse to remove the selection. As stated above, setting "minSize" to 0 will -destroy this behavior. - -The plugin allso adds the following methods to the plot object: - -- setSelection( ranges, preventEvent ) - - Set the selection rectangle. The passed in ranges is on the same form as - returned in the "plotselected" event. If the selection mode is "x", you - should put in either an xaxis range, if the mode is "y" you need to put in - an yaxis range and both xaxis and yaxis if the selection mode is "xy", like - this: - - setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } }); - - setSelection will trigger the "plotselected" event when called. If you don't - want that to happen, e.g. if you're inside a "plotselected" handler, pass - true as the second parameter. If you are using multiple axes, you can - specify the ranges on any of those, e.g. as x2axis/x3axis/... instead of - xaxis, the plugin picks the first one it sees. - -- clearSelection( preventEvent ) - - Clear the selection rectangle. Pass in true to avoid getting a - "plotunselected" event. - -- getSelection() - - Returns the current selection in the same format as the "plotselected" - event. If there's currently no selection, the function returns null. - -*/ - -(function ($) { - function init(plot) { - var selection = { - first: { x: -1, y: -1}, second: { x: -1, y: -1}, - show: false, - active: false - }; - - // FIXME: The drag handling implemented here should be - // abstracted out, there's some similar code from a library in - // the navigation plugin, this should be massaged a bit to fit - // the Flot cases here better and reused. Doing this would - // make this plugin much slimmer. - var savedhandlers = {}; - - var mouseUpHandler = null; - - function onMouseMove(e) { - if (selection.active) { - updateSelection(e); - - plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]); - } - } - - function onMouseDown(e) { - if (e.which != 1) // only accept left-click - return; - - // cancel out any text selections - document.body.focus(); - - // prevent text selection and drag in old-school browsers - if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) { - savedhandlers.onselectstart = document.onselectstart; - document.onselectstart = function () { return false; }; - } - if (document.ondrag !== undefined && savedhandlers.ondrag == null) { - savedhandlers.ondrag = document.ondrag; - document.ondrag = function () { return false; }; - } - - setSelectionPos(selection.first, e); - - selection.active = true; - - // this is a bit silly, but we have to use a closure to be - // able to whack the same handler again - mouseUpHandler = function (e) { onMouseUp(e); }; - - $(document).one("mouseup", mouseUpHandler); - } - - function onMouseUp(e) { - mouseUpHandler = null; - - // revert drag stuff for old-school browsers - if (document.onselectstart !== undefined) - document.onselectstart = savedhandlers.onselectstart; - if (document.ondrag !== undefined) - document.ondrag = savedhandlers.ondrag; - - // no more dragging - selection.active = false; - updateSelection(e); - - if (selectionIsSane()) - triggerSelectedEvent(); - else { - // this counts as a clear - plot.getPlaceholder().trigger("plotunselected", [ ]); - plot.getPlaceholder().trigger("plotselecting", [ null ]); - } - - return false; - } - - function getSelection() { - if (!selectionIsSane()) - return null; - - if (!selection.show) return null; - - var r = {}, c1 = selection.first, c2 = selection.second; - $.each(plot.getAxes(), function (name, axis) { - if (axis.used) { - var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]); - r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) }; - } - }); - return r; - } - - function triggerSelectedEvent() { - var r = getSelection(); - - plot.getPlaceholder().trigger("plotselected", [ r ]); - - // backwards-compat stuff, to be removed in future - if (r.xaxis && r.yaxis) - plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]); - } - - function clamp(min, value, max) { - return value < min ? min: (value > max ? max: value); - } - - function setSelectionPos(pos, e) { - var o = plot.getOptions(); - var offset = plot.getPlaceholder().offset(); - var plotOffset = plot.getPlotOffset(); - pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width()); - pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height()); - - if (o.selection.mode == "y") - pos.x = pos == selection.first ? 0 : plot.width(); - - if (o.selection.mode == "x") - pos.y = pos == selection.first ? 0 : plot.height(); - } - - function updateSelection(pos) { - if (pos.pageX == null) - return; - - setSelectionPos(selection.second, pos); - if (selectionIsSane()) { - selection.show = true; - plot.triggerRedrawOverlay(); - } - else - clearSelection(true); - } - - function clearSelection(preventEvent) { - if (selection.show) { - selection.show = false; - plot.triggerRedrawOverlay(); - if (!preventEvent) - plot.getPlaceholder().trigger("plotunselected", [ ]); - } - } - - // function taken from markings support in Flot - function extractRange(ranges, coord) { - var axis, from, to, key, axes = plot.getAxes(); - - for (var k in axes) { - axis = axes[k]; - if (axis.direction == coord) { - key = coord + axis.n + "axis"; - if (!ranges[key] && axis.n == 1) - key = coord + "axis"; // support x1axis as xaxis - if (ranges[key]) { - from = ranges[key].from; - to = ranges[key].to; - break; - } - } - } - - // backwards-compat stuff - to be removed in future - if (!ranges[key]) { - axis = coord == "x" ? plot.getXAxes()[0] : plot.getYAxes()[0]; - from = ranges[coord + "1"]; - to = ranges[coord + "2"]; - } - - // auto-reverse as an added bonus - if (from != null && to != null && from > to) { - var tmp = from; - from = to; - to = tmp; - } - - return { from: from, to: to, axis: axis }; - } - - function setSelection(ranges, preventEvent) { - var axis, range, o = plot.getOptions(); - - if (o.selection.mode == "y") { - selection.first.x = 0; - selection.second.x = plot.width(); - } - else { - range = extractRange(ranges, "x"); - - selection.first.x = range.axis.p2c(range.from); - selection.second.x = range.axis.p2c(range.to); - } - - if (o.selection.mode == "x") { - selection.first.y = 0; - selection.second.y = plot.height(); - } - else { - range = extractRange(ranges, "y"); - - selection.first.y = range.axis.p2c(range.from); - selection.second.y = range.axis.p2c(range.to); - } - - selection.show = true; - plot.triggerRedrawOverlay(); - if (!preventEvent && selectionIsSane()) - triggerSelectedEvent(); - } - - function selectionIsSane() { - var minSize = plot.getOptions().selection.minSize; - return Math.abs(selection.second.x - selection.first.x) >= minSize && - Math.abs(selection.second.y - selection.first.y) >= minSize; - } - - plot.clearSelection = clearSelection; - plot.setSelection = setSelection; - plot.getSelection = getSelection; - - plot.hooks.bindEvents.push(function(plot, eventHolder) { - var o = plot.getOptions(); - if (o.selection.mode != null) { - eventHolder.mousemove(onMouseMove); - eventHolder.mousedown(onMouseDown); - } - }); - - - plot.hooks.drawOverlay.push(function (plot, ctx) { - // draw selection - if (selection.show && selectionIsSane()) { - var plotOffset = plot.getPlotOffset(); - var o = plot.getOptions(); - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - var c = $.color.parse(o.selection.color); - - ctx.strokeStyle = c.scale('a', 0.8).toString(); - ctx.lineWidth = 1; - ctx.lineJoin = o.selection.shape; - ctx.fillStyle = c.scale('a', 0.4).toString(); - - var x = Math.min(selection.first.x, selection.second.x) + 0.5, - y = Math.min(selection.first.y, selection.second.y) + 0.5, - w = Math.abs(selection.second.x - selection.first.x) - 1, - h = Math.abs(selection.second.y - selection.first.y) - 1; - - ctx.fillRect(x, y, w, h); - ctx.strokeRect(x, y, w, h); - - ctx.restore(); - } - }); - - plot.hooks.shutdown.push(function (plot, eventHolder) { - eventHolder.unbind("mousemove", onMouseMove); - eventHolder.unbind("mousedown", onMouseDown); - - if (mouseUpHandler) - $(document).unbind("mouseup", mouseUpHandler); - }); - - } - - $.plot.plugins.push({ - init: init, - options: { - selection: { - mode: null, // one of null, "x", "y" or "xy" - color: "#e8cfac", - shape: "round", // one of "round", "miter", or "bevel" - minSize: 5 // minimum number of pixels - } - }, - name: 'selection', - version: '1.1' - }); -})(jQuery); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.stack.js b/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.stack.js deleted file mode 100644 index e75a7dfc07434..0000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.stack.js +++ /dev/null @@ -1,188 +0,0 @@ -/* Flot plugin for stacking data sets rather than overlyaing them. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin assumes the data is sorted on x (or y if stacking horizontally). -For line charts, it is assumed that if a line has an undefined gap (from a -null point), then the line above it should have the same gap - insert zeros -instead of "null" if you want another behaviour. This also holds for the start -and end of the chart. Note that stacking a mix of positive and negative values -in most instances doesn't make sense (so it looks weird). - -Two or more series are stacked when their "stack" attribute is set to the same -key (which can be any number or string or just "true"). To specify the default -stack, you can set the stack option like this: - - series: { - stack: null/false, true, or a key (number/string) - } - -You can also specify it for a single series, like this: - - $.plot( $("#placeholder"), [{ - data: [ ... ], - stack: true - }]) - -The stacking order is determined by the order of the data series in the array -(later series end up on top of the previous). - -Internally, the plugin modifies the datapoints in each series, adding an -offset to the y value. For line series, extra data points are inserted through -interpolation. If there's a second y value, it's also adjusted (e.g for bar -charts or filled areas). - -*/ - -(function ($) { - var options = { - series: { stack: null } // or number/string - }; - - function init(plot) { - function findMatchingSeries(s, allseries) { - var res = null; - for (var i = 0; i < allseries.length; ++i) { - if (s == allseries[i]) - break; - - if (allseries[i].stack == s.stack) - res = allseries[i]; - } - - return res; - } - - function stackData(plot, s, datapoints) { - if (s.stack == null || s.stack === false) - return; - - var other = findMatchingSeries(s, plot.getData()); - if (!other) - return; - - var ps = datapoints.pointsize, - points = datapoints.points, - otherps = other.datapoints.pointsize, - otherpoints = other.datapoints.points, - newpoints = [], - px, py, intery, qx, qy, bottom, - withlines = s.lines.show, - horizontal = s.bars.horizontal, - withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y), - withsteps = withlines && s.lines.steps, - fromgap = true, - keyOffset = horizontal ? 1 : 0, - accumulateOffset = horizontal ? 0 : 1, - i = 0, j = 0, l, m; - - while (true) { - if (i >= points.length) - break; - - l = newpoints.length; - - if (points[i] == null) { - // copy gaps - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - i += ps; - } - else if (j >= otherpoints.length) { - // for lines, we can't use the rest of the points - if (!withlines) { - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - } - i += ps; - } - else if (otherpoints[j] == null) { - // oops, got a gap - for (m = 0; m < ps; ++m) - newpoints.push(null); - fromgap = true; - j += otherps; - } - else { - // cases where we actually got two points - px = points[i + keyOffset]; - py = points[i + accumulateOffset]; - qx = otherpoints[j + keyOffset]; - qy = otherpoints[j + accumulateOffset]; - bottom = 0; - - if (px == qx) { - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - - newpoints[l + accumulateOffset] += qy; - bottom = qy; - - i += ps; - j += otherps; - } - else if (px > qx) { - // we got past point below, might need to - // insert interpolated extra point - if (withlines && i > 0 && points[i - ps] != null) { - intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); - newpoints.push(qx); - newpoints.push(intery + qy); - for (m = 2; m < ps; ++m) - newpoints.push(points[i + m]); - bottom = qy; - } - - j += otherps; - } - else { // px < qx - if (fromgap && withlines) { - // if we come from a gap, we just skip this point - i += ps; - continue; - } - - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - - // we might be able to interpolate a point below, - // this can give us a better y - if (withlines && j > 0 && otherpoints[j - otherps] != null) - bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx); - - newpoints[l + accumulateOffset] += bottom; - - i += ps; - } - - fromgap = false; - - if (l != newpoints.length && withbottom) - newpoints[l + 2] += bottom; - } - - // maintain the line steps invariant - if (withsteps && l != newpoints.length && l > 0 - && newpoints[l] != null - && newpoints[l] != newpoints[l - ps] - && newpoints[l + 1] != newpoints[l - ps + 1]) { - for (m = 0; m < ps; ++m) - newpoints[l + ps + m] = newpoints[l + m]; - newpoints[l + 1] = newpoints[l - ps + 1]; - } - } - - datapoints.points = newpoints; - } - - plot.hooks.processDatapoints.push(stackData); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'stack', - version: '1.2' - }); -})(jQuery); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.symbol.js b/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.symbol.js deleted file mode 100644 index 79f634971b6fa..0000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.symbol.js +++ /dev/null @@ -1,71 +0,0 @@ -/* Flot plugin that adds some extra symbols for plotting points. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The symbols are accessed as strings through the standard symbol options: - - series: { - points: { - symbol: "square" // or "diamond", "triangle", "cross" - } - } - -*/ - -(function ($) { - function processRawData(plot, series, datapoints) { - // we normalize the area of each symbol so it is approximately the - // same as a circle of the given radius - - var handlers = { - square: function (ctx, x, y, radius, shadow) { - // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 - var size = radius * Math.sqrt(Math.PI) / 2; - ctx.rect(x - size, y - size, size + size, size + size); - }, - diamond: function (ctx, x, y, radius, shadow) { - // pi * r^2 = 2s^2 => s = r * sqrt(pi/2) - var size = radius * Math.sqrt(Math.PI / 2); - ctx.moveTo(x - size, y); - ctx.lineTo(x, y - size); - ctx.lineTo(x + size, y); - ctx.lineTo(x, y + size); - ctx.lineTo(x - size, y); - }, - triangle: function (ctx, x, y, radius, shadow) { - // pi * r^2 = 1/2 * s^2 * sin (pi / 3) => s = r * sqrt(2 * pi / sin(pi / 3)) - var size = radius * Math.sqrt(2 * Math.PI / Math.sin(Math.PI / 3)); - var height = size * Math.sin(Math.PI / 3); - ctx.moveTo(x - size/2, y + height/2); - ctx.lineTo(x + size/2, y + height/2); - if (!shadow) { - ctx.lineTo(x, y - height/2); - ctx.lineTo(x - size/2, y + height/2); - } - }, - cross: function (ctx, x, y, radius, shadow) { - // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 - var size = radius * Math.sqrt(Math.PI) / 2; - ctx.moveTo(x - size, y - size); - ctx.lineTo(x + size, y + size); - ctx.moveTo(x - size, y + size); - ctx.lineTo(x + size, y - size); - } - }; - - var s = series.points.symbol; - if (handlers[s]) - series.points.symbol = handlers[s]; - } - - function init(plot) { - plot.hooks.processDatapoints.push(processRawData); - } - - $.plot.plugins.push({ - init: init, - name: 'symbols', - version: '1.0' - }); -})(jQuery); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.time.js b/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.time.js deleted file mode 100644 index 34c1d121259a2..0000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts/jquery.flot.time.js +++ /dev/null @@ -1,432 +0,0 @@ -/* Pretty handling of time axes. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -Set axis.mode to "time" to enable. See the section "Time series data" in -API.txt for details. - -*/ - -(function($) { - - var options = { - xaxis: { - timezone: null, // "browser" for local to the client or timezone for timezone-js - timeformat: null, // format string to use - twelveHourClock: false, // 12 or 24 time in time mode - monthNames: null // list of names of months - } - }; - - // round to nearby lower multiple of base - - function floorInBase(n, base) { - return base * Math.floor(n / base); - } - - // Returns a string with the date d formatted according to fmt. - // A subset of the Open Group's strftime format is supported. - - function formatDate(d, fmt, monthNames, dayNames) { - - if (typeof d.strftime == "function") { - return d.strftime(fmt); - } - - var leftPad = function(n, pad) { - n = "" + n; - pad = "" + (pad == null ? "0" : pad); - return n.length == 1 ? pad + n : n; - }; - - var r = []; - var escape = false; - var hours = d.getHours(); - var isAM = hours < 12; - - if (monthNames == null) { - monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; - } - - if (dayNames == null) { - dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; - } - - var hours12; - - if (hours > 12) { - hours12 = hours - 12; - } else if (hours == 0) { - hours12 = 12; - } else { - hours12 = hours; - } - - for (var i = 0; i < fmt.length; ++i) { - - var c = fmt.charAt(i); - - if (escape) { - switch (c) { - case 'a': c = "" + dayNames[d.getDay()]; break; - case 'b': c = "" + monthNames[d.getMonth()]; break; - case 'd': c = leftPad(d.getDate()); break; - case 'e': c = leftPad(d.getDate(), " "); break; - case 'h': // For back-compat with 0.7; remove in 1.0 - case 'H': c = leftPad(hours); break; - case 'I': c = leftPad(hours12); break; - case 'l': c = leftPad(hours12, " "); break; - case 'm': c = leftPad(d.getMonth() + 1); break; - case 'M': c = leftPad(d.getMinutes()); break; - // quarters not in Open Group's strftime specification - case 'q': - c = "" + (Math.floor(d.getMonth() / 3) + 1); break; - case 'S': c = leftPad(d.getSeconds()); break; - case 'y': c = leftPad(d.getFullYear() % 100); break; - case 'Y': c = "" + d.getFullYear(); break; - case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; - case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; - case 'w': c = "" + d.getDay(); break; - } - r.push(c); - escape = false; - } else { - if (c == "%") { - escape = true; - } else { - r.push(c); - } - } - } - - return r.join(""); - } - - // To have a consistent view of time-based data independent of which time - // zone the client happens to be in we need a date-like object independent - // of time zones. This is done through a wrapper that only calls the UTC - // versions of the accessor methods. - - function makeUtcWrapper(d) { - - function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) { - sourceObj[sourceMethod] = function() { - return targetObj[targetMethod].apply(targetObj, arguments); - }; - }; - - var utc = { - date: d - }; - - // support strftime, if found - - if (d.strftime != undefined) { - addProxyMethod(utc, "strftime", d, "strftime"); - } - - addProxyMethod(utc, "getTime", d, "getTime"); - addProxyMethod(utc, "setTime", d, "setTime"); - - var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"]; - - for (var p = 0; p < props.length; p++) { - addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]); - addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]); - } - - return utc; - }; - - // select time zone strategy. This returns a date-like object tied to the - // desired timezone - - function dateGenerator(ts, opts) { - if (opts.timezone == "browser") { - return new Date(ts); - } else if (!opts.timezone || opts.timezone == "utc") { - return makeUtcWrapper(new Date(ts)); - } else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") { - var d = new timezoneJS.Date(); - // timezone-js is fickle, so be sure to set the time zone before - // setting the time. - d.setTimezone(opts.timezone); - d.setTime(ts); - return d; - } else { - return makeUtcWrapper(new Date(ts)); - } - } - - // map of app. size of time units in milliseconds - - var timeUnitSize = { - "second": 1000, - "minute": 60 * 1000, - "hour": 60 * 60 * 1000, - "day": 24 * 60 * 60 * 1000, - "month": 30 * 24 * 60 * 60 * 1000, - "quarter": 3 * 30 * 24 * 60 * 60 * 1000, - "year": 365.2425 * 24 * 60 * 60 * 1000 - }; - - // the allowed tick sizes, after 1 year we use - // an integer algorithm - - var baseSpec = [ - [1, "second"], [2, "second"], [5, "second"], [10, "second"], - [30, "second"], - [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], - [30, "minute"], - [1, "hour"], [2, "hour"], [4, "hour"], - [8, "hour"], [12, "hour"], - [1, "day"], [2, "day"], [3, "day"], - [0.25, "month"], [0.5, "month"], [1, "month"], - [2, "month"] - ]; - - // we don't know which variant(s) we'll need yet, but generating both is - // cheap - - var specMonths = baseSpec.concat([[3, "month"], [6, "month"], - [1, "year"]]); - var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"], - [1, "year"]]); - - function init(plot) { - plot.hooks.processOptions.push(function (plot, options) { - $.each(plot.getAxes(), function(axisName, axis) { - - var opts = axis.options; - - if (opts.mode == "time") { - axis.tickGenerator = function(axis) { - - var ticks = []; - var d = dateGenerator(axis.min, opts); - var minSize = 0; - - // make quarter use a possibility if quarters are - // mentioned in either of these options - - var spec = (opts.tickSize && opts.tickSize[1] === - "quarter") || - (opts.minTickSize && opts.minTickSize[1] === - "quarter") ? specQuarters : specMonths; - - if (opts.minTickSize != null) { - if (typeof opts.tickSize == "number") { - minSize = opts.tickSize; - } else { - minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; - } - } - - for (var i = 0; i < spec.length - 1; ++i) { - if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]] - + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 - && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) { - break; - } - } - - var size = spec[i][0]; - var unit = spec[i][1]; - - // special-case the possibility of several years - - if (unit == "year") { - - // if given a minTickSize in years, just use it, - // ensuring that it's an integer - - if (opts.minTickSize != null && opts.minTickSize[1] == "year") { - size = Math.floor(opts.minTickSize[0]); - } else { - - var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10)); - var norm = (axis.delta / timeUnitSize.year) / magn; - - if (norm < 1.5) { - size = 1; - } else if (norm < 3) { - size = 2; - } else if (norm < 7.5) { - size = 5; - } else { - size = 10; - } - - size *= magn; - } - - // minimum size for years is 1 - - if (size < 1) { - size = 1; - } - } - - axis.tickSize = opts.tickSize || [size, unit]; - var tickSize = axis.tickSize[0]; - unit = axis.tickSize[1]; - - var step = tickSize * timeUnitSize[unit]; - - if (unit == "second") { - d.setSeconds(floorInBase(d.getSeconds(), tickSize)); - } else if (unit == "minute") { - d.setMinutes(floorInBase(d.getMinutes(), tickSize)); - } else if (unit == "hour") { - d.setHours(floorInBase(d.getHours(), tickSize)); - } else if (unit == "month") { - d.setMonth(floorInBase(d.getMonth(), tickSize)); - } else if (unit == "quarter") { - d.setMonth(3 * floorInBase(d.getMonth() / 3, - tickSize)); - } else if (unit == "year") { - d.setFullYear(floorInBase(d.getFullYear(), tickSize)); - } - - // reset smaller components - - d.setMilliseconds(0); - - if (step >= timeUnitSize.minute) { - d.setSeconds(0); - } - if (step >= timeUnitSize.hour) { - d.setMinutes(0); - } - if (step >= timeUnitSize.day) { - d.setHours(0); - } - if (step >= timeUnitSize.day * 4) { - d.setDate(1); - } - if (step >= timeUnitSize.month * 2) { - d.setMonth(floorInBase(d.getMonth(), 3)); - } - if (step >= timeUnitSize.quarter * 2) { - d.setMonth(floorInBase(d.getMonth(), 6)); - } - if (step >= timeUnitSize.year) { - d.setMonth(0); - } - - var carry = 0; - var v = Number.NaN; - var prev; - - do { - - prev = v; - v = d.getTime(); - ticks.push(v); - - if (unit == "month" || unit == "quarter") { - if (tickSize < 1) { - - // a bit complicated - we'll divide the - // month/quarter up but we need to take - // care of fractions so we don't end up in - // the middle of a day - - d.setDate(1); - var start = d.getTime(); - d.setMonth(d.getMonth() + - (unit == "quarter" ? 3 : 1)); - var end = d.getTime(); - d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); - carry = d.getHours(); - d.setHours(0); - } else { - d.setMonth(d.getMonth() + - tickSize * (unit == "quarter" ? 3 : 1)); - } - } else if (unit == "year") { - d.setFullYear(d.getFullYear() + tickSize); - } else { - d.setTime(v + step); - } - } while (v < axis.max && v != prev); - - return ticks; - }; - - axis.tickFormatter = function (v, axis) { - - var d = dateGenerator(v, axis.options); - - // first check global format - - if (opts.timeformat != null) { - return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames); - } - - // possibly use quarters if quarters are mentioned in - // any of these places - - var useQuarters = (axis.options.tickSize && - axis.options.tickSize[1] == "quarter") || - (axis.options.minTickSize && - axis.options.minTickSize[1] == "quarter"); - - var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; - var span = axis.max - axis.min; - var suffix = (opts.twelveHourClock) ? " %p" : ""; - var hourCode = (opts.twelveHourClock) ? "%I" : "%H"; - var fmt; - - if (t < timeUnitSize.minute) { - fmt = hourCode + ":%M:%S" + suffix; - } else if (t < timeUnitSize.day) { - if (span < 2 * timeUnitSize.day) { - fmt = hourCode + ":%M" + suffix; - } else { - fmt = "%b %d " + hourCode + ":%M" + suffix; - } - } else if (t < timeUnitSize.month) { - fmt = "%b %d"; - } else if ((useQuarters && t < timeUnitSize.quarter) || - (!useQuarters && t < timeUnitSize.year)) { - if (span < timeUnitSize.year) { - fmt = "%b"; - } else { - fmt = "%b %Y"; - } - } else if (useQuarters && t < timeUnitSize.year) { - if (span < timeUnitSize.year) { - fmt = "Q%q"; - } else { - fmt = "Q%q %Y"; - } - } else { - fmt = "%Y"; - } - - var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames); - - return rt; - }; - } - }); - }); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'time', - version: '1.0' - }); - - // Time-axis support used to be in Flot core, which exposed the - // formatDate function on the plot object. Various plugins depend - // on the function, so we need to re-expose it here. - - $.plot.formatDate = formatDate; - $.plot.dateGenerator = dateGenerator; - -})(jQuery); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/pie/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/pie/index.tsx index 29e823e0a373b..e8bffc873307b 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/pie/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/pie/index.tsx @@ -7,10 +7,8 @@ // This bit of hackiness is required because this isn't part of the main kibana bundle import 'jquery'; -import { debounce, includes } from 'lodash'; +import { debounce } from 'lodash'; import { RendererStrings } from '../../../i18n'; -// @ts-expect-error Untyped local: Will not convert -import { pie as piePlugin } from './plugins/pie'; import { Pie } from '../../functions/common/pie'; import { RendererFactory } from '../../../types'; @@ -22,13 +20,6 @@ export const pie: RendererFactory = () => ({ help: strings.getHelpDescription(), reuseDomNode: false, render: async (domNode, config, handlers) => { - // @ts-expect-error - await import('../../lib/flot-charts'); - - if (!includes($.plot.plugins, piePlugin)) { - $.plot.plugins.push(piePlugin); - } - config.options.legend.labelBoxBorderColor = 'transparent'; if (config.font) { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/plot/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/plot/index.ts index 9d70ca418f491..62af4fe7c7360 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/plot/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/plot/index.ts @@ -18,9 +18,6 @@ import { text } from './plugins/text'; const { plot: strings } = RendererStrings; const render: RendererSpec['render'] = async (domNode, config, handlers) => { - // @ts-expect-error - await import('../../lib/flot-charts'); - // TODO: OH NOES if (!includes($.plot.plugins, size)) { $.plot.plugins.push(size); diff --git a/x-pack/plugins/canvas/scripts/storybook.js b/x-pack/plugins/canvas/scripts/storybook.js index 23703810569b6..3b06b7e7e2112 100644 --- a/x-pack/plugins/canvas/scripts/storybook.js +++ b/x-pack/plugins/canvas/scripts/storybook.js @@ -8,6 +8,8 @@ const path = require('path'); const fs = require('fs'); const del = require('del'); const { run } = require('@kbn/dev-utils'); +// This is included in the main Kibana package.json +// eslint-disable-next-line import/no-extraneous-dependencies const storybook = require('@storybook/react/standalone'); const execa = require('execa'); const { DLL_OUTPUT } = require('./../storybook/constants'); diff --git a/x-pack/plugins/canvas/storybook/storyshots.test.tsx b/x-pack/plugins/canvas/storybook/storyshots.test.tsx index 1e993f9c54617..420923972ed6a 100644 --- a/x-pack/plugins/canvas/storybook/storyshots.test.tsx +++ b/x-pack/plugins/canvas/storybook/storyshots.test.tsx @@ -111,6 +111,7 @@ addSerializer(styleSheetSerializer); // Initialize Storyshots and build the Jest Snapshots initStoryshots({ configPath: path.resolve(__dirname, './../storybook'), + framework: 'react', test: multiSnapshotWithOptions({}), // Don't snapshot tests that start with 'redux' storyNameRegex: /^((?!.*?redux).)*$/, diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts index ccc93316482c2..43ad4a9ed9b8b 100644 --- a/x-pack/plugins/data_enhanced/public/plugin.ts +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -40,6 +40,7 @@ export class DataEnhancedPlugin uiSettings: core.uiSettings, startServices: core.getStartServices(), usageCollector: data.search.usageCollector, + session: data.search.session, }); data.__enhance({ diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts index 6e34e4c1964c5..3187b41a2c55f 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts @@ -9,6 +9,7 @@ import { EnhancedSearchInterceptor } from './search_interceptor'; import { CoreSetup, CoreStart } from 'kibana/public'; import { AbortError, UI_SETTINGS } from '../../../../../src/plugins/data/common'; import { SearchTimeoutError } from 'src/plugins/data/public'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; const timeTravel = (msToRun = 0) => { jest.advanceTimersByTime(msToRun); @@ -43,6 +44,7 @@ describe('EnhancedSearchInterceptor', () => { beforeEach(() => { mockCoreSetup = coreMock.createSetup(); mockCoreStart = coreMock.createStart(); + const dataPluginMockStart = dataPluginMock.createStartContract(); mockCoreSetup.uiSettings.get.mockImplementation((name: string) => { switch (name) { @@ -77,6 +79,7 @@ describe('EnhancedSearchInterceptor', () => { http: mockCoreSetup.http, uiSettings: mockCoreSetup.uiSettings, usageCollector: mockUsageCollector, + session: dataPluginMockStart.search.session, }); }); diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts index cca87c85e326c..aee32a7c62759 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -98,7 +98,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { if (id !== undefined) { this.deps.http.delete(`/internal/search/${strategy}/${id}`); } - return throwError(this.handleSearchError(e, request, timeoutSignal, options?.abortSignal)); + return throwError(this.handleSearchError(e, request, timeoutSignal, options)); }), finalize(() => { this.pendingCount$.next(this.pendingCount$.getValue() - 1); diff --git a/x-pack/plugins/infra/public/components/loading_page.tsx b/x-pack/plugins/infra/public/components/loading_page.tsx index ae8e18a2f98ea..87cd1e9aebf6a 100644 --- a/x-pack/plugins/infra/public/components/loading_page.tsx +++ b/x-pack/plugins/infra/public/components/loading_page.tsx @@ -27,10 +27,8 @@ export const LoadingPage = ({ - - - - + + {message} diff --git a/x-pack/plugins/infra/public/components/saved_views/manage_views_flyout.tsx b/x-pack/plugins/infra/public/components/saved_views/manage_views_flyout.tsx index 698034f8154d1..1515175b5115f 100644 --- a/x-pack/plugins/infra/public/components/saved_views/manage_views_flyout.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/manage_views_flyout.tsx @@ -16,6 +16,7 @@ import { EuiInMemoryTable, EuiFlexGroup, EuiButton, + EuiPortal, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -157,34 +158,36 @@ export function SavedViewManageViewsFlyout({ ]; return ( - - - -

- -

-
-
+ + + + +

+ +

+
+
- - - + + + - - - - - -
+ + + + + +
+ ); } diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index ac2c87248ae77..022c62b6bb06b 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -121,24 +121,29 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { ]} />
- - - - - - - - - - {ADD_DATA_LABEL} - + + + + + + + + + + + {ADD_DATA_LABEL} + + diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx index 9cb84c7fff438..7c6e58125b48b 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx @@ -47,15 +47,12 @@ export const BottomDrawer: React.FC<{ style={{ position: 'relative', minWidth: 400, - alignSelf: 'center', height: '16px', }} > {children} - - - + @@ -85,3 +82,7 @@ const BottomActionTopBar = euiStyled(EuiFlexGroup).attrs({ const ShowHideButton = euiStyled(EuiButtonEmpty).attrs({ size: 's' })` width: 140px; `; + +const RightSideSpacer = euiStyled(EuiSpacer).attrs({ size: 'xs' })` + width: 140px; +`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx index 712578be7dffd..b9caef704d071 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx @@ -104,46 +104,57 @@ export const Layout = () => { <> - - - - - - - - - - - - - - - - {({ measureRef, bounds: { height = 0 } }) => ( + {({ measureRef: topActionMeasureRef, bounds: { height: topActionHeight = 0 } }) => ( <> - - - - + + + + + + + + + + + + + + + + + {({ measureRef, bounds: { height = 0 } }) => ( + <> + + + + + + )} + )} diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx index a705a0be3a39e..aa6157dc48d5c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; import React, { useCallback } from 'react'; +import { getBreakpoint } from '@elastic/eui'; import { InventoryItemType } from '../../../../../common/inventory_models/types'; import { euiStyled } from '../../../../../../observability/public'; @@ -35,6 +36,7 @@ interface Props { autoBounds: boolean; formatter: InfraFormatter; bottomMargin: number; + topMargin: number; } export const NodesOverview = ({ @@ -50,6 +52,7 @@ export const NodesOverview = ({ formatter, onDrilldown, bottomMargin, + topMargin, }: Props) => { const handleDrilldown = useCallback( (filter: string) => { @@ -94,6 +97,7 @@ export const NodesOverview = ({ } const dataBounds = calculateBoundsFromNodes(nodes); const bounds = autoBounds ? dataBounds : boundsOverride; + const isStatic = ['xs', 's'].includes(getBreakpoint(window.innerWidth)!); if (view === 'table') { return ( @@ -110,7 +114,7 @@ export const NodesOverview = ({ ); } return ( - + ); @@ -130,10 +135,10 @@ const TableContainer = euiStyled.div` padding: ${(props) => props.theme.eui.paddingSizes.l}; `; -const MapContainer = euiStyled.div` - position: absolute; +const MapContainer = euiStyled.div<{ top: number; positionStatic: boolean }>` + position: ${(props) => (props.positionStatic ? 'static' : 'absolute')}; display: flex; - top: 70px; + top: ${(props) => props.top}px; right: 0; bottom: 0; left: 0; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx index a3b02b858385e..d66fd44feba56 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx @@ -27,7 +27,7 @@ import { EuiIcon } from '@elastic/eui'; import { useUiSetting } from '../../../../../../../../../src/plugins/kibana_react/public'; import { toMetricOpt } from '../../../../../../common/snapshot_metric_i18n'; import { MetricsExplorerAggregation } from '../../../../../../common/http_api'; -import { Color } from '../../../../../../common/color_palette'; +import { colorTransformer, Color } from '../../../../../../common/color_palette'; import { useSourceContext } from '../../../../../containers/source'; import { useTimeline } from '../../hooks/use_timeline'; import { useWaffleOptionsContext } from '../../hooks/use_waffle_options'; @@ -102,11 +102,12 @@ export const Timeline: React.FC = ({ interval, yAxisFormatter, isVisible }, [nodeType, metricsHostsAnomalies, metricsK8sAnomalies]); const metricLabel = toMetricOpt(metric.type)?.textLC; + const metricPopoverLabel = toMetricOpt(metric.type)?.text; const chartMetric = { color: Color.color0, aggregation: 'avg' as MetricsExplorerAggregation, - label: metricLabel, + label: metricPopoverLabel, }; const dateFormatter = useMemo(() => { @@ -225,10 +226,7 @@ export const Timeline: React.FC = ({ interval, yAxisFormatter, isVisible - + @@ -335,11 +333,11 @@ const TimelineLoadingContainer = euiStyled.div` `; const noHistoryDataTitle = i18n.translate('xpack.infra.inventoryTimeline.noHistoryDataTitle', { - defaultMessage: 'There is no history data to display.', + defaultMessage: 'There is no historical data to display.', }); const errorTitle = i18n.translate('xpack.infra.inventoryTimeline.errorTitle', { - defaultMessage: 'Unable to display history data.', + defaultMessage: 'Unable to show historical data.', }); const checkNewDataButtonLabel = i18n.translate( diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx index 449c0a89b4642..6922398e57d70 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { EuiFlexItem } from '@elastic/eui'; +import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { fieldToName } from '../../lib/field_to_display_name'; import { useSourceContext } from '../../../../../containers/source'; import { useWaffleOptionsContext } from '../../hooks/use_waffle_options'; @@ -38,7 +38,7 @@ export const ToolbarWrapper = (props: Props) => { } = useWaffleOptionsContext(); const { createDerivedIndexPattern } = useSourceContext(); return ( - <> + @@ -62,7 +62,7 @@ export const ToolbarWrapper = (props: Props) => { customMetrics, changeCustomMetrics, })} - + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/map.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/map.tsx index 89b1b9b2211d9..6621b110a6dfd 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/map.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/map.tsx @@ -27,6 +27,7 @@ interface Props { bounds: InfraWaffleMapBounds; dataBounds: InfraWaffleMapBounds; bottomMargin: number; + staticHeight: boolean; } export const Map: React.FC = ({ @@ -39,6 +40,7 @@ export const Map: React.FC = ({ nodeType, dataBounds, bottomMargin, + staticHeight, }) => { const sortedNodes = sortNodes(options.sort, nodes); const map = nodesToWaffleMap(sortedNodes); @@ -51,6 +53,7 @@ export const Map: React.FC = ({ ref={(el: any) => measureRef(el)} bottomMargin={bottomMargin} data-test-subj="waffleMap" + staticHeight={staticHeight} > {groupsWithLayout.map((group) => { @@ -92,7 +95,7 @@ export const Map: React.FC = ({ ); }; -const WaffleMapOuterContainer = euiStyled.div<{ bottomMargin: number }>` +const WaffleMapOuterContainer = euiStyled.div<{ bottomMargin: number; staticHeight: boolean }>` flex: 1 0 0%; display: flex; justify-content: flex-start; @@ -100,6 +103,7 @@ const WaffleMapOuterContainer = euiStyled.div<{ bottomMargin: number }>` overflow-x: hidden; overflow-y: auto; margin-bottom: ${(props) => props.bottomMargin}px; + ${(props) => props.staticHeight && 'min-height: 300px;'} `; const WaffleMapInnerContainer = euiStyled.div` diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx index 76756637eb69e..3dbe881cd5dc4 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx @@ -6,7 +6,6 @@ import { EuiButtonGroup, EuiButtonGroupProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; - import React from 'react'; interface Props { diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index 444530c4d79f0..3767144a1b798 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -51,7 +51,7 @@ export const METRICS_FEATURE = { read: ['infrastructure-ui-source', 'index-pattern'], }, alerting: { - all: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], + read: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], }, management: { insightsAndAlerting: ['triggersActions'], @@ -92,7 +92,7 @@ export const LOGS_FEATURE = { catalogue: ['infralogging', 'logs'], api: ['infra'], alerting: { - all: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], + read: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], }, savedObject: { all: [], diff --git a/x-pack/plugins/ingest_manager/common/services/full_agent_policy_kibana_config.test.ts b/x-pack/plugins/ingest_manager/common/services/full_agent_policy_kibana_config.test.ts new file mode 100644 index 0000000000000..07d3f68d7b971 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/services/full_agent_policy_kibana_config.test.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getFullAgentPolicyKibanaConfig } from './full_agent_policy_kibana_config'; + +describe('Fleet - getFullAgentPolicyKibanaConfig', () => { + it('should return no path when there is no path', () => { + expect(getFullAgentPolicyKibanaConfig(['http://localhost:5601'])).toEqual({ + hosts: ['localhost:5601'], + protocol: 'http', + }); + }); + it('should return correct config when there is a path', () => { + expect(getFullAgentPolicyKibanaConfig(['http://localhost:5601/ssg'])).toEqual({ + hosts: ['localhost:5601'], + protocol: 'http', + path: '/ssg/', + }); + }); + it('should return correct config when there is a path that ends in a slash', () => { + expect(getFullAgentPolicyKibanaConfig(['http://localhost:5601/ssg/'])).toEqual({ + hosts: ['localhost:5601'], + protocol: 'http', + path: '/ssg/', + }); + }); + it('should return correct config when there are multiple hosts', () => { + expect( + getFullAgentPolicyKibanaConfig(['http://localhost:5601/ssg/', 'http://localhost:3333/ssg/']) + ).toEqual({ + hosts: ['localhost:5601', 'localhost:3333'], + protocol: 'http', + path: '/ssg/', + }); + }); +}); diff --git a/x-pack/plugins/ingest_manager/common/services/full_agent_policy_kibana_config.ts b/x-pack/plugins/ingest_manager/common/services/full_agent_policy_kibana_config.ts new file mode 100644 index 0000000000000..ae6e34fe82d1d --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/services/full_agent_policy_kibana_config.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FullAgentPolicyKibanaConfig } from '../types'; + +export function getFullAgentPolicyKibanaConfig(kibanaUrls: string[]): FullAgentPolicyKibanaConfig { + // paths and protocol are validated to be the same for all urls, so use the first to get them + const firstUrlParsed = new URL(kibanaUrls[0]); + const config: FullAgentPolicyKibanaConfig = { + // remove the : from http: + protocol: firstUrlParsed.protocol.replace(':', ''), + hosts: kibanaUrls.map((url) => new URL(url).host), + }; + + // add path if user provided one + if (firstUrlParsed.pathname !== '/') { + // make sure the path ends with / + config.path = firstUrlParsed.pathname.endsWith('/') + ? firstUrlParsed.pathname + : `${firstUrlParsed.pathname}/`; + } + return config; +} diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent_policy.ts b/x-pack/plugins/ingest_manager/common/types/models/agent_policy.ts index 8d8344aed6c4c..0232bd766ca53 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent_policy.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent_policy.ts @@ -62,10 +62,7 @@ export interface FullAgentPolicy { }; }; fleet?: { - kibana: { - hosts: string[]; - protocol: string; - }; + kibana: FullAgentPolicyKibanaConfig; }; inputs: FullAgentPolicyInput[]; revision?: number; @@ -78,3 +75,9 @@ export interface FullAgentPolicy { }; }; } + +export interface FullAgentPolicyKibanaConfig { + hosts: string[]; + protocol: string; + path?: string; +} diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index e13c023d0d11a..a8b986be048ae 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -30,8 +30,8 @@ export const config: PluginConfigDescriptor = { ], schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), - registryUrl: schema.maybe(schema.uri()), - registryProxyUrl: schema.maybe(schema.uri()), + registryUrl: schema.maybe(schema.uri({ scheme: ['http', 'https'] })), + registryProxyUrl: schema.maybe(schema.uri({ scheme: ['http', 'https'] })), agents: schema.object({ enabled: schema.boolean({ defaultValue: true }), tlsCheckDisabled: schema.boolean({ defaultValue: false }), diff --git a/x-pack/plugins/ingest_manager/server/services/agent_policy.ts b/x-pack/plugins/ingest_manager/server/services/agent_policy.ts index 3a28286617111..75c16df483a76 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_policy.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_policy.ts @@ -33,6 +33,7 @@ import { outputService } from './output'; import { agentPolicyUpdateEventHandler } from './agent_policy_update'; import { getSettings } from './settings'; import { normalizeKuery, escapeSearchQueryPhrase } from './saved_object'; +import { getFullAgentPolicyKibanaConfig } from '../../common/services/full_agent_policy_kibana_config'; const SAVED_OBJECT_TYPE = AGENT_POLICY_SAVED_OBJECT_TYPE; @@ -128,25 +129,24 @@ class AgentPolicyService { public async requireUniqueName( soClient: SavedObjectsClientContract, - { name, namespace }: Pick + givenPolicy: { id?: string; name: string } ) { const results = await soClient.find({ type: SAVED_OBJECT_TYPE, - searchFields: ['namespace', 'name'], - search: `${namespace} + ${escapeSearchQueryPhrase(name)}`, + searchFields: ['name'], + search: escapeSearchQueryPhrase(givenPolicy.name), }); - - if (results.total) { - const policies = results.saved_objects; - const isSinglePolicy = policies.length === 1; - const policyList = isSinglePolicy ? policies[0].id : policies.map(({ id }) => id).join(','); - const existClause = isSinglePolicy - ? `Agent Policy '${policyList}' already exists` - : `Agent Policies '${policyList}' already exist`; - - throw new AgentPolicyNameExistsError( - `${existClause} in '${namespace}' namespace with name '${name}'` - ); + const idsWithName = results.total && results.saved_objects.map(({ id }) => id); + if (Array.isArray(idsWithName)) { + const isEditingSelf = givenPolicy.id && idsWithName.includes(givenPolicy.id); + if (!givenPolicy.id || !isEditingSelf) { + const isSinglePolicy = idsWithName.length === 1; + const existClause = isSinglePolicy + ? `Agent Policy '${idsWithName[0]}' already exists` + : `Agent Policies '${idsWithName.join(',')}' already exist`; + + throw new AgentPolicyNameExistsError(`${existClause} with name '${givenPolicy.name}'`); + } } } @@ -235,10 +235,10 @@ class AgentPolicyService { agentPolicy: Partial, options?: { user?: AuthenticatedUser } ): Promise { - if (agentPolicy.name && agentPolicy.namespace) { + if (agentPolicy.name) { await this.requireUniqueName(soClient, { + id, name: agentPolicy.name, - namespace: agentPolicy.namespace, }); } return this._update(soClient, id, agentPolicy, options?.user); @@ -538,18 +538,11 @@ class AgentPolicyService { } if (!settings.kibana_urls || !settings.kibana_urls.length) throw new Error('kibana_urls is missing'); - const hostsWithoutProtocol = settings.kibana_urls.map((url) => { - const parsedURL = new URL(url); - return `${parsedURL.host}${parsedURL.pathname !== '/' ? parsedURL.pathname : ''}`; - }); + fullAgentPolicy.fleet = { - kibana: { - protocol: new URL(settings.kibana_urls[0]).protocol.replace(':', ''), - hosts: hostsWithoutProtocol, - }, + kibana: getFullAgentPolicyKibanaConfig(settings.kibana_urls), }; } - return fullAgentPolicy; } } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts index ff9a7871a7db8..efc25cc2efb5d 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts @@ -28,12 +28,14 @@ const getDefaultRegistryUrl = (): string => { } }; -// Custom registry URL is currently only for internal Elastic development and is unsupported export const getRegistryUrl = (): string => { const customUrl = appContextService.getConfig()?.registryUrl; const isEnterprise = licenseService.isEnterprise(); if (customUrl && isEnterprise) { + appContextService + .getLogger() + .info('Custom registry url is an experimental feature and is unsupported.'); return customUrl; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx index 16b861ae034fa..96f4120e3df78 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx @@ -25,7 +25,12 @@ import { keys } from '@elastic/eui'; import { IFieldFormat } from '../../../../../../../../src/plugins/data/common'; import { RangeTypeLens, isValidRange, isValidNumber } from './ranges'; import { FROM_PLACEHOLDER, TO_PLACEHOLDER, TYPING_DEBOUNCE_TIME } from './constants'; -import { NewBucketButton, DragDropBuckets, DraggableBucketContainer } from '../shared_components'; +import { + NewBucketButton, + DragDropBuckets, + DraggableBucketContainer, + LabelInput, +} from '../shared_components'; const generateId = htmlIdGenerator(); @@ -63,7 +68,7 @@ export const RangePopover = ({ // send the range back to the main state setRange(newRange); }; - const { from, to } = tempRange; + const { from, to, label } = tempRange; const lteAppendLabel = i18n.translate('xpack.lens.indexPattern.ranges.lessThanOrEqualAppend', { defaultMessage: '\u2264', @@ -159,6 +164,25 @@ export const RangePopover = ({ + + { + const newRange = { + ...tempRange, + label: newLabel, + }; + setTempRange(newRange); + saveRangeAndReset(newRange); + }} + placeholder={i18n.translate( + 'xpack.lens.indexPattern.ranges.customRangeLabelPlaceholder', + { defaultMessage: 'Custom label' } + )} + onSubmit={onSubmit} + dataTestSubj="indexPattern-ranges-label" + /> + ); }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx index fb6cf6df8573f..5317ee913fcdd 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx @@ -23,6 +23,7 @@ import { } from './constants'; import { RangePopover } from './advanced_editor'; import { DragDropBuckets } from '../shared_components'; +import { EuiFieldText } from '@elastic/eui'; const dataPluginMockValue = dataPluginMock.createStartContract(); // need to overwrite the formatter field first @@ -152,6 +153,25 @@ describe('ranges', () => { }) ); }); + + it('should include custom labels', () => { + setToRangeMode(); + (state.layers.first.columns.col1 as RangeIndexPatternColumn).params.ranges = [ + { from: 0, to: 100, label: 'customlabel' }, + ]; + + const esAggsConfig = rangeOperation.toEsAggsConfig( + state.layers.first.columns.col1 as RangeIndexPatternColumn, + 'col1', + {} as IndexPattern + ); + + expect((esAggsConfig as { params: unknown }).params).toEqual( + expect.objectContaining({ + ranges: [{ from: 0, to: 100, label: 'customlabel' }], + }) + ); + }); }); describe('getPossibleOperationForField', () => { @@ -419,6 +439,63 @@ describe('ranges', () => { }); }); + it('should add a new range with custom label', () => { + const setStateSpy = jest.fn(); + + const instance = mount( + + ); + + // This series of act clojures are made to make it work properly the update flush + act(() => { + instance.find(EuiButtonEmpty).prop('onClick')!({} as ReactMouseEvent); + }); + + act(() => { + // need another wrapping for this in order to work + instance.update(); + + expect(instance.find(RangePopover)).toHaveLength(2); + + // edit the label and check + instance.find(RangePopover).find(EuiFieldText).first().prop('onChange')!({ + target: { + value: 'customlabel', + }, + } as React.ChangeEvent); + jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4); + + expect(setStateSpy).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + ...state.layers.first.columns.col1, + params: { + ...state.layers.first.columns.col1.params, + ranges: [ + { from: 0, to: DEFAULT_INTERVAL, label: '' }, + { from: DEFAULT_INTERVAL, to: Infinity, label: 'customlabel' }, + ], + }, + }, + }, + }, + }, + }); + }); + }); + it('should open a popover to edit an existing range', () => { const setStateSpy = jest.fn(); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx index a8304456262eb..a256f5e4ecfa1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx @@ -61,9 +61,9 @@ function getEsAggsParams({ sourceField, params }: RangeIndexPatternColumn) { field: sourceField, ranges: params.ranges.filter(isValidRange).map>((range) => { if (isFullRange(range)) { - return { from: range.from, to: range.to }; + return range; } - const partialRange: Partial = {}; + const partialRange: Partial = { label: range.label }; // be careful with the fields to set on partial ranges if (isValidNumber(range.from)) { partialRange.from = range.from; diff --git a/x-pack/plugins/lists/server/scripts/lists/files/hosts.txt b/x-pack/plugins/lists/server/scripts/lists/files/hosts.txt index aee32e3a4bd92..5a8a1f4123910 100644 --- a/x-pack/plugins/lists/server/scripts/lists/files/hosts.txt +++ b/x-pack/plugins/lists/server/scripts/lists/files/hosts.txt @@ -1,2 +1,3 @@ -kibana +siem-kibana +siem-windows rock01 diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.js b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.js index be214e3b01e67..813d01ff90861 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.js +++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.js @@ -175,6 +175,16 @@ export function convertESShapeToGeojsonGeometry(value) { geoJson.type = GEO_JSON_TYPE.GEOMETRY_COLLECTION; break; case 'envelope': + // format defined here https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-shape.html#_envelope + const polygon = formatEnvelopeAsPolygon({ + minLon: geoJson.coordinates[0][0], + maxLon: geoJson.coordinates[1][0], + minLat: geoJson.coordinates[1][1], + maxLat: geoJson.coordinates[0][1], + }); + geoJson.type = polygon.type; + geoJson.coordinates = polygon.coordinates; + break; case 'circle': const errorMessage = i18n.translate( 'xpack.maps.es_geo_utils.convert.unsupportedGeometryTypeErrorMessage', diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js index a8d5d650740cd..ccab57dd18339 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js +++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js @@ -250,6 +250,30 @@ describe('geoShapeToGeometry', () => { expect(shapes[0].coordinates).toEqual(coordinates); }); + it('Should convert envelope to geojson', () => { + const coordinates = [ + [100.0, 1.0], + [101.0, 0.0], + ]; + const value = { + type: 'envelope', + coordinates: coordinates, + }; + const shapes = []; + geoShapeToGeometry(value, shapes); + expect(shapes.length).toBe(1); + expect(shapes[0].type).toBe('Polygon'); + expect(shapes[0].coordinates).toEqual([ + [ + [100, 1], + [100, 0], + [101, 0], + [101, 1], + [100, 1], + ], + ]); + }); + it('Should convert array of values', () => { const linestringCoordinates = [ [-77.03653, 38.897676], diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index 42f056b890828..0d208dc0b1b28 100644 --- a/x-pack/plugins/ml/common/types/capabilities.ts +++ b/x-pack/plugins/ml/common/types/capabilities.ts @@ -9,6 +9,7 @@ import { PLUGIN_ID } from '../constants/app'; export const apmUserMlCapabilities = { canGetJobs: false, + canAccessML: false, }; export const userMlCapabilities = { diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts index a33b2e6b3e2d6..f88694a1952b2 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -208,14 +208,24 @@ export const useRenderCellValue = ( return results[cId.replace(`${resultsField}.`, '')]; } - return tableItems.hasOwnProperty(adjustedRowIndex) - ? getNestedProperty(tableItems[adjustedRowIndex], cId, null) - : null; + if (tableItems.hasOwnProperty(adjustedRowIndex)) { + const item = tableItems[adjustedRowIndex]; + + // Try if the field name is available as is. + if (item.hasOwnProperty(cId)) { + return item[cId]; + } + + // Try if the field name is available as a nested field. + return getNestedProperty(tableItems[adjustedRowIndex], cId, null); + } + + return null; } const cellValue = getCellValue(columnId); - // React by default doesn't all us to use a hook in a callback. + // React by default doesn't allow us to use a hook in a callback. // However, this one will be passed on to EuiDataGrid and its docs // recommend wrapping `setCellProps` in a `useEffect()` hook // so we're ignoring the linting rule here. diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx index e3ab0abc18e71..53065c624543d 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx @@ -25,6 +25,7 @@ import React, { useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import euiVars from '@elastic/eui/dist/eui_theme_light.json'; import { DecisionPathPlotData } from './use_classification_path_data'; +import { formatSingleValue } from '../../../formatters/format_value'; const { euiColorFullShade, euiColorMediumShade } = euiVars; const axisColor = euiColorMediumShade; @@ -79,7 +80,6 @@ interface DecisionPathChartProps { const DECISION_PATH_MARGIN = 125; const DECISION_PATH_ROW_HEIGHT = 10; -const NUM_PRECISION = 3; const AnnotationBaselineMarker = ; export const DecisionPathChart = ({ @@ -95,7 +95,7 @@ export const DecisionPathChart = ({ () => [ { dataValue: baseline, - header: baseline ? baseline.toPrecision(NUM_PRECISION) : '', + header: baseline ? formatSingleValue(baseline).toString() : '', details: i18n.translate( 'xpack.ml.dataframe.analytics.explorationResults.decisionPathBaselineText', { @@ -110,7 +110,7 @@ export const DecisionPathChart = ({ // if regression, guarantee up to num_precision significant digits without having it in scientific notation // if classification, hide the numeric values since we only want to show the path const tickFormatter = useCallback( - (d) => (showValues === false ? '' : Number(d.toPrecision(NUM_PRECISION)).toString()), + (d) => (showValues === false ? '' : formatSingleValue(d).toString()), [] ); diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx index b97ddb2690982..08b2d48a982d6 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiDataGridSorting, EuiDataGridColumn } from '@elastic/eui'; @@ -93,10 +93,8 @@ export const useDataGrid = ( [columns] ); - return { - chartsVisible, - chartsButtonVisible: true, - columnsWithCharts: columns.map((c, index) => { + const columnsWithCharts = useMemo(() => { + const updatedColumns = columns.map((c, index) => { const chartData = columnCharts.find((cd) => cd.id === c.id); return { @@ -110,7 +108,32 @@ export const useDataGrid = ( /> ) : undefined, }; - }), + }); + + // Sort the columns to be in line with the current order of visible columns. + // EuiDataGrid misses a callback for the order of all available columns, so + // this only can retain the order of visible columns. + return updatedColumns.sort((a, b) => { + // This will always move visible columns above invisible ones. + if (visibleColumns.indexOf(a.id) === -1 && visibleColumns.indexOf(b.id) > -1) { + return 1; + } + if (visibleColumns.indexOf(b.id) === -1 && visibleColumns.indexOf(a.id) > -1) { + return -1; + } + if (visibleColumns.indexOf(a.id) === -1 && visibleColumns.indexOf(b.id) === -1) { + return a.id.localeCompare(b.id); + } + + // If both columns are visible sort by their visible sorting order. + return visibleColumns.indexOf(a.id) - visibleColumns.indexOf(b.id); + }); + }, [columns, columnCharts, chartsVisible, JSON.stringify(visibleColumns)]); + + return { + chartsVisible, + chartsButtonVisible: true, + columnsWithCharts, errorMessage, invalidSortingColumnns, noDataMessage, diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx b/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx index d25d2c3a858ed..76e62160ca8cd 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx +++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx @@ -165,7 +165,11 @@ export function JobSelector({ dateFormatTz, singleSelection, timeseriesOnly }: J function renderFlyout() { if (isFlyoutVisible) { return ( - + = ({ } return ( - - {(resizeRef) => ( - { - flyoutEl.current = e; - resizeRef(e); - }} - aria-labelledby="jobSelectorFlyout" - data-test-subj="mlFlyoutJobSelector" - > - - -

- {i18n.translate('xpack.ml.jobSelector.flyoutTitle', { - defaultMessage: 'Job selection', - })} -

-
-
- - {isLoading ? ( - - ) : ( - <> - - - - setShowAllBadges(!showAllBadges)} - showAllBadges={showAllBadges} - /> - - - - - - {!singleSelection && newSelection.length > 0 && ( - - {i18n.translate('xpack.ml.jobSelector.clearAllFlyoutButton', { - defaultMessage: 'Clear all', - })} - - )} - - {withTimeRangeSelector && ( + <> + + +

+ {i18n.translate('xpack.ml.jobSelector.flyoutTitle', { + defaultMessage: 'Job selection', + })} +

+
+
+ + + + {(resizeRef) => ( +
{ + flyoutEl.current = e; + resizeRef(e); + }} + > + {isLoading ? ( + + ) : ( + <> + + + + setShowAllBadges(!showAllBadges)} + showAllBadges={showAllBadges} + /> + + + + - + {!singleSelection && newSelection.length > 0 && ( + + {i18n.translate('xpack.ml.jobSelector.clearAllFlyoutButton', { + defaultMessage: 'Clear all', + })} + + )} - )} - - - - - - )} - - - - - - {i18n.translate('xpack.ml.jobSelector.applyFlyoutButton', { - defaultMessage: 'Apply', - })} - - - - - {i18n.translate('xpack.ml.jobSelector.closeFlyoutButton', { - defaultMessage: 'Close', - })} - - - - + {withTimeRangeSelector && ( + + + + )} + + + + + + )} +
+ )} +
+
+ + + + + + {i18n.translate('xpack.ml.jobSelector.applyFlyoutButton', { + defaultMessage: 'Apply', + })} + + + + + {i18n.translate('xpack.ml.jobSelector.closeFlyoutButton', { + defaultMessage: 'Close', + })} + + - )} -
+ + ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx index 06bcdfd364d66..c837fcbacdd55 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx @@ -149,11 +149,11 @@ export const ExplorationQueryBar: FC = ({ placeholder={ searchInput.language === SEARCH_QUERY_LANGUAGE.KUERY ? i18n.translate('xpack.ml.stepDefineForm.queryPlaceholderKql', { - defaultMessage: 'e.g. {example}', + defaultMessage: 'Search for e.g. {example}', values: { example: 'method : "GET" or status : "404"' }, }) : i18n.translate('xpack.ml.stepDefineForm.queryPlaceholderLucene', { - defaultMessage: 'e.g. {example}', + defaultMessage: 'Search for e.g. {example}', values: { example: 'method:GET OR status:404' }, }) } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts index 3746fa12bdc1e..d1889a8acb990 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts @@ -17,7 +17,14 @@ export const getFeatureCount = (resultsField: string, tableItems: DataGridItem[] return 0; } - return Object.keys(tableItems[0]).filter((key) => - key.includes(`${resultsField}.${FEATURE_INFLUENCE}.`) - ).length; + const fullItem = tableItems[0]; + + if ( + fullItem[resultsField] !== undefined && + Array.isArray(fullItem[resultsField][FEATURE_INFLUENCE]) + ) { + return fullItem[resultsField][FEATURE_INFLUENCE].length; + } + + return 0; }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts index eded8e82a7919..88aa06808e8a7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts @@ -52,17 +52,21 @@ export const useOutlierData = ( const needsDestIndexFields = indexPattern !== undefined && indexPattern.title === jobConfig?.source.index[0]; - const columns: EuiDataGridColumn[] = []; - - if (jobConfig !== undefined && indexPattern !== undefined) { - const resultsField = jobConfig.dest.results_field; - const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields); - columns.push( - ...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField).sort((a: any, b: any) => - sortExplorationResultsFields(a.id, b.id, jobConfig) - ) - ); - } + const columns = useMemo(() => { + const newColumns: EuiDataGridColumn[] = []; + + if (jobConfig !== undefined && indexPattern !== undefined) { + const resultsField = jobConfig.dest.results_field; + const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields); + newColumns.push( + ...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField).sort((a: any, b: any) => + sortExplorationResultsFields(a.id, b.id, jobConfig) + ) + ); + } + + return newColumns; + }, [jobConfig, indexPattern]); const dataGrid = useDataGrid( columns, @@ -124,7 +128,10 @@ export const useOutlierData = ( }, [ dataGrid.chartsVisible, jobConfig?.dest.index, - JSON.stringify([searchQuery, dataGrid.visibleColumns]), + // Only trigger when search or the visible columns changes. + // We're only interested in the visible columns but not their order, that's + // why we sort for comparison (and copying it via spread to avoid sort in place). + JSON.stringify([searchQuery, [...dataGrid.visibleColumns].sort()]), ]); const colorRange = useColorRange( diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index 9d6d6c14ed659..9c7d0f6fe78e2 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -213,7 +213,8 @@ export const SwimlaneContainer: FC = ({ // Persists container height during loading to prevent page from jumping return isLoading ? containerHeightRef.current - : rowsCount * CELL_HEIGHT + LEGEND_HEIGHT + (showTimeline ? Y_AXIS_HEIGHT : 0); + : // TODO update when elastic charts X label will be fixed + rowsCount * CELL_HEIGHT + LEGEND_HEIGHT + (true ? Y_AXIS_HEIGHT : 0); }, [isLoading, rowsCount, showTimeline]); useEffect(() => { @@ -282,7 +283,7 @@ export const SwimlaneContainer: FC = ({ }, }, xAxisLabel: { - visible: showTimeline, + visible: true, // eui color subdued fill: `#98A2B3`, formatter: (v: number) => { diff --git a/x-pack/plugins/ml/public/application/formatters/format_value.ts b/x-pack/plugins/ml/public/application/formatters/format_value.ts index 1a696d6e01dde..36425c65374bd 100644 --- a/x-pack/plugins/ml/public/application/formatters/format_value.ts +++ b/x-pack/plugins/ml/public/application/formatters/format_value.ts @@ -53,9 +53,9 @@ export function formatValue( // For time_of_day or time_of_week functions the anomaly record // containing the timestamp of the anomaly should be supplied in // order to correctly format the day or week offset to the time of the anomaly. -function formatSingleValue( +export function formatSingleValue( value: number, - mlFunction: string, + mlFunction?: string, fieldFormat?: any, record?: AnomalyRecordDoc ) { diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx index 8e591d8bdbcb2..3f58449f81a9a 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx @@ -94,8 +94,9 @@ export async function resolveAnomalySwimlaneUserInput( ), { - 'data-test-subj': 'mlAnomalySwimlaneEmbeddable', + 'data-test-subj': 'mlFlyoutJobSelector', ownFocus: true, + closeButtonAriaLabel: 'jobSelectorFlyout', } ); diff --git a/x-pack/plugins/ml/readme.md b/x-pack/plugins/ml/readme.md new file mode 100644 index 0000000000000..0e50867e57ad6 --- /dev/null +++ b/x-pack/plugins/ml/readme.md @@ -0,0 +1,151 @@ +# Documentation for ML UI developers + +This plugin provides access to the machine learning features provided by +Elastic. + +## Requirements + +To use machine learning features, you must have a Platinum or Enterprise license +or a free 14-day trial. File Data Visualizer requires a Basic license. For more +info, refer to +[Set up machine learning features](https://www.elastic.co/guide/en/machine-learning/master/setup.html). + +## Setup local environment + +### Kibana + +1. Fork and clone the [Kibana repo](https://github.com/elastic/kibana). + +1. Install `nvm`, `node`, `yarn` (for example, by using Homebrew). See + [Install dependencies](https://www.elastic.co/guide/en/kibana/master/development-getting-started.html#_install_dependencies). + +1. Make sure that Elasticsearch is deployed and running on localhost:9200. + +1. Navigate to the directory of the `kibana` repository on your machine. + +1. Fetch the latest changes from the repository. + +1. Checkout the branch of the version you want to use. For example, if you want + to use a 7.9 version, run `git checkout 7.9`. + +1. Run `nvm use`. The response shows the Node version that the environment uses. + If you need to update your Node version, the response message contains the + command you need to run to do it. + +1. Run `yarn kbn bootstrap`. It takes all the dependencies in the code and + installs/checks them. It is recommended to use it every time when you switch + between branches. + +1. Make a copy of `kibana.yml` and save as `kibana.dev.yml`. (Git will not track + the changes in `kibana.dev.yml` but yarn will use it.) + +1. Provide the appropriate password and user name in `kibana.dev.yml`. + +1. Run `yarn start` to start Kibana. + +1. Go to http://localhost:560x/xxx (check the terminal message for the exact + path). + +For more details, refer to this [getting started](https://www.elastic.co/guide/en/kibana/master/development-getting-started.html) page. + +### Adding sample data to Kibana + +Kibana has sample data sets that you can add to your setup so that you can test +different configurations on sample data. + +1. Click the Elastic logo in the upper left hand corner of your browser to + navigate to the Kibana home page. + +1. Click *Load a data set and a Kibana dashboard*. + +1. Pick a data set or feel free to click *Add* on all of the available sample + data sets. + +These data sets are now ready be analyzed in ML jobs in Kibana. + + +## Running tests + +### Jest tests + +Run the test following jest tests from `kibana/x-pack`. + +New snapshots, all plugins: + +``` +node scripts/jest +``` + +Update snapshots for the ML plugin: + +``` +node scripts/jest plugins/ml -u +``` + +Update snapshots for a specific directory only: + +``` +node scripts/jest plugins/ml/public/application/settings/filter_lists +``` + +Run tests with verbose output: + +``` +node scripts/jest plugins/ml --verbose +``` + +### Functional tests + +Before running the test server, make sure to quit all other instances of +Elasticsearch. + +1. From one terminal, in the x-pack directory, run: + + node scripts/functional_tests_server.js --config test/functional/config.js + + This command starts an Elasticsearch and Kibana instance that the tests will be run against. + +1. In another tab, run the following command to perform API integration tests (from the x-pack directory): + + node scripts/functional_test_runner.js --include-tag mlqa --config test/api_integration/config + + ML API integration tests are located in `x-pack/test/api_integration/apis/ml`. + +1. In another tab, run the following command to perform UI functional tests (from the x-pack directory): + + node scripts/functional_test_runner.js --include-tag mlqa + + ML functional tests are located in `x-pack/test/functional/apps/ml`. + +## Shared functions + + +You can find the ML shared functions in the following files in GitHub: + +``` +https://github.com/elastic/kibana/blob/master/x-pack/plugins/ml/public/shared.ts +``` + +``` +https://github.com/elastic/kibana/blob/master/x-pack/plugins/ml/server/shared.ts +``` + +These functions are shared from the root of the ML plugin, you can import them with an import statement. For example: + +``` +import { MlPluginSetup } from '../../../../ml/server'; +``` + +or + +``` +import { ANOMALY_SEVERITY } from '../../ml/common'; +``` + +Functions are shared from the following directories: + +``` +ml/common +ml/public +ml/server +``` diff --git a/x-pack/plugins/monitoring/kibana.json b/x-pack/plugins/monitoring/kibana.json index 926c5e265b030..a1e28985a352f 100644 --- a/x-pack/plugins/monitoring/kibana.json +++ b/x-pack/plugins/monitoring/kibana.json @@ -9,14 +9,21 @@ "data", "navigation", "kibanaLegacy", + "observability" + ], + "optionalPlugins": [ + "infra", + "telemetryCollectionManager", + "usageCollection", + "home", + "cloud", "triggersActionsUi", "alerts", "actions", "encryptedSavedObjects", - "observability" + "encryptedSavedObjects" ], - "optionalPlugins": ["infra", "telemetryCollectionManager", "usageCollection", "home", "cloud"], "server": true, "ui": true, - "requiredBundles": ["kibanaUtils", "home", "alerts", "kibanaReact", "licenseManagement"] + "requiredBundles": ["kibanaUtils", "home", "alerts", "kibanaReact", "licenseManagement", "triggersActionsUi"] } diff --git a/x-pack/plugins/monitoring/public/components/chart/chart_target.js b/x-pack/plugins/monitoring/public/components/chart/chart_target.js index 31199c5b092f6..9a590d803bb19 100644 --- a/x-pack/plugins/monitoring/public/components/chart/chart_target.js +++ b/x-pack/plugins/monitoring/public/components/chart/chart_target.js @@ -5,8 +5,8 @@ */ import _ from 'lodash'; +import $ from 'jquery'; import React from 'react'; -import $ from '../../lib/jquery_flot'; import { eventBus } from './event_bus'; import { getChartOptions } from './get_chart_options'; diff --git a/x-pack/plugins/monitoring/public/components/sparkline/sparkline_flot_chart.js b/x-pack/plugins/monitoring/public/components/sparkline/sparkline_flot_chart.js index ae19921631eba..b8af713e1692b 100644 --- a/x-pack/plugins/monitoring/public/components/sparkline/sparkline_flot_chart.js +++ b/x-pack/plugins/monitoring/public/components/sparkline/sparkline_flot_chart.js @@ -5,7 +5,7 @@ */ import { last, isFunction, debounce } from 'lodash'; -import $ from '../../lib/jquery_flot'; +import $ from 'jquery'; import { DEBOUNCE_FAST_MS } from '../../../common/constants'; /** diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.colorhelpers.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.colorhelpers.js deleted file mode 100644 index b2f6dc4e433a3..0000000000000 --- a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.colorhelpers.js +++ /dev/null @@ -1,180 +0,0 @@ -/* Plugin for jQuery for working with colors. - * - * Version 1.1. - * - * Inspiration from jQuery color animation plugin by John Resig. - * - * Released under the MIT license by Ole Laursen, October 2009. - * - * Examples: - * - * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() - * var c = $.color.extract($("#mydiv"), 'background-color'); - * console.log(c.r, c.g, c.b, c.a); - * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" - * - * Note that .scale() and .add() return the same modified object - * instead of making a new one. - * - * V. 1.1: Fix error handling so e.g. parsing an empty string does - * produce a color rather than just crashing. - */ - -(function($) { - $.color = {}; - - // construct color object with some convenient chainable helpers - $.color.make = function (r, g, b, a) { - var o = {}; - o.r = r || 0; - o.g = g || 0; - o.b = b || 0; - o.a = a != null ? a : 1; - - o.add = function (c, d) { - for (var i = 0; i < c.length; ++i) - o[c.charAt(i)] += d; - return o.normalize(); - }; - - o.scale = function (c, f) { - for (var i = 0; i < c.length; ++i) - o[c.charAt(i)] *= f; - return o.normalize(); - }; - - o.toString = function () { - if (o.a >= 1.0) { - return "rgb("+[o.r, o.g, o.b].join(",")+")"; - } else { - return "rgba("+[o.r, o.g, o.b, o.a].join(",")+")"; - } - }; - - o.normalize = function () { - function clamp(min, value, max) { - return value < min ? min: (value > max ? max: value); - } - - o.r = clamp(0, parseInt(o.r), 255); - o.g = clamp(0, parseInt(o.g), 255); - o.b = clamp(0, parseInt(o.b), 255); - o.a = clamp(0, o.a, 1); - return o; - }; - - o.clone = function () { - return $.color.make(o.r, o.b, o.g, o.a); - }; - - return o.normalize(); - } - - // extract CSS color property from element, going up in the DOM - // if it's "transparent" - $.color.extract = function (elem, css) { - var c; - - do { - c = elem.css(css).toLowerCase(); - // keep going until we find an element that has color, or - // we hit the body or root (have no parent) - if (c != '' && c != 'transparent') - break; - elem = elem.parent(); - } while (elem.length && !$.nodeName(elem.get(0), "body")); - - // catch Safari's way of signalling transparent - if (c == "rgba(0, 0, 0, 0)") - c = "transparent"; - - return $.color.parse(c); - } - - // parse CSS color string (like "rgb(10, 32, 43)" or "#fff"), - // returns color object, if parsing failed, you get black (0, 0, - // 0) out - $.color.parse = function (str) { - var res, m = $.color.make; - - // Look for rgb(num,num,num) - if (res = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str)) - return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10)); - - // Look for rgba(num,num,num,num) - if (res = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)) - return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10), parseFloat(res[4])); - - // Look for rgb(num%,num%,num%) - if (res = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str)) - return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55); - - // Look for rgba(num%,num%,num%,num) - if (res = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)) - return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55, parseFloat(res[4])); - - // Look for #a0b1c2 - if (res = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str)) - return m(parseInt(res[1], 16), parseInt(res[2], 16), parseInt(res[3], 16)); - - // Look for #fff - if (res = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str)) - return m(parseInt(res[1]+res[1], 16), parseInt(res[2]+res[2], 16), parseInt(res[3]+res[3], 16)); - - // Otherwise, we're most likely dealing with a named color - var name = $.trim(str).toLowerCase(); - if (name == "transparent") - return m(255, 255, 255, 0); - else { - // default to black - res = lookupColors[name] || [0, 0, 0]; - return m(res[0], res[1], res[2]); - } - } - - var lookupColors = { - aqua:[0,255,255], - azure:[240,255,255], - beige:[245,245,220], - black:[0,0,0], - blue:[0,0,255], - brown:[165,42,42], - cyan:[0,255,255], - darkblue:[0,0,139], - darkcyan:[0,139,139], - darkgrey:[169,169,169], - darkgreen:[0,100,0], - darkkhaki:[189,183,107], - darkmagenta:[139,0,139], - darkolivegreen:[85,107,47], - darkorange:[255,140,0], - darkorchid:[153,50,204], - darkred:[139,0,0], - darksalmon:[233,150,122], - darkviolet:[148,0,211], - fuchsia:[255,0,255], - gold:[255,215,0], - green:[0,128,0], - indigo:[75,0,130], - khaki:[240,230,140], - lightblue:[173,216,230], - lightcyan:[224,255,255], - lightgreen:[144,238,144], - lightgrey:[211,211,211], - lightpink:[255,182,193], - lightyellow:[255,255,224], - lime:[0,255,0], - magenta:[255,0,255], - maroon:[128,0,0], - navy:[0,0,128], - olive:[128,128,0], - orange:[255,165,0], - pink:[255,192,203], - purple:[128,0,128], - violet:[128,0,128], - red:[255,0,0], - silver:[192,192,192], - white:[255,255,255], - yellow:[255,255,0] - }; -})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.canvas.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.canvas.js deleted file mode 100644 index 29328d5812127..0000000000000 --- a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.canvas.js +++ /dev/null @@ -1,345 +0,0 @@ -/* Flot plugin for drawing all elements of a plot on the canvas. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -Flot normally produces certain elements, like axis labels and the legend, using -HTML elements. This permits greater interactivity and customization, and often -looks better, due to cross-browser canvas text inconsistencies and limitations. - -It can also be desirable to render the plot entirely in canvas, particularly -if the goal is to save it as an image, or if Flot is being used in a context -where the HTML DOM does not exist, as is the case within Node.js. This plugin -switches out Flot's standard drawing operations for canvas-only replacements. - -Currently the plugin supports only axis labels, but it will eventually allow -every element of the plot to be rendered directly to canvas. - -The plugin supports these options: - -{ - canvas: boolean -} - -The "canvas" option controls whether full canvas drawing is enabled, making it -possible to toggle on and off. This is useful when a plot uses HTML text in the -browser, but needs to redraw with canvas text when exporting as an image. - -*/ - -(function($) { - - var options = { - canvas: true - }; - - var render, getTextInfo, addText; - - // Cache the prototype hasOwnProperty for faster access - - var hasOwnProperty = Object.prototype.hasOwnProperty; - - function init(plot, classes) { - - var Canvas = classes.Canvas; - - // We only want to replace the functions once; the second time around - // we would just get our new function back. This whole replacing of - // prototype functions is a disaster, and needs to be changed ASAP. - - if (render == null) { - getTextInfo = Canvas.prototype.getTextInfo, - addText = Canvas.prototype.addText, - render = Canvas.prototype.render; - } - - // Finishes rendering the canvas, including overlaid text - - Canvas.prototype.render = function() { - - if (!plot.getOptions().canvas) { - return render.call(this); - } - - var context = this.context, - cache = this._textCache; - - // For each text layer, render elements marked as active - - context.save(); - context.textBaseline = "middle"; - - for (var layerKey in cache) { - if (hasOwnProperty.call(cache, layerKey)) { - var layerCache = cache[layerKey]; - for (var styleKey in layerCache) { - if (hasOwnProperty.call(layerCache, styleKey)) { - var styleCache = layerCache[styleKey], - updateStyles = true; - for (var key in styleCache) { - if (hasOwnProperty.call(styleCache, key)) { - - var info = styleCache[key], - positions = info.positions, - lines = info.lines; - - // Since every element at this level of the cache have the - // same font and fill styles, we can just change them once - // using the values from the first element. - - if (updateStyles) { - context.fillStyle = info.font.color; - context.font = info.font.definition; - updateStyles = false; - } - - for (var i = 0, position; position = positions[i]; i++) { - if (position.active) { - for (var j = 0, line; line = position.lines[j]; j++) { - context.fillText(lines[j].text, line[0], line[1]); - } - } else { - positions.splice(i--, 1); - } - } - - if (positions.length == 0) { - delete styleCache[key]; - } - } - } - } - } - } - } - - context.restore(); - }; - - // Creates (if necessary) and returns a text info object. - // - // When the canvas option is set, the object looks like this: - // - // { - // width: Width of the text's bounding box. - // height: Height of the text's bounding box. - // positions: Array of positions at which this text is drawn. - // lines: [{ - // height: Height of this line. - // widths: Width of this line. - // text: Text on this line. - // }], - // font: { - // definition: Canvas font property string. - // color: Color of the text. - // }, - // } - // - // The positions array contains objects that look like this: - // - // { - // active: Flag indicating whether the text should be visible. - // lines: Array of [x, y] coordinates at which to draw the line. - // x: X coordinate at which to draw the text. - // y: Y coordinate at which to draw the text. - // } - - Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { - - if (!plot.getOptions().canvas) { - return getTextInfo.call(this, layer, text, font, angle, width); - } - - var textStyle, layerCache, styleCache, info; - - // Cast the value to a string, in case we were given a number - - text = "" + text; - - // If the font is a font-spec object, generate a CSS definition - - if (typeof font === "object") { - textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family; - } else { - textStyle = font; - } - - // Retrieve (or create) the cache for the text's layer and styles - - layerCache = this._textCache[layer]; - - if (layerCache == null) { - layerCache = this._textCache[layer] = {}; - } - - styleCache = layerCache[textStyle]; - - if (styleCache == null) { - styleCache = layerCache[textStyle] = {}; - } - - info = styleCache[text]; - - if (info == null) { - - var context = this.context; - - // If the font was provided as CSS, create a div with those - // classes and examine it to generate a canvas font spec. - - if (typeof font !== "object") { - - var element = $("
 
") - .css("position", "absolute") - .addClass(typeof font === "string" ? font : null) - .appendTo(this.getTextLayer(layer)); - - font = { - lineHeight: element.height(), - style: element.css("font-style"), - variant: element.css("font-variant"), - weight: element.css("font-weight"), - family: element.css("font-family"), - color: element.css("color") - }; - - // Setting line-height to 1, without units, sets it equal - // to the font-size, even if the font-size is abstract, - // like 'smaller'. This enables us to read the real size - // via the element's height, working around browsers that - // return the literal 'smaller' value. - - font.size = element.css("line-height", 1).height(); - - element.remove(); - } - - textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family; - - // Create a new info object, initializing the dimensions to - // zero so we can count them up line-by-line. - - info = styleCache[text] = { - width: 0, - height: 0, - positions: [], - lines: [], - font: { - definition: textStyle, - color: font.color - } - }; - - context.save(); - context.font = textStyle; - - // Canvas can't handle multi-line strings; break on various - // newlines, including HTML brs, to build a list of lines. - // Note that we could split directly on regexps, but IE < 9 is - // broken; revisit when we drop IE 7/8 support. - - var lines = (text + "").replace(/
|\r\n|\r/g, "\n").split("\n"); - - for (var i = 0; i < lines.length; ++i) { - - var lineText = lines[i], - measured = context.measureText(lineText); - - info.width = Math.max(measured.width, info.width); - info.height += font.lineHeight; - - info.lines.push({ - text: lineText, - width: measured.width, - height: font.lineHeight - }); - } - - context.restore(); - } - - return info; - }; - - // Adds a text string to the canvas text overlay. - - Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { - - if (!plot.getOptions().canvas) { - return addText.call(this, layer, x, y, text, font, angle, width, halign, valign); - } - - var info = this.getTextInfo(layer, text, font, angle, width), - positions = info.positions, - lines = info.lines; - - // Text is drawn with baseline 'middle', which we need to account - // for by adding half a line's height to the y position. - - y += info.height / lines.length / 2; - - // Tweak the initial y-position to match vertical alignment - - if (valign == "middle") { - y = Math.round(y - info.height / 2); - } else if (valign == "bottom") { - y = Math.round(y - info.height); - } else { - y = Math.round(y); - } - - // FIXME: LEGACY BROWSER FIX - // AFFECTS: Opera < 12.00 - - // Offset the y coordinate, since Opera is off pretty - // consistently compared to the other browsers. - - if (!!(window.opera && window.opera.version().split(".")[0] < 12)) { - y -= 2; - } - - // Determine whether this text already exists at this position. - // If so, mark it for inclusion in the next render pass. - - for (var i = 0, position; position = positions[i]; i++) { - if (position.x == x && position.y == y) { - position.active = true; - return; - } - } - - // If the text doesn't exist at this position, create a new entry - - position = { - active: true, - lines: [], - x: x, - y: y - }; - - positions.push(position); - - // Fill in the x & y positions of each line, adjusting them - // individually for horizontal alignment. - - for (var i = 0, line; line = lines[i]; i++) { - if (halign == "center") { - position.lines.push([Math.round(x - line.width / 2), y]); - } else if (halign == "right") { - position.lines.push([Math.round(x - line.width), y]); - } else { - position.lines.push([Math.round(x), y]); - } - y += line.height; - } - }; - } - - $.plot.plugins.push({ - init: init, - options: options, - name: "canvas", - version: "1.0" - }); - -})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.categories.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.categories.js deleted file mode 100644 index 2f9b257971499..0000000000000 --- a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.categories.js +++ /dev/null @@ -1,190 +0,0 @@ -/* Flot plugin for plotting textual data or categories. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -Consider a dataset like [["February", 34], ["March", 20], ...]. This plugin -allows you to plot such a dataset directly. - -To enable it, you must specify mode: "categories" on the axis with the textual -labels, e.g. - - $.plot("#placeholder", data, { xaxis: { mode: "categories" } }); - -By default, the labels are ordered as they are met in the data series. If you -need a different ordering, you can specify "categories" on the axis options -and list the categories there: - - xaxis: { - mode: "categories", - categories: ["February", "March", "April"] - } - -If you need to customize the distances between the categories, you can specify -"categories" as an object mapping labels to values - - xaxis: { - mode: "categories", - categories: { "February": 1, "March": 3, "April": 4 } - } - -If you don't specify all categories, the remaining categories will be numbered -from the max value plus 1 (with a spacing of 1 between each). - -Internally, the plugin works by transforming the input data through an auto- -generated mapping where the first category becomes 0, the second 1, etc. -Hence, a point like ["February", 34] becomes [0, 34] internally in Flot (this -is visible in hover and click events that return numbers rather than the -category labels). The plugin also overrides the tick generator to spit out the -categories as ticks instead of the values. - -If you need to map a value back to its label, the mapping is always accessible -as "categories" on the axis object, e.g. plot.getAxes().xaxis.categories. - -*/ - -(function ($) { - var options = { - xaxis: { - categories: null - }, - yaxis: { - categories: null - } - }; - - function processRawData(plot, series, data, datapoints) { - // if categories are enabled, we need to disable - // auto-transformation to numbers so the strings are intact - // for later processing - - var xCategories = series.xaxis.options.mode == "categories", - yCategories = series.yaxis.options.mode == "categories"; - - if (!(xCategories || yCategories)) - return; - - var format = datapoints.format; - - if (!format) { - // FIXME: auto-detection should really not be defined here - var s = series; - format = []; - format.push({ x: true, number: true, required: true }); - format.push({ y: true, number: true, required: true }); - - if (s.bars.show || (s.lines.show && s.lines.fill)) { - var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); - format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale }); - if (s.bars.horizontal) { - delete format[format.length - 1].y; - format[format.length - 1].x = true; - } - } - - datapoints.format = format; - } - - for (var m = 0; m < format.length; ++m) { - if (format[m].x && xCategories) - format[m].number = false; - - if (format[m].y && yCategories) - format[m].number = false; - } - } - - function getNextIndex(categories) { - var index = -1; - - for (var v in categories) - if (categories[v] > index) - index = categories[v]; - - return index + 1; - } - - function categoriesTickGenerator(axis) { - var res = []; - for (var label in axis.categories) { - var v = axis.categories[label]; - if (v >= axis.min && v <= axis.max) - res.push([v, label]); - } - - res.sort(function (a, b) { return a[0] - b[0]; }); - - return res; - } - - function setupCategoriesForAxis(series, axis, datapoints) { - if (series[axis].options.mode != "categories") - return; - - if (!series[axis].categories) { - // parse options - var c = {}, o = series[axis].options.categories || {}; - if ($.isArray(o)) { - for (var i = 0; i < o.length; ++i) - c[o[i]] = i; - } - else { - for (var v in o) - c[v] = o[v]; - } - - series[axis].categories = c; - } - - // fix ticks - if (!series[axis].options.ticks) - series[axis].options.ticks = categoriesTickGenerator; - - transformPointsOnAxis(datapoints, axis, series[axis].categories); - } - - function transformPointsOnAxis(datapoints, axis, categories) { - // go through the points, transforming them - var points = datapoints.points, - ps = datapoints.pointsize, - format = datapoints.format, - formatColumn = axis.charAt(0), - index = getNextIndex(categories); - - for (var i = 0; i < points.length; i += ps) { - if (points[i] == null) - continue; - - for (var m = 0; m < ps; ++m) { - var val = points[i + m]; - - if (val == null || !format[m][formatColumn]) - continue; - - if (!(val in categories)) { - categories[val] = index; - ++index; - } - - points[i + m] = categories[val]; - } - } - } - - function processDatapoints(plot, series, datapoints) { - setupCategoriesForAxis(series, "xaxis", datapoints); - setupCategoriesForAxis(series, "yaxis", datapoints); - } - - function init(plot) { - plot.hooks.processRawData.push(processRawData); - plot.hooks.processDatapoints.push(processDatapoints); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'categories', - version: '1.0' - }); -})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.crosshair.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.crosshair.js deleted file mode 100644 index 5111695e3d12c..0000000000000 --- a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.crosshair.js +++ /dev/null @@ -1,176 +0,0 @@ -/* Flot plugin for showing crosshairs when the mouse hovers over the plot. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin supports these options: - - crosshair: { - mode: null or "x" or "y" or "xy" - color: color - lineWidth: number - } - -Set the mode to one of "x", "y" or "xy". The "x" mode enables a vertical -crosshair that lets you trace the values on the x axis, "y" enables a -horizontal crosshair and "xy" enables them both. "color" is the color of the -crosshair (default is "rgba(170, 0, 0, 0.80)"), "lineWidth" is the width of -the drawn lines (default is 1). - -The plugin also adds four public methods: - - - setCrosshair( pos ) - - Set the position of the crosshair. Note that this is cleared if the user - moves the mouse. "pos" is in coordinates of the plot and should be on the - form { x: xpos, y: ypos } (you can use x2/x3/... if you're using multiple - axes), which is coincidentally the same format as what you get from a - "plothover" event. If "pos" is null, the crosshair is cleared. - - - clearCrosshair() - - Clear the crosshair. - - - lockCrosshair(pos) - - Cause the crosshair to lock to the current location, no longer updating if - the user moves the mouse. Optionally supply a position (passed on to - setCrosshair()) to move it to. - - Example usage: - - var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } }; - $("#graph").bind( "plothover", function ( evt, position, item ) { - if ( item ) { - // Lock the crosshair to the data point being hovered - myFlot.lockCrosshair({ - x: item.datapoint[ 0 ], - y: item.datapoint[ 1 ] - }); - } else { - // Return normal crosshair operation - myFlot.unlockCrosshair(); - } - }); - - - unlockCrosshair() - - Free the crosshair to move again after locking it. -*/ - -(function ($) { - var options = { - crosshair: { - mode: null, // one of null, "x", "y" or "xy", - color: "rgba(170, 0, 0, 0.80)", - lineWidth: 1 - } - }; - - function init(plot) { - // position of crosshair in pixels - var crosshair = { x: -1, y: -1, locked: false }; - - plot.setCrosshair = function setCrosshair(pos) { - if (!pos) - crosshair.x = -1; - else { - var o = plot.p2c(pos); - crosshair.x = Math.max(0, Math.min(o.left, plot.width())); - crosshair.y = Math.max(0, Math.min(o.top, plot.height())); - } - - plot.triggerRedrawOverlay(); - }; - - plot.clearCrosshair = plot.setCrosshair; // passes null for pos - - plot.lockCrosshair = function lockCrosshair(pos) { - if (pos) - plot.setCrosshair(pos); - crosshair.locked = true; - }; - - plot.unlockCrosshair = function unlockCrosshair() { - crosshair.locked = false; - }; - - function onMouseOut(e) { - if (crosshair.locked) - return; - - if (crosshair.x != -1) { - crosshair.x = -1; - plot.triggerRedrawOverlay(); - } - } - - function onMouseMove(e) { - if (crosshair.locked) - return; - - if (plot.getSelection && plot.getSelection()) { - crosshair.x = -1; // hide the crosshair while selecting - return; - } - - var offset = plot.offset(); - crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width())); - crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height())); - plot.triggerRedrawOverlay(); - } - - plot.hooks.bindEvents.push(function (plot, eventHolder) { - if (!plot.getOptions().crosshair.mode) - return; - - eventHolder.mouseout(onMouseOut); - eventHolder.mousemove(onMouseMove); - }); - - plot.hooks.drawOverlay.push(function (plot, ctx) { - var c = plot.getOptions().crosshair; - if (!c.mode) - return; - - var plotOffset = plot.getPlotOffset(); - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - if (crosshair.x != -1) { - var adj = plot.getOptions().crosshair.lineWidth % 2 ? 0.5 : 0; - - ctx.strokeStyle = c.color; - ctx.lineWidth = c.lineWidth; - ctx.lineJoin = "round"; - - ctx.beginPath(); - if (c.mode.indexOf("x") != -1) { - var drawX = Math.floor(crosshair.x) + adj; - ctx.moveTo(drawX, 0); - ctx.lineTo(drawX, plot.height()); - } - if (c.mode.indexOf("y") != -1) { - var drawY = Math.floor(crosshair.y) + adj; - ctx.moveTo(0, drawY); - ctx.lineTo(plot.width(), drawY); - } - ctx.stroke(); - } - ctx.restore(); - }); - - plot.hooks.shutdown.push(function (plot, eventHolder) { - eventHolder.unbind("mouseout", onMouseOut); - eventHolder.unbind("mousemove", onMouseMove); - }); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'crosshair', - version: '1.0' - }); -})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.fillbetween.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.fillbetween.js deleted file mode 100644 index 18b15d26db8c9..0000000000000 --- a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.fillbetween.js +++ /dev/null @@ -1,226 +0,0 @@ -/* Flot plugin for computing bottoms for filled line and bar charts. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The case: you've got two series that you want to fill the area between. In Flot -terms, you need to use one as the fill bottom of the other. You can specify the -bottom of each data point as the third coordinate manually, or you can use this -plugin to compute it for you. - -In order to name the other series, you need to give it an id, like this: - - var dataset = [ - { data: [ ... ], id: "foo" } , // use default bottom - { data: [ ... ], fillBetween: "foo" }, // use first dataset as bottom - ]; - - $.plot($("#placeholder"), dataset, { lines: { show: true, fill: true }}); - -As a convenience, if the id given is a number that doesn't appear as an id in -the series, it is interpreted as the index in the array instead (so fillBetween: -0 can also mean the first series). - -Internally, the plugin modifies the datapoints in each series. For line series, -extra data points might be inserted through interpolation. Note that at points -where the bottom line is not defined (due to a null point or start/end of line), -the current line will show a gap too. The algorithm comes from the -jquery.flot.stack.js plugin, possibly some code could be shared. - -*/ - -(function ( $ ) { - - var options = { - series: { - fillBetween: null // or number - } - }; - - function init( plot ) { - - function findBottomSeries( s, allseries ) { - - var i; - - for ( i = 0; i < allseries.length; ++i ) { - if ( allseries[ i ].id === s.fillBetween ) { - return allseries[ i ]; - } - } - - if ( typeof s.fillBetween === "number" ) { - if ( s.fillBetween < 0 || s.fillBetween >= allseries.length ) { - return null; - } - return allseries[ s.fillBetween ]; - } - - return null; - } - - function computeFillBottoms( plot, s, datapoints ) { - - if ( s.fillBetween == null ) { - return; - } - - var other = findBottomSeries( s, plot.getData() ); - - if ( !other ) { - return; - } - - var ps = datapoints.pointsize, - points = datapoints.points, - otherps = other.datapoints.pointsize, - otherpoints = other.datapoints.points, - newpoints = [], - px, py, intery, qx, qy, bottom, - withlines = s.lines.show, - withbottom = ps > 2 && datapoints.format[2].y, - withsteps = withlines && s.lines.steps, - fromgap = true, - i = 0, - j = 0, - l, m; - - while ( true ) { - - if ( i >= points.length ) { - break; - } - - l = newpoints.length; - - if ( points[ i ] == null ) { - - // copy gaps - - for ( m = 0; m < ps; ++m ) { - newpoints.push( points[ i + m ] ); - } - - i += ps; - - } else if ( j >= otherpoints.length ) { - - // for lines, we can't use the rest of the points - - if ( !withlines ) { - for ( m = 0; m < ps; ++m ) { - newpoints.push( points[ i + m ] ); - } - } - - i += ps; - - } else if ( otherpoints[ j ] == null ) { - - // oops, got a gap - - for ( m = 0; m < ps; ++m ) { - newpoints.push( null ); - } - - fromgap = true; - j += otherps; - - } else { - - // cases where we actually got two points - - px = points[ i ]; - py = points[ i + 1 ]; - qx = otherpoints[ j ]; - qy = otherpoints[ j + 1 ]; - bottom = 0; - - if ( px === qx ) { - - for ( m = 0; m < ps; ++m ) { - newpoints.push( points[ i + m ] ); - } - - //newpoints[ l + 1 ] += qy; - bottom = qy; - - i += ps; - j += otherps; - - } else if ( px > qx ) { - - // we got past point below, might need to - // insert interpolated extra point - - if ( withlines && i > 0 && points[ i - ps ] != null ) { - intery = py + ( points[ i - ps + 1 ] - py ) * ( qx - px ) / ( points[ i - ps ] - px ); - newpoints.push( qx ); - newpoints.push( intery ); - for ( m = 2; m < ps; ++m ) { - newpoints.push( points[ i + m ] ); - } - bottom = qy; - } - - j += otherps; - - } else { // px < qx - - // if we come from a gap, we just skip this point - - if ( fromgap && withlines ) { - i += ps; - continue; - } - - for ( m = 0; m < ps; ++m ) { - newpoints.push( points[ i + m ] ); - } - - // we might be able to interpolate a point below, - // this can give us a better y - - if ( withlines && j > 0 && otherpoints[ j - otherps ] != null ) { - bottom = qy + ( otherpoints[ j - otherps + 1 ] - qy ) * ( px - qx ) / ( otherpoints[ j - otherps ] - qx ); - } - - //newpoints[l + 1] += bottom; - - i += ps; - } - - fromgap = false; - - if ( l !== newpoints.length && withbottom ) { - newpoints[ l + 2 ] = bottom; - } - } - - // maintain the line steps invariant - - if ( withsteps && l !== newpoints.length && l > 0 && - newpoints[ l ] !== null && - newpoints[ l ] !== newpoints[ l - ps ] && - newpoints[ l + 1 ] !== newpoints[ l - ps + 1 ] ) { - for (m = 0; m < ps; ++m) { - newpoints[ l + ps + m ] = newpoints[ l + m ]; - } - newpoints[ l + 1 ] = newpoints[ l - ps + 1 ]; - } - } - - datapoints.points = newpoints; - } - - plot.hooks.processDatapoints.push( computeFillBottoms ); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: "fillbetween", - version: "1.0" - }); - -})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.navigate.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.navigate.js deleted file mode 100644 index 13fb7f17d04b2..0000000000000 --- a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.navigate.js +++ /dev/null @@ -1,346 +0,0 @@ -/* Flot plugin for adding the ability to pan and zoom the plot. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The default behaviour is double click and scrollwheel up/down to zoom in, drag -to pan. The plugin defines plot.zoom({ center }), plot.zoomOut() and -plot.pan( offset ) so you easily can add custom controls. It also fires -"plotpan" and "plotzoom" events, useful for synchronizing plots. - -The plugin supports these options: - - zoom: { - interactive: false - trigger: "dblclick" // or "click" for single click - amount: 1.5 // 2 = 200% (zoom in), 0.5 = 50% (zoom out) - } - - pan: { - interactive: false - cursor: "move" // CSS mouse cursor value used when dragging, e.g. "pointer" - frameRate: 20 - } - - xaxis, yaxis, x2axis, y2axis: { - zoomRange: null // or [ number, number ] (min range, max range) or false - panRange: null // or [ number, number ] (min, max) or false - } - -"interactive" enables the built-in drag/click behaviour. If you enable -interactive for pan, then you'll have a basic plot that supports moving -around; the same for zoom. - -"amount" specifies the default amount to zoom in (so 1.5 = 150%) relative to -the current viewport. - -"cursor" is a standard CSS mouse cursor string used for visual feedback to the -user when dragging. - -"frameRate" specifies the maximum number of times per second the plot will -update itself while the user is panning around on it (set to null to disable -intermediate pans, the plot will then not update until the mouse button is -released). - -"zoomRange" is the interval in which zooming can happen, e.g. with zoomRange: -[1, 100] the zoom will never scale the axis so that the difference between min -and max is smaller than 1 or larger than 100. You can set either end to null -to ignore, e.g. [1, null]. If you set zoomRange to false, zooming on that axis -will be disabled. - -"panRange" confines the panning to stay within a range, e.g. with panRange: -[-10, 20] panning stops at -10 in one end and at 20 in the other. Either can -be null, e.g. [-10, null]. If you set panRange to false, panning on that axis -will be disabled. - -Example API usage: - - plot = $.plot(...); - - // zoom default amount in on the pixel ( 10, 20 ) - plot.zoom({ center: { left: 10, top: 20 } }); - - // zoom out again - plot.zoomOut({ center: { left: 10, top: 20 } }); - - // zoom 200% in on the pixel (10, 20) - plot.zoom({ amount: 2, center: { left: 10, top: 20 } }); - - // pan 100 pixels to the left and 20 down - plot.pan({ left: -100, top: 20 }) - -Here, "center" specifies where the center of the zooming should happen. Note -that this is defined in pixel space, not the space of the data points (you can -use the p2c helpers on the axes in Flot to help you convert between these). - -"amount" is the amount to zoom the viewport relative to the current range, so -1 is 100% (i.e. no change), 1.5 is 150% (zoom in), 0.7 is 70% (zoom out). You -can set the default in the options. - -*/ - -// First two dependencies, jquery.event.drag.js and -// jquery.mousewheel.js, we put them inline here to save people the -// effort of downloading them. - -/* -jquery.event.drag.js ~ v1.5 ~ Copyright (c) 2008, Three Dub Media (http://threedubmedia.com) -Licensed under the MIT License ~ http://threedubmedia.googlecode.com/files/MIT-LICENSE.txt -*/ -(function(a){function e(h){var k,j=this,l=h.data||{};if(l.elem)j=h.dragTarget=l.elem,h.dragProxy=d.proxy||j,h.cursorOffsetX=l.pageX-l.left,h.cursorOffsetY=l.pageY-l.top,h.offsetX=h.pageX-h.cursorOffsetX,h.offsetY=h.pageY-h.cursorOffsetY;else if(d.dragging||l.which>0&&h.which!=l.which||a(h.target).is(l.not))return;switch(h.type){case"mousedown":return a.extend(l,a(j).offset(),{elem:j,target:h.target,pageX:h.pageX,pageY:h.pageY}),b.add(document,"mousemove mouseup",e,l),i(j,!1),d.dragging=null,!1;case!d.dragging&&"mousemove":if(g(h.pageX-l.pageX)+g(h.pageY-l.pageY) max) { - // make sure min < max - var tmp = min; - min = max; - max = tmp; - } - - //Check that we are in panRange - if (pr) { - if (pr[0] != null && min < pr[0]) { - min = pr[0]; - } - if (pr[1] != null && max > pr[1]) { - max = pr[1]; - } - } - - var range = max - min; - if (zr && - ((zr[0] != null && range < zr[0] && amount >1) || - (zr[1] != null && range > zr[1] && amount <1))) - return; - - opts.min = min; - opts.max = max; - }); - - plot.setupGrid(); - plot.draw(); - - if (!args.preventEvent) - plot.getPlaceholder().trigger("plotzoom", [ plot, args ]); - }; - - plot.pan = function (args) { - var delta = { - x: +args.left, - y: +args.top - }; - - if (isNaN(delta.x)) - delta.x = 0; - if (isNaN(delta.y)) - delta.y = 0; - - $.each(plot.getAxes(), function (_, axis) { - var opts = axis.options, - min, max, d = delta[axis.direction]; - - min = axis.c2p(axis.p2c(axis.min) + d), - max = axis.c2p(axis.p2c(axis.max) + d); - - var pr = opts.panRange; - if (pr === false) // no panning on this axis - return; - - if (pr) { - // check whether we hit the wall - if (pr[0] != null && pr[0] > min) { - d = pr[0] - min; - min += d; - max += d; - } - - if (pr[1] != null && pr[1] < max) { - d = pr[1] - max; - min += d; - max += d; - } - } - - opts.min = min; - opts.max = max; - }); - - plot.setupGrid(); - plot.draw(); - - if (!args.preventEvent) - plot.getPlaceholder().trigger("plotpan", [ plot, args ]); - }; - - function shutdown(plot, eventHolder) { - eventHolder.unbind(plot.getOptions().zoom.trigger, onZoomClick); - eventHolder.unbind("mousewheel", onMouseWheel); - eventHolder.unbind("dragstart", onDragStart); - eventHolder.unbind("drag", onDrag); - eventHolder.unbind("dragend", onDragEnd); - if (panTimeout) - clearTimeout(panTimeout); - } - - plot.hooks.bindEvents.push(bindEvents); - plot.hooks.shutdown.push(shutdown); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'navigate', - version: '1.3' - }); -})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.pie.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.pie.js deleted file mode 100644 index 24148c0a2e223..0000000000000 --- a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.pie.js +++ /dev/null @@ -1,824 +0,0 @@ -/* Flot plugin for rendering pie charts. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin assumes that each series has a single data value, and that each -value is a positive integer or zero. Negative numbers don't make sense for a -pie chart, and have unpredictable results. The values do NOT need to be -passed in as percentages; the plugin will calculate the total and per-slice -percentages internally. - -* Created by Brian Medendorp - -* Updated with contributions from btburnett3, Anthony Aragues and Xavi Ivars - -The plugin supports these options: - - series: { - pie: { - show: true/false - radius: 0-1 for percentage of fullsize, or a specified pixel length, or 'auto' - innerRadius: 0-1 for percentage of fullsize or a specified pixel length, for creating a donut effect - startAngle: 0-2 factor of PI used for starting angle (in radians) i.e 3/2 starts at the top, 0 and 2 have the same result - tilt: 0-1 for percentage to tilt the pie, where 1 is no tilt, and 0 is completely flat (nothing will show) - offset: { - top: integer value to move the pie up or down - left: integer value to move the pie left or right, or 'auto' - }, - stroke: { - color: any hexadecimal color value (other formats may or may not work, so best to stick with something like '#FFF') - width: integer pixel width of the stroke - }, - label: { - show: true/false, or 'auto' - formatter: a user-defined function that modifies the text/style of the label text - radius: 0-1 for percentage of fullsize, or a specified pixel length - background: { - color: any hexadecimal color value (other formats may or may not work, so best to stick with something like '#000') - opacity: 0-1 - }, - threshold: 0-1 for the percentage value at which to hide labels (if they're too small) - }, - combine: { - threshold: 0-1 for the percentage value at which to combine slices (if they're too small) - color: any hexadecimal color value (other formats may or may not work, so best to stick with something like '#CCC'), if null, the plugin will automatically use the color of the first slice to be combined - label: any text value of what the combined slice should be labeled - } - highlight: { - opacity: 0-1 - } - } - } - -More detail and specific examples can be found in the included HTML file. - -*/ - -import { i18n } from '@kbn/i18n'; - -(function($) { - // Maximum redraw attempts when fitting labels within the plot - - var REDRAW_ATTEMPTS = 10; - - // Factor by which to shrink the pie when fitting labels within the plot - - var REDRAW_SHRINK = 0.95; - - function init(plot) { - - var canvas = null, - target = null, - options = null, - maxRadius = null, - centerLeft = null, - centerTop = null, - processed = false, - ctx = null; - - // interactive variables - - var highlights = []; - - // add hook to determine if pie plugin in enabled, and then perform necessary operations - - plot.hooks.processOptions.push(function(plot, options) { - if (options.series.pie.show) { - - options.grid.show = false; - - // set labels.show - - if (options.series.pie.label.show == "auto") { - if (options.legend.show) { - options.series.pie.label.show = false; - } else { - options.series.pie.label.show = true; - } - } - - // set radius - - if (options.series.pie.radius == "auto") { - if (options.series.pie.label.show) { - options.series.pie.radius = 3/4; - } else { - options.series.pie.radius = 1; - } - } - - // ensure sane tilt - - if (options.series.pie.tilt > 1) { - options.series.pie.tilt = 1; - } else if (options.series.pie.tilt < 0) { - options.series.pie.tilt = 0; - } - } - }); - - plot.hooks.bindEvents.push(function(plot, eventHolder) { - var options = plot.getOptions(); - if (options.series.pie.show) { - if (options.grid.hoverable) { - eventHolder.unbind("mousemove").mousemove(onMouseMove); - } - if (options.grid.clickable) { - eventHolder.unbind("click").click(onClick); - } - } - }); - - plot.hooks.processDatapoints.push(function(plot, series, data, datapoints) { - var options = plot.getOptions(); - if (options.series.pie.show) { - processDatapoints(plot, series, data, datapoints); - } - }); - - plot.hooks.drawOverlay.push(function(plot, octx) { - var options = plot.getOptions(); - if (options.series.pie.show) { - drawOverlay(plot, octx); - } - }); - - plot.hooks.draw.push(function(plot, newCtx) { - var options = plot.getOptions(); - if (options.series.pie.show) { - draw(plot, newCtx); - } - }); - - function processDatapoints(plot, series, datapoints) { - if (!processed) { - processed = true; - canvas = plot.getCanvas(); - target = $(canvas).parent(); - options = plot.getOptions(); - plot.setData(combine(plot.getData())); - } - } - - function combine(data) { - - var total = 0, - combined = 0, - numCombined = 0, - color = options.series.pie.combine.color, - newdata = []; - - // Fix up the raw data from Flot, ensuring the data is numeric - - for (var i = 0; i < data.length; ++i) { - - var value = data[i].data; - - // If the data is an array, we'll assume that it's a standard - // Flot x-y pair, and are concerned only with the second value. - - // Note how we use the original array, rather than creating a - // new one; this is more efficient and preserves any extra data - // that the user may have stored in higher indexes. - - if ($.isArray(value) && value.length == 1) { - value = value[0]; - } - - if ($.isArray(value)) { - // Equivalent to $.isNumeric() but compatible with jQuery < 1.7 - if (!isNaN(parseFloat(value[1])) && isFinite(value[1])) { - value[1] = +value[1]; - } else { - value[1] = 0; - } - } else if (!isNaN(parseFloat(value)) && isFinite(value)) { - value = [1, +value]; - } else { - value = [1, 0]; - } - - data[i].data = [value]; - } - - // Sum up all the slices, so we can calculate percentages for each - - for (var i = 0; i < data.length; ++i) { - total += data[i].data[0][1]; - } - - // Count the number of slices with percentages below the combine - // threshold; if it turns out to be just one, we won't combine. - - for (var i = 0; i < data.length; ++i) { - var value = data[i].data[0][1]; - if (value / total <= options.series.pie.combine.threshold) { - combined += value; - numCombined++; - if (!color) { - color = data[i].color; - } - } - } - - for (var i = 0; i < data.length; ++i) { - var value = data[i].data[0][1]; - if (numCombined < 2 || value / total > options.series.pie.combine.threshold) { - newdata.push( - $.extend(data[i], { /* extend to allow keeping all other original data values - and using them e.g. in labelFormatter. */ - data: [[1, value]], - color: data[i].color, - label: data[i].label, - angle: value * Math.PI * 2 / total, - percent: value / (total / 100) - }) - ); - } - } - - if (numCombined > 1) { - newdata.push({ - data: [[1, combined]], - color: color, - label: options.series.pie.combine.label, - angle: combined * Math.PI * 2 / total, - percent: combined / (total / 100) - }); - } - - return newdata; - } - - function draw(plot, newCtx) { - - if (!target) { - return; // if no series were passed - } - - var canvasWidth = plot.getPlaceholder().width(), - canvasHeight = plot.getPlaceholder().height(), - legendWidth = target.children().filter(".legend").children().width() || 0; - - ctx = newCtx; - - // WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE! - - // When combining smaller slices into an 'other' slice, we need to - // add a new series. Since Flot gives plugins no way to modify the - // list of series, the pie plugin uses a hack where the first call - // to processDatapoints results in a call to setData with the new - // list of series, then subsequent processDatapoints do nothing. - - // The plugin-global 'processed' flag is used to control this hack; - // it starts out false, and is set to true after the first call to - // processDatapoints. - - // Unfortunately this turns future setData calls into no-ops; they - // call processDatapoints, the flag is true, and nothing happens. - - // To fix this we'll set the flag back to false here in draw, when - // all series have been processed, so the next sequence of calls to - // processDatapoints once again starts out with a slice-combine. - // This is really a hack; in 0.9 we need to give plugins a proper - // way to modify series before any processing begins. - - processed = false; - - // calculate maximum radius and center point - - maxRadius = Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2; - centerTop = canvasHeight / 2 + options.series.pie.offset.top; - centerLeft = canvasWidth / 2; - - if (options.series.pie.offset.left == "auto") { - if (options.legend.position.match("w")) { - centerLeft += legendWidth / 2; - } else { - centerLeft -= legendWidth / 2; - } - if (centerLeft < maxRadius) { - centerLeft = maxRadius; - } else if (centerLeft > canvasWidth - maxRadius) { - centerLeft = canvasWidth - maxRadius; - } - } else { - centerLeft += options.series.pie.offset.left; - } - - var slices = plot.getData(), - attempts = 0; - - // Keep shrinking the pie's radius until drawPie returns true, - // indicating that all the labels fit, or we try too many times. - - do { - if (attempts > 0) { - maxRadius *= REDRAW_SHRINK; - } - attempts += 1; - clear(); - if (options.series.pie.tilt <= 0.8) { - drawShadow(); - } - } while (!drawPie() && attempts < REDRAW_ATTEMPTS) - - if (attempts >= REDRAW_ATTEMPTS) { - clear(); - const errorMessage = i18n.translate('xpack.monitoring.pie.unableToDrawLabelsInsideCanvasErrorMessage', { - defaultMessage: 'Could not draw pie with labels contained inside canvas', - }); - target.prepend(`
${errorMessage}
`); - } - - if (plot.setSeries && plot.insertLegend) { - plot.setSeries(slices); - plot.insertLegend(); - } - - // we're actually done at this point, just defining internal functions at this point - - function clear() { - ctx.clearRect(0, 0, canvasWidth, canvasHeight); - target.children().filter(".pieLabel, .pieLabelBackground").remove(); - } - - function drawShadow() { - - var shadowLeft = options.series.pie.shadow.left; - var shadowTop = options.series.pie.shadow.top; - var edge = 10; - var alpha = options.series.pie.shadow.alpha; - var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; - - if (radius >= canvasWidth / 2 - shadowLeft || radius * options.series.pie.tilt >= canvasHeight / 2 - shadowTop || radius <= edge) { - return; // shadow would be outside canvas, so don't draw it - } - - ctx.save(); - ctx.translate(shadowLeft,shadowTop); - ctx.globalAlpha = alpha; - ctx.fillStyle = "#000"; - - // center and rotate to starting position - - ctx.translate(centerLeft,centerTop); - ctx.scale(1, options.series.pie.tilt); - - //radius -= edge; - - for (var i = 1; i <= edge; i++) { - ctx.beginPath(); - ctx.arc(0, 0, radius, 0, Math.PI * 2, false); - ctx.fill(); - radius -= i; - } - - ctx.restore(); - } - - function drawPie() { - - var startAngle = Math.PI * options.series.pie.startAngle; - var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; - - // center and rotate to starting position - - ctx.save(); - ctx.translate(centerLeft,centerTop); - ctx.scale(1, options.series.pie.tilt); - //ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera - - // draw slices - - ctx.save(); - var currentAngle = startAngle; - for (var i = 0; i < slices.length; ++i) { - slices[i].startAngle = currentAngle; - drawSlice(slices[i].angle, slices[i].color, true); - } - ctx.restore(); - - // draw slice outlines - - if (options.series.pie.stroke.width > 0) { - ctx.save(); - ctx.lineWidth = options.series.pie.stroke.width; - currentAngle = startAngle; - for (var i = 0; i < slices.length; ++i) { - drawSlice(slices[i].angle, options.series.pie.stroke.color, false); - } - ctx.restore(); - } - - // draw donut hole - - drawDonutHole(ctx); - - ctx.restore(); - - // Draw the labels, returning true if they fit within the plot - - if (options.series.pie.label.show) { - return drawLabels(); - } else return true; - - function drawSlice(angle, color, fill) { - - if (angle <= 0 || isNaN(angle)) { - return; - } - - if (fill) { - ctx.fillStyle = color; - } else { - ctx.strokeStyle = color; - ctx.lineJoin = "round"; - } - - ctx.beginPath(); - if (Math.abs(angle - Math.PI * 2) > 0.000000001) { - ctx.moveTo(0, 0); // Center of the pie - } - - //ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera - ctx.arc(0, 0, radius,currentAngle, currentAngle + angle / 2, false); - ctx.arc(0, 0, radius,currentAngle + angle / 2, currentAngle + angle, false); - ctx.closePath(); - //ctx.rotate(angle); // This doesn't work properly in Opera - currentAngle += angle; - - if (fill) { - ctx.fill(); - } else { - ctx.stroke(); - } - } - - function drawLabels() { - - var currentAngle = startAngle; - var radius = options.series.pie.label.radius > 1 ? options.series.pie.label.radius : maxRadius * options.series.pie.label.radius; - - for (var i = 0; i < slices.length; ++i) { - if (slices[i].percent >= options.series.pie.label.threshold * 100) { - if (!drawLabel(slices[i], currentAngle, i)) { - return false; - } - } - currentAngle += slices[i].angle; - } - - return true; - - function drawLabel(slice, startAngle, index) { - - if (slice.data[0][1] == 0) { - return true; - } - - // format label text - - var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter; - - if (lf) { - text = lf(slice.label, slice); - } else { - text = slice.label; - } - - if (plf) { - text = plf(text, slice); - } - - var halfAngle = ((startAngle + slice.angle) + startAngle) / 2; - var x = centerLeft + Math.round(Math.cos(halfAngle) * radius); - var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt; - - var html = "" + text + ""; - target.append(html); - - var label = target.children("#pieLabel" + index); - var labelTop = (y - label.height() / 2); - var labelLeft = (x - label.width() / 2); - - label.css("top", labelTop); - label.css("left", labelLeft); - - // check to make sure that the label is not outside the canvas - - if (0 - labelTop > 0 || 0 - labelLeft > 0 || canvasHeight - (labelTop + label.height()) < 0 || canvasWidth - (labelLeft + label.width()) < 0) { - return false; - } - - if (options.series.pie.label.background.opacity != 0) { - - // put in the transparent background separately to avoid blended labels and label boxes - - var c = options.series.pie.label.background.color; - - if (c == null) { - c = slice.color; - } - - var pos = "top:" + labelTop + "px;left:" + labelLeft + "px;"; - $("
") - .css("opacity", options.series.pie.label.background.opacity) - .insertBefore(label); - } - - return true; - } // end individual label function - } // end drawLabels function - } // end drawPie function - } // end draw function - - // Placed here because it needs to be accessed from multiple locations - - function drawDonutHole(layer) { - if (options.series.pie.innerRadius > 0) { - - // subtract the center - - layer.save(); - var innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius; - layer.globalCompositeOperation = "destination-out"; // this does not work with excanvas, but it will fall back to using the stroke color - layer.beginPath(); - layer.fillStyle = options.series.pie.stroke.color; - layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); - layer.fill(); - layer.closePath(); - layer.restore(); - - // add inner stroke - - layer.save(); - layer.beginPath(); - layer.strokeStyle = options.series.pie.stroke.color; - layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); - layer.stroke(); - layer.closePath(); - layer.restore(); - - // TODO: add extra shadow inside hole (with a mask) if the pie is tilted. - } - } - - //-- Additional Interactive related functions -- - - function isPointInPoly(poly, pt) { - for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i) - ((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1]< poly[i][1])) - && (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0]) - && (c = !c); - return c; - } - - function findNearbySlice(mouseX, mouseY) { - - var slices = plot.getData(), - options = plot.getOptions(), - radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius, - x, y; - - for (var i = 0; i < slices.length; ++i) { - - var s = slices[i]; - - if (s.pie.show) { - - ctx.save(); - ctx.beginPath(); - ctx.moveTo(0, 0); // Center of the pie - //ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here. - ctx.arc(0, 0, radius, s.startAngle, s.startAngle + s.angle / 2, false); - ctx.arc(0, 0, radius, s.startAngle + s.angle / 2, s.startAngle + s.angle, false); - ctx.closePath(); - x = mouseX - centerLeft; - y = mouseY - centerTop; - - if (ctx.isPointInPath) { - if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) { - ctx.restore(); - return { - datapoint: [s.percent, s.data], - dataIndex: 0, - series: s, - seriesIndex: i - }; - } - } else { - - // excanvas for IE doesn;t support isPointInPath, this is a workaround. - - var p1X = radius * Math.cos(s.startAngle), - p1Y = radius * Math.sin(s.startAngle), - p2X = radius * Math.cos(s.startAngle + s.angle / 4), - p2Y = radius * Math.sin(s.startAngle + s.angle / 4), - p3X = radius * Math.cos(s.startAngle + s.angle / 2), - p3Y = radius * Math.sin(s.startAngle + s.angle / 2), - p4X = radius * Math.cos(s.startAngle + s.angle / 1.5), - p4Y = radius * Math.sin(s.startAngle + s.angle / 1.5), - p5X = radius * Math.cos(s.startAngle + s.angle), - p5Y = radius * Math.sin(s.startAngle + s.angle), - arrPoly = [[0, 0], [p1X, p1Y], [p2X, p2Y], [p3X, p3Y], [p4X, p4Y], [p5X, p5Y]], - arrPoint = [x, y]; - - // TODO: perhaps do some mathematical trickery here with the Y-coordinate to compensate for pie tilt? - - if (isPointInPoly(arrPoly, arrPoint)) { - ctx.restore(); - return { - datapoint: [s.percent, s.data], - dataIndex: 0, - series: s, - seriesIndex: i - }; - } - } - - ctx.restore(); - } - } - - return null; - } - - function onMouseMove(e) { - triggerClickHoverEvent("plothover", e); - } - - function onClick(e) { - triggerClickHoverEvent("plotclick", e); - } - - // trigger click or hover event (they send the same parameters so we share their code) - - function triggerClickHoverEvent(eventname, e) { - - var offset = plot.offset(); - var canvasX = parseInt(e.pageX - offset.left); - var canvasY = parseInt(e.pageY - offset.top); - var item = findNearbySlice(canvasX, canvasY); - - if (options.grid.autoHighlight) { - - // clear auto-highlights - - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.auto == eventname && !(item && h.series == item.series)) { - unhighlight(h.series); - } - } - } - - // highlight the slice - - if (item) { - highlight(item.series, eventname); - } - - // trigger any hover bind events - - var pos = { pageX: e.pageX, pageY: e.pageY }; - target.trigger(eventname, [pos, item]); - } - - function highlight(s, auto) { - //if (typeof s == "number") { - // s = series[s]; - //} - - var i = indexOfHighlight(s); - - if (i == -1) { - highlights.push({ series: s, auto: auto }); - plot.triggerRedrawOverlay(); - } else if (!auto) { - highlights[i].auto = false; - } - } - - function unhighlight(s) { - if (s == null) { - highlights = []; - plot.triggerRedrawOverlay(); - } - - //if (typeof s == "number") { - // s = series[s]; - //} - - var i = indexOfHighlight(s); - - if (i != -1) { - highlights.splice(i, 1); - plot.triggerRedrawOverlay(); - } - } - - function indexOfHighlight(s) { - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.series == s) - return i; - } - return -1; - } - - function drawOverlay(plot, octx) { - - var options = plot.getOptions(); - - var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; - - octx.save(); - octx.translate(centerLeft, centerTop); - octx.scale(1, options.series.pie.tilt); - - for (var i = 0; i < highlights.length; ++i) { - drawHighlight(highlights[i].series); - } - - drawDonutHole(octx); - - octx.restore(); - - function drawHighlight(series) { - - if (series.angle <= 0 || isNaN(series.angle)) { - return; - } - - //octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString(); - octx.fillStyle = "rgba(255, 255, 255, " + options.series.pie.highlight.opacity + ")"; // this is temporary until we have access to parseColor - octx.beginPath(); - if (Math.abs(series.angle - Math.PI * 2) > 0.000000001) { - octx.moveTo(0, 0); // Center of the pie - } - octx.arc(0, 0, radius, series.startAngle, series.startAngle + series.angle / 2, false); - octx.arc(0, 0, radius, series.startAngle + series.angle / 2, series.startAngle + series.angle, false); - octx.closePath(); - octx.fill(); - } - } - } // end init (plugin body) - - // define pie specific options and their default values - - var options = { - series: { - pie: { - show: false, - radius: "auto", // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value) - innerRadius: 0, /* for donut */ - startAngle: 3/2, - tilt: 1, - shadow: { - left: 5, // shadow left offset - top: 15, // shadow top offset - alpha: 0.02 // shadow alpha - }, - offset: { - top: 0, - left: "auto" - }, - stroke: { - color: "#fff", - width: 1 - }, - label: { - show: "auto", - formatter: function(label, slice) { - return "
" + label + "
" + Math.round(slice.percent) + "%
"; - }, // formatter function - radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value) - background: { - color: null, - opacity: 0 - }, - threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow) - }, - combine: { - threshold: -1, // percentage at which to combine little slices into one larger slice - color: null, // color to give the new slice (auto-generated if null) - label: "Other" // label to give the new slice - }, - highlight: { - //color: "#fff", // will add this functionality once parseColor is available - opacity: 0.5 - } - } - } - }; - - $.plot.plugins.push({ - init: init, - options: options, - name: "pie", - version: "1.1" - }); - -})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.resize.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.resize.js deleted file mode 100644 index 8a626dda0addb..0000000000000 --- a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.resize.js +++ /dev/null @@ -1,59 +0,0 @@ -/* Flot plugin for automatically redrawing plots as the placeholder resizes. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -It works by listening for changes on the placeholder div (through the jQuery -resize event plugin) - if the size changes, it will redraw the plot. - -There are no options. If you need to disable the plugin for some plots, you -can just fix the size of their placeholders. - -*/ - -/* Inline dependency: - * jQuery resize event - v1.1 - 3/14/2010 - * http://benalman.com/projects/jquery-resize-plugin/ - * - * Copyright (c) 2010 "Cowboy" Ben Alman - * Dual licensed under the MIT and GPL licenses. - * http://benalman.com/about/license/ - */ -(function($,e,t){"$:nomunge";var i=[],n=$.resize=$.extend($.resize,{}),a,r=false,s="setTimeout",u="resize",m=u+"-special-event",o="pendingDelay",l="activeDelay",f="throttleWindow";n[o]=200;n[l]=20;n[f]=true;$.event.special[u]={setup:function(){if(!n[f]&&this[s]){return false}var e=$(this);i.push(this);e.data(m,{w:e.width(),h:e.height()});if(i.length===1){a=t;h()}},teardown:function(){if(!n[f]&&this[s]){return false}var e=$(this);for(var t=i.length-1;t>=0;t--){if(i[t]==this){i.splice(t,1);break}}e.removeData(m);if(!i.length){if(r){cancelAnimationFrame(a)}else{clearTimeout(a)}a=null}},add:function(e){if(!n[f]&&this[s]){return false}var i;function a(e,n,a){var r=$(this),s=r.data(m)||{};s.w=n!==t?n:r.width();s.h=a!==t?a:r.height();i.apply(this,arguments)}if($.isFunction(e)){i=e;return a}else{i=e.handler;e.handler=a}}};function h(t){if(r===true){r=t||1}for(var s=i.length-1;s>=0;s--){var l=$(i[s]);if(l[0]==e||l.is(":visible")){var f=l.width(),c=l.height(),d=l.data(m);if(d&&(f!==d.w||c!==d.h)){l.trigger(u,[d.w=f,d.h=c]);r=t||true}}else{d=l.data(m);d.w=0;d.h=0}}if(a!==null){if(r&&(t==null||t-r<1e3)){a=e.requestAnimationFrame(h)}else{a=setTimeout(h,n[o]);r=false}}}if(!e.requestAnimationFrame){e.requestAnimationFrame=function(){return e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||e.msRequestAnimationFrame||function(t,i){return e.setTimeout(function(){t((new Date).getTime())},n[l])}}()}if(!e.cancelAnimationFrame){e.cancelAnimationFrame=function(){return e.webkitCancelRequestAnimationFrame||e.mozCancelRequestAnimationFrame||e.oCancelRequestAnimationFrame||e.msCancelRequestAnimationFrame||clearTimeout}()}})(jQuery,this); - -(function ($) { - var options = { }; // no options - - function init(plot) { - function onResize() { - var placeholder = plot.getPlaceholder(); - - // somebody might have hidden us and we can't plot - // when we don't have the dimensions - if (placeholder.width() == 0 || placeholder.height() == 0) - return; - - plot.resize(); - plot.setupGrid(); - plot.draw(); - } - - function bindEvents(plot, eventHolder) { - plot.getPlaceholder().resize(onResize); - } - - function shutdown(plot, eventHolder) { - plot.getPlaceholder().unbind("resize", onResize); - } - - plot.hooks.bindEvents.push(bindEvents); - plot.hooks.shutdown.push(shutdown); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'resize', - version: '1.0' - }); -})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.selection.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.selection.js deleted file mode 100644 index c8707b30f4e6f..0000000000000 --- a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.selection.js +++ /dev/null @@ -1,360 +0,0 @@ -/* Flot plugin for selecting regions of a plot. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin supports these options: - -selection: { - mode: null or "x" or "y" or "xy", - color: color, - shape: "round" or "miter" or "bevel", - minSize: number of pixels -} - -Selection support is enabled by setting the mode to one of "x", "y" or "xy". -In "x" mode, the user will only be able to specify the x range, similarly for -"y" mode. For "xy", the selection becomes a rectangle where both ranges can be -specified. "color" is color of the selection (if you need to change the color -later on, you can get to it with plot.getOptions().selection.color). "shape" -is the shape of the corners of the selection. - -"minSize" is the minimum size a selection can be in pixels. This value can -be customized to determine the smallest size a selection can be and still -have the selection rectangle be displayed. When customizing this value, the -fact that it refers to pixels, not axis units must be taken into account. -Thus, for example, if there is a bar graph in time mode with BarWidth set to 1 -minute, setting "minSize" to 1 will not make the minimum selection size 1 -minute, but rather 1 pixel. Note also that setting "minSize" to 0 will prevent -"plotunselected" events from being fired when the user clicks the mouse without -dragging. - -When selection support is enabled, a "plotselected" event will be emitted on -the DOM element you passed into the plot function. The event handler gets a -parameter with the ranges selected on the axes, like this: - - placeholder.bind( "plotselected", function( event, ranges ) { - alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to) - // similar for yaxis - with multiple axes, the extra ones are in - // x2axis, x3axis, ... - }); - -The "plotselected" event is only fired when the user has finished making the -selection. A "plotselecting" event is fired during the process with the same -parameters as the "plotselected" event, in case you want to know what's -happening while it's happening, - -A "plotunselected" event with no arguments is emitted when the user clicks the -mouse to remove the selection. As stated above, setting "minSize" to 0 will -destroy this behavior. - -The plugin also adds the following methods to the plot object: - -- setSelection( ranges, preventEvent ) - - Set the selection rectangle. The passed in ranges is on the same form as - returned in the "plotselected" event. If the selection mode is "x", you - should put in either an xaxis range, if the mode is "y" you need to put in - an yaxis range and both xaxis and yaxis if the selection mode is "xy", like - this: - - setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } }); - - setSelection will trigger the "plotselected" event when called. If you don't - want that to happen, e.g. if you're inside a "plotselected" handler, pass - true as the second parameter. If you are using multiple axes, you can - specify the ranges on any of those, e.g. as x2axis/x3axis/... instead of - xaxis, the plugin picks the first one it sees. - -- clearSelection( preventEvent ) - - Clear the selection rectangle. Pass in true to avoid getting a - "plotunselected" event. - -- getSelection() - - Returns the current selection in the same format as the "plotselected" - event. If there's currently no selection, the function returns null. - -*/ - -(function ($) { - function init(plot) { - var selection = { - first: { x: -1, y: -1}, second: { x: -1, y: -1}, - show: false, - active: false - }; - - // FIXME: The drag handling implemented here should be - // abstracted out, there's some similar code from a library in - // the navigation plugin, this should be massaged a bit to fit - // the Flot cases here better and reused. Doing this would - // make this plugin much slimmer. - var savedhandlers = {}; - - var mouseUpHandler = null; - - function onMouseMove(e) { - if (selection.active) { - updateSelection(e); - - plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]); - } - } - - function onMouseDown(e) { - if (e.which != 1) // only accept left-click - return; - - // cancel out any text selections - document.body.focus(); - - // prevent text selection and drag in old-school browsers - if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) { - savedhandlers.onselectstart = document.onselectstart; - document.onselectstart = function () { return false; }; - } - if (document.ondrag !== undefined && savedhandlers.ondrag == null) { - savedhandlers.ondrag = document.ondrag; - document.ondrag = function () { return false; }; - } - - setSelectionPos(selection.first, e); - - selection.active = true; - - // this is a bit silly, but we have to use a closure to be - // able to whack the same handler again - mouseUpHandler = function (e) { onMouseUp(e); }; - - $(document).one("mouseup", mouseUpHandler); - } - - function onMouseUp(e) { - mouseUpHandler = null; - - // revert drag stuff for old-school browsers - if (document.onselectstart !== undefined) - document.onselectstart = savedhandlers.onselectstart; - if (document.ondrag !== undefined) - document.ondrag = savedhandlers.ondrag; - - // no more dragging - selection.active = false; - updateSelection(e); - - if (selectionIsSane()) - triggerSelectedEvent(); - else { - // this counts as a clear - plot.getPlaceholder().trigger("plotunselected", [ ]); - plot.getPlaceholder().trigger("plotselecting", [ null ]); - } - - return false; - } - - function getSelection() { - if (!selectionIsSane()) - return null; - - if (!selection.show) return null; - - var r = {}, c1 = selection.first, c2 = selection.second; - $.each(plot.getAxes(), function (name, axis) { - if (axis.used) { - var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]); - r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) }; - } - }); - return r; - } - - function triggerSelectedEvent() { - var r = getSelection(); - - plot.getPlaceholder().trigger("plotselected", [ r ]); - - // backwards-compat stuff, to be removed in future - if (r.xaxis && r.yaxis) - plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]); - } - - function clamp(min, value, max) { - return value < min ? min: (value > max ? max: value); - } - - function setSelectionPos(pos, e) { - var o = plot.getOptions(); - var offset = plot.getPlaceholder().offset(); - var plotOffset = plot.getPlotOffset(); - pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width()); - pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height()); - - if (o.selection.mode == "y") - pos.x = pos == selection.first ? 0 : plot.width(); - - if (o.selection.mode == "x") - pos.y = pos == selection.first ? 0 : plot.height(); - } - - function updateSelection(pos) { - if (pos.pageX == null) - return; - - setSelectionPos(selection.second, pos); - if (selectionIsSane()) { - selection.show = true; - plot.triggerRedrawOverlay(); - } - else - clearSelection(true); - } - - function clearSelection(preventEvent) { - if (selection.show) { - selection.show = false; - plot.triggerRedrawOverlay(); - if (!preventEvent) - plot.getPlaceholder().trigger("plotunselected", [ ]); - } - } - - // function taken from markings support in Flot - function extractRange(ranges, coord) { - var axis, from, to, key, axes = plot.getAxes(); - - for (var k in axes) { - axis = axes[k]; - if (axis.direction == coord) { - key = coord + axis.n + "axis"; - if (!ranges[key] && axis.n == 1) - key = coord + "axis"; // support x1axis as xaxis - if (ranges[key]) { - from = ranges[key].from; - to = ranges[key].to; - break; - } - } - } - - // backwards-compat stuff - to be removed in future - if (!ranges[key]) { - axis = coord == "x" ? plot.getXAxes()[0] : plot.getYAxes()[0]; - from = ranges[coord + "1"]; - to = ranges[coord + "2"]; - } - - // auto-reverse as an added bonus - if (from != null && to != null && from > to) { - var tmp = from; - from = to; - to = tmp; - } - - return { from: from, to: to, axis: axis }; - } - - function setSelection(ranges, preventEvent) { - var axis, range, o = plot.getOptions(); - - if (o.selection.mode == "y") { - selection.first.x = 0; - selection.second.x = plot.width(); - } - else { - range = extractRange(ranges, "x"); - - selection.first.x = range.axis.p2c(range.from); - selection.second.x = range.axis.p2c(range.to); - } - - if (o.selection.mode == "x") { - selection.first.y = 0; - selection.second.y = plot.height(); - } - else { - range = extractRange(ranges, "y"); - - selection.first.y = range.axis.p2c(range.from); - selection.second.y = range.axis.p2c(range.to); - } - - selection.show = true; - plot.triggerRedrawOverlay(); - if (!preventEvent && selectionIsSane()) - triggerSelectedEvent(); - } - - function selectionIsSane() { - var minSize = plot.getOptions().selection.minSize; - return Math.abs(selection.second.x - selection.first.x) >= minSize && - Math.abs(selection.second.y - selection.first.y) >= minSize; - } - - plot.clearSelection = clearSelection; - plot.setSelection = setSelection; - plot.getSelection = getSelection; - - plot.hooks.bindEvents.push(function(plot, eventHolder) { - var o = plot.getOptions(); - if (o.selection.mode != null) { - eventHolder.mousemove(onMouseMove); - eventHolder.mousedown(onMouseDown); - } - }); - - - plot.hooks.drawOverlay.push(function (plot, ctx) { - // draw selection - if (selection.show && selectionIsSane()) { - var plotOffset = plot.getPlotOffset(); - var o = plot.getOptions(); - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - var c = $.color.parse(o.selection.color); - - ctx.strokeStyle = c.scale('a', 0.8).toString(); - ctx.lineWidth = 1; - ctx.lineJoin = o.selection.shape; - ctx.fillStyle = c.scale('a', 0.4).toString(); - - var x = Math.min(selection.first.x, selection.second.x) + 0.5, - y = Math.min(selection.first.y, selection.second.y) + 0.5, - w = Math.abs(selection.second.x - selection.first.x) - 1, - h = Math.abs(selection.second.y - selection.first.y) - 1; - - ctx.fillRect(x, y, w, h); - ctx.strokeRect(x, y, w, h); - - ctx.restore(); - } - }); - - plot.hooks.shutdown.push(function (plot, eventHolder) { - eventHolder.unbind("mousemove", onMouseMove); - eventHolder.unbind("mousedown", onMouseDown); - - if (mouseUpHandler) - $(document).unbind("mouseup", mouseUpHandler); - }); - - } - - $.plot.plugins.push({ - init: init, - options: { - selection: { - mode: null, // one of null, "x", "y" or "xy" - color: "#e8cfac", - shape: "round", // one of "round", "miter", or "bevel" - minSize: 5 // minimum number of pixels - } - }, - name: 'selection', - version: '1.1' - }); -})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.stack.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.stack.js deleted file mode 100644 index 0d91c0f3c0160..0000000000000 --- a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.stack.js +++ /dev/null @@ -1,188 +0,0 @@ -/* Flot plugin for stacking data sets rather than overlaying them. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin assumes the data is sorted on x (or y if stacking horizontally). -For line charts, it is assumed that if a line has an undefined gap (from a -null point), then the line above it should have the same gap - insert zeros -instead of "null" if you want another behaviour. This also holds for the start -and end of the chart. Note that stacking a mix of positive and negative values -in most instances doesn't make sense (so it looks weird). - -Two or more series are stacked when their "stack" attribute is set to the same -key (which can be any number or string or just "true"). To specify the default -stack, you can set the stack option like this: - - series: { - stack: null/false, true, or a key (number/string) - } - -You can also specify it for a single series, like this: - - $.plot( $("#placeholder"), [{ - data: [ ... ], - stack: true - }]) - -The stacking order is determined by the order of the data series in the array -(later series end up on top of the previous). - -Internally, the plugin modifies the datapoints in each series, adding an -offset to the y value. For line series, extra data points are inserted through -interpolation. If there's a second y value, it's also adjusted (e.g for bar -charts or filled areas). - -*/ - -(function ($) { - var options = { - series: { stack: null } // or number/string - }; - - function init(plot) { - function findMatchingSeries(s, allseries) { - var res = null; - for (var i = 0; i < allseries.length; ++i) { - if (s == allseries[i]) - break; - - if (allseries[i].stack == s.stack) - res = allseries[i]; - } - - return res; - } - - function stackData(plot, s, datapoints) { - if (s.stack == null || s.stack === false) - return; - - var other = findMatchingSeries(s, plot.getData()); - if (!other) - return; - - var ps = datapoints.pointsize, - points = datapoints.points, - otherps = other.datapoints.pointsize, - otherpoints = other.datapoints.points, - newpoints = [], - px, py, intery, qx, qy, bottom, - withlines = s.lines.show, - horizontal = s.bars.horizontal, - withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y), - withsteps = withlines && s.lines.steps, - fromgap = true, - keyOffset = horizontal ? 1 : 0, - accumulateOffset = horizontal ? 0 : 1, - i = 0, j = 0, l, m; - - while (true) { - if (i >= points.length) - break; - - l = newpoints.length; - - if (points[i] == null) { - // copy gaps - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - i += ps; - } - else if (j >= otherpoints.length) { - // for lines, we can't use the rest of the points - if (!withlines) { - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - } - i += ps; - } - else if (otherpoints[j] == null) { - // oops, got a gap - for (m = 0; m < ps; ++m) - newpoints.push(null); - fromgap = true; - j += otherps; - } - else { - // cases where we actually got two points - px = points[i + keyOffset]; - py = points[i + accumulateOffset]; - qx = otherpoints[j + keyOffset]; - qy = otherpoints[j + accumulateOffset]; - bottom = 0; - - if (px == qx) { - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - - newpoints[l + accumulateOffset] += qy; - bottom = qy; - - i += ps; - j += otherps; - } - else if (px > qx) { - // we got past point below, might need to - // insert interpolated extra point - if (withlines && i > 0 && points[i - ps] != null) { - intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); - newpoints.push(qx); - newpoints.push(intery + qy); - for (m = 2; m < ps; ++m) - newpoints.push(points[i + m]); - bottom = qy; - } - - j += otherps; - } - else { // px < qx - if (fromgap && withlines) { - // if we come from a gap, we just skip this point - i += ps; - continue; - } - - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - - // we might be able to interpolate a point below, - // this can give us a better y - if (withlines && j > 0 && otherpoints[j - otherps] != null) - bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx); - - newpoints[l + accumulateOffset] += bottom; - - i += ps; - } - - fromgap = false; - - if (l != newpoints.length && withbottom) - newpoints[l + 2] += bottom; - } - - // maintain the line steps invariant - if (withsteps && l != newpoints.length && l > 0 - && newpoints[l] != null - && newpoints[l] != newpoints[l - ps] - && newpoints[l + 1] != newpoints[l - ps + 1]) { - for (m = 0; m < ps; ++m) - newpoints[l + ps + m] = newpoints[l + m]; - newpoints[l + 1] = newpoints[l - ps + 1]; - } - } - - datapoints.points = newpoints; - } - - plot.hooks.processDatapoints.push(stackData); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'stack', - version: '1.2' - }); -})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.symbol.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.symbol.js deleted file mode 100644 index 79f634971b6fa..0000000000000 --- a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.symbol.js +++ /dev/null @@ -1,71 +0,0 @@ -/* Flot plugin that adds some extra symbols for plotting points. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The symbols are accessed as strings through the standard symbol options: - - series: { - points: { - symbol: "square" // or "diamond", "triangle", "cross" - } - } - -*/ - -(function ($) { - function processRawData(plot, series, datapoints) { - // we normalize the area of each symbol so it is approximately the - // same as a circle of the given radius - - var handlers = { - square: function (ctx, x, y, radius, shadow) { - // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 - var size = radius * Math.sqrt(Math.PI) / 2; - ctx.rect(x - size, y - size, size + size, size + size); - }, - diamond: function (ctx, x, y, radius, shadow) { - // pi * r^2 = 2s^2 => s = r * sqrt(pi/2) - var size = radius * Math.sqrt(Math.PI / 2); - ctx.moveTo(x - size, y); - ctx.lineTo(x, y - size); - ctx.lineTo(x + size, y); - ctx.lineTo(x, y + size); - ctx.lineTo(x - size, y); - }, - triangle: function (ctx, x, y, radius, shadow) { - // pi * r^2 = 1/2 * s^2 * sin (pi / 3) => s = r * sqrt(2 * pi / sin(pi / 3)) - var size = radius * Math.sqrt(2 * Math.PI / Math.sin(Math.PI / 3)); - var height = size * Math.sin(Math.PI / 3); - ctx.moveTo(x - size/2, y + height/2); - ctx.lineTo(x + size/2, y + height/2); - if (!shadow) { - ctx.lineTo(x, y - height/2); - ctx.lineTo(x - size/2, y + height/2); - } - }, - cross: function (ctx, x, y, radius, shadow) { - // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 - var size = radius * Math.sqrt(Math.PI) / 2; - ctx.moveTo(x - size, y - size); - ctx.lineTo(x + size, y + size); - ctx.moveTo(x - size, y + size); - ctx.lineTo(x + size, y - size); - } - }; - - var s = series.points.symbol; - if (handlers[s]) - series.points.symbol = handlers[s]; - } - - function init(plot) { - plot.hooks.processDatapoints.push(processRawData); - } - - $.plot.plugins.push({ - init: init, - name: 'symbols', - version: '1.0' - }); -})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.threshold.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.threshold.js deleted file mode 100644 index 8c99c401d87e5..0000000000000 --- a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.threshold.js +++ /dev/null @@ -1,142 +0,0 @@ -/* Flot plugin for thresholding data. - -Copyright (c) 2007-2014 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin supports these options: - - series: { - threshold: { - below: number - color: colorspec - } - } - -It can also be applied to a single series, like this: - - $.plot( $("#placeholder"), [{ - data: [ ... ], - threshold: { ... } - }]) - -An array can be passed for multiple thresholding, like this: - - threshold: [{ - below: number1 - color: color1 - },{ - below: number2 - color: color2 - }] - -These multiple threshold objects can be passed in any order since they are -sorted by the processing function. - -The data points below "below" are drawn with the specified color. This makes -it easy to mark points below 0, e.g. for budget data. - -Internally, the plugin works by splitting the data into two series, above and -below the threshold. The extra series below the threshold will have its label -cleared and the special "originSeries" attribute set to the original series. -You may need to check for this in hover events. - -*/ - -(function ($) { - var options = { - series: { threshold: null } // or { below: number, color: color spec} - }; - - function init(plot) { - function thresholdData(plot, s, datapoints, below, color) { - var ps = datapoints.pointsize, i, x, y, p, prevp, - thresholded = $.extend({}, s); // note: shallow copy - - thresholded.datapoints = { points: [], pointsize: ps, format: datapoints.format }; - thresholded.label = null; - thresholded.color = color; - thresholded.threshold = null; - thresholded.originSeries = s; - thresholded.data = []; - - var origpoints = datapoints.points, - addCrossingPoints = s.lines.show; - - var threspoints = []; - var newpoints = []; - var m; - - for (i = 0; i < origpoints.length; i += ps) { - x = origpoints[i]; - y = origpoints[i + 1]; - - prevp = p; - if (y < below) - p = threspoints; - else - p = newpoints; - - if (addCrossingPoints && prevp != p && x != null - && i > 0 && origpoints[i - ps] != null) { - var interx = x + (below - y) * (x - origpoints[i - ps]) / (y - origpoints[i - ps + 1]); - prevp.push(interx); - prevp.push(below); - for (m = 2; m < ps; ++m) - prevp.push(origpoints[i + m]); - - p.push(null); // start new segment - p.push(null); - for (m = 2; m < ps; ++m) - p.push(origpoints[i + m]); - p.push(interx); - p.push(below); - for (m = 2; m < ps; ++m) - p.push(origpoints[i + m]); - } - - p.push(x); - p.push(y); - for (m = 2; m < ps; ++m) - p.push(origpoints[i + m]); - } - - datapoints.points = newpoints; - thresholded.datapoints.points = threspoints; - - if (thresholded.datapoints.points.length > 0) { - var origIndex = $.inArray(s, plot.getData()); - // Insert newly-generated series right after original one (to prevent it from becoming top-most) - plot.getData().splice(origIndex + 1, 0, thresholded); - } - - // FIXME: there are probably some edge cases left in bars - } - - function processThresholds(plot, s, datapoints) { - if (!s.threshold) - return; - - if (s.threshold instanceof Array) { - s.threshold.sort(function(a, b) { - return a.below - b.below; - }); - - $(s.threshold).each(function(i, th) { - thresholdData(plot, s, datapoints, th.below, th.color); - }); - } - else { - thresholdData(plot, s, datapoints, s.threshold.below, s.threshold.color); - } - } - - plot.hooks.processDatapoints.push(processThresholds); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'threshold', - version: '1.2' - }); -})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/jquery_flot.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/jquery_flot.js deleted file mode 100644 index 28a4d5f56df15..0000000000000 --- a/x-pack/plugins/monitoring/public/lib/jquery_flot/jquery_flot.js +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import $ from 'jquery'; -if (window) { - window.jQuery = $; -} -import './flot-charts/jquery.flot'; - -// load flot plugins -// avoid the `canvas` plugin, it causes blurry fonts -import './flot-charts/jquery.flot.time'; -import './flot-charts/jquery.flot.crosshair'; -import './flot-charts/jquery.flot.selection'; - -export default $; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts index fdd7253550624..824eeab7245b4 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts @@ -7,12 +7,16 @@ import { fetchStatus } from './fetch_status'; import { AlertUiState, AlertState } from '../../alerts/types'; import { AlertSeverity } from '../../../common/enums'; -import { ALERT_CPU_USAGE, ALERT_CLUSTER_HEALTH } from '../../../common/constants'; +import { + ALERT_CPU_USAGE, + ALERT_CLUSTER_HEALTH, + ALERT_DISK_USAGE, + ALERT_MISSING_MONITORING_DATA, +} from '../../../common/constants'; describe('fetchStatus', () => { const alertType = ALERT_CPU_USAGE; const alertTypes = [alertType]; - const log = { warn: jest.fn() }; const start = 0; const end = 0; const id = 1; @@ -53,6 +57,7 @@ describe('fetchStatus', () => { afterEach(() => { (alertsClient.find as jest.Mock).mockClear(); (alertsClient.getAlertState as jest.Mock).mockClear(); + alertStates.length = 0; }); it('should fetch from the alerts client', async () => { @@ -62,8 +67,7 @@ describe('fetchStatus', () => { alertTypes, defaultClusterState.clusterUuid, start, - end, - log as any + end ); expect(status).toEqual({ monitoring_alert_cpu_usage: { @@ -98,8 +102,7 @@ describe('fetchStatus', () => { alertTypes, defaultClusterState.clusterUuid, start, - end, - log as any + end ); expect(Object.values(status).length).toBe(1); expect(Object.keys(status)).toEqual(alertTypes); @@ -126,8 +129,7 @@ describe('fetchStatus', () => { alertTypes, defaultClusterState.clusterUuid, customStart, - customEnd, - log as any + customEnd ); expect(Object.values(status).length).toBe(1); expect(Object.keys(status)).toEqual(alertTypes); @@ -141,8 +143,7 @@ describe('fetchStatus', () => { alertTypes, defaultClusterState.clusterUuid, start, - end, - log as any + end ); expect((alertsClient.find as jest.Mock).mock.calls[0][0].options.filter).toBe( `alert.attributes.alertTypeId:${alertType}` @@ -160,8 +161,7 @@ describe('fetchStatus', () => { alertTypes, defaultClusterState.clusterUuid, start, - end, - log as any + end ); expect(status[alertType].states.length).toEqual(0); }); @@ -178,8 +178,7 @@ describe('fetchStatus', () => { alertTypes, defaultClusterState.clusterUuid, start, - end, - log as any + end ); expect(status).toEqual({}); }); @@ -197,9 +196,51 @@ describe('fetchStatus', () => { [ALERT_CLUSTER_HEALTH], defaultClusterState.clusterUuid, start, - end, - log as any + end ); expect(customLicenseService.getWatcherFeature).toHaveBeenCalled(); }); + + it('should sort the alerts', async () => { + const customAlertsClient = { + find: jest.fn(() => ({ + total: 1, + data: [ + { + id, + }, + ], + })), + getAlertState: jest.fn(() => ({ + alertInstances: { + abc: { + state: { + alertStates: [ + { + cluster: defaultClusterState, + ui: { + ...defaultUiState, + isFiring: true, + }, + }, + ], + }, + }, + }, + })), + }; + const status = await fetchStatus( + customAlertsClient as any, + licenseService as any, + [ALERT_CPU_USAGE, ALERT_DISK_USAGE, ALERT_MISSING_MONITORING_DATA], + defaultClusterState.clusterUuid, + start, + end + ); + expect(Object.keys(status)).toEqual([ + ALERT_CPU_USAGE, + ALERT_DISK_USAGE, + ALERT_MISSING_MONITORING_DATA, + ]); + }); }); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts index 49e688fafbee5..ed49f42e4908c 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -18,8 +18,9 @@ export async function fetchStatus( clusterUuid: string, start: number, end: number, - filters: CommonAlertFilter[] + filters: CommonAlertFilter[] = [] ): Promise<{ [type: string]: CommonAlertStatus }> { + const types: Array<{ type: string; result: CommonAlertStatus }> = []; const byType: { [type: string]: CommonAlertStatus } = {}; await Promise.all( (alertTypes || ALERTS).map(async (type) => { @@ -39,7 +40,7 @@ export async function fetchStatus( alert: serialized, }; - byType[type] = result; + types.push({ type, result }); const id = alert.getId(); if (!id) { @@ -75,5 +76,10 @@ export async function fetchStatus( }) ); + types.sort((a, b) => (a.type === b.type ? 0 : a.type.length > b.type.length ? 1 : -1)); + for (const { type, result } of types) { + byType[type] = result; + } + return byType; } diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts index 047b14bd37fbc..c8aa730dd4774 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts @@ -26,7 +26,7 @@ export interface XPackUsageSecurity { export class AlertingSecurity { public static readonly getSecurityHealth = async ( context: RequestHandlerContext, - encryptedSavedObjects: EncryptedSavedObjectsPluginSetup + encryptedSavedObjects?: EncryptedSavedObjectsPluginSetup ): Promise => { const { security: { @@ -43,7 +43,7 @@ export class AlertingSecurity { return { isSufficientlySecure: !isSecurityEnabled || (isSecurityEnabled && isTLSEnabled), - hasPermanentEncryptionKey: !encryptedSavedObjects.usingEphemeralEncryptionKey, + hasPermanentEncryptionKey: !encryptedSavedObjects?.usingEphemeralEncryptionKey, }; }; } diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 4e1205cac7b8b..79c8e01c4cffd 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -21,6 +21,7 @@ import { CustomHttpResponseOptions, ResponseError, IClusterClient, + SavedObjectsServiceStart, } from 'kibana/server'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { @@ -76,6 +77,7 @@ export class Plugin { private legacyShimDependencies = {} as LegacyShimDependencies; private bulkUploader: IBulkUploader = {} as IBulkUploader; private telemetryElasticsearchClient: IClusterClient | undefined; + private telemetrySavedObjectsService: SavedObjectsServiceStart | undefined; constructor(initializerContext: PluginInitializerContext) { this.initializerContext = initializerContext; @@ -140,19 +142,20 @@ export class Plugin { kibanaUrl, isCloud ); - plugins.alerts.registerType(alert.getAlertType()); + plugins.alerts?.registerType(alert.getAlertType()); } // Initialize telemetry if (plugins.telemetryCollectionManager) { - registerMonitoringCollection( - plugins.telemetryCollectionManager, - this.cluster, - () => this.telemetryElasticsearchClient, - { + registerMonitoringCollection({ + telemetryCollectionManager: plugins.telemetryCollectionManager, + esCluster: this.cluster, + esClientGetter: () => this.telemetryElasticsearchClient, + soServiceGetter: () => this.telemetrySavedObjectsService, + customContext: { maxBucketSize: config.ui.max_bucket_size, - } - ); + }, + }); } // Register collector objects for stats to show up in the APIs @@ -249,12 +252,15 @@ export class Plugin { }; } - start({ elasticsearch }: CoreStart) { + start({ elasticsearch, savedObjects }: CoreStart) { // TODO: For the telemetry plugin to work, we need to provide the new ES client. // The new client should be inititalized with a similar config to `this.cluster` but, since we're not using - // the new client in Monitoring Telemetry collection yet, setting the local client allos progress for now. + // the new client in Monitoring Telemetry collection yet, setting the local client allows progress for now. + // The usage collector `fetch` method has been refactored to accept a `collectorFetchContext` object, + // exposing both es clients and the saved objects client. // We will update the client in a follow up PR. this.telemetryElasticsearchClient = elasticsearch.client; + this.telemetrySavedObjectsService = savedObjects; } stop() { diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts index 64beb5c58dc07..ac38d7a59b773 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts @@ -44,7 +44,7 @@ export function enableAlertsRoute(_server: unknown, npRoute: RouteDependencies) const actionsClient = context.actions?.getActionsClient(); const types = context.actions?.listTypes(); if (!alertsClient || !actionsClient || !types) { - return response.notFound(); + return response.ok({ body: undefined }); } // Get or create the default log action diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts index 78daa5e47c49f..d97bc34c2adb0 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts @@ -39,7 +39,7 @@ export function alertStatusRoute(server: any, npRoute: RouteDependencies) { } = request.body; const alertsClient = context.alerting?.getAlertsClient(); if (!alertsClient) { - return response.notFound(); + return response.ok({ body: undefined }); } const status = await fetchStatus( diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts index 89f09d349014f..129b798740806 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts @@ -16,6 +16,7 @@ describe('get_all_stats', () => { const end = 1; const callCluster = sinon.stub(); const esClient = sinon.stub(); + const soClient = sinon.stub(); const esClusters = [ { cluster_uuid: 'a' }, @@ -178,6 +179,7 @@ describe('get_all_stats', () => { { callCluster: callCluster as any, esClient: esClient as any, + soClient: soClient as any, usageCollection: {} as any, start, end, @@ -204,6 +206,7 @@ describe('get_all_stats', () => { { callCluster: callCluster as any, esClient: esClient as any, + soClient: soClient as any, usageCollection: {} as any, start, end, diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.ts index 1170380b26ac8..9ebd73ffbc833 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.ts @@ -28,7 +28,7 @@ export interface CustomContext { */ export const getAllStats: StatsGetter = async ( clustersDetails, - { callCluster, start, end, esClient }, + { callCluster, start, end, esClient, soClient }, { maxBucketSize } ) => { const clusterUuids = clustersDetails.map((clusterDetails) => clusterDetails.clusterUuid); diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.test.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.test.ts index b2f3cb6c61526..c885bc9be4408 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.test.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.test.ts @@ -5,7 +5,7 @@ */ import sinon from 'sinon'; -import { elasticsearchServiceMock } from 'src/core/server/mocks'; +import { elasticsearchServiceMock, savedObjectsRepositoryMock } from 'src/core/server/mocks'; import { getClusterUuids, fetchClusterUuids, @@ -15,6 +15,7 @@ import { describe('get_cluster_uuids', () => { const callCluster = sinon.stub(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const soClient = savedObjectsRepositoryMock.create(); const response = { aggregations: { cluster_uuids: { @@ -32,9 +33,12 @@ describe('get_cluster_uuids', () => { it('returns cluster UUIDs', async () => { callCluster.withArgs('search').returns(Promise.resolve(response)); expect( - await getClusterUuids({ callCluster, esClient, start, end, usageCollection: {} as any }, { - maxBucketSize: 1, - } as any) + await getClusterUuids( + { callCluster, esClient, soClient, start, end, usageCollection: {} as any }, + { + maxBucketSize: 1, + } as any + ) ).toStrictEqual(expectedUuids); }); }); @@ -43,9 +47,12 @@ describe('get_cluster_uuids', () => { it('searches for clusters', async () => { callCluster.returns(Promise.resolve(response)); expect( - await fetchClusterUuids({ callCluster, esClient, start, end, usageCollection: {} as any }, { - maxBucketSize: 1, - } as any) + await fetchClusterUuids( + { callCluster, esClient, soClient, start, end, usageCollection: {} as any }, + { + maxBucketSize: 1, + } as any + ) ).toStrictEqual(response); }); }); diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_collection.ts b/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_collection.ts index 3648ae4bd8551..109fefd2eb8de 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_collection.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_collection.ts @@ -4,21 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyCustomClusterClient, IClusterClient } from 'kibana/server'; +import { + ILegacyCustomClusterClient, + IClusterClient, + SavedObjectsServiceStart, +} from 'kibana/server'; import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; import { getAllStats, CustomContext } from './get_all_stats'; import { getClusterUuids } from './get_cluster_uuids'; import { getLicenses } from './get_licenses'; -export function registerMonitoringCollection( - telemetryCollectionManager: TelemetryCollectionManagerPluginSetup, - esCluster: ILegacyCustomClusterClient, - esClientGetter: () => IClusterClient | undefined, - customContext: CustomContext -) { +export function registerMonitoringCollection({ + telemetryCollectionManager, + esCluster, + esClientGetter, + soServiceGetter, + customContext, +}: { + telemetryCollectionManager: TelemetryCollectionManagerPluginSetup; + esCluster: ILegacyCustomClusterClient; + esClientGetter: () => IClusterClient | undefined; + soServiceGetter: () => SavedObjectsServiceStart | undefined; + customContext: CustomContext; +}) { telemetryCollectionManager.setCollection({ esCluster, esClientGetter, + soServiceGetter, title: 'monitoring', priority: 2, statsGetter: getAllStats, diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index e6a4b174df55d..42ac721a34c77 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -34,14 +34,14 @@ export interface MonitoringElasticsearchConfig { } export interface PluginsSetup { - encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; + encryptedSavedObjects?: EncryptedSavedObjectsPluginSetup; telemetryCollectionManager?: TelemetryCollectionManagerPluginSetup; usageCollection?: UsageCollectionSetup; licensing: LicensingPluginSetup; features: FeaturesPluginSetupContract; - alerts: AlertingPluginSetupContract; + alerts?: AlertingPluginSetupContract; infra: InfraPluginSetup; - cloud: CloudSetup; + cloud?: CloudSetup; } export interface PluginsStart { @@ -56,7 +56,7 @@ export interface MonitoringCoreConfig { export interface RouteDependencies { router: IRouter; licenseService: MonitoringLicenseService; - encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; + encryptedSavedObjects?: EncryptedSavedObjectsPluginSetup; } export interface MonitoringCore { diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 2f83576f9dc5a..765cce0baaa1a 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -81,7 +81,7 @@ export class Plugin implements PluginClass { + return ( + <> + + + + + + + + + ); +}; diff --git a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx index 22b97f45db186..18895f9e623eb 100644 --- a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx @@ -13,7 +13,7 @@ import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { BaseParams } from '../../common/types'; import { ReportingAPIClient } from '../lib/reporting_api_client'; -interface Props { +export interface Props { apiClient: ReportingAPIClient; toasts: ToastsSetup; reportType: string; diff --git a/x-pack/plugins/reporting/public/components/reporting_panel_content_lazy.tsx b/x-pack/plugins/reporting/public/components/reporting_panel_content_lazy.tsx new file mode 100644 index 0000000000000..45a7d60a60966 --- /dev/null +++ b/x-pack/plugins/reporting/public/components/reporting_panel_content_lazy.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { lazy, Suspense, FC } from 'react'; +import { PanelSpinner } from './panel_spinner'; +import type { Props } from './reporting_panel_content'; + +const LazyComponent = lazy(() => + import('./reporting_panel_content').then(({ ReportingPanelContent }) => ({ + default: ReportingPanelContent, + })) +); + +export const ReportingPanelContent: FC> = (props) => { + return ( + }> + + + ); +}; diff --git a/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx b/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx index 4a62ab2b76508..ff81ced43e0b4 100644 --- a/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx +++ b/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx @@ -12,7 +12,7 @@ import { BaseParams } from '../../common/types'; import { ReportingAPIClient } from '../lib/reporting_api_client'; import { ReportingPanelContent } from './reporting_panel_content'; -interface Props { +export interface Props { apiClient: ReportingAPIClient; toasts: ToastsSetup; reportType: string; diff --git a/x-pack/plugins/reporting/public/components/screen_capture_panel_content_lazy.tsx b/x-pack/plugins/reporting/public/components/screen_capture_panel_content_lazy.tsx new file mode 100644 index 0000000000000..52080e16dd6a3 --- /dev/null +++ b/x-pack/plugins/reporting/public/components/screen_capture_panel_content_lazy.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { lazy, Suspense, FC } from 'react'; +import { PanelSpinner } from './panel_spinner'; +import type { Props } from './screen_capture_panel_content'; + +const LazyComponent = lazy(() => + import('./screen_capture_panel_content').then(({ ScreenCapturePanelContent }) => ({ + default: ScreenCapturePanelContent, + })) +); + +export const ScreenCapturePanelContent: FC = (props) => { + return ( + }> + + + ); +}; diff --git a/x-pack/plugins/reporting/public/mount_management_section.tsx b/x-pack/plugins/reporting/public/mount_management_section.tsx new file mode 100644 index 0000000000000..ac737e4a318ac --- /dev/null +++ b/x-pack/plugins/reporting/public/mount_management_section.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { I18nProvider } from '@kbn/i18n/react'; +import { CoreSetup, CoreStart } from 'src/core/public'; +import { Observable } from 'rxjs'; +import { ReportListing } from './components/report_listing'; +import { ManagementAppMountParams } from '../../../../src/plugins/management/public'; +import { ILicense } from '../../licensing/public'; +import { ClientConfigType } from './plugin'; +import { ReportingAPIClient } from './lib/reporting_api_client'; + +export async function mountManagementSection( + coreSetup: CoreSetup, + coreStart: CoreStart, + license$: Observable, + pollConfig: ClientConfigType['poll'], + apiClient: ReportingAPIClient, + params: ManagementAppMountParams +) { + render( + + + , + params.element + ); + + return () => { + unmountComponentAtNode(params.element); + }; +} diff --git a/x-pack/plugins/reporting/public/plugin.tsx b/x-pack/plugins/reporting/public/plugin.ts similarity index 78% rename from x-pack/plugins/reporting/public/plugin.tsx rename to x-pack/plugins/reporting/public/plugin.ts index cc5964f737988..33f4fd4abf72c 100644 --- a/x-pack/plugins/reporting/public/plugin.tsx +++ b/x-pack/plugins/reporting/public/plugin.ts @@ -5,9 +5,6 @@ */ import { i18n } from '@kbn/i18n'; -import { I18nProvider } from '@kbn/i18n/react'; -import React from 'react'; -import ReactDOM from 'react-dom'; import * as Rx from 'rxjs'; import { catchError, filter, map, mergeMap, takeUntil } from 'rxjs/operators'; import { @@ -17,21 +14,21 @@ import { Plugin, PluginInitializerContext, } from 'src/core/public'; -import { UiActionsSetup } from 'src/plugins/ui_actions/public'; +import { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public'; import { CONTEXT_MENU_TRIGGER } from '../../../../src/plugins/embeddable/public'; import { FeatureCatalogueCategory, HomePublicPluginSetup, + HomePublicPluginStart, } from '../../../../src/plugins/home/public'; -import { ManagementSetup } from '../../../../src/plugins/management/public'; -import { SharePluginSetup } from '../../../../src/plugins/share/public'; -import { LicensingPluginSetup } from '../../licensing/public'; +import { ManagementSetup, ManagementStart } from '../../../../src/plugins/management/public'; +import { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public'; +import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public'; import { durationToNumber } from '../common/schema_utils'; import { JobId, ReportingConfigType } from '../common/types'; import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../constants'; import { JobSummarySet } from './'; import { getGeneralErrorToast } from './components'; -import { ReportListing } from './components/report_listing'; import { ReportingAPIClient } from './lib/reporting_api_client'; import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_handler'; import { GetCsvReportPanelAction } from './panel_actions/get_csv_panel_action'; @@ -60,7 +57,25 @@ function handleError(notifications: NotificationsSetup, err: Error): Rx.Observab return Rx.of({ completed: [], failed: [] }); } -export class ReportingPublicPlugin implements Plugin { +export interface ReportingPublicPluginSetupDendencies { + home: HomePublicPluginSetup; + management: ManagementSetup; + licensing: LicensingPluginSetup; + uiActions: UiActionsSetup; + share: SharePluginSetup; +} + +export interface ReportingPublicPluginStartDendencies { + home: HomePublicPluginStart; + management: ManagementStart; + licensing: LicensingPluginStart; + uiActions: UiActionsStart; + share: SharePluginStart; +} + +export class ReportingPublicPlugin + implements + Plugin { private config: ClientConfigType; private readonly stop$ = new Rx.ReplaySubject(1); private readonly title = i18n.translate('xpack.reporting.management.reportingTitle', { @@ -76,19 +91,7 @@ export class ReportingPublicPlugin implements Plugin { public setup( core: CoreSetup, - { - home, - management, - licensing, - uiActions, - share, - }: { - home: HomePublicPluginSetup; - management: ManagementSetup; - licensing: LicensingPluginSetup; - uiActions: UiActionsSetup; - share: SharePluginSetup; - } + { home, management, licensing, uiActions, share }: ReportingPublicPluginSetupDendencies ) { const { http, @@ -119,24 +122,19 @@ export class ReportingPublicPlugin implements Plugin { title: this.title, order: 1, mount: async (params) => { - const [start] = await getStartServices(); params.setBreadcrumbs([{ text: this.breadcrumbText }]); - ReactDOM.render( - - - , - params.element + const [[start], { mountManagementSection }] = await Promise.all([ + getStartServices(), + import('./mount_management_section'), + ]); + return await mountManagementSection( + core, + start, + license$, + this.config.poll, + apiClient, + params ); - - return () => { - ReactDOM.unmountComponentAtNode(params.element); - }; }, }); diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx index 451d907199c4c..e90d6786b58f2 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx @@ -11,7 +11,7 @@ import { IUiSettingsClient, ToastsSetup } from 'src/core/public'; import { ShareContext } from '../../../../../src/plugins/share/public'; import { LicensingPluginSetup } from '../../../licensing/public'; import { JobParamsCSV, SearchRequest } from '../../server/export_types/csv/types'; -import { ReportingPanelContent } from '../components/reporting_panel_content'; +import { ReportingPanelContent } from '../components/reporting_panel_content_lazy'; import { checkLicense } from '../lib/license_check'; import { ReportingAPIClient } from '../lib/reporting_api_client'; diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx index 2dab66187bb25..d17d4af3c0102 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx @@ -13,7 +13,7 @@ import { LicensingPluginSetup } from '../../../licensing/public'; import { LayoutParams } from '../../common/types'; import { JobParamsPNG } from '../../server/export_types/png/types'; import { JobParamsPDF } from '../../server/export_types/printable_pdf/types'; -import { ScreenCapturePanelContent } from '../components/screen_capture_panel_content'; +import { ScreenCapturePanelContent } from '../components/screen_capture_panel_content_lazy'; import { checkLicense } from '../lib/license_check'; import { ReportingAPIClient } from '../lib/reporting_api_client'; diff --git a/x-pack/plugins/reporting/server/config/schema.ts b/x-pack/plugins/reporting/server/config/schema.ts index 8276e8b49d348..7a21c5a1f6104 100644 --- a/x-pack/plugins/reporting/server/config/schema.ts +++ b/x-pack/plugins/reporting/server/config/schema.ts @@ -45,7 +45,15 @@ const QueueSchema = schema.object({ const RulesSchema = schema.object({ allow: schema.boolean(), host: schema.maybe(schema.string()), - protocol: schema.maybe(schema.string()), + protocol: schema.maybe( + schema.string({ + validate(value) { + if (!/:$/.test(value)) { + return 'must end in colon'; + } + }, + }) + ), }); const CaptureSchema = schema.object({ diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts index 340f93150ce5c..08c544b9246e0 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts @@ -111,6 +111,74 @@ export const getThreatMatchingSchemaMock = (anchorDate: string = ANCHOR_DATE): R }; }; +/** + * Useful for e2e backend tests where it doesn't have date time and other + * server side properties attached to it. + */ +export const getThreatMatchingSchemaPartialMock = (): Partial => { + return { + author: [], + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + immutable: false, + interval: '5m', + rule_id: 'rule-1', + output_index: '.siem-signals-default', + max_signals: 100, + risk_score: 55, + risk_score_mapping: [], + name: 'Query with a rule id', + references: [], + severity: 'high', + severity_mapping: [], + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'threat_match', + threat: [], + version: 1, + exceptions_list: [], + actions: [], + throttle: 'no_actions', + query: 'user.name: root or user.name: admin', + language: 'kuery', + threat_query: '*:*', + threat_index: ['list-index'], + threat_mapping: [ + { + entries: [ + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [ + { + bool: { + must: [ + { + query_string: { + query: 'host.name: linux', + analyze_wildcard: true, + time_zone: 'Zulu', + }, + }, + ], + filter: [], + should: [], + must_not: [], + }, + }, + ], + }; +}; + export const getRulesEqlSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchema => { return { ...getRulesSchemaMock(anchorDate), diff --git a/x-pack/plugins/security_solution/common/ecs/geo/index.ts b/x-pack/plugins/security_solution/common/ecs/geo/index.ts index 409b5bbdc17a4..4a4c76adb097b 100644 --- a/x-pack/plugins/security_solution/common/ecs/geo/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/geo/index.ts @@ -6,22 +6,15 @@ export interface GeoEcs { city_name?: string[]; - continent_name?: string[]; - country_iso_code?: string[]; - country_name?: string[]; - location?: Location; - region_iso_code?: string[]; - region_name?: string[]; } export interface Location { lon?: number[]; - lat?: number[]; } diff --git a/x-pack/plugins/security_solution/common/ecs/source/index.ts b/x-pack/plugins/security_solution/common/ecs/source/index.ts index 9e6b6563cec68..2c8618f4edcd0 100644 --- a/x-pack/plugins/security_solution/common/ecs/source/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/source/index.ts @@ -8,14 +8,9 @@ import { GeoEcs } from '../geo'; export interface SourceEcs { bytes?: number[]; - ip?: string[]; - port?: number[]; - domain?: string[]; - geo?: GeoEcs; - packets?: number[]; } diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts index 7e3b3d125fb5d..66119e098238e 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts @@ -26,6 +26,53 @@ interface Node { parent_entity_id?: string; } +describe('data generator data streams', () => { + // these tests cast the result of the generate methods so that we can specifically compare the `data_stream` fields + it('creates a generator with default data streams', () => { + const generator = new EndpointDocGenerator('seed'); + expect(generator.generateHostMetadata().data_stream).toEqual({ + type: 'metrics', + dataset: 'endpoint.metadata', + namespace: 'default', + }); + expect(generator.generatePolicyResponse().data_stream).toEqual({ + type: 'metrics', + dataset: 'endpoint.policy', + namespace: 'default', + }); + expect(generator.generateEvent().data_stream).toEqual({ + type: 'logs', + dataset: 'endpoint.events.process', + namespace: 'default', + }); + expect(generator.generateAlert().data_stream).toEqual({ + type: 'logs', + dataset: 'endpoint.alerts', + namespace: 'default', + }); + }); + + it('creates a generator with custom data streams', () => { + const metadataDataStream = { type: 'meta', dataset: 'dataset', namespace: 'name' }; + const policyDataStream = { type: 'policy', dataset: 'fake', namespace: 'something' }; + const eventsDataStream = { type: 'events', dataset: 'events stuff', namespace: 'name' }; + const alertsDataStream = { type: 'alerts', dataset: 'alerts stuff', namespace: 'name' }; + const generator = new EndpointDocGenerator('seed'); + expect(generator.generateHostMetadata(0, metadataDataStream).data_stream).toStrictEqual( + metadataDataStream + ); + expect(generator.generatePolicyResponse({ policyDataStream }).data_stream).toStrictEqual( + policyDataStream + ); + expect(generator.generateEvent({ eventsDataStream }).data_stream).toStrictEqual( + eventsDataStream + ); + expect(generator.generateAlert({ alertsDataStream }).data_stream).toStrictEqual( + alertsDataStream + ); + }); +}); + describe('data generator', () => { let generator: EndpointDocGenerator; beforeEach(() => { @@ -69,7 +116,7 @@ describe('data generator', () => { it('creates policy response documents', () => { const timestamp = new Date().getTime(); - const hostPolicyResponse = generator.generatePolicyResponse(timestamp); + const hostPolicyResponse = generator.generatePolicyResponse({ ts: timestamp }); expect(hostPolicyResponse['@timestamp']).toEqual(timestamp); expect(hostPolicyResponse.event.created).toEqual(timestamp); expect(hostPolicyResponse.Endpoint).not.toBeNull(); @@ -80,7 +127,7 @@ describe('data generator', () => { it('creates alert event documents', () => { const timestamp = new Date().getTime(); - const alert = generator.generateAlert(timestamp); + const alert = generator.generateAlert({ ts: timestamp }); expect(alert['@timestamp']).toEqual(timestamp); expect(alert.event?.action).not.toBeNull(); expect(alert.Endpoint).not.toBeNull(); diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index f0254616e6c9d..07b230ffc6cc5 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -7,6 +7,7 @@ import uuid from 'uuid'; import seedrandom from 'seedrandom'; import { AlertEvent, + DataStream, EndpointStatus, Host, HostMetadata, @@ -59,6 +60,7 @@ interface EventOptions { pid?: number; parentPid?: number; extensions?: object; + eventsDataStream?: DataStream; } const Windows: OSFields[] = [ @@ -330,6 +332,8 @@ export interface TreeOptions { percentTerminated?: number; alwaysGenMaxChildrenPerNode?: boolean; ancestryArraySize?: number; + eventsDataStream?: DataStream; + alertsDataStream?: DataStream; } type TreeOptionDefaults = Required; @@ -351,19 +355,51 @@ export function getTreeOptionsWithDef(options?: TreeOptions): TreeOptionDefaults percentTerminated: options?.percentTerminated ?? 100, alwaysGenMaxChildrenPerNode: options?.alwaysGenMaxChildrenPerNode ?? false, ancestryArraySize: options?.ancestryArraySize ?? ANCESTRY_LIMIT, + eventsDataStream: options?.eventsDataStream ?? eventsDefaultDataStream, + alertsDataStream: options?.alertsDataStream ?? alertsDefaultDataStream, }; } +const metadataDefaultDataStream = { + type: 'metrics', + dataset: 'endpoint.metadata', + namespace: 'default', +}; + +const policyDefaultDataStream = { + type: 'metrics', + dataset: 'endpoint.policy', + namespace: 'default', +}; + +const eventsDefaultDataStream = { + type: 'logs', + dataset: 'endpoint.events.process', + namespace: 'default', +}; + +const alertsDefaultDataStream = { + type: 'logs', + dataset: 'endpoint.alerts', + namespace: 'default', +}; + export class EndpointDocGenerator { commonInfo: HostInfo; random: seedrandom.prng; sequence: number = 0; + /** + * The EndpointDocGenerator parameters + * + * @param seed either a string to seed the random number generator or a random number generator function + */ constructor(seed: string | seedrandom.prng = Math.random().toString()) { if (typeof seed === 'string') { this.random = seedrandom(seed); } else { this.random = seed; } + this.commonInfo = this.createHostData(); } @@ -383,6 +419,21 @@ export class EndpointDocGenerator { this.commonInfo.Endpoint.policy.applied.status = this.randomChoice(POLICY_RESPONSE_STATUSES); } + /** + * Parses an index and returns the data stream fields extracted from the index. + * + * @param index the index name to parse into the data stream parts + */ + public static createDataStreamFromIndex(index: string): DataStream { + // e.g. logs-endpoint.events.network-default + const parts = index.split('-'); + return { + type: parts[0], // logs + dataset: parts[1], // endpoint.events.network + namespace: parts[2], // default + }; + } + private createHostData(): HostInfo { const hostName = this.randomHostname(); return { @@ -417,8 +468,12 @@ export class EndpointDocGenerator { /** * Creates a host metadata document * @param ts - Timestamp to put in the event + * @param metadataDataStream the values to populate the data_stream fields when generating metadata documents */ - public generateHostMetadata(ts = new Date().getTime()): HostMetadata { + public generateHostMetadata( + ts = new Date().getTime(), + metadataDataStream = metadataDefaultDataStream + ): HostMetadata { return { '@timestamp': ts, event: { @@ -432,6 +487,7 @@ export class EndpointDocGenerator { dataset: 'endpoint.metadata', }, ...this.commonInfo, + data_stream: metadataDataStream, }; } @@ -441,15 +497,24 @@ export class EndpointDocGenerator { * @param entityID - entityID of the originating process * @param parentEntityID - optional entityID of the parent process, if it exists * @param ancestry - an array of ancestors for the generated alert + * @param alertsDataStream the values to populate the data_stream fields when generating alert documents */ - public generateAlert( + public generateAlert({ ts = new Date().getTime(), entityID = this.randomString(10), - parentEntityID?: string, - ancestry: string[] = [] - ): AlertEvent { + parentEntityID, + ancestry = [], + alertsDataStream = alertsDefaultDataStream, + }: { + ts?: number; + entityID?: string; + parentEntityID?: string; + ancestry?: string[]; + alertsDataStream?: DataStream; + } = {}): AlertEvent { return { ...this.commonInfo, + data_stream: alertsDataStream, '@timestamp': ts, ecs: { version: '1.4.0', @@ -598,6 +663,7 @@ export class EndpointDocGenerator { return {}; })(options.eventCategory); return { + data_stream: options?.eventsDataStream ?? eventsDefaultDataStream, '@timestamp': options.timestamp ? options.timestamp : new Date().getTime(), agent: { ...this.commonInfo.agent, type: 'endpoint' }, ecs: { @@ -813,6 +879,7 @@ export class EndpointDocGenerator { const startDate = new Date().getTime(); const root = this.generateEvent({ timestamp: startDate + 1000, + eventsDataStream: opts.eventsDataStream, }); events.push(root); let ancestor = root; @@ -824,18 +891,24 @@ export class EndpointDocGenerator { secBeforeAlert: number, eventList: Event[] ) => { - for (const relatedAlert of this.relatedAlertsGenerator(node, alertsPerNode, secBeforeAlert)) { + for (const relatedAlert of this.relatedAlertsGenerator({ + node, + relatedAlerts: alertsPerNode, + alertCreationTime: secBeforeAlert, + alertsDataStream: opts.alertsDataStream, + })) { eventList.push(relatedAlert); } }; const addRelatedEvents = (node: Event, secBeforeEvent: number, eventList: Event[]) => { - for (const relatedEvent of this.relatedEventsGenerator( + for (const relatedEvent of this.relatedEventsGenerator({ node, - opts.relatedEvents, - secBeforeEvent, - opts.relatedEventsOrdered - )) { + relatedEvents: opts.relatedEvents, + processDuration: secBeforeEvent, + ordered: opts.relatedEventsOrdered, + eventsDataStream: opts.eventsDataStream, + })) { eventList.push(relatedEvent); } }; @@ -857,6 +930,7 @@ export class EndpointDocGenerator { parentEntityID: parentEntityIDSafeVersion(root), eventCategory: ['process'], eventType: ['end'], + eventsDataStream: opts.eventsDataStream, }) ); } @@ -877,6 +951,7 @@ export class EndpointDocGenerator { ancestryArrayLimit: opts.ancestryArraySize, parentPid: firstNonNullValue(ancestor.process?.pid), pid: this.randomN(5000), + eventsDataStream: opts.eventsDataStream, }); events.push(ancestor); timestamp = timestamp + 1000; @@ -892,6 +967,7 @@ export class EndpointDocGenerator { eventType: ['end'], ancestry: ancestryArray(ancestor), ancestryArrayLimit: opts.ancestryArraySize, + eventsDataStream: opts.eventsDataStream, }) ); } @@ -912,12 +988,13 @@ export class EndpointDocGenerator { timestamp = timestamp + 1000; events.push( - this.generateAlert( - timestamp, - entityIDSafeVersion(ancestor), - parentEntityIDSafeVersion(ancestor), - ancestryArray(ancestor) - ) + this.generateAlert({ + ts: timestamp, + entityID: entityIDSafeVersion(ancestor), + parentEntityID: parentEntityIDSafeVersion(ancestor), + ancestry: ancestryArray(ancestor), + alertsDataStream: opts.alertsDataStream, + }) ); return events; } @@ -973,6 +1050,7 @@ export class EndpointDocGenerator { parentEntityID: currentStateEntityID, ancestry, ancestryArrayLimit: opts.ancestryArraySize, + eventsDataStream: opts.eventsDataStream, }); maxChildren = this.randomN(opts.children + 1); @@ -996,16 +1074,23 @@ export class EndpointDocGenerator { eventType: ['end'], ancestry, ancestryArrayLimit: opts.ancestryArraySize, + eventsDataStream: opts.eventsDataStream, }); } if (this.randomN(100) < opts.percentWithRelated) { - yield* this.relatedEventsGenerator( - child, - opts.relatedEvents, + yield* this.relatedEventsGenerator({ + node: child, + relatedEvents: opts.relatedEvents, processDuration, - opts.relatedEventsOrdered - ); - yield* this.relatedAlertsGenerator(child, opts.relatedAlerts, processDuration); + ordered: opts.relatedEventsOrdered, + eventsDataStream: opts.eventsDataStream, + }); + yield* this.relatedAlertsGenerator({ + node: child, + relatedAlerts: opts.relatedAlerts, + alertCreationTime: processDuration, + alertsDataStream: opts.alertsDataStream, + }); } } } @@ -1019,12 +1104,19 @@ export class EndpointDocGenerator { * @param ordered - if true the events will have an increasing timestamp, otherwise their timestamp will be random but * guaranteed to be greater than or equal to the originating event */ - public *relatedEventsGenerator( - node: Event, - relatedEvents: RelatedEventInfo[] | number = 10, - processDuration: number = 6 * 3600, - ordered: boolean = false - ) { + public *relatedEventsGenerator({ + node, + relatedEvents = 10, + processDuration = 6 * 3600, + ordered = false, + eventsDataStream = eventsDefaultDataStream, + }: { + node: Event; + relatedEvents?: RelatedEventInfo[] | number; + processDuration?: number; + ordered?: boolean; + eventsDataStream?: DataStream; + }) { let relatedEventsInfo: RelatedEventInfo[]; const nodeTimestamp = timestampSafeVersion(node) ?? 0; let ts = nodeTimestamp + 1; @@ -1056,6 +1148,7 @@ export class EndpointDocGenerator { eventCategory: eventInfo.category, eventType: eventInfo.creationType, ancestry: ancestryArray(node), + eventsDataStream, }); } } @@ -1067,19 +1160,26 @@ export class EndpointDocGenerator { * @param relatedAlerts - number which defines the number of related alerts to create * @param alertCreationTime - maximum number of seconds after process event that related alert timestamp can be */ - public *relatedAlertsGenerator( - node: Event, - relatedAlerts: number = 3, - alertCreationTime: number = 6 * 3600 - ) { + public *relatedAlertsGenerator({ + node, + relatedAlerts = 3, + alertCreationTime = 6 * 3600, + alertsDataStream = alertsDefaultDataStream, + }: { + node: Event; + relatedAlerts: number; + alertCreationTime: number; + alertsDataStream: DataStream; + }) { for (let i = 0; i < relatedAlerts; i++) { const ts = (timestampSafeVersion(node) ?? 0) + this.randomN(alertCreationTime) * 1000; - yield this.generateAlert( + yield this.generateAlert({ ts, - entityIDSafeVersion(node), - parentEntityIDSafeVersion(node), - ancestryArray(node) - ); + entityID: entityIDSafeVersion(node), + parentEntityID: parentEntityIDSafeVersion(node), + ancestry: ancestryArray(node), + alertsDataStream, + }); } } @@ -1227,15 +1327,21 @@ export class EndpointDocGenerator { /** * Generates a Host Policy response message */ - public generatePolicyResponse( + public generatePolicyResponse({ ts = new Date().getTime(), - allStatus?: HostPolicyResponseActionStatus - ): HostPolicyResponse { + allStatus, + policyDataStream = policyDefaultDataStream, + }: { + ts?: number; + allStatus?: HostPolicyResponseActionStatus; + policyDataStream?: DataStream; + } = {}): HostPolicyResponse { const policyVersion = this.seededUUIDv4(); const status = () => { return allStatus || this.randomHostPolicyResponseActionStatus(); }; return { + data_stream: policyDataStream, '@timestamp': ts, agent: { id: this.commonInfo.agent.id, diff --git a/x-pack/plugins/security_solution/common/endpoint/index_data.ts b/x-pack/plugins/security_solution/common/endpoint/index_data.ts index bf3d12f231c86..c0c70f9ca11af 100644 --- a/x-pack/plugins/security_solution/common/endpoint/index_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/index_data.ts @@ -52,10 +52,9 @@ export async function indexHostsAndAlerts( const epmEndpointPackage = await getEndpointPackageInfo(kbnClient); // Keep a map of host applied policy ids (fake) to real ingest package configs (policy record) const realPolicies: Record = {}; - for (let i = 0; i < numHosts; i++) { const generator = new EndpointDocGenerator(random); - await indexHostDocs( + await indexHostDocs({ numDocs, client, kbnClient, @@ -63,10 +62,17 @@ export async function indexHostsAndAlerts( epmEndpointPackage, metadataIndex, policyResponseIndex, - fleet, - generator - ); - await indexAlerts(client, eventIndex, alertIndex, generator, alertsPerHost, options); + enrollFleet: fleet, + generator, + }); + await indexAlerts({ + client, + eventIndex, + alertIndex, + generator, + numAlerts: alertsPerHost, + options, + }); } await client.indices.refresh({ index: eventIndex, @@ -81,17 +87,27 @@ function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } -async function indexHostDocs( - numDocs: number, - client: Client, - kbnClient: KbnClientWithApiKeySupport, - realPolicies: Record, - epmEndpointPackage: GetPackagesResponse['response'][0], - metadataIndex: string, - policyResponseIndex: string, - enrollFleet: boolean, - generator: EndpointDocGenerator -) { +async function indexHostDocs({ + numDocs, + client, + kbnClient, + realPolicies, + epmEndpointPackage, + metadataIndex, + policyResponseIndex, + enrollFleet, + generator, +}: { + numDocs: number; + client: Client; + kbnClient: KbnClientWithApiKeySupport; + realPolicies: Record; + epmEndpointPackage: GetPackagesResponse['response'][0]; + metadataIndex: string; + policyResponseIndex: string; + enrollFleet: boolean; + generator: EndpointDocGenerator; +}) { const timeBetweenDocs = 6 * 3600 * 1000; // 6 hours between metadata documents const timestamp = new Date().getTime(); let hostMetadata: HostMetadata; @@ -102,7 +118,10 @@ async function indexHostDocs( generator.updateHostData(); generator.updateHostPolicyData(); - hostMetadata = generator.generateHostMetadata(timestamp - timeBetweenDocs * (numDocs - j - 1)); + hostMetadata = generator.generateHostMetadata( + timestamp - timeBetweenDocs * (numDocs - j - 1), + EndpointDocGenerator.createDataStreamFromIndex(metadataIndex) + ); if (enrollFleet) { const { id: appliedPolicyId, name: appliedPolicyName } = hostMetadata.Endpoint.policy.applied; @@ -156,20 +175,30 @@ async function indexHostDocs( }); await client.index({ index: policyResponseIndex, - body: generator.generatePolicyResponse(timestamp - timeBetweenDocs * (numDocs - j - 1)), + body: generator.generatePolicyResponse({ + ts: timestamp - timeBetweenDocs * (numDocs - j - 1), + policyDataStream: EndpointDocGenerator.createDataStreamFromIndex(policyResponseIndex), + }), op_type: 'create', }); } } -async function indexAlerts( - client: Client, - eventIndex: string, - alertIndex: string, - generator: EndpointDocGenerator, - numAlerts: number, - options: TreeOptions = {} -) { +async function indexAlerts({ + client, + eventIndex, + alertIndex, + generator, + numAlerts, + options = {}, +}: { + client: Client; + eventIndex: string; + alertIndex: string; + generator: EndpointDocGenerator; + numAlerts: number; + options: TreeOptions; +}) { const alertGenerator = generator.alertsGenerator(numAlerts, options); let result = alertGenerator.next(); while (!result.done) { diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/policy.ts b/x-pack/plugins/security_solution/common/endpoint/schema/policy.ts index 17d0cdff57ee0..35ba1266066e9 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/policy.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/policy.ts @@ -7,6 +7,6 @@ import { schema } from '@kbn/config-schema'; export const GetPolicyResponseSchema = { query: schema.object({ - hostId: schema.string(), + agentId: schema.string(), }), }; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index 510f1833b793b..f2033e064ef72 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -300,6 +300,15 @@ export interface HostResultList { query_strategy_version: MetadataQueryStrategyVersions; } +/** + * The data_stream fields in an elasticsearch document. + */ +export interface DataStream { + dataset: string; + namespace: string; + type: string; +} + /** * Operating System metadata. */ @@ -556,6 +565,7 @@ export type HostMetadata = Immutable<{ version: string; }; host: Host; + data_stream: DataStream; }>; export interface LegacyEndpointEvent { @@ -675,6 +685,11 @@ export type SafeEndpointEvent = Partial<{ version: ECSField; type: ECSField; }>; + data_stream: Partial<{ + type: ECSField; + dataset: ECSField; + namespace: ECSField; + }>; ecs: Partial<{ version: ECSField; }>; @@ -1002,6 +1017,7 @@ interface HostPolicyResponseAppliedArtifact { */ export interface HostPolicyResponse { '@timestamp': number; + data_stream: DataStream; elastic: { agent: { id: string; diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts index 28889920e00e5..41665cf6d20a4 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts @@ -168,7 +168,7 @@ describe('Custom detection rules creation', () => { goToRuleDetails(); - cy.get(RULE_NAME_HEADER).should('have.text', `${newRule.name} Beta`); + cy.get(RULE_NAME_HEADER).should('have.text', `${newRule.name}`); cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newRule.description); cy.get(ABOUT_DETAILS).within(() => { getDetails(SEVERITY_DETAILS).should('have.text', newRule.severity); @@ -328,7 +328,7 @@ describe('Custom detection rules deletion and edition', () => { fillAboutRule(editedRule); saveEditedRule(); - cy.get(RULE_NAME_HEADER).should('have.text', `${editedRule.name} Beta`); + cy.get(RULE_NAME_HEADER).should('have.text', `${editedRule.name}`); cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', editedRule.description); cy.get(ABOUT_DETAILS).within(() => { getDetails(SEVERITY_DETAILS).should('have.text', editedRule.severity); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts index 252ffb6c8c660..5502f35d6f0f8 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts @@ -131,7 +131,7 @@ describe.skip('Detection rules, EQL', () => { goToRuleDetails(); - cy.get(RULE_NAME_HEADER).should('have.text', `${eqlRule.name} Beta`); + cy.get(RULE_NAME_HEADER).should('have.text', `${eqlRule.name}`); cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', eqlRule.description); cy.get(ABOUT_DETAILS).within(() => { getDetails(SEVERITY_DETAILS).should('have.text', eqlRule.severity); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts index 49ec6381cbc89..0f34e7d71e5fa 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts @@ -115,7 +115,7 @@ describe('Detection rules, machine learning', () => { goToRuleDetails(); - cy.get(RULE_NAME_HEADER).should('have.text', `${machineLearningRule.name} Beta`); + cy.get(RULE_NAME_HEADER).should('have.text', `${machineLearningRule.name}`); cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', machineLearningRule.description); cy.get(ABOUT_DETAILS).within(() => { getDetails(SEVERITY_DETAILS).should('have.text', machineLearningRule.severity); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts index abc873f2df0ee..edf7305f6916e 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts @@ -132,7 +132,7 @@ describe('Detection rules, override', () => { goToRuleDetails(); - cy.get(RULE_NAME_HEADER).should('have.text', `${newOverrideRule.name} Beta`); + cy.get(RULE_NAME_HEADER).should('have.text', `${newOverrideRule.name}`); cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newOverrideRule.description); cy.get(ABOUT_DETAILS).within(() => { getDetails(SEVERITY_DETAILS).should('have.text', newOverrideRule.severity); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts index 9d988a46662fa..5095e856e3f65 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts @@ -129,7 +129,7 @@ describe('Detection rules, threshold', () => { goToRuleDetails(); - cy.get(RULE_NAME_HEADER).should('have.text', `${newThresholdRule.name} Beta`); + cy.get(RULE_NAME_HEADER).should('have.text', `${newThresholdRule.name}`); cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newThresholdRule.description); cy.get(ABOUT_DETAILS).within(() => { getDetails(SEVERITY_DETAILS).should('have.text', newThresholdRule.severity); diff --git a/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts index a45b1fd18a4b6..ec3887ad72625 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts @@ -60,7 +60,7 @@ describe('Cases', () => { createNewCaseWithTimeline(case1); backToCases(); - cy.get(ALL_CASES_PAGE_TITLE).should('have.text', 'Cases Beta'); + cy.get(ALL_CASES_PAGE_TITLE).should('have.text', 'Cases'); cy.get(ALL_CASES_OPEN_CASES_STATS).should('have.text', 'Open cases1'); cy.get(ALL_CASES_CLOSED_CASES_STATS).should('have.text', 'Closed cases0'); cy.get(ALL_CASES_OPEN_CASES_COUNT).should('have.text', 'Open cases (1)'); diff --git a/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts index e2f5ca9025bd9..7ccd588e16a89 100644 --- a/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HOST_STATS, NETWORK_STATS } from '../screens/overview'; +import { HOST_STATS, NETWORK_STATS, OVERVIEW_EMPTY_PAGE } from '../screens/overview'; import { expandHostStats, expandNetworkStats } from '../tasks/overview'; import { loginAndWaitForPage } from '../tasks/login'; import { OVERVIEW_URL } from '../urls/navigation'; +import { esArchiverUnload, esArchiverLoad } from '../tasks/es_archiver'; describe('Overview Page', () => { before(() => { @@ -33,4 +34,19 @@ describe('Overview Page', () => { cy.get(stat.domId).invoke('text').should('eq', stat.value); }); }); + + describe('with no data', () => { + before(() => { + esArchiverUnload('auditbeat'); + loginAndWaitForPage(OVERVIEW_URL); + }); + + after(() => { + esArchiverLoad('auditbeat'); + }); + + it('Splash screen should be here', () => { + cy.get(OVERVIEW_EMPTY_PAGE).should('be.visible'); + }); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts index 8ce60450671b9..9f61d11b7ac0f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts @@ -45,8 +45,7 @@ import { openTimeline } from '../tasks/timelines'; import { OVERVIEW_URL } from '../urls/navigation'; -// FLAKY: https://github.com/elastic/kibana/issues/79389 -describe.skip('Timelines', () => { +describe('Timelines', () => { before(() => { cy.server(); cy.route('PATCH', '**/api/timeline').as('timeline'); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_template_creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_template_creation.spec.ts index 91255d6110d59..377b2100b36cd 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_template_creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_template_creation.spec.ts @@ -12,8 +12,8 @@ import { NOTES_BUTTON, NOTES_COUNT, NOTES_TEXT_AREA, + PIN_EVENT, TIMELINE_DESCRIPTION, - // TIMELINE_FILTER, TIMELINE_QUERY, TIMELINE_TITLE, } from '../screens/timeline'; @@ -35,7 +35,7 @@ import { closeTimeline, createNewTimelineTemplate, markAsFavorite, - openTimelineFromSettings, + openTimelineTemplateFromSettings, populateTimeline, waitForTimelineChanges, } from '../tasks/timeline'; @@ -43,8 +43,7 @@ import { openTimeline } from '../tasks/timelines'; import { OVERVIEW_URL } from '../urls/navigation'; -// FLAKY: https://github.com/elastic/kibana/issues/79967 -describe.skip('Timeline Templates', () => { +describe('Timeline Templates', () => { before(() => { cy.server(); cy.route('PATCH', '**/api/timeline').as('timeline'); @@ -56,12 +55,11 @@ describe.skip('Timeline Templates', () => { createNewTimelineTemplate(); populateTimeline(); addFilter(timeline.filter); - // To fix - // cy.get(PIN_EVENT).should( - // 'have.attr', - // 'aria-label', - // 'This event may not be pinned while editing a template timeline' - // ); + cy.get(PIN_EVENT).should( + 'have.attr', + 'aria-label', + 'This event may not be pinned while editing a template timeline' + ); cy.get(LOCKED_ICON).should('be.visible'); addNameToTimeline(timeline.title); @@ -77,7 +75,7 @@ describe.skip('Timeline Templates', () => { waitForTimelineChanges(); createNewTimelineTemplate(); closeTimeline(); - openTimelineFromSettings(); + openTimelineTemplateFromSettings(timelineId); cy.contains(timeline.title).should('exist'); cy.get(TIMELINES_DESCRIPTION).first().should('have.text', timeline.description); diff --git a/x-pack/plugins/security_solution/cypress/screens/overview.ts b/x-pack/plugins/security_solution/cypress/screens/overview.ts index 95facc8974400..006d5fdf5a665 100644 --- a/x-pack/plugins/security_solution/cypress/screens/overview.ts +++ b/x-pack/plugins/security_solution/cypress/screens/overview.ts @@ -142,3 +142,5 @@ export const NETWORK_STATS = [ export const OVERVIEW_HOST_STATS = '[data-test-subj="overview-hosts-stats"]'; export const OVERVIEW_NETWORK_STATS = '[data-test-subj="overview-network-stats"]'; + +export const OVERVIEW_EMPTY_PAGE = '[data-test-subj="empty-page"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index e397dd9b5a41a..98e6502ffe94f 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -58,6 +58,9 @@ export const NOTES_COUNT = '[data-test-subj="timeline-notes-count"]'; export const OPEN_TIMELINE_ICON = '[data-test-subj="open-timeline-button"]'; +export const OPEN_TIMELINE_TEMPLATE_ICON = + '[data-test-subj="open-timeline-modal-body-filter-template"]'; + export const PIN_EVENT = '[data-test-subj="pin"]'; export const PROVIDER_BADGE = '[data-test-subj="providerBadge"]'; @@ -98,6 +101,8 @@ export const TIMELINE_FILTER = (filter: TimelineFilter) => { export const TIMELINE_FILTER_FIELD = '[data-test-subj="filterFieldSuggestionList"]'; +export const TIMELINE_TITLE_BY_ID = (id: string) => `[data-test-subj="title-${id}"]`; + export const TIMELINE_FILTER_OPERATOR = '[data-test-subj="filterOperatorList"]'; export const TIMELINE_FILTER_VALUE = diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index 7c9c95427a4d0..b101793385488 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -41,9 +41,11 @@ import { TIMELINE_INSPECT_BUTTON, TIMELINE_SETTINGS_ICON, TIMELINE_TITLE, + TIMELINE_TITLE_BY_ID, TIMESTAMP_TOGGLE_FIELD, TOGGLE_TIMELINE_EXPAND_EVENT, CREATE_NEW_TIMELINE_TEMPLATE, + OPEN_TIMELINE_TEMPLATE_ICON, } from '../screens/timeline'; import { TIMELINES_TABLE } from '../screens/timelines'; @@ -69,8 +71,7 @@ export const addNotesToTimeline = (notes: string) => { export const addFilter = (filter: TimelineFilter) => { cy.get(ADD_FILTER).click(); - cy.get(TIMELINE_FILTER_FIELD).type(filter.field); - cy.get(COMBO_BOX).contains(filter.field).click(); + cy.get(TIMELINE_FILTER_FIELD).type(`${filter.field}{downarrow}{enter}`); cy.get(TIMELINE_FILTER_OPERATOR).type(filter.operator); cy.get(COMBO_BOX).contains(filter.operator).click(); if (filter.operator !== 'exists') { @@ -146,6 +147,12 @@ export const openTimelineFromSettings = () => { cy.get(OPEN_TIMELINE_ICON).click({ force: true }); }; +export const openTimelineTemplateFromSettings = (id: string) => { + openTimelineFromSettings(); + cy.get(OPEN_TIMELINE_TEMPLATE_ICON).click({ force: true }); + cy.get(TIMELINE_TITLE_BY_ID(id)).click({ force: true }); +}; + export const openTimelineSettings = () => { cy.get(TIMELINE_SETTINGS_ICON).trigger('click', { force: true }); }; diff --git a/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx index 4f7b17a730b6a..5e4db16d6d9cb 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx @@ -7,18 +7,9 @@ import React from 'react'; import { HeaderPage, HeaderPageProps } from '../../../common/components/header_page'; -import * as i18n from './translations'; const CaseHeaderPageComponent: React.FC = (props) => ( ); -CaseHeaderPageComponent.defaultProps = { - badgeOptions: { - beta: true, - text: i18n.PAGE_BADGE_LABEL, - tooltip: i18n.PAGE_BADGE_TOOLTIP, - }, -}; - export const CaseHeaderPage = React.memo(CaseHeaderPageComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_header_page/translations.ts b/x-pack/plugins/security_solution/public/cases/components/case_header_page/translations.ts deleted file mode 100644 index 8cdc287b1584c..0000000000000 --- a/x-pack/plugins/security_solution/public/cases/components/case_header_page/translations.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const PAGE_BADGE_LABEL = i18n.translate( - 'xpack.securitySolution.case.caseView.pageBadgeLabel', - { - defaultMessage: 'Beta', - } -); - -export const PAGE_BADGE_TOOLTIP = i18n.translate( - 'xpack.securitySolution.case.caseView.pageBadgeTooltip', - { - defaultMessage: - 'Case Workflow is still in beta. Please help us improve by reporting issues or bugs in the Kibana repo.', - } -); diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/translations.ts b/x-pack/plugins/security_solution/public/cases/components/settings/jira/translations.ts index 54c46f064aa75..05a05ac6dd586 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/jira/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/settings/jira/translations.ts @@ -36,14 +36,14 @@ export const GET_ISSUE_API_ERROR = (id: string) => export const SEARCH_ISSUES_COMBO_BOX_ARIA_LABEL = i18n.translate( 'xpack.securitySolution.components.settings.jira.searchIssuesComboBoxAriaLabel', { - defaultMessage: 'Select parent issue', + defaultMessage: 'Type to search', } ); export const SEARCH_ISSUES_PLACEHOLDER = i18n.translate( 'xpack.securitySolution.components.settings.jira.searchIssuesComboBoxPlaceholder', { - defaultMessage: 'Select parent issue', + defaultMessage: 'Type to search', } ); diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap index 4bd2cd05d49d0..8014431192170 100644 --- a/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/__snapshots__/index.test.tsx.snap @@ -1,22 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`item_details_card ItemDetailsAction should render correctly 1`] = ` - - + - - primary - - - + primary + + `; exports[`item_details_card ItemDetailsCard should render correctly with actions 1`] = ` @@ -24,103 +23,98 @@ exports[`item_details_card ItemDetailsCard should render correctly with actions paddingSize="none" > - + + + + + + + + - - - - - - - - + some text + + some node + +
+ + - some text - - some node - + primary + + + secondary + + + - - - - primary - - - - - secondary - - - - - danger - - - + danger + - + - + `; @@ -130,66 +124,61 @@ exports[`item_details_card ItemDetailsCard should render correctly with no actio paddingSize="none" > - + + + + + + + + - - - - - - - - + some text + + some node + +
+ + - - some text - - some node - - - - - - - + gutterSize="s" + justifyContent="flexEnd" + /> + - + `; diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.stories.tsx index e9d1825658bee..74f31a623969c 100644 --- a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.stories.tsx @@ -21,7 +21,7 @@ storiesOf('Components/ItemDetailsCard', module).add('default', () => { - {'content text'} + {'content text '} {'content node'} diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx index c41c5f89c0068..37003961d67d0 100644 --- a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.tsx @@ -81,11 +81,17 @@ ItemDetailsPropertySummary.displayName = 'ItemPropertySummary'; export const ItemDetailsAction: FC> = memo( ({ children, ...rest }) => ( - - - {children} - - + <> + + {children} + + ) ); @@ -99,32 +105,30 @@ export const ItemDetailsCard: FC = memo(({ children }) => { return ( - - - - - - {childElements.get(ItemDetailsPropertySummary)} - - - - - {childElements.get(OTHER_NODES)} - {childElements.has(ItemDetailsAction) && ( - - - {childElements.get(ItemDetailsAction)?.map((action, index) => ( - - {action} - - ))} - - - )} - - + + + + {childElements.get(ItemDetailsPropertySummary)} + + + + + +
{childElements.get(OTHER_NODES)}
+
+ {childElements.has(ItemDetailsAction) && ( + + + {childElements.get(ItemDetailsAction)?.map((action, index) => ( + + {action} + + ))} + + + )}
-
+
); diff --git a/x-pack/plugins/security_solution/public/common/components/tables/__snapshots__/helpers.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/tables/__snapshots__/helpers.test.tsx.snap index 5372ccfcd1188..b585bfc613315 100644 --- a/x-pack/plugins/security_solution/public/common/components/tables/__snapshots__/helpers.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/tables/__snapshots__/helpers.test.tsx.snap @@ -47,8 +47,6 @@ exports[`Table Helpers #getRowItemDraggables it returns correctly against snapsh key="idPrefix-attrName-item1-0" render={[Function]} /> - , - - , - - {index !== 0 && ( - <> - {','} - - - )} = ({ AnomaliesTableComponent, flowTarget, ip, + hostName, indexNames, }) => { const { jobs } = useInstalledSecurityJobs(); @@ -71,6 +72,7 @@ const AnomaliesQueryTabBodyComponent: React.FC = ({ narrowDateRange={narrowDateRange} flowTarget={flowTarget} ip={ip} + hostName={hostName} /> ); diff --git a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/types.ts b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/types.ts index 3ce4b8b6d4494..7621749348a90 100644 --- a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/types.ts +++ b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/types.ts @@ -32,4 +32,5 @@ export type AnomaliesQueryTabBodyProps = QueryTabBodyProps & { updateDateRange?: UpdateDateRange; hideHistogramIfEmpty?: boolean; ip?: string; + hostName?: string; }; diff --git a/x-pack/plugins/security_solution/public/common/store/reducer.test.ts b/x-pack/plugins/security_solution/public/common/store/reducer.test.ts new file mode 100644 index 0000000000000..3e47478b783eb --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/store/reducer.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createInitialState } from './reducer'; + +jest.mock('../lib/kibana', () => ({ + KibanaServices: { + get: jest.fn(() => ({ uiSettings: { get: () => ({ from: 'now-24h', to: 'now' }) } })), + }, +})); + +describe('createInitialState', () => { + describe('sourcerer -> default -> indicesExist', () => { + test('indicesExist should be TRUE if configIndexPatterns is NOT empty', () => { + const initState = createInitialState( + {}, + { + kibanaIndexPatterns: [{ id: '1234567890987654321', title: 'mock-kibana' }], + configIndexPatterns: ['auditbeat-*', 'filebeat'], + } + ); + + expect(initState.sourcerer?.sourcererScopes.default.indicesExist).toEqual(true); + }); + + test('indicesExist should be FALSE if configIndexPatterns is empty', () => { + const initState = createInitialState( + {}, + { + kibanaIndexPatterns: [{ id: '1234567890987654321', title: 'mock-kibana' }], + configIndexPatterns: [], + } + ); + + expect(initState.sourcerer?.sourcererScopes.default.indicesExist).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/store/reducer.ts b/x-pack/plugins/security_solution/public/common/store/reducer.ts index 60cb6a4e960bd..8d528f4279955 100644 --- a/x-pack/plugins/security_solution/public/common/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/store/reducer.ts @@ -43,6 +43,13 @@ export const createInitialState = ( inputs: createInitialInputsState(), sourcerer: { ...sourcererModel.initialSourcererState, + sourcererScopes: { + ...sourcererModel.initialSourcererState.sourcererScopes, + default: { + ...sourcererModel.initialSourcererState.sourcererScopes.default, + indicesExist: configIndexPatterns.length > 0, + }, + }, kibanaIndexPatterns, configIndexPatterns, }, diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts index 93f7ff95dfb00..18aa4e65a03cf 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts @@ -56,7 +56,7 @@ export const initSourcererScope = { errorMessage: null, indexPattern: EMPTY_INDEX_PATTERN, indicesExist: true, - loading: true, + loading: false, selectedPatterns: [], }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 47da1e93cf004..bfc104b105236 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -21,6 +21,7 @@ import { CreateTimeline, UpdateTimelineLoading } from './types'; import { Ecs } from '../../../../common/ecs'; import { TimelineId, TimelineType, TimelineStatus } from '../../../../common/types/timeline'; import { ISearchStart } from '../../../../../../../src/plugins/data/public'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; jest.mock('apollo-client'); @@ -29,7 +30,7 @@ describe('alert actions', () => { const unix = moment(anchor).valueOf(); let createTimeline: CreateTimeline; let updateTimelineIsLoading: UpdateTimelineLoading; - let searchStrategyClient: ISearchStart; + let searchStrategyClient: jest.Mocked; let clock: sinon.SinonFakeTimers; beforeEach(() => { @@ -42,11 +43,13 @@ describe('alert actions', () => { createTimeline = jest.fn() as jest.Mocked; updateTimelineIsLoading = jest.fn() as jest.Mocked; + searchStrategyClient = { aggs: {} as ISearchStart['aggs'], showError: jest.fn(), search: jest.fn().mockResolvedValue({ data: mockTimelineDetails }), searchSource: {} as ISearchStart['searchSource'], + session: dataPluginMock.createStartContract().search.session, }; jest.spyOn(apolloClient, 'query').mockImplementation((obj) => { diff --git a/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.tsx b/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.tsx index 1a2deb059ad4f..293ed4d488c7d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.tsx @@ -7,20 +7,11 @@ import React from 'react'; import { HeaderPage, HeaderPageProps } from '../../../common/components/header_page'; -import * as i18n from './translations'; const DetectionEngineHeaderPageComponent: React.FC = (props) => ( ); -DetectionEngineHeaderPageComponent.defaultProps = { - badgeOptions: { - beta: true, - text: i18n.PAGE_BADGE_LABEL, - tooltip: i18n.PAGE_BADGE_TOOLTIP, - }, -}; - export const DetectionEngineHeaderPage = React.memo(DetectionEngineHeaderPageComponent); DetectionEngineHeaderPage.displayName = 'DetectionEngineHeaderPage'; diff --git a/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/translations.ts b/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/translations.ts deleted file mode 100644 index f59be16923805..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/translations.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const PAGE_BADGE_LABEL = i18n.translate( - 'xpack.securitySolution.detectionEngine.headerPage.pageBadgeLabel', - { - defaultMessage: 'Beta', - } -); - -export const PAGE_BADGE_TOOLTIP = i18n.translate( - 'xpack.securitySolution.detectionEngine.headerPage.pageBadgeTooltip', - { - defaultMessage: - 'Alerts is still in beta. Please help us improve by reporting issues or bugs in the Kibana repo.', - } -); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx index a41da908085bc..75ab1524c5c06 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx @@ -5,10 +5,12 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { waitFor } from '@testing-library/react'; +import { shallow, mount, ReactWrapper } from 'enzyme'; import '../../../../common/mock/match_media'; import { PrePackagedRulesPrompt } from './load_empty_prompt'; +import { getPrePackagedRulesStatus } from '../../../containers/detection_engine/rules/api'; jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -23,16 +25,94 @@ jest.mock('react-router-dom', () => { jest.mock('../../../../common/components/link_to'); +jest.mock('../../../containers/detection_engine/rules/api', () => ({ + getPrePackagedRulesStatus: jest.fn().mockResolvedValue({ + rules_not_installed: 0, + rules_installed: 0, + rules_not_updated: 0, + timelines_not_installed: 0, + timelines_installed: 0, + timelines_not_updated: 0, + }), + createPrepackagedRules: jest.fn(), +})); + +const props = { + createPrePackagedRules: jest.fn(), + loading: false, + userHasNoPermissions: false, + 'data-test-subj': 'load-prebuilt-rules', +}; + describe('PrePackagedRulesPrompt', () => { it('renders correctly', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(); expect(wrapper.find('EmptyPrompt')).toHaveLength(1); }); }); + +describe('LoadPrebuiltRulesAndTemplatesButton', () => { + it('renders correct button with correct text - Load Elastic prebuilt rules and timeline templates', async () => { + (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_not_installed: 3, + rules_installed: 0, + rules_not_updated: 0, + timelines_not_installed: 3, + timelines_installed: 0, + timelines_not_updated: 0, + }); + + const wrapper: ReactWrapper = mount(); + await waitFor(() => { + wrapper.update(); + + expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').last().text()).toEqual( + 'Load Elastic prebuilt rules and timeline templates' + ); + }); + }); + + it('renders correct button with correct text - Load Elastic prebuilt rules', async () => { + (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_not_installed: 3, + rules_installed: 0, + rules_not_updated: 0, + timelines_not_installed: 0, + timelines_installed: 0, + timelines_not_updated: 0, + }); + + const wrapper: ReactWrapper = mount(); + await waitFor(() => { + wrapper.update(); + + expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').last().text()).toEqual( + 'Load Elastic prebuilt rules' + ); + }); + }); + + it('renders correct button with correct text - Load Elastic prebuilt timeline templates', async () => { + (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_not_installed: 0, + rules_installed: 0, + rules_not_updated: 0, + timelines_not_installed: 3, + timelines_installed: 0, + timelines_not_updated: 0, + }); + + const wrapper: ReactWrapper = mount(); + await waitFor(() => { + wrapper.update(); + + expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="load-prebuilt-rules"]').last().text()).toEqual( + 'Load Elastic prebuilt timeline templates' + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx index 99968cd4d9fe8..64b3cfa3aa991 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; -import React, { memo, useCallback } from 'react'; +import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { memo, useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { useHistory } from 'react-router-dom'; @@ -14,6 +14,8 @@ import * as i18n from './translations'; import { LinkButton } from '../../../../common/components/links'; import { SecurityPageName } from '../../../../app/types'; import { useFormatUrl } from '../../../../common/components/link_to'; +import { usePrePackagedRules } from '../../../containers/detection_engine/rules'; +import { useUserData } from '../../user_info'; const EmptyPrompt = styled(EuiEmptyPrompt)` align-self: center; /* Corrects horizontal centering in IE11 */ @@ -46,24 +48,36 @@ const PrePackagedRulesPromptComponent: React.FC = ( [history] ); + const [ + { isSignalIndexExists, isAuthenticated, hasEncryptionKey, canUserCRUD, hasIndexWrite }, + ] = useUserData(); + + const { getLoadPrebuiltRulesAndTemplatesButton } = usePrePackagedRules({ + canUserCRUD, + hasIndexWrite, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + }); + + const loadPrebuiltRulesAndTemplatesButton = useMemo( + () => + getLoadPrebuiltRulesAndTemplatesButton({ + isDisabled: userHasNoPermissions, + onClick: handlePreBuiltCreation, + fill: true, + 'data-test-subj': 'load-prebuilt-rules', + }), + [getLoadPrebuiltRulesAndTemplatesButton, handlePreBuiltCreation, userHasNoPermissions] + ); + return ( {i18n.PRE_BUILT_TITLE}} body={

{i18n.PRE_BUILT_MSG}

} actions={ - - - {i18n.PRE_BUILT_ACTION} - - + {loadPrebuiltRulesAndTemplatesButton} = ({ }, [onSubmit]); useEffect(() => { - if (setForm) { + let didCancel = false; + if (setForm && !didCancel) { setForm(RuleStep.aboutRule, getData); } + return () => { + didCancel = true; + }; }, [getData, setForm]); return isReadOnlyView ? ( diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index 63b283308dd59..27d69c6887011 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -190,9 +190,13 @@ const StepDefineRuleComponent: FC = ({ }, [getFormData, submit]); useEffect(() => { - if (setForm) { + let didCancel = false; + if (setForm && !didCancel) { setForm(RuleStep.defineRule, getData); } + return () => { + didCancel = true; + }; }, [getData, setForm]); const handleResetIndices = useCallback(() => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx index de473b6fc6aec..349a79d4e40f9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx @@ -120,9 +120,13 @@ const StepRuleActionsComponent: FC = ({ }, [getFormData, submit]); useEffect(() => { - if (setForm) { + let didCancel = false; + if (setForm && !didCancel) { setForm(RuleStep.ruleActions, getData); } + return () => { + didCancel = true; + }; }, [getData, setForm]); const throttleOptions = useMemo(() => { @@ -142,55 +146,59 @@ const StepRuleActionsComponent: FC = ({ [isLoading, throttleOptions] ); - if (isReadOnlyView) { - return ( - - - - ); - } - - const displayActionsOptions = - throttle !== stepActionsDefaultValue.throttle ? ( + const displayActionsOptions = useMemo( + () => + throttle !== stepActionsDefaultValue.throttle ? ( + <> + + + + ) : ( + + ), + [throttle, actionMessageParams] + ); + // only display the actions dropdown if the user has "read" privileges for actions + const displayActionsDropDown = useMemo(() => { + return application.capabilities.actions.show ? ( <> - + {displayActionsOptions} + + ) : ( - + <> + {I18n.NO_ACTIONS_READ_PERMISSIONS} + + + + + ); + }, [application.capabilities.actions.show, displayActionsOptions, throttleFieldComponentProps]); - // only display the actions dropdown if the user has "read" privileges for actions - const displayActionsDropDown = application.capabilities.actions.show ? ( - <> - - {displayActionsOptions} - - - - ) : ( - <> - {I18n.NO_ACTIONS_READ_PERMISSIONS} - - - - - - ); + if (isReadOnlyView) { + return ( + + + + ); + } return ( <> diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx index d451932a6b634..0bc06e3dafc69 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx @@ -64,9 +64,13 @@ const StepScheduleRuleComponent: FC = ({ }, [getFormData, submit]); useEffect(() => { - if (setForm) { + let didCancel = false; + if (setForm && !didCancel) { setForm(RuleStep.scheduleRule, getData); } + return () => { + didCancel = true; + }; }, [getData, setForm]); return isReadOnlyView ? ( diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx index b01edac2605eb..9b15007136b2e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx @@ -4,20 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { renderHook } from '@testing-library/react-hooks'; -import { useUserInfo } from './index'; +import { renderHook, act } from '@testing-library/react-hooks'; +import { useUserInfo, ManageUserInfo } from './index'; -import { usePrivilegeUser } from '../../containers/detection_engine/alerts/use_privilege_user'; -import { useSignalIndex } from '../../containers/detection_engine/alerts/use_signal_index'; import { useKibana } from '../../../common/lib/kibana'; -jest.mock('../../containers/detection_engine/alerts/use_privilege_user'); -jest.mock('../../containers/detection_engine/alerts/use_signal_index'); +import * as api from '../../containers/detection_engine/alerts/api'; + jest.mock('../../../common/lib/kibana'); +jest.mock('../../containers/detection_engine/alerts/api'); describe('useUserInfo', () => { beforeAll(() => { - (usePrivilegeUser as jest.Mock).mockReturnValue({}); - (useSignalIndex as jest.Mock).mockReturnValue({}); (useKibana as jest.Mock).mockReturnValue({ services: { application: { @@ -30,21 +27,40 @@ describe('useUserInfo', () => { }, }); }); - it('returns default state', () => { - const { result } = renderHook(() => useUserInfo()); + it('returns default state', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useUserInfo()); + await waitForNextUpdate(); - expect(result).toEqual({ - current: { - canUserCRUD: null, - hasEncryptionKey: null, - hasIndexManage: null, - hasIndexWrite: null, - isAuthenticated: null, - isSignalIndexExists: null, - loading: true, - signalIndexName: null, - }, - error: undefined, + expect(result).toEqual({ + current: { + canUserCRUD: null, + hasEncryptionKey: null, + hasIndexManage: null, + hasIndexWrite: null, + isAuthenticated: null, + isSignalIndexExists: null, + loading: true, + signalIndexName: null, + signalIndexTemplateOutdated: null, + }, + error: undefined, + }); + }); + }); + + it('calls createSignalIndex if signal index template is outdated', async () => { + const spyOnCreateSignalIndex = jest.spyOn(api, 'createSignalIndex'); + const spyOnGetSignalIndex = jest.spyOn(api, 'getSignalIndex').mockResolvedValueOnce({ + name: 'mock-signal-index', + template_outdated: true, + }); + await act(async () => { + const { waitForNextUpdate } = renderHook(() => useUserInfo(), { wrapper: ManageUserInfo }); + await waitForNextUpdate(); + await waitForNextUpdate(); }); + expect(spyOnGetSignalIndex).toHaveBeenCalledTimes(2); + expect(spyOnCreateSignalIndex).toHaveBeenCalledTimes(1); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx index 92d1491707260..ac2bf438d7fa6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx @@ -20,6 +20,7 @@ export interface State { hasEncryptionKey: boolean | null; loading: boolean; signalIndexName: string | null; + signalIndexTemplateOutdated: boolean | null; } export const initialState: State = { @@ -31,6 +32,7 @@ export const initialState: State = { hasEncryptionKey: null, loading: true, signalIndexName: null, + signalIndexTemplateOutdated: null, }; export type Action = @@ -62,6 +64,10 @@ export type Action = | { type: 'updateSignalIndexName'; signalIndexName: string | null; + } + | { + type: 'updateSignalIndexTemplateOutdated'; + signalIndexTemplateOutdated: boolean | null; }; export const userInfoReducer = (state: State, action: Action): State => { @@ -114,6 +120,12 @@ export const userInfoReducer = (state: State, action: Action): State => { signalIndexName: action.signalIndexName, }; } + case 'updateSignalIndexTemplateOutdated': { + return { + ...state, + signalIndexTemplateOutdated: action.signalIndexTemplateOutdated, + }; + } default: return state; } @@ -144,6 +156,7 @@ export const useUserInfo = (): State => { hasEncryptionKey, loading, signalIndexName, + signalIndexTemplateOutdated, }, dispatch, ] = useUserData(); @@ -158,6 +171,7 @@ export const useUserInfo = (): State => { loading: indexNameLoading, signalIndexExists: isApiSignalIndexExists, signalIndexName: apiSignalIndexName, + signalIndexTemplateOutdated: apiSignalIndexTemplateOutdated, createDeSignalIndex: createSignalIndex, } = useSignalIndex(); @@ -166,7 +180,7 @@ export const useUserInfo = (): State => { typeof uiCapabilities.siem.crud === 'boolean' ? uiCapabilities.siem.crud : false; useEffect(() => { - if (loading !== privilegeLoading || indexNameLoading) { + if (loading !== (privilegeLoading || indexNameLoading)) { dispatch({ type: 'updateLoading', loading: privilegeLoading || indexNameLoading }); } }, [dispatch, loading, privilegeLoading, indexNameLoading]); @@ -217,18 +231,38 @@ export const useUserInfo = (): State => { } }, [dispatch, loading, signalIndexName, apiSignalIndexName]); + useEffect(() => { + if ( + !loading && + signalIndexTemplateOutdated !== apiSignalIndexTemplateOutdated && + apiSignalIndexTemplateOutdated != null + ) { + dispatch({ + type: 'updateSignalIndexTemplateOutdated', + signalIndexTemplateOutdated: apiSignalIndexTemplateOutdated, + }); + } + }, [dispatch, loading, signalIndexTemplateOutdated, apiSignalIndexTemplateOutdated]); + useEffect(() => { if ( isAuthenticated && hasEncryptionKey && hasIndexManage && - isSignalIndexExists != null && - !isSignalIndexExists && + ((isSignalIndexExists != null && !isSignalIndexExists) || + (signalIndexTemplateOutdated != null && signalIndexTemplateOutdated)) && createSignalIndex != null ) { createSignalIndex(); } - }, [createSignalIndex, isAuthenticated, hasEncryptionKey, isSignalIndexExists, hasIndexManage]); + }, [ + createSignalIndex, + isAuthenticated, + hasEncryptionKey, + isSignalIndexExists, + hasIndexManage, + signalIndexTemplateOutdated, + ]); return { loading, @@ -239,5 +273,6 @@ export const useUserInfo = (): State => { hasIndexManage, hasIndexWrite, signalIndexName, + signalIndexTemplateOutdated, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts index cd2cc1fe390ba..4fd240348f0f3 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts @@ -980,6 +980,7 @@ export const mockStatusAlertQuery: object = { export const mockSignalIndex: AlertsIndex = { name: 'mock-signal-index', + template_outdated: false, }; export const mockUserPrivilege: Privilege = { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts index 2eb2145c6c34d..59ab416ecc824 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts @@ -44,6 +44,7 @@ export interface UpdateAlertStatusProps { export interface AlertsIndex { name: string; + template_outdated: boolean; } export interface Privilege { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx index d0571bfca5b2b..1db952526414a 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx @@ -26,6 +26,7 @@ describe('useSignalIndex', () => { loading: true, signalIndexExists: null, signalIndexName: null, + signalIndexTemplateOutdated: null, }); }); }); @@ -42,6 +43,7 @@ describe('useSignalIndex', () => { loading: false, signalIndexExists: true, signalIndexName: 'mock-signal-index', + signalIndexTemplateOutdated: false, }); }); }); @@ -62,6 +64,7 @@ describe('useSignalIndex', () => { loading: false, signalIndexExists: true, signalIndexName: 'mock-signal-index', + signalIndexTemplateOutdated: false, }); }); }); @@ -101,6 +104,7 @@ describe('useSignalIndex', () => { loading: false, signalIndexExists: false, signalIndexName: null, + signalIndexTemplateOutdated: null, }); }); }); @@ -121,6 +125,7 @@ describe('useSignalIndex', () => { loading: false, signalIndexExists: false, signalIndexName: null, + signalIndexTemplateOutdated: null, }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx index 14fd9ffa50843..f7d2202736169 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.tsx @@ -17,6 +17,7 @@ export interface ReturnSignalIndex { loading: boolean; signalIndexExists: boolean | null; signalIndexName: string | null; + signalIndexTemplateOutdated: boolean | null; createDeSignalIndex: Func | null; } @@ -27,11 +28,10 @@ export interface ReturnSignalIndex { */ export const useSignalIndex = (): ReturnSignalIndex => { const [loading, setLoading] = useState(true); - const [signalIndex, setSignalIndex] = useState< - Pick - >({ + const [signalIndex, setSignalIndex] = useState>({ signalIndexExists: null, signalIndexName: null, + signalIndexTemplateOutdated: null, createDeSignalIndex: null, }); const [, dispatchToaster] = useStateToaster(); @@ -49,6 +49,7 @@ export const useSignalIndex = (): ReturnSignalIndex => { setSignalIndex({ signalIndexExists: true, signalIndexName: signal.name, + signalIndexTemplateOutdated: signal.template_outdated, createDeSignalIndex: createIndex, }); } @@ -57,6 +58,7 @@ export const useSignalIndex = (): ReturnSignalIndex => { setSignalIndex({ signalIndexExists: false, signalIndexName: null, + signalIndexTemplateOutdated: null, createDeSignalIndex: createIndex, }); if (isSecurityAppError(error) && error.body.status_code !== 404) { @@ -87,6 +89,7 @@ export const useSignalIndex = (): ReturnSignalIndex => { setSignalIndex({ signalIndexExists: false, signalIndexName: null, + signalIndexTemplateOutdated: null, createDeSignalIndex: createIndex, }); errorToToaster({ title: i18n.SIGNAL_POST_FAILURE, error, dispatchToaster }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts index f878b40b99dc3..721790a36b27f 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts @@ -40,3 +40,57 @@ export const TAG_FETCH_FAILURE = i18n.translate( defaultMessage: 'Failed to fetch Tags', } ); + +export const LOAD_PREPACKAGED_RULES = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.loadPrePackagedRulesButton', + { + defaultMessage: 'Load Elastic prebuilt rules', + } +); + +export const LOAD_PREPACKAGED_TIMELINE_TEMPLATES = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.loadPrePackagedTimelineTemplatesButton', + { + defaultMessage: 'Load Elastic prebuilt timeline templates', + } +); + +export const LOAD_PREPACKAGED_RULES_AND_TEMPLATES = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.loadPrePackagedRulesAndTemplatesButton', + { + defaultMessage: 'Load Elastic prebuilt rules and timeline templates', + } +); + +export const RELOAD_MISSING_PREPACKAGED_RULES = (missingRules: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesButton', + { + values: { missingRules }, + defaultMessage: + 'Install {missingRules} Elastic prebuilt {missingRules, plural, =1 {rule} other {rules}} ', + } + ); + +export const RELOAD_MISSING_PREPACKAGED_TIMELINES = (missingTimelines: number) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedTimelinesButton', + { + values: { missingTimelines }, + defaultMessage: + 'Install {missingTimelines} Elastic prebuilt {missingTimelines, plural, =1 {timeline} other {timelines}} ', + } + ); + +export const RELOAD_MISSING_PREPACKAGED_RULES_AND_TIMELINES = ( + missingRules: number, + missingTimelines: number +) => + i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesAndTimelinesButton', + { + values: { missingRules, missingTimelines }, + defaultMessage: + 'Install {missingRules} Elastic prebuilt {missingRules, plural, =1 {rule} other {rules}} and {missingTimelines} Elastic prebuilt {missingTimelines, plural, =1 {timeline} other {timelines}} ', + } + ); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx index 92d46a785b034..7f74e92584494 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx @@ -32,6 +32,10 @@ describe('usePrePackagedRules', () => { await waitForNextUpdate(); expect(result.current).toEqual({ + getLoadPrebuiltRulesAndTemplatesButton: + result.current.getLoadPrebuiltRulesAndTemplatesButton, + getReloadPrebuiltRulesAndTemplatesButton: + result.current.getReloadPrebuiltRulesAndTemplatesButton, createPrePackagedRules: null, loading: true, loadingCreatePrePackagedRules: false, @@ -63,6 +67,10 @@ describe('usePrePackagedRules', () => { await waitForNextUpdate(); expect(result.current).toEqual({ + getLoadPrebuiltRulesAndTemplatesButton: + result.current.getLoadPrebuiltRulesAndTemplatesButton, + getReloadPrebuiltRulesAndTemplatesButton: + result.current.getReloadPrebuiltRulesAndTemplatesButton, createPrePackagedRules: result.current.createPrePackagedRules, loading: false, loadingCreatePrePackagedRules: false, @@ -100,6 +108,10 @@ describe('usePrePackagedRules', () => { expect(resp).toEqual(true); expect(spyOnCreatePrepackagedRules).toHaveBeenCalled(); expect(result.current).toEqual({ + getLoadPrebuiltRulesAndTemplatesButton: + result.current.getLoadPrebuiltRulesAndTemplatesButton, + getReloadPrebuiltRulesAndTemplatesButton: + result.current.getReloadPrebuiltRulesAndTemplatesButton, createPrePackagedRules: result.current.createPrePackagedRules, loading: false, loadingCreatePrePackagedRules: false, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx index d82d97883a3d0..4d19f44bcfc84 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_pre_packaged_rules.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useState } from 'react'; +import React, { useCallback, useMemo, useState, useEffect } from 'react'; +import { EuiButton } from '@elastic/eui'; import { errorToToaster, @@ -14,6 +15,11 @@ import { import { getPrePackagedRulesStatus, createPrepackagedRules } from './api'; import * as i18n from './translations'; +import { + getPrePackagedRuleStatus, + getPrePackagedTimelineStatus, +} from '../../../pages/detection_engine/rules/helpers'; + type Func = () => void; export type CreatePreBuiltRules = () => Promise; @@ -23,6 +29,23 @@ interface ReturnPrePackagedTimelines { timelinesNotUpdated: number | null; } +type GetLoadPrebuiltRulesAndTemplatesButton = (args: { + isDisabled: boolean; + onClick: () => void; + fill?: boolean; + 'data-test-subj'?: string; +}) => React.ReactNode | null; + +type GetReloadPrebuiltRulesAndTemplatesButton = ({ + isDisabled, + onClick, + fill, +}: { + isDisabled: boolean; + onClick: () => void; + fill?: boolean; +}) => React.ReactNode | null; + interface ReturnPrePackagedRules { createPrePackagedRules: null | CreatePreBuiltRules; loading: boolean; @@ -32,6 +55,8 @@ interface ReturnPrePackagedRules { rulesInstalled: number | null; rulesNotInstalled: number | null; rulesNotUpdated: number | null; + getLoadPrebuiltRulesAndTemplatesButton: GetLoadPrebuiltRulesAndTemplatesButton; + getReloadPrebuiltRulesAndTemplatesButton: GetReloadPrebuiltRulesAndTemplatesButton; } export type ReturnPrePackagedRulesAndTimelines = ReturnPrePackagedRules & @@ -89,7 +114,6 @@ export const usePrePackagedRules = ({ const [loadingCreatePrePackagedRules, setLoadingCreatePrePackagedRules] = useState(false); const [loading, setLoading] = useState(true); const [, dispatchToaster] = useStateToaster(); - useEffect(() => { let isSubscribed = true; const abortCtrl = new AbortController(); @@ -100,7 +124,6 @@ export const usePrePackagedRules = ({ const prePackagedRuleStatusResponse = await getPrePackagedRulesStatus({ signal: abortCtrl.signal, }); - if (isSubscribed) { setPrepackagedDataStatus({ createPrePackagedRules: createElasticRules, @@ -225,9 +248,108 @@ export const usePrePackagedRules = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [canUserCRUD, hasIndexWrite, isAuthenticated, hasEncryptionKey, isSignalIndexExists]); + const prePackagedRuleStatus = useMemo( + () => + getPrePackagedRuleStatus( + prepackagedDataStatus.rulesInstalled, + prepackagedDataStatus.rulesNotInstalled, + prepackagedDataStatus.rulesNotUpdated + ), + [ + prepackagedDataStatus.rulesInstalled, + prepackagedDataStatus.rulesNotInstalled, + prepackagedDataStatus.rulesNotUpdated, + ] + ); + + const prePackagedTimelineStatus = useMemo( + () => + getPrePackagedTimelineStatus( + prepackagedDataStatus.timelinesInstalled, + prepackagedDataStatus.timelinesNotInstalled, + prepackagedDataStatus.timelinesNotUpdated + ), + [ + prepackagedDataStatus.timelinesInstalled, + prepackagedDataStatus.timelinesNotInstalled, + prepackagedDataStatus.timelinesNotUpdated, + ] + ); + const getLoadPrebuiltRulesAndTemplatesButton = useCallback( + ({ isDisabled, onClick, fill, 'data-test-subj': dataTestSubj = 'loadPrebuiltRulesBtn' }) => { + return prePackagedRuleStatus === 'ruleNotInstalled' || + prePackagedTimelineStatus === 'timelinesNotInstalled' ? ( + + {prePackagedRuleStatus === 'ruleNotInstalled' && + prePackagedTimelineStatus === 'timelinesNotInstalled' && + i18n.LOAD_PREPACKAGED_RULES_AND_TEMPLATES} + + {prePackagedRuleStatus === 'ruleNotInstalled' && + prePackagedTimelineStatus !== 'timelinesNotInstalled' && + i18n.LOAD_PREPACKAGED_RULES} + + {prePackagedRuleStatus !== 'ruleNotInstalled' && + prePackagedTimelineStatus === 'timelinesNotInstalled' && + i18n.LOAD_PREPACKAGED_TIMELINE_TEMPLATES} + + ) : null; + }, + [loadingCreatePrePackagedRules, prePackagedRuleStatus, prePackagedTimelineStatus] + ); + + const getMissingRulesOrTimelinesButtonTitle = useCallback( + (missingRules: number, missingTimelines: number) => { + if (missingRules > 0 && missingTimelines === 0) + return i18n.RELOAD_MISSING_PREPACKAGED_RULES(missingRules); + else if (missingRules === 0 && missingTimelines > 0) + return i18n.RELOAD_MISSING_PREPACKAGED_TIMELINES(missingTimelines); + else if (missingRules > 0 && missingTimelines > 0) + return i18n.RELOAD_MISSING_PREPACKAGED_RULES_AND_TIMELINES(missingRules, missingTimelines); + }, + [] + ); + + const getReloadPrebuiltRulesAndTemplatesButton = useCallback( + ({ isDisabled, onClick, fill = false }) => { + return prePackagedRuleStatus === 'someRuleUninstall' || + prePackagedTimelineStatus === 'someTimelineUninstall' ? ( + + {getMissingRulesOrTimelinesButtonTitle( + prepackagedDataStatus.rulesNotInstalled ?? 0, + prepackagedDataStatus.timelinesNotInstalled ?? 0 + )} + + ) : null; + }, + [ + getMissingRulesOrTimelinesButtonTitle, + loadingCreatePrePackagedRules, + prePackagedRuleStatus, + prePackagedTimelineStatus, + prepackagedDataStatus.rulesNotInstalled, + prepackagedDataStatus.timelinesNotInstalled, + ] + ); + return { loading, loadingCreatePrePackagedRules, ...prepackagedDataStatus, + getLoadPrebuiltRulesAndTemplatesButton, + getReloadPrebuiltRulesAndTemplatesButton, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx index 886a24dd7cbe8..58e61c5b47486 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx @@ -5,13 +5,14 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, mount, ReactWrapper } from 'enzyme'; import '../../../../common/mock/match_media'; import { RulesPage } from './index'; import { useUserData } from '../../../components/user_info'; -import { usePrePackagedRules } from '../../../containers/detection_engine/rules'; - +import { waitFor } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import { getPrePackagedRulesStatus } from '../../../containers/detection_engine/rules/api'; jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -26,16 +27,164 @@ jest.mock('react-router-dom', () => { jest.mock('../../../containers/detection_engine/lists/use_lists_config'); jest.mock('../../../../common/components/link_to'); jest.mock('../../../components/user_info'); -jest.mock('../../../containers/detection_engine/rules'); +jest.mock('../../../../common/components/toasters', () => { + const actual = jest.requireActual('../../../../common/components/toasters'); + return { + ...actual, + errorToToaster: jest.fn(), + useStateToaster: jest.fn().mockReturnValue([jest.fn(), jest.fn()]), + displaySuccessToast: jest.fn(), + }; +}); + +jest.mock('../../../containers/detection_engine/rules/api', () => ({ + getPrePackagedRulesStatus: jest.fn().mockResolvedValue({ + rules_not_installed: 0, + rules_installed: 0, + rules_not_updated: 0, + timelines_not_installed: 0, + timelines_installed: 0, + timelines_not_updated: 0, + }), + createPrepackagedRules: jest.fn(), +})); + +jest.mock('../../../../common/lib/kibana', () => { + return { + useToast: jest.fn(), + useHttp: jest.fn(), + }; +}); + +jest.mock('../../../components/value_lists_management_modal', () => { + return { + ValueListsModal: jest.fn().mockReturnValue(
), + }; +}); + +jest.mock('./all', () => { + return { + AllRules: jest.fn().mockReturnValue(
), + }; +}); + +jest.mock('../../../../common/utils/route/spy_routes', () => { + return { + SpyRoute: jest.fn().mockReturnValue(
), + }; +}); + +jest.mock('../../../components/rules/pre_packaged_rules/update_callout', () => { + return { + UpdatePrePackagedRulesCallOut: jest.fn().mockReturnValue(
), + }; +}); describe('RulesPage', () => { beforeAll(() => { (useUserData as jest.Mock).mockReturnValue([{}]); - (usePrePackagedRules as jest.Mock).mockReturnValue({}); }); - it('renders correctly', () => { + + it('renders AllRules', () => { const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="all-rules"]').exists()).toEqual(true); + }); + + it('renders correct button with correct text - Load Elastic prebuilt rules and timeline templates', async () => { + (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_not_installed: 3, + rules_installed: 0, + rules_not_updated: 0, + timelines_not_installed: 3, + timelines_installed: 0, + timelines_not_updated: 0, + }); + + const wrapper: ReactWrapper = mount( + + + + ); + await waitFor(() => { + wrapper.update(); + + expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').last().text()).toEqual( + 'Load Elastic prebuilt rules and timeline templates' + ); + }); + }); + + it('renders correct button with correct text - Load Elastic prebuilt rules', async () => { + (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_not_installed: 3, + rules_installed: 0, + rules_not_updated: 0, + timelines_not_installed: 0, + timelines_installed: 0, + timelines_not_updated: 0, + }); + + const wrapper: ReactWrapper = mount( + + + + ); + + await waitFor(() => { + wrapper.update(); + + expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').last().text()).toEqual( + 'Load Elastic prebuilt rules' + ); + }); + }); + + it('renders correct button with correct text - Load Elastic prebuilt timeline templates', async () => { + (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_not_installed: 0, + rules_installed: 0, + rules_not_updated: 0, + timelines_not_installed: 3, + timelines_installed: 0, + timelines_not_updated: 0, + }); + + const wrapper: ReactWrapper = mount( + + + + ); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="loadPrebuiltRulesBtn"]').last().text()).toEqual( + 'Load Elastic prebuilt timeline templates' + ); + }); + }); + + it('renders a callout - Update Elastic prebuilt rules', async () => { + (getPrePackagedRulesStatus as jest.Mock).mockResolvedValue({ + rules_not_installed: 2, + rules_installed: 1, + rules_not_updated: 1, + timelines_not_installed: 0, + timelines_installed: 0, + timelines_not_updated: 0, + }); + + const wrapper: ReactWrapper = mount( + + + + ); - expect(wrapper.find('AllRules')).toHaveLength(1); + await waitFor(() => { + wrapper.update(); + expect(wrapper.find('[data-test-subj="update-callout-button"]').exists()).toEqual(true); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx index 53c82569f94ae..8c7cb6a0d9284 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx @@ -5,7 +5,7 @@ */ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; -import React, { useCallback, useRef, useState } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import { useHistory } from 'react-router-dom'; import { usePrePackagedRules, importRules } from '../../../containers/detection_engine/rules'; @@ -70,6 +70,8 @@ const RulesPageComponent: React.FC = () => { timelinesInstalled, timelinesNotInstalled, timelinesNotUpdated, + getLoadPrebuiltRulesAndTemplatesButton, + getReloadPrebuiltRulesAndTemplatesButton, } = usePrePackagedRules({ canUserCRUD, hasIndexWrite, @@ -113,18 +115,6 @@ const RulesPageComponent: React.FC = () => { refreshRulesData.current = refreshRule; }, []); - const getMissingRulesOrTimelinesButtonTitle = useCallback( - (missingRules: number, missingTimelines: number) => { - if (missingRules > 0 && missingTimelines === 0) - return i18n.RELOAD_MISSING_PREPACKAGED_RULES(missingRules); - else if (missingRules === 0 && missingTimelines > 0) - return i18n.RELOAD_MISSING_PREPACKAGED_TIMELINES(missingTimelines); - else if (missingRules > 0 && missingTimelines > 0) - return i18n.RELOAD_MISSING_PREPACKAGED_RULES_AND_TIMELINES(missingRules, missingTimelines); - }, - [] - ); - const goToNewRule = useCallback( (ev) => { ev.preventDefault(); @@ -133,6 +123,24 @@ const RulesPageComponent: React.FC = () => { [history] ); + const loadPrebuiltRulesAndTemplatesButton = useMemo( + () => + getLoadPrebuiltRulesAndTemplatesButton({ + isDisabled: userHasNoPermissions(canUserCRUD) || loading, + onClick: handleCreatePrePackagedRules, + }), + [canUserCRUD, getLoadPrebuiltRulesAndTemplatesButton, handleCreatePrePackagedRules, loading] + ); + + const reloadPrebuiltRulesAndTemplatesButton = useMemo( + () => + getReloadPrebuiltRulesAndTemplatesButton({ + isDisabled: userHasNoPermissions(canUserCRUD) || loading, + onClick: handleCreatePrePackagedRules, + }), + [canUserCRUD, getReloadPrebuiltRulesAndTemplatesButton, handleCreatePrePackagedRules, loading] + ); + if ( redirectToDetections( isSignalIndexExists, @@ -177,35 +185,11 @@ const RulesPageComponent: React.FC = () => { title={i18n.PAGE_TITLE} > - {(prePackagedRuleStatus === 'ruleNotInstalled' || - prePackagedTimelineStatus === 'timelinesNotInstalled') && ( - - - {i18n.LOAD_PREPACKAGED_RULES} - - + {loadPrebuiltRulesAndTemplatesButton && ( + {loadPrebuiltRulesAndTemplatesButton} )} - {(prePackagedRuleStatus === 'someRuleUninstall' || - prePackagedTimelineStatus === 'someTimelineUninstall') && ( - - - {getMissingRulesOrTimelinesButtonTitle( - rulesNotInstalled ?? 0, - timelinesNotInstalled ?? 0 - )} - - + {reloadPrebuiltRulesAndTemplatesButton && ( + {reloadPrebuiltRulesAndTemplatesButton} )} @@ -247,6 +231,7 @@ const RulesPageComponent: React.FC = () => { {(prePackagedRuleStatus === 'ruleNeedUpdate' || prePackagedTimelineStatus === 'timelineNeedUpdate') && ( { )} - i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesButton', - { - values: { missingRules }, - defaultMessage: - 'Install {missingRules} Elastic prebuilt {missingRules, plural, =1 {rule} other {rules}} ', - } - ); - -export const RELOAD_MISSING_PREPACKAGED_TIMELINES = (missingTimelines: number) => - i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedTimelinesButton', - { - values: { missingTimelines }, - defaultMessage: - 'Install {missingTimelines} Elastic prebuilt {missingTimelines, plural, =1 {timeline} other {timelines}} ', - } - ); - -export const RELOAD_MISSING_PREPACKAGED_RULES_AND_TIMELINES = ( - missingRules: number, - missingTimelines: number -) => - i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesAndTimelinesButton', - { - values: { missingRules, missingTimelines }, - defaultMessage: - 'Install {missingRules} Elastic prebuilt {missingRules, plural, =1 {rule} other {rules}} and {missingTimelines} Elastic prebuilt {missingTimelines, plural, =1 {timeline} other {timelines}} ', - } - ); - export const IMPORT_RULE_BTN_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.components.importRuleModal.importRuleTitle', { diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index cc91d23905814..30dec34ab39b7 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -1412,6 +1412,14 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "agent", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "AgentFields", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "cloud", "description": "", @@ -1458,6 +1466,25 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "AgentFields", + "description": "", + "fields": [ + { + "name": "id", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "CloudFields", diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 52598b5f44943..17f8e19a60552 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -490,6 +490,8 @@ export interface HostsEdges { export interface HostItem { _id?: Maybe; + agent?: Maybe; + cloud?: Maybe; endpoint?: Maybe; @@ -501,6 +503,10 @@ export interface HostItem { lastSeen?: Maybe; } +export interface AgentFields { + id?: Maybe; +} + export interface CloudFields { instance?: Maybe; @@ -1728,6 +1734,8 @@ export namespace GetHostOverviewQuery { _id: Maybe; + agent: Maybe; + host: Maybe; cloud: Maybe; @@ -1737,6 +1745,12 @@ export namespace GetHostOverviewQuery { endpoint: Maybe; }; + export type Agent = { + __typename?: 'AgentFields'; + + id: Maybe; + } + export type Host = { __typename?: 'HostEcsFields'; diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx index 3d291d9bf7b28..88fd1ad5f98b0 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx @@ -256,12 +256,7 @@ const getAuthenticationColumns = (): AuthTableColumns => [ hideForMobile: false, render: ({ node }) => getRowItemDraggables({ - rowItems: - node.lastSuccess != null && - node.lastSuccess.source != null && - node.lastSuccess.source.ip != null - ? node.lastSuccess.source.ip - : null, + rowItems: node.lastSuccess?.source?.ip || null, attrName: 'source.ip', idPrefix: `authentications-table-${node._id}-lastSuccessSource`, render: (item) => , @@ -273,12 +268,7 @@ const getAuthenticationColumns = (): AuthTableColumns => [ hideForMobile: false, render: ({ node }) => getRowItemDraggables({ - rowItems: - node.lastSuccess != null && - node.lastSuccess.host != null && - node.lastSuccess.host.name != null - ? node.lastSuccess.host.name - : null, + rowItems: node.lastSuccess?.host?.name ?? null, attrName: 'host.name', idPrefix: `authentications-table-${node._id}-lastSuccessfulDestination`, render: (item) => , @@ -301,12 +291,7 @@ const getAuthenticationColumns = (): AuthTableColumns => [ hideForMobile: false, render: ({ node }) => getRowItemDraggables({ - rowItems: - node.lastFailure != null && - node.lastFailure.source != null && - node.lastFailure.source.ip != null - ? node.lastFailure.source.ip - : null, + rowItems: node.lastFailure?.source?.ip || null, attrName: 'source.ip', idPrefix: `authentications-table-${node._id}-lastFailureSource`, render: (item) => , @@ -318,12 +303,7 @@ const getAuthenticationColumns = (): AuthTableColumns => [ hideForMobile: false, render: ({ node }) => getRowItemDraggables({ - rowItems: - node.lastFailure != null && - node.lastFailure.host != null && - node.lastFailure.host.name != null - ? node.lastFailure.host.name - : null, + rowItems: node.lastFailure?.host?.name || null, attrName: 'host.name', idPrefix: `authentications-table-${node._id}-lastFailureDestination`, render: (item) => , diff --git a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx index 41f443f14cafe..54cb0c0883e14 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx @@ -129,7 +129,7 @@ describe('Uncommon Process Table Component', () => { ); expect(wrapper.find('.euiTableRow').at(2).find('.euiTableRowCell').at(3).text()).toBe( - 'Host nameshello-world,hello-world-2 ' + 'Host nameshello-worldhello-world-2 ' ); }); @@ -214,7 +214,7 @@ describe('Uncommon Process Table Component', () => { ); expect(wrapper.find('.euiTableRow').at(4).find('.euiTableRowCell').at(3).text()).toBe( - 'Host nameshello-world,hello-world-2 ' + 'Host nameshello-worldhello-world-2 ' ); }); }); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/host_overview.gql_query.ts b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/host_overview.gql_query.ts index 89937d0adf81e..c0724ea3dd414 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/host_overview.gql_query.ts +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/host_overview.gql_query.ts @@ -18,6 +18,9 @@ export const HostOverviewQuery = gql` id HostOverview(hostName: $hostName, timerange: $timerange, defaultIndex: $defaultIndex) { _id + agent { + id + } host { architecture id diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index 17e0101426b07..80b2d2b0192f8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -314,7 +314,7 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory { // @ts-expect-error - apiHandlers[`/api/endpoint/metadata/${host.metadata.host.id}`] = () => host; + apiHandlers[`/api/endpoint/metadata/${host.metadata.agent.id}`] = () => host; }); } diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx index 47f4fbb8830ae..c0763a21f0947 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx @@ -89,16 +89,16 @@ export const EndpointDetails = memo(({ details }: { details: HostMetadata }) => getEndpointDetailsPath({ name: 'endpointPolicyResponse', ...currentUrlParams, - selected_endpoint: details.host.id, + selected_endpoint: details.agent.id, }) ), getEndpointDetailsPath({ name: 'endpointPolicyResponse', ...currentUrlParams, - selected_endpoint: details.host.id, + selected_endpoint: details.agent.id, }), ]; - }, [details.host.id, formatUrl, queryParams]); + }, [details.agent.id, formatUrl, queryParams]); const agentDetailsWithFlyoutPath = `${agentDetailsAppPath}${openReassignFlyoutSearch}`; const agentDetailsWithFlyoutUrl = `${agentDetailsUrl}${openReassignFlyoutSearch}`; @@ -112,7 +112,7 @@ export const EndpointDetails = memo(({ details }: { details: HostMetadata }) => { path: getEndpointDetailsPath({ name: 'endpointDetails', - selected_endpoint: details.host.id, + selected_endpoint: details.agent.id, }), }, ], diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx index 43d3b39474fc7..6bc3445c8e745 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx @@ -131,16 +131,16 @@ const PolicyResponseFlyoutPanel = memo<{ getEndpointListPath({ name: 'endpointList', ...queryParams, - selected_endpoint: hostMeta.host.id, + selected_endpoint: hostMeta.agent.id, }) ), getEndpointListPath({ name: 'endpointList', ...queryParams, - selected_endpoint: hostMeta.host.id, + selected_endpoint: hostMeta.agent.id, }), ], - [hostMeta.host.id, formatUrl, queryParams] + [hostMeta.agent.id, formatUrl, queryParams] ); const backToDetailsClickHandler = useNavigateByRouterEventHandler(detailsRoutePath); const backButtonProp = useMemo((): FlyoutSubHeaderProps['backButton'] => { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index debdde901407a..d785e3b3a131a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -397,13 +397,13 @@ describe('when on the list page', () => { describe('when there is a selected host in the url', () => { let hostDetails: HostInfo; - let agentId: string; + let elasticAgentId: string; let renderAndWaitForData: () => Promise>; const mockEndpointListApi = (mockedPolicyResponse?: HostPolicyResponse) => { const { // eslint-disable-next-line @typescript-eslint/naming-convention host_status, - metadata: { host, ...details }, + metadata: { agent, ...details }, // eslint-disable-next-line @typescript-eslint/naming-convention query_strategy_version, } = mockEndpointDetailsApiResult(); @@ -412,15 +412,15 @@ describe('when on the list page', () => { host_status, metadata: { ...details, - host: { - ...host, + agent: { + ...agent, id: '1', }, }, query_strategy_version, }; - agentId = hostDetails.metadata.elastic.agent.id; + elasticAgentId = hostDetails.metadata.elastic.agent.id; const policy = docGenerator.generatePolicyPackagePolicy(); policy.id = hostDetails.metadata.Endpoint.policy.applied.id; @@ -618,7 +618,7 @@ describe('when on the list page', () => { expect(linkToReassign).not.toBeNull(); expect(linkToReassign.textContent).toEqual('Reassign Policy'); expect(linkToReassign.getAttribute('href')).toEqual( - `/app/ingestManager#/fleet/agents/${agentId}/activity?openReassignFlyout=true` + `/app/ingestManager#/fleet/agents/${elasticAgentId}/activity?openReassignFlyout=true` ); }); @@ -702,10 +702,10 @@ describe('when on the list page', () => { }); it('should not show any numbered badges if all actions are successful', () => { - const policyResponse = docGenerator.generatePolicyResponse( - new Date().getTime(), - HostPolicyResponseActionStatus.success - ); + const policyResponse = docGenerator.generatePolicyResponse({ + ts: new Date().getTime(), + allStatus: HostPolicyResponseActionStatus.success, + }); reactTestingLibrary.act(() => { store.dispatch({ type: 'serverReturnedEndpointPolicyResponse', diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index b514707da6f6e..c5d3c3c25313d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -261,11 +261,11 @@ export const EndpointList = () => { return [ { - field: 'metadata.host', + field: 'metadata', name: i18n.translate('xpack.securitySolution.endpoint.list.hostname', { defaultMessage: 'Hostname', }), - render: ({ hostname, id }: HostInfo['metadata']['host']) => { + render: ({ host: { hostname }, agent: { id } }: HostInfo['metadata']) => { const toRoutePath = getEndpointDetailsPath( { ...queryParams, @@ -342,7 +342,7 @@ export const EndpointList = () => { const toRoutePath = getEndpointDetailsPath({ name: 'endpointPolicyResponse', ...queryParams, - selected_endpoint: item.metadata.host.id, + selected_endpoint: item.metadata.agent.id, }); const toRouteUrl = formatUrl(toRoutePath); return ( @@ -561,7 +561,7 @@ export const EndpointList = () => { return ( { )}
-
-
-
+
+ -
- Name -
-
- - - trusted app 0 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 0 - - -
-
-
-
+ trusted app 0 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 0 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -388,255 +375,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 1 - - -
-
- OS -
-
- - - Mac OS - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 1 - - -
-
-
-
+ trusted app 1 + + + +
-
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 1 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -652,255 +626,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 2 - - -
-
- OS -
-
- - - Linux - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 2 - - -
-
-
-
+ trusted app 2 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 2 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -916,255 +877,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 3 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 3 - - -
-
-
-
+ trusted app 3 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 3 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -1180,255 +1128,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 4 - - -
-
- OS -
-
- - - Mac OS - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 4 - - -
-
-
-
+ trusted app 4 + + + +
-
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 4 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -1444,255 +1379,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 5 - - -
-
- OS -
-
- - - Linux - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 5 - - -
-
-
-
+ trusted app 5 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 5 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
-
-
+
+
+
+
+ -
-
-
-
+ Remove + + +
@@ -1708,255 +1630,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
+ Name + +
-
-
- Name -
-
- - - trusted app 6 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 6 - - -
-
-
-
+ trusted app 6 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 6 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -1972,255 +1881,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
+ Name + +
-
-
- Name -
-
- - - trusted app 7 - - -
-
- OS -
-
- - - Mac OS - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 7 - - -
-
-
-
+ trusted app 7 + + + +
-
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 7 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
-
+ + + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -2236,255 +2132,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
+ Name + +
-
-
- Name -
-
- - - trusted app 8 - - -
-
- OS -
-
- - - Linux - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 8 - - -
-
-
-
+ trusted app 8 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 8 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
-
+ + + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -2500,255 +2383,242 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiPanel" >
-
-
+ Name + +
-
-
- Name -
-
- - - trusted app 9 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 9 - - -
-
-
-
+ trusted app 9 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 9 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
-
+ + + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
-
-
-
-
-
- -
-
-
-
+
+
+
+
+
+
@@ -3054,255 +2924,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 0 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 0 - - -
-
-
-
+ trusted app 0 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 0 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -3318,255 +3175,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 1 - - -
-
- OS -
-
- - - Mac OS - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 1 - - -
-
-
-
+ trusted app 1 + + + +
-
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 1 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -3582,255 +3426,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 2 - - -
-
- OS -
-
- - - Linux - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 2 - - -
-
-
-
+ trusted app 2 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 2 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -3846,255 +3677,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 3 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 3 - - -
-
-
-
+ trusted app 3 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 3 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -4110,255 +3928,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 4 - - -
-
- OS -
-
- - - Mac OS - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 4 - - -
-
-
-
+ trusted app 4 + + + +
-
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 4 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -4374,255 +4179,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 5 - - -
-
- OS -
-
- - - Linux - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 5 - - -
-
-
-
+ trusted app 5 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 5 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -4638,255 +4430,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 6 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 6 - - -
-
-
-
+ trusted app 6 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 6 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -4902,255 +4681,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 7 - - -
-
- OS -
-
- - - Mac OS - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 7 - - -
-
-
-
+ trusted app 7 + + + +
-
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 7 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -5166,255 +4932,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 8 - - -
-
- OS -
-
- - - Linux - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 8 - - -
-
-
-
+ trusted app 8 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 8 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -5430,255 +5183,242 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 9 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 9 - - -
-
-
-
+ trusted app 9 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 9 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -5942,255 +5682,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 0 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 0 - - -
-
-
-
+ trusted app 0 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 0 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -6206,255 +5933,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 1 - - -
-
- OS -
-
- - - Mac OS - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 1 - - -
-
-
+ + trusted app 1 + + + +
+ OS +
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 1 + + +
+ +
+
+
-
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -6470,255 +6184,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 2 - - -
-
- OS -
-
- - - Linux - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 2 - - -
-
-
-
+ trusted app 2 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 2 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -6734,255 +6435,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 3 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 3 - - -
-
-
-
+ trusted app 3 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 3 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -6998,255 +6686,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 4 - - -
-
- OS -
-
- - - Mac OS - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 4 - - -
-
-
-
+ trusted app 4 + + + +
+ OS +
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
-
+
+ + + Trusted App 4 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -7262,255 +6937,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 5 - - -
-
- OS -
-
- - - Linux - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 5 - - -
-
-
-
+ trusted app 5 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 5 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -7526,519 +7188,493 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 6 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 6 - - -
-
-
-
+ trusted app 6 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 6 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ Name +
+
+ + + trusted app 7 + + +
+
+ OS +
+
+ + + Mac OS + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 7 + + +
+
+
-
-
- Name -
-
- - - trusted app 7 - - -
-
- OS -
-
- - - Mac OS - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 7 - - -
-
-
-
-
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -8054,255 +7690,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 8 - - -
-
- OS -
-
- - - Linux - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 8 - - -
-
-
-
+ trusted app 8 + + + +
-
+
+ + + Linux + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 8 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
@@ -8318,255 +7941,242 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiPanel" >
-
-
-
+
+ -
- Name -
-
- - - trusted app 9 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 9 - - -
-
-
-
+ trusted app 9 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 9 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
+
-
+ + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap index 181b59c65a3d5..5f652b39ffd56 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap @@ -779,252 +779,239 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiPanel" >
-
-
+ Name + +
-
-
- Name -
-
- - - trusted app 0 - - -
-
- OS -
-
- - - Windows - - -
-
- Date Created -
-
- 1 minute ago -
-
- Created By -
-
- - - someone - - -
-
- Description -
-
- - - Trusted App 0 - - -
-
-
-
+ trusted app 0 + + + +
-
+
+ + + Windows + + +
+
+ Date Created +
+
+ 1 minute ago +
+
+ Created By +
+
+ + + someone + + +
+
+ Description +
+
+ + + Trusted App 0 + + +
+ +
+
+
+
+
-
-
+
+
-
-
-
-
- - - - + +
-
+ + + + + + - + - + - - - - + + + + + + + - - -
+
+
-
- - Field - -
-
+ + +
-
- - Operator - -
-
+ + +
-
- - Value - -
-
- -
- - No items found - -
-
- + No items found + + + +
+
+
+
+
-
-
-
-
- -
-
-
-
+ Remove + + +
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx index 03bde8c7673bf..822c57b92b4e5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx @@ -67,7 +67,7 @@ export const TrustedAppsPage = memo(() => { return ( { (createEmbeddable as jest.Mock).mockResolvedValue(mockCreateEmbeddable); - let wrapper: ReactWrapper; - await act(async () => { - wrapper = mount( - - - - ); - }); + const wrapper: ReactWrapper = mount( + + + + ); - wrapper!.update(); + await waitFor(() => { + wrapper.update(); - expect(wrapper!.find('[data-test-subj="EmbeddablePanel"]').exists()).toEqual(true); - expect(wrapper!.find('[data-test-subj="IndexPatternsMissingPrompt"]').exists()).toEqual(false); - expect(wrapper!.find('[data-test-subj="loading-panel"]').exists()).toEqual(false); + expect(wrapper.find('[data-test-subj="EmbeddablePanel"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="IndexPatternsMissingPrompt"]').exists()).toEqual(false); + expect(wrapper.find('[data-test-subj="loading-panel"]').exists()).toEqual(false); + }); }); test('renders IndexPatternsMissingPrompt', async () => { @@ -132,20 +131,18 @@ describe('EmbeddedMapComponent', () => { (createEmbeddable as jest.Mock).mockResolvedValue(mockCreateEmbeddable); - let wrapper: ReactWrapper; - await act(async () => { - wrapper = mount( - - - - ); - }); - - wrapper!.update(); + const wrapper: ReactWrapper = mount( + + + + ); + await waitFor(() => { + wrapper.update(); - expect(wrapper!.find('[data-test-subj="EmbeddablePanel"]').exists()).toEqual(false); - expect(wrapper!.find('[data-test-subj="IndexPatternsMissingPrompt"]').exists()).toEqual(true); - expect(wrapper!.find('[data-test-subj="loading-panel"]').exists()).toEqual(false); + expect(wrapper.find('[data-test-subj="EmbeddablePanel"]').exists()).toEqual(false); + expect(wrapper.find('[data-test-subj="IndexPatternsMissingPrompt"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="loading-panel"]').exists()).toEqual(false); + }); }); test('renders Loader', async () => { @@ -154,19 +151,17 @@ describe('EmbeddedMapComponent', () => { (createEmbeddable as jest.Mock).mockResolvedValue(null); - let wrapper: ReactWrapper; - await act(async () => { - wrapper = mount( - - - - ); - }); - - wrapper!.update(); + const wrapper: ReactWrapper = mount( + + + + ); + await waitFor(() => { + wrapper.update(); - expect(wrapper!.find('[data-test-subj="EmbeddablePanel"]').exists()).toEqual(false); - expect(wrapper!.find('[data-test-subj="IndexPatternsMissingPrompt"]').exists()).toEqual(false); - expect(wrapper!.find('[data-test-subj="loading-panel"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="EmbeddablePanel"]').exists()).toEqual(false); + expect(wrapper.find('[data-test-subj="IndexPatternsMissingPrompt"]').exists()).toEqual(false); + expect(wrapper.find('[data-test-subj="loading-panel"]').exists()).toEqual(true); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx index 7ae8aecdab606..ac7c5078e4ba0 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx @@ -198,15 +198,13 @@ export const EmbeddedMapComponent = ({ if (embeddable != null) { embeddable.updateInput({ query }); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [query]); + }, [embeddable, query]); useEffect(() => { if (embeddable != null) { embeddable.updateInput({ filters }); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [filters]); + }, [embeddable, filters]); // DateRange updated useEffect useEffect(() => { @@ -217,8 +215,7 @@ export const EmbeddedMapComponent = ({ }; embeddable.updateInput({ timeRange }); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [startDate, endDate]); + }, [embeddable, startDate, endDate]); return isError ? null : ( diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.test.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.test.tsx index 0c6b90ec2b9dd..c503690e776af 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.test.tsx @@ -12,9 +12,12 @@ import { mockAPMRegexIndexPattern, mockAPMTransactionIndexPattern, mockAuditbeatIndexPattern, + mockCCSGlobIndexPattern, + mockCommaFilebeatAuditbeatCCSGlobIndexPattern, + mockCommaFilebeatAuditbeatGlobIndexPattern, + mockCommaFilebeatExclusionGlobIndexPattern, mockFilebeatIndexPattern, mockGlobIndexPattern, - mockCCSGlobIndexPattern, } from './__mocks__/mock'; const mockEmbeddable = embeddablePluginMock.createStartContract(); @@ -122,5 +125,44 @@ describe('embedded_map_helpers', () => { }); expect(matchingIndexPatterns).toEqual([mockFilebeatIndexPattern]); }); + + test('matches on comma separated Kibana index pattern', () => { + const matchingIndexPatterns = findMatchingIndexPatterns({ + kibanaIndexPatterns: [ + mockCommaFilebeatAuditbeatGlobIndexPattern, + mockAuditbeatIndexPattern, + ], + siemDefaultIndices, + }); + expect(matchingIndexPatterns).toEqual([ + mockCommaFilebeatAuditbeatGlobIndexPattern, + mockAuditbeatIndexPattern, + ]); + }); + + test('matches on excluded comma separated Kibana index pattern', () => { + const matchingIndexPatterns = findMatchingIndexPatterns({ + kibanaIndexPatterns: [ + mockCommaFilebeatExclusionGlobIndexPattern, + mockAuditbeatIndexPattern, + ], + siemDefaultIndices, + }); + expect(matchingIndexPatterns).toEqual([ + mockCommaFilebeatExclusionGlobIndexPattern, + mockAuditbeatIndexPattern, + ]); + }); + + test('matches on CCS comma separated Kibana index pattern', () => { + const matchingIndexPatterns = findMatchingIndexPatterns({ + kibanaIndexPatterns: [ + mockCommaFilebeatAuditbeatCCSGlobIndexPattern, + mockAuditbeatIndexPattern, + ], + siemDefaultIndices: ['cluster2:filebeat-*', 'cluster1:auditbeat-*'], + }); + expect(matchingIndexPatterns).toEqual([mockCommaFilebeatAuditbeatCCSGlobIndexPattern]); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx index 25928197590ea..4ac759ea534ec 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx @@ -150,7 +150,15 @@ export const findMatchingIndexPatterns = ({ const pattern = kip.attributes.title; return ( !ignoredIndexPatterns.includes(pattern) && - siemDefaultIndices.some((sdi) => minimatch(sdi, pattern)) + siemDefaultIndices.some((sdi) => { + const splitPattern = pattern.split(',') ?? []; + return splitPattern.length > 1 + ? splitPattern.some((p) => { + const isMatch = minimatch(sdi, p); + return isMatch && p.charAt(0) === '-' ? false : isMatch; + }) + : minimatch(sdi, pattern); + }) ); }); } catch { diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/__snapshots__/map_tool_tip.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/__snapshots__/map_tool_tip.test.tsx.snap index 775329553cbeb..dc94b1039dfc5 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/__snapshots__/map_tool_tip.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/__snapshots__/map_tool_tip.test.tsx.snap @@ -1,29 +1,37 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`MapToolTip full component renders correctly against snapshot 1`] = ` - - - - - + + + + + `; exports[`MapToolTip placeholder component renders correctly against snapshot 1`] = ` - - - - - + + + + + `; diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap index 8927e492993d0..8801e455c95b6 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap @@ -2,7 +2,6 @@ exports[`PointToolTipContent renders correctly against snapshot 1`] = ` (null); const [, setLayerName] = useState(''); + const handleCloseTooltip = useCallback(() => { + if (closeTooltip != null) { + closeTooltip(); + setFeatureIndex(0); + } + }, [closeTooltip]); + + const handlePreviousFeature = useCallback(() => { + setFeatureIndex((prevFeatureIndex) => prevFeatureIndex - 1); + setIsLoadingNextFeature(true); + }, []); + + const handleNextFeature = useCallback(() => { + setFeatureIndex((prevFeatureIndex) => prevFeatureIndex + 1); + setIsLoadingNextFeature(true); + }, []); + + const content = useMemo(() => { + if (isError) { + return ( + + {i18n.MAP_TOOL_TIP_ERROR} + + ); + } + + if (isLoading && !isLoadingNextFeature) { + return ( + + + + + + ); + } + + return ( +
+ {featureGeometry != null && featureGeometry.type === 'LineString' ? ( + + ) : ( + + )} + {features.length > 1 && ( + + )} + {isLoadingNextFeature && } +
+ ); + }, [ + featureGeometry, + featureIndex, + featureProps, + features, + handleNextFeature, + handlePreviousFeature, + isError, + isLoading, + isLoadingNextFeature, + ]); + useEffect(() => { // Early return if component doesn't yet have props -- result of mounting in portal before actual rendering if ( @@ -77,69 +149,17 @@ export const MapToolTipComponent = ({ }; fetchFeatureProps(); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [ featureIndex, - // eslint-disable-next-line react-hooks/exhaustive-deps - features - .map((f) => `${f.id}-${f.layerId}`) - .sort() - .join(), + features, + getLayerName, + isLoadingNextFeature, + loadFeatureGeometry, + loadFeatureProperties, ]); - if (isError) { - return ( - - {i18n.MAP_TOOL_TIP_ERROR} - - ); - } - - return isLoading && !isLoadingNextFeature ? ( - - - - - - ) : ( - { - if (closeTooltip != null) { - closeTooltip(); - setFeatureIndex(0); - } - }} - > -
- {featureGeometry != null && featureGeometry.type === 'LineString' ? ( - - ) : ( - - )} - {features.length > 1 && ( - { - setFeatureIndex(featureIndex - 1); - setIsLoadingNextFeature(true); - }} - nextFeature={() => { - setFeatureIndex(featureIndex + 1); - setIsLoadingNextFeature(true); - }} - /> - )} - {isLoadingNextFeature && } -
-
+ return ( + {content} ); }; diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx index 27fe27adc99c2..87b972e9d7053 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx @@ -24,15 +24,9 @@ describe('PointToolTipContent', () => { ]; test('renders correctly against snapshot', () => { - const closeTooltip = jest.fn(); - const wrapper = shallow( - + ); expect(wrapper.find('PointToolTipContentComponent')).toMatchSnapshot(); diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.tsx index 57113a1395778..a3a5ddf4d53b3 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { sourceDestinationFieldMappings } from '../map_config'; import { getEmptyTagValue, @@ -20,36 +20,38 @@ import { ITooltipProperty } from '../../../../../../maps/public/classes/tooltips interface PointToolTipContentProps { contextId: string; featureProps: ITooltipProperty[]; - closeTooltip?(): void; } export const PointToolTipContentComponent = ({ contextId, featureProps, - closeTooltip, }: PointToolTipContentProps) => { - const featureDescriptionListItems = featureProps.map((featureProp) => { - const key = featureProp.getPropertyKey(); - const value = featureProp.getRawValue() ?? []; + const featureDescriptionListItems = useMemo( + () => + featureProps.map((featureProp) => { + const key = featureProp.getPropertyKey(); + const value = featureProp.getRawValue() ?? []; - return { - title: sourceDestinationFieldMappings[key], - description: ( - <> - {value != null ? ( - getRenderedFieldValue(key, item)} - /> - ) : ( - getEmptyTagValue() - )} - - ), - }; - }); + return { + title: sourceDestinationFieldMappings[key], + description: ( + <> + {value != null ? ( + getRenderedFieldValue(key, item)} + /> + ) : ( + getEmptyTagValue() + )} + + ), + }; + }), + [contextId, featureProps] + ); return ; }; diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/country_flag.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/country_flag.tsx index cb1af5513c846..31ad679ce41bf 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/country_flag.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/country_flag.tsx @@ -9,6 +9,13 @@ import { isEmpty } from 'lodash/fp'; import { EuiToolTip } from '@elastic/eui'; import countries from 'i18n-iso-countries'; import countryJson from 'i18n-iso-countries/langs/en.json'; +import styled from 'styled-components'; + +// Fixes vertical alignment of the flag +const FlagWrapper = styled.span` + position: relative; + top: 1px; +`; /** * Returns the flag for the specified country code, or null if the specified @@ -38,10 +45,10 @@ export const CountryFlag = memo<{ if (flag !== null) { return displayCountryNameOnHover ? ( - {flag} + {flag} ) : ( - {flag} + {flag} ); } return null; @@ -49,7 +56,7 @@ export const CountryFlag = memo<{ CountryFlag.displayName = 'CountryFlag'; -/** Renders an emjoi flag with country name for the specified country code */ +/** Renders an emoji flag with country name for the specified country code */ export const CountryFlagAndName = memo<{ countryCode: string; displayCountryNameOnHover?: boolean; @@ -67,10 +74,13 @@ export const CountryFlagAndName = memo<{ if (flag !== null && localesLoaded) { return displayCountryNameOnHover ? ( - {flag} + {flag} ) : ( - {`${flag} ${countries.getName(countryCode, 'en')}`} + <> + {flag} + {` ${countries.getName(countryCode, 'en')}`} + ); } return null; diff --git a/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx index 12c3cc481cfc1..356173fa2ac71 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx @@ -198,6 +198,7 @@ export const useNetworkHttp = ({ factoryQueryType: NetworkQueries.http, filterQuery: createFilter(filterQuery), id: ID, + ip, pagination: generateTablePaginationOptions(activePage, limit), sort: sort as SortField, timerange: { @@ -211,7 +212,7 @@ export const useNetworkHttp = ({ } return prevRequest; }); - }, [activePage, indexNames, endDate, filterQuery, limit, startDate, sort, skip]); + }, [activePage, indexNames, endDate, filterQuery, ip, limit, startDate, sort, skip]); useEffect(() => { networkHttpSearch(networkHttpRequest); diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx index 0b864d66842d1..c2dc638fa719f 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx @@ -61,6 +61,7 @@ export const useNetworkTopCountries = ({ filterQuery, flowTarget, indexNames, + ip, skip, startDate, type, @@ -86,6 +87,7 @@ export const useNetworkTopCountries = ({ filterQuery: createFilter(filterQuery), flowTarget, id: queryId, + ip, pagination: generateTablePaginationOptions(activePage, limit), sort, timerange: { @@ -203,6 +205,7 @@ export const useNetworkTopCountries = ({ filterQuery: createFilter(filterQuery), flowTarget, id: queryId, + ip, pagination: generateTablePaginationOptions(activePage, limit), sort, timerange: { @@ -221,6 +224,7 @@ export const useNetworkTopCountries = ({ indexNames, endDate, filterQuery, + ip, limit, startDate, sort, diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx index c68ad2422c514..87968e7a03522 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx @@ -61,6 +61,7 @@ export const useNetworkTopNFlow = ({ filterQuery, flowTarget, indexNames, + ip, skip, startDate, type, @@ -84,7 +85,8 @@ export const useNetworkTopNFlow = ({ factoryQueryType: NetworkQueries.topNFlow, filterQuery: createFilter(filterQuery), flowTarget, - id: ID, + id: `${ID}-${flowTarget}`, + ip, pagination: generateTablePaginationOptions(activePage, limit), sort, timerange: { @@ -199,7 +201,8 @@ export const useNetworkTopNFlow = ({ factoryQueryType: NetworkQueries.topNFlow, filterQuery: createFilter(filterQuery), flowTarget, - id: ID, + id: `${ID}-${flowTarget}`, + ip, pagination: generateTablePaginationOptions(activePage, limit), timerange: { interval: '12h', @@ -213,7 +216,7 @@ export const useNetworkTopNFlow = ({ } return prevRequest; }); - }, [activePage, indexNames, endDate, filterQuery, limit, startDate, sort, skip, flowTarget]); + }, [activePage, endDate, filterQuery, indexNames, ip, limit, startDate, sort, skip, flowTarget]); useEffect(() => { networkTopNFlowSearch(networkTopNFlowRequest); diff --git a/x-pack/plugins/security_solution/public/resolver/lib/date.test.ts b/x-pack/plugins/security_solution/public/resolver/lib/date.test.ts index 5555578e44f7b..d121b9c9c81c4 100644 --- a/x-pack/plugins/security_solution/public/resolver/lib/date.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/lib/date.test.ts @@ -57,8 +57,8 @@ describe('date', () => { const almostAYear = new Date(initialTime + 11.9 * month).getTime(); const threeYears = new Date(initialTime + 3 * year).getTime(); - it('should return null if invalid times are given', () => { - expect(getFriendlyElapsedTime(initialTime, 'ImTimeless')).toEqual(null); + it('should return undefined if invalid times are given', () => { + expect(getFriendlyElapsedTime(initialTime, 'ImTimeless')).toEqual(undefined); }); it('should return the correct singular relative time', () => { diff --git a/x-pack/plugins/security_solution/public/resolver/lib/date.ts b/x-pack/plugins/security_solution/public/resolver/lib/date.ts index 3cd0c910f46f3..ff8119a5e25fb 100644 --- a/x-pack/plugins/security_solution/public/resolver/lib/date.ts +++ b/x-pack/plugins/security_solution/public/resolver/lib/date.ts @@ -9,7 +9,7 @@ import { DurationDetails, DurationTypes } from '../types'; /** * Given a time, it will convert it to a unix timestamp if not one already. If it is unable to do so, it will return NaN */ -export const getUnixTime = (time: number | string): number | typeof NaN => { +export const getUnixTime = (time: number | string): number => { if (!time) { return NaN; } @@ -30,16 +30,17 @@ export const getUnixTime = (time: number | string): number | typeof NaN => { * Given two unix timestamps, it will return an object containing the time difference and properly pluralized friendly version of the time difference. * i.e. a time difference of 1000ms will yield => { duration: 1, durationType: 'second' } and 10000ms will yield => { duration: 10, durationType: 'seconds' } * + * If `from` or `to` cannot be parsed, `undefined` will be returned. */ export const getFriendlyElapsedTime = ( from: number | string, to: number | string -): DurationDetails | null => { +): DurationDetails | undefined => { const startTime = getUnixTime(from); const endTime = getUnixTime(to); if (Number.isNaN(startTime) || Number.isNaN(endTime)) { - return null; + return undefined; } const elapsedTimeInMs = endTime - startTime; diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap index fc0d646fd62ca..b77a5d09008cc 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap +++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap @@ -182,7 +182,7 @@ Object { "edgeLineSegments": Array [ Object { "metadata": Object { - "uniqueId": "parentToMidedge:0:1", + "reactKey": "parentToMidedge:0:1", }, "points": Array [ Array [ @@ -197,7 +197,7 @@ Object { }, Object { "metadata": Object { - "uniqueId": "midwayedge:0:1", + "reactKey": "midwayedge:0:1", }, "points": Array [ Array [ @@ -216,7 +216,7 @@ Object { "duration": "<1", "durationType": "millisecond", }, - "uniqueId": "edge:0:1", + "reactKey": "edge:0:1", }, "points": Array [ Array [ @@ -235,7 +235,7 @@ Object { "duration": "<1", "durationType": "millisecond", }, - "uniqueId": "edge:0:2", + "reactKey": "edge:0:2", }, "points": Array [ Array [ @@ -254,7 +254,7 @@ Object { "duration": "<1", "durationType": "millisecond", }, - "uniqueId": "edge:0:8", + "reactKey": "edge:0:8", }, "points": Array [ Array [ @@ -269,7 +269,7 @@ Object { }, Object { "metadata": Object { - "uniqueId": "parentToMidedge:1:3", + "reactKey": "parentToMidedge:1:3", }, "points": Array [ Array [ @@ -284,7 +284,7 @@ Object { }, Object { "metadata": Object { - "uniqueId": "midwayedge:1:3", + "reactKey": "midwayedge:1:3", }, "points": Array [ Array [ @@ -303,7 +303,7 @@ Object { "duration": "<1", "durationType": "millisecond", }, - "uniqueId": "edge:1:3", + "reactKey": "edge:1:3", }, "points": Array [ Array [ @@ -322,7 +322,7 @@ Object { "duration": "<1", "durationType": "millisecond", }, - "uniqueId": "edge:1:4", + "reactKey": "edge:1:4", }, "points": Array [ Array [ @@ -337,7 +337,7 @@ Object { }, Object { "metadata": Object { - "uniqueId": "parentToMidedge:2:5", + "reactKey": "parentToMidedge:2:5", }, "points": Array [ Array [ @@ -352,7 +352,7 @@ Object { }, Object { "metadata": Object { - "uniqueId": "midwayedge:2:5", + "reactKey": "midwayedge:2:5", }, "points": Array [ Array [ @@ -371,7 +371,7 @@ Object { "duration": "<1", "durationType": "millisecond", }, - "uniqueId": "edge:2:5", + "reactKey": "edge:2:5", }, "points": Array [ Array [ @@ -390,7 +390,7 @@ Object { "duration": "<1", "durationType": "millisecond", }, - "uniqueId": "edge:2:6", + "reactKey": "edge:2:6", }, "points": Array [ Array [ @@ -409,7 +409,7 @@ Object { "duration": "<1", "durationType": "millisecond", }, - "uniqueId": "edge:6:7", + "reactKey": "edge:6:7", }, "points": Array [ Array [ @@ -620,7 +620,7 @@ Object { "duration": "<1", "durationType": "millisecond", }, - "uniqueId": "edge:0:1", + "reactKey": "edge:0:1", }, "points": Array [ Array [ diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts index f0880fa635a24..0003be827aca8 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/isometric_taxi_layout.ts @@ -191,7 +191,6 @@ function processEdgeLineSegments( ): EdgeLineSegment[] { const edgeLineSegments: EdgeLineSegment[] = []; for (const metadata of levelOrderWithWidths(indexedProcessTree, widths)) { - const edgeLineMetadata: EdgeLineMetadata = { uniqueId: '' }; /** * We only handle children, drawing lines back to their parents. The root has no parent, so we skip it */ @@ -219,10 +218,16 @@ function processEdgeLineSegments( const parentTime = eventModel.timestampSafeVersion(parent); const processTime = eventModel.timestampSafeVersion(process); - if (parentTime && processTime) { - edgeLineMetadata.elapsedTime = elapsedTime(parentTime, processTime) ?? undefined; - } - edgeLineMetadata.uniqueId = edgeLineID; + + const timeBetweenParentAndNode = + parentTime !== undefined && processTime !== undefined + ? elapsedTime(parentTime, processTime) + : undefined; + + const edgeLineMetadata: EdgeLineMetadata = { + elapsedTime: timeBetweenParentAndNode, + reactKey: edgeLineID, + }; /** * The point halfway between the parent and child on the y axis, we sometimes have a hard angle here in the edge line @@ -270,7 +275,7 @@ function processEdgeLineSegments( const lineFromParentToMidwayLine: EdgeLineSegment = { points: [parentPosition, [parentPosition[0], midwayY]], - metadata: { uniqueId: `parentToMid${edgeLineID}` }, + metadata: { reactKey: `parentToMid${edgeLineID}` }, }; const widthOfMidline = parentWidth - firstChildWidth / 2 - lastChildWidth / 2; @@ -291,7 +296,7 @@ function processEdgeLineSegments( midwayY, ], ], - metadata: { uniqueId: `midway${edgeLineID}` }, + metadata: { reactKey: `midway${edgeLineID}` }, }; edgeLineSegments.push( @@ -501,13 +506,26 @@ const distanceBetweenNodesInUnits = 2; */ const distanceBetweenNodes = distanceBetweenNodesInUnits * unit; -export function nodePosition( +/** + * @deprecated use `nodePosition` + */ +export function processPosition( model: IsometricTaxiLayout, node: SafeResolverEvent ): Vector2 | undefined { return model.processNodePositions.get(node); } +export function nodePosition(model: IsometricTaxiLayout, nodeID: string): Vector2 | undefined { + // Find the indexed object matching the nodeID + // NB: this is O(n) now, but we will be indexing the nodeIDs in the future. + for (const candidate of model.processNodePositions.keys()) { + if (eventModel.entityIDSafeVersion(candidate) === nodeID) { + return processPosition(model, candidate); + } + } +} + /** * Return a clone of `model` with all positions incremented by `translation`. * Use this to move the layout around. @@ -525,7 +543,7 @@ export function translated(model: IsometricTaxiLayout, translation: Vector2): Is ]) ), edgeLineSegments: model.edgeLineSegments.map(({ points, metadata }) => ({ - points: points.map((point) => vector2.add(point, translation)), + points: [vector2.add(points[0], translation), vector2.add(points[1], translation)], metadata, })), // these are unchanged diff --git a/x-pack/plugins/security_solution/public/resolver/store/actions.ts b/x-pack/plugins/security_solution/public/resolver/store/actions.ts index 3348c962efdea..66a32ba29cd74 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/actions.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/actions.ts @@ -4,19 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ import { CameraAction } from './camera'; -import { SafeResolverEvent } from '../../../common/endpoint/types'; import { DataAction } from './data/action'; /** - * When the user wants to bring a process node front-and-center on the map. + * When the user wants to bring a node front-and-center on the map. */ -interface UserBroughtProcessIntoView { - readonly type: 'userBroughtProcessIntoView'; +interface UserBroughtNodeIntoView { + readonly type: 'userBroughtNodeIntoView'; readonly payload: { /** - * Used to identify the process node that should be brought into view. + * Used to identify the node that should be brought into view. */ - readonly process: SafeResolverEvent; + readonly nodeID: string; /** * The time (since epoch in milliseconds) when the action was dispatched. */ @@ -97,7 +96,7 @@ export type ResolverAction = | CameraAction | DataAction | AppReceivedNewExternalProperties - | UserBroughtProcessIntoView + | UserBroughtNodeIntoView | UserFocusedOnResolverNode | UserSelectedResolverNode | UserRequestedRelatedEventData; diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index 5eb920ca835f4..505e6cfc3ee72 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -16,6 +16,7 @@ import { AABB, VisibleEntites, TreeFetcherParameters, + IsometricTaxiLayout, } from '../../types'; import { isGraphableProcess, isTerminatedProcess } from '../../models/process_event'; import * as indexedProcessTreeModel from '../../models/indexed_process_tree'; @@ -346,7 +347,7 @@ export function treeParametersToFetch(state: DataState): TreeFetcherParameters | } } -export const layout = createSelector( +export const layout: (state: DataState) => IsometricTaxiLayout = createSelector( tree, originID, function processNodePositionsAndEdgeLineSegments( @@ -372,7 +373,7 @@ export const layout = createSelector( } // Find the position of the origin, we'll center the map on it intrinsically - const originPosition = isometricTaxiLayoutModel.nodePosition(taxiLayout, originNode); + const originPosition = isometricTaxiLayoutModel.processPosition(taxiLayout, originNode); // adjust the position of everything so that the origin node is at `(0, 0)` if (originPosition === undefined) { diff --git a/x-pack/plugins/security_solution/public/resolver/store/methods.ts b/x-pack/plugins/security_solution/public/resolver/store/methods.ts deleted file mode 100644 index f121b2aa86888..0000000000000 --- a/x-pack/plugins/security_solution/public/resolver/store/methods.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { animatePanning } from './camera/methods'; -import { layout } from './selectors'; -import { ResolverState } from '../types'; -import { SafeResolverEvent } from '../../../common/endpoint/types'; - -const animationDuration = 1000; - -/** - * Return new `ResolverState` with the camera animating to focus on `process`. - */ -export function animateProcessIntoView( - state: ResolverState, - startTime: number, - process: SafeResolverEvent -): ResolverState { - const { processNodePositions } = layout(state); - const position = processNodePositions.get(process); - if (position) { - return { - ...state, - camera: animatePanning(state.camera, startTime, position, animationDuration), - }; - } - return state; -} diff --git a/x-pack/plugins/security_solution/public/resolver/store/reducer.ts b/x-pack/plugins/security_solution/public/resolver/store/reducer.ts index ae1e9a58a2097..997a3d0ae6b38 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/reducer.ts @@ -3,13 +3,15 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { Reducer, combineReducers } from 'redux'; -import { animateProcessIntoView } from './methods'; +import { animatePanning } from './camera/methods'; +import { layout } from './selectors'; import { cameraReducer } from './camera/reducer'; import { dataReducer } from './data/reducer'; import { ResolverAction } from './actions'; import { ResolverState, ResolverUIState } from '../types'; -import * as eventModel from '../../../common/endpoint/models/event'; +import { nodePosition } from '../models/indexed_process_tree/isometric_taxi_layout'; const uiReducer: Reducer = ( state = { @@ -37,18 +39,15 @@ const uiReducer: Reducer = ( selectedNode: action.payload, }; return next; - } else if (action.type === 'userBroughtProcessIntoView') { - const nodeID = eventModel.entityIDSafeVersion(action.payload.process); - if (nodeID !== undefined) { - const next: ResolverUIState = { - ...state, - ariaActiveDescendant: nodeID, - selectedNode: nodeID, - }; - return next; - } else { - return state; - } + } else if (action.type === 'userBroughtNodeIntoView') { + const { nodeID } = action.payload; + const next: ResolverUIState = { + ...state, + // Select the node. NB: Animation is handled in the reducer as well. + ariaActiveDescendant: nodeID, + selectedNode: nodeID, + }; + return next; } else if (action.type === 'appReceivedNewExternalProperties') { const next: ResolverUIState = { ...state, @@ -66,11 +65,21 @@ const concernReducers = combineReducers({ data: dataReducer, ui: uiReducer, }); +const animationDuration = 1000; export const resolverReducer: Reducer = (state, action) => { const nextState = concernReducers(state, action); - if (action.type === 'userBroughtProcessIntoView') { - return animateProcessIntoView(nextState, action.payload.time, action.payload.process); + if (action.type === 'userBroughtNodeIntoView') { + const position = nodePosition(layout(nextState), action.payload.nodeID); + if (position) { + const withAnimation: ResolverState = { + ...nextState, + camera: animatePanning(nextState.camera, action.payload.time, position, animationDuration), + }; + return withAnimation; + } else { + return nextState; + } } else { return nextState; } diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index ea603f2583431..2a399b6844bd7 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -45,6 +45,23 @@ export class Simulator { */ private readonly sideEffectSimulator: SideEffectSimulator; + /** + * An `enzyme` supported CSS selector for process node elements. + */ + public static nodeElementSelector({ + entityID, + selected = false, + }: ProcessNodeElementSelectorOptions = {}): string { + let selector: string = baseNodeElementSelector; + if (entityID !== undefined) { + selector += `[data-test-resolver-node-id="${entityID}"]`; + } + if (selected) { + selector += '[aria-selected="true"]'; + } + return selector; + } + constructor({ dataAccessLayer, resolverComponentInstanceID, @@ -193,7 +210,7 @@ export class Simulator { * returns a `ReactWrapper` even if nothing is found, as that is how `enzyme` does things. */ public processNodeElements(options: ProcessNodeElementSelectorOptions = {}): ReactWrapper { - return this.domNodes(processNodeElementSelector(options)); + return this.domNodes(Simulator.nodeElementSelector(options)); } /** @@ -230,7 +247,7 @@ export class Simulator { */ public unselectedProcessNode(entityID: string): ReactWrapper { return this.processNodeElements({ entityID }).not( - processNodeElementSelector({ entityID, selected: true }) + Simulator.nodeElementSelector({ entityID, selected: true }) ); } @@ -265,6 +282,13 @@ export class Simulator { return this.resolveWrapper(() => this.domNodes(`[data-test-subj="${selector}"]`)); } + /** + * Given a `role`, return DOM nodes that have it. Use this to assert that ARIA roles are present as expected. + */ + public domNodesWithRole(role: string): ReactWrapper { + return this.domNodes(`[role="${role}"]`); + } + /** * Given a 'data-test-subj' selector, it will return the domNode */ @@ -318,7 +342,7 @@ export class Simulator { } } -const baseResolverSelector = '[data-test-subj="resolver:node"]'; +const baseNodeElementSelector = '[data-test-subj="resolver:node"]'; interface ProcessNodeElementSelectorOptions { /** @@ -330,20 +354,3 @@ interface ProcessNodeElementSelectorOptions { */ selected?: boolean; } - -/** - * An `enzyme` supported CSS selector for process node elements. - */ -function processNodeElementSelector({ - entityID, - selected = false, -}: ProcessNodeElementSelectorOptions = {}): string { - let selector: string = baseResolverSelector; - if (entityID !== undefined) { - selector += `[data-test-resolver-node-id="${entityID}"]`; - } - if (selected) { - selector += '[aria-selected="true"]'; - } - return selector; -} diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index 5007b7cffa5c6..fb57f85639e33 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -429,20 +429,22 @@ export interface DurationDetails { * Values shared between two vertices joined by an edge line. */ export interface EdgeLineMetadata { + /** + * Represents a time duration for this edge line segment. Used to show a time duration in the UI. + * This is only ever present on one of the segments in an edge. + */ elapsedTime?: DurationDetails; - // A string of the two joined process nodes concatenated together. - uniqueId: string; + /** + * Used to represent a react key value for the edge line. + */ + reactKey: string; } -/** - * A tuple of 2 vector2 points forming a poly-line. Used to connect process nodes in the graph. - */ -export type EdgeLinePoints = Vector2[]; /** * Edge line components including the points joining the edge-line and any optional associated metadata */ export interface EdgeLineSegment { - points: EdgeLinePoints; + points: [Vector2, Vector2]; metadata: EdgeLineMetadata; } @@ -538,6 +540,7 @@ export interface IsometricTaxiLayout { * A map of events to position. Each event represents its own node. */ processNodePositions: Map; + /** * A map of edge-line segments, which graphically connect nodes. */ diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx index dba1136193ee1..c781832dc8a3b 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -137,28 +137,32 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children', it('should render 3 elements with "treeitem" roles, each owned by an element with a "tree" role', async () => { await expect( - simulator.map(() => ({ - nodesOwnedByTrees: simulator.testSubject('resolver:node').filterWhere((domNode) => { - /** - * This test verifies corectness w.r.t. the tree/treeitem roles - * From W3C: `Authors MUST ensure elements with role treeitem are contained in, or owned by, an element with the role group or tree.` - * - * https://www.w3.org/TR/wai-aria-1.1/#tree - * https://www.w3.org/TR/wai-aria-1.1/#treeitem - * - * w3c defines two ways for an element to be an "owned element" - * 1. Any DOM descendant - * 2. Any element specified as a child via aria-owns - * (see: https://www.w3.org/TR/wai-aria-1.1/#dfn-owned-element) - * - * In the context of Resolver (as of this writing) nodes/treeitems are children of the tree, - * but they could be moved out of the tree, provided that the tree is given an `aria-owns` - * attribute referring to them (method 2 above). - */ - return domNode.closest('[role="tree"]').length === 1; - }).length, - })) - ).toYieldEqualTo({ nodesOwnedByTrees: 3 }); + simulator.map(() => { + /** + * This test verifies corectness w.r.t. the tree/treeitem roles + * From W3C: `Authors MUST ensure elements with role treeitem are contained in, or owned by, an element with the role group or tree.` + * + * https://www.w3.org/TR/wai-aria-1.1/#tree + * https://www.w3.org/TR/wai-aria-1.1/#treeitem + * + * w3c defines two ways for an element to be an "owned element" + * 1. Any DOM descendant + * 2. Any element specified as a child via aria-owns + * (see: https://www.w3.org/TR/wai-aria-1.1/#dfn-owned-element) + * + * In the context of Resolver (as of this writing) nodes/treeitems are children of the tree, + * but they could be moved out of the tree, provided that the tree is given an `aria-owns` + * attribute referring to them (method 2 above). + */ + const tree = simulator.domNodesWithRole('tree'); + return { + // There should be only one tree. + treeCount: tree.length, + // The tree should have 3 nodes in it. + nodesOwnedByTrees: tree.find(Simulator.nodeElementSelector()).length, + }; + }) + ).toYieldEqualTo({ treeCount: 1, nodesOwnedByTrees: 3 }); }); it(`should show links to the 3 nodes (with icons) in the node list.`, async () => { diff --git a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx index 777a7292e9c23..411e4b3e3a5b0 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx @@ -25,6 +25,7 @@ const StyledEdgeLine = styled.div` return `${fontSize(props.magFactorX, 12, 8.5)}px`; }}; background-color: ${(props) => props.resolverEdgeColor}; + z-index: 10; `; interface StyledElapsedTime { diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx index 06e3acfb3dc6d..9ef72c414bb63 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx @@ -164,14 +164,14 @@ function NodeDetailLink({ (mouseEvent: React.MouseEvent) => { linkProps.onClick(mouseEvent); dispatch({ - type: 'userBroughtProcessIntoView', + type: 'userBroughtNodeIntoView', payload: { - process: event, + nodeID, time: timestamp(), }, }); }, - [timestamp, linkProps, dispatch, event] + [timestamp, linkProps, dispatch, nodeID] ); return ( diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index d93b46dcb0620..7968b4a3a1fd0 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -62,6 +62,7 @@ const StyledDescriptionText = styled.div` text-align: left; text-transform: uppercase; width: fit-content; + z-index: 40; `; const StyledOuterGroup = styled.g` @@ -311,6 +312,7 @@ const UnstyledProcessEventDot = React.memo( outline: 'transparent', border: 'none', pointerEvents: 'none', + zIndex: 30, }} > @@ -391,6 +393,7 @@ const UnstyledProcessEventDot = React.memo( backgroundColor: colorMap.resolverBackground, alignSelf: 'flex-start', padding: 0, + zIndex: 40, }} > ( { let element: HTMLElement; @@ -198,11 +199,15 @@ describe('useCamera on an unpainted element', () => { throw new Error('missing the process to bring into view'); } simulator.controls.time = 0; + const nodeID = entityIDSafeVersion(process); + if (!nodeID) { + throw new Error('could not find nodeID for process'); + } const cameraAction: ResolverAction = { - type: 'userBroughtProcessIntoView', + type: 'userBroughtNodeIntoView', payload: { time: simulator.controls.time, - process, + nodeID, }, }; await waitFor(() => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.tsx index 72386a2b287f1..2bc202c65f6ab 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/edit_data_provider/index.tsx @@ -15,7 +15,6 @@ import { EuiFormRow, EuiPanel, EuiSpacer, - EuiToolTip, } from '@elastic/eui'; import React, { useEffect, useMemo, useState, useCallback } from 'react'; import styled from 'styled-components'; @@ -193,18 +192,16 @@ export const StatefulEditDataProvider = React.memo( - 0 ? updatedField[0].label : null}> - - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx index 1f76c2840e8b7..cb913287b24d8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx @@ -7,7 +7,7 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPopover, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { getOr } from 'lodash/fp'; -import React, { Fragment, useState } from 'react'; +import React, { useCallback, Fragment, useMemo, useState } from 'react'; import styled from 'styled-components'; import { HostEcs } from '../../../../common/ecs/host'; @@ -260,25 +260,31 @@ MoreContainer.displayName = 'MoreContainer'; export const DefaultFieldRendererOverflow = React.memo( ({ idPrefix, moreMaxHeight, overflowIndexStart = 5, render, rowItems }) => { const [isOpen, setIsOpen] = useState(false); + const handleClose = useCallback(() => setIsOpen(false), []); + const button = useMemo( + () => ( + <> + {' ,'} + + {`+${rowItems.length - overflowIndexStart} `} + + + + ), + [handleClose, overflowIndexStart, rowItems.length] + ); + return ( {rowItems.length > overflowIndexStart && ( - {' ,'} - setIsOpen(!isOpen)}> - {`+${rowItems.length - overflowIndexStart} `} - - - - } + button={button} isOpen={isOpen} - closePopover={() => setIsOpen(!isOpen)} + closePopover={handleClose} repositionOnScroll > ({ + useShallowEqualSelector: jest.fn().mockReturnValue(mockTimelineModel), +})); + +jest.mock('../../../common/containers/use_full_screen', () => ({ + useFullScreen: jest.fn(), +})); + +describe('GraphOverlay', () => { + beforeEach(() => { + (useFullScreen as jest.Mock).mockReturnValue({ + timelineFullScreen: false, + setTimelineFullScreen: jest.fn(), + globalFullScreen: false, + setGlobalFullScreen: jest.fn(), + }); + }); + + describe('when used in an events viewer (i.e. in the Detections view, or the Host > Events view)', () => { + const isEventViewer = true; + const timelineId = 'used-as-an-events-viewer'; + + test('it has 100% width when isEventViewer is true and NOT in full screen mode', async () => { + const wrapper = mount( + + + + ); + + await waitFor(() => { + const overlayContainer = wrapper.find('[data-test-subj="overlayContainer"]').first(); + expect(overlayContainer).toHaveStyleRule('width', '100%'); + }); + }); + + test('it has a calculated width that makes room for the Timeline flyout button when isEventViewer is true in full screen mode', async () => { + (useFullScreen as jest.Mock).mockReturnValue({ + timelineFullScreen: false, + setTimelineFullScreen: jest.fn(), + globalFullScreen: true, // <-- true when an events viewer is in full screen mode + setGlobalFullScreen: jest.fn(), + }); + + const wrapper = mount( + + + + ); + + await waitFor(() => { + const overlayContainer = wrapper.find('[data-test-subj="overlayContainer"]').first(); + expect(overlayContainer).toHaveStyleRule('width', 'calc(100% - 36px)'); + }); + }); + }); + + describe('when used in the active timeline', () => { + const isEventViewer = false; + const timelineId = TimelineId.active; + + test('it has 100% width when isEventViewer is false and NOT in full screen mode', async () => { + const wrapper = mount( + + + + ); + + await waitFor(() => { + const overlayContainer = wrapper.find('[data-test-subj="overlayContainer"]').first(); + expect(overlayContainer).toHaveStyleRule('width', '100%'); + }); + }); + + test('it has 100% width when isEventViewer is false and the active timeline is in full screen mode', async () => { + (useFullScreen as jest.Mock).mockReturnValue({ + timelineFullScreen: true, // <-- true when the active timeline is in full screen mode + setTimelineFullScreen: jest.fn(), + globalFullScreen: false, + setGlobalFullScreen: jest.fn(), + }); + + const wrapper = mount( + + + + ); + + await waitFor(() => { + const overlayContainer = wrapper.find('[data-test-subj="overlayContainer"]').first(); + expect(overlayContainer).toHaveStyleRule('width', '100%'); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx index 7b229b3fbb17e..c3247c337ac3a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx @@ -38,10 +38,13 @@ import { useUiSetting$ } from '../../../common/lib/kibana'; import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index'; const OverlayContainer = styled.div` - height: 100%; - width: 100%; - display: flex; - flex-direction: column; + ${({ $restrictWidth }: { $restrictWidth: boolean }) => + ` + display: flex; + flex-direction: column; + height: 100%; + width: ${$restrictWidth ? 'calc(100% - 36px)' : '100%'}; + `} `; const StyledResolver = styled(Resolver)` @@ -54,6 +57,7 @@ const FullScreenButtonIcon = styled(EuiButtonIcon)` interface OwnProps { graphEventId?: string; + isEventViewer: boolean; timelineId: string; timelineType: TimelineType; } @@ -75,8 +79,8 @@ const Navigation = ({ }) => ( - - {i18n.BACK_TO_EVENTS} + + {i18n.CLOSE_ANALYZER} @@ -100,6 +104,7 @@ const Navigation = ({ const GraphOverlayComponent = ({ graphEventId, + isEventViewer, status, timelineId, title, @@ -151,7 +156,10 @@ const GraphOverlayComponent = ({ }, [signalIndexName, siemDefaultIndices]); return ( - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/translations.ts index c7cd9253de038..58e7045128182 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/translations.ts @@ -6,9 +6,9 @@ import { i18n } from '@kbn/i18n'; -export const BACK_TO_EVENTS = i18n.translate( - 'xpack.securitySolution.timeline.graphOverlay.backToEventsButton', +export const CLOSE_ANALYZER = i18n.translate( + 'xpack.securitySolution.timeline.graphOverlay.closeAnalyzerButton', { - defaultMessage: '< Back to events', + defaultMessage: 'Close analyzer', } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.test.tsx index d0cfbaccde7dd..31051a51a58d5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.test.tsx @@ -12,7 +12,7 @@ import { mockSelectedTimeline } from './mocks'; import * as i18n from '../translations'; import { ReactWrapper, mount } from 'enzyme'; -import { act } from 'react-dom/test-utils'; +import { waitFor } from '@testing-library/react'; import { useParams } from 'react-router-dom'; jest.mock('../translations', () => { @@ -102,15 +102,14 @@ describe('TimelineDownloader', () => { ...defaultTestProps, }; - await act(() => { - wrapper = mount(); - }); - - wrapper.update(); + wrapper = mount(); + await waitFor(() => { + wrapper.update(); - expect(mockDispatchToaster.mock.calls[0][0].title).toEqual( - i18n.SUCCESSFULLY_EXPORTED_TIMELINES - ); + expect(mockDispatchToaster.mock.calls[0][0].title).toEqual( + i18n.SUCCESSFULLY_EXPORTED_TIMELINES + ); + }); }); test('With correct toast message on success for exported templates', async () => { @@ -119,15 +118,15 @@ describe('TimelineDownloader', () => { }; (useParams as jest.Mock).mockReturnValue({ tabName: 'template' }); - await act(() => { - wrapper = mount(); - }); + wrapper = mount(); - wrapper.update(); + await waitFor(() => { + wrapper.update(); - expect(mockDispatchToaster.mock.calls[0][0].title).toEqual( - i18n.SUCCESSFULLY_EXPORTED_TIMELINES - ); + expect(mockDispatchToaster.mock.calls[0][0].title).toEqual( + i18n.SUCCESSFULLY_EXPORTED_TIMELINES + ); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index bf19671687e87..83b8b119faaec 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -134,6 +134,7 @@ export const Body = React.memo( {graphEventId && ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx index 87956647c11f1..36116de8d33d9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx @@ -13,11 +13,16 @@ import { mockBrowserFields } from '../../../../common/containers/source/mock'; import { convertKueryToElasticSearchQuery } from '../../../../common/lib/keury'; import { mockIndexPattern, TestProviders } from '../../../../common/mock'; import { QueryBar } from '../../../../common/components/query_bar'; -import { FilterManager } from '../../../../../../../../src/plugins/data/public'; +import { esFilters, FilterManager } from '../../../../../../../../src/plugins/data/public'; import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; import { buildGlobalQuery } from '../helpers'; -import { QueryBarTimeline, QueryBarTimelineComponentProps, getDataProviderFilter } from './index'; +import { + QueryBarTimeline, + QueryBarTimelineComponentProps, + getDataProviderFilter, + TIMELINE_FILTER_DROP_AREA, +} from './index'; const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings; @@ -39,13 +44,43 @@ describe('Timeline QueryBar ', () => { }); test('check if we format the appropriate props to QueryBar', () => { + const filters = [ + { + $state: { store: esFilters.FilterStateStore.APP_STATE }, + meta: { + alias: null, + controlledBy: TIMELINE_FILTER_DROP_AREA, + disabled: false, + index: undefined, + key: 'event.category', + negate: true, + params: { query: 'file' }, + type: 'phrase', + }, + query: { match: { 'event.category': { query: 'file', type: 'phrase' } } }, + }, + { + $state: { store: esFilters.FilterStateStore.APP_STATE }, + meta: { + alias: null, + controlledBy: undefined, + disabled: false, + index: undefined, + key: 'event.category', + negate: true, + params: { query: 'process' }, + type: 'phrase', + }, + query: { match: { 'event.category': { query: 'process', type: 'phrase' } } }, + }, + ]; const wrapper = mount( { expect(queryBarProps.dateRangeTo).toEqual('now'); expect(queryBarProps.filterQuery).toEqual({ query: 'here: query', language: 'kuery' }); expect(queryBarProps.savedQuery).toEqual(null); + expect(queryBarProps.filters).toHaveLength(1); + expect(queryBarProps.filters[0].query).toEqual(filters[1].query); }); describe('#onChangeQuery', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx index 74f21fecd0fda..3b882c1e1bd14 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx @@ -53,7 +53,10 @@ export interface QueryBarTimelineComponentProps { updateReduxTime: DispatchUpdateReduxTime; } -const timelineFilterDropArea = 'timeline-filter-drop-area'; +export const TIMELINE_FILTER_DROP_AREA = 'timeline-filter-drop-area'; + +const getNonDropAreaFilters = (filters: Filter[] = []) => + filters.filter((f: Filter) => f.meta.controlledBy !== TIMELINE_FILTER_DROP_AREA); export const QueryBarTimeline = memo( ({ @@ -91,7 +94,9 @@ export const QueryBarTimeline = memo( query: filterQuery != null ? filterQuery.expression : '', language: filterQuery != null ? filterQuery.kind : 'kuery', }); - const [queryBarFilters, setQueryBarFilters] = useState([]); + const [queryBarFilters, setQueryBarFilters] = useState( + getNonDropAreaFilters(filters) + ); const [dataProvidersDsl, setDataProvidersDsl] = useState( convertKueryToElasticSearchQuery(buildGlobalQuery(dataProviders, browserFields), indexPattern) ); @@ -106,9 +111,7 @@ export const QueryBarTimeline = memo( filterManager.getUpdates$().subscribe({ next: () => { if (isSubscribed) { - const filterWithoutDropArea = filterManager - .getFilters() - .filter((f: Filter) => f.meta.controlledBy !== timelineFilterDropArea); + const filterWithoutDropArea = getNonDropAreaFilters(filterManager.getFilters()); setFilters(filterWithoutDropArea); setQueryBarFilters(filterWithoutDropArea); } @@ -124,9 +127,7 @@ export const QueryBarTimeline = memo( }, []); useEffect(() => { - const filterWithoutDropArea = filterManager - .getFilters() - .filter((f: Filter) => f.meta.controlledBy !== timelineFilterDropArea); + const filterWithoutDropArea = getNonDropAreaFilters(filterManager.getFilters()); if (!deepEqual(filters, filterWithoutDropArea)) { filterManager.setFilters(filters); } @@ -175,7 +176,7 @@ export const QueryBarTimeline = memo( ...mySavedQuery, attributes: { ...mySavedQuery.attributes, - filters: filters.filter((f) => f.meta.controlledBy !== timelineFilterDropArea), + filters: getNonDropAreaFilters(filters), }, }); } @@ -250,7 +251,7 @@ export const QueryBarTimeline = memo( const dataProviderFilterExists = newSavedQuery.attributes.filters != null ? newSavedQuery.attributes.filters.findIndex( - (f) => f.meta.controlledBy === timelineFilterDropArea + (f) => f.meta.controlledBy === TIMELINE_FILTER_DROP_AREA ) : -1; savedQueryServices.saveQuery( @@ -311,8 +312,8 @@ export const getDataProviderFilter = (dataProviderDsl: string): Filter => { return { ...dslObject, meta: { - alias: timelineFilterDropArea, - controlledBy: timelineFilterDropArea, + alias: TIMELINE_FILTER_DROP_AREA, + controlledBy: TIMELINE_FILTER_DROP_AREA, negate: false, disabled: false, type: 'custom', diff --git a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts index 9fa2257afd411..c513c4576d890 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts @@ -10,7 +10,7 @@ import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import { KbnClient, ToolingLog } from '@kbn/dev-utils'; import { AxiosResponse } from 'axios'; import { indexHostsAndAlerts } from '../../common/endpoint/index_data'; -import { ANCESTRY_LIMIT } from '../../common/endpoint/generate_data'; +import { ANCESTRY_LIMIT, EndpointDocGenerator } from '../../common/endpoint/generate_data'; import { AGENTS_SETUP_API_ROUTES, SETUP_API_ROUTE } from '../../../ingest_manager/common/constants'; import { CreateFleetSetupResponse, @@ -250,6 +250,8 @@ async function main() { percentTerminated: argv.percentTerminated, alwaysGenMaxChildrenPerNode: argv.maxChildrenPerNode, ancestryArraySize: argv.ancestryArraySize, + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(argv.eventIndex), + alertsDataStream: EndpointDocGenerator.createDataStreamFromIndex(argv.alertIndex), } ); console.log(`Creating and indexing documents took: ${new Date().getTime() - startTime}ms`); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts index cc371f9120ba0..c8e0292e8d93a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts @@ -255,11 +255,11 @@ async function enrichHostMetadata( const log = metadataRequestContext.logger; try { /** - * Get agent status by elastic agent id if available or use the host id. + * Get agent status by elastic agent id if available or use the endpoint-agent id. */ if (!elasticAgentId) { - elasticAgentId = hostMetadata.host.id; + elasticAgentId = hostMetadata.agent.id; log.warn(`Missing elastic agent id, using host id instead ${elasticAgentId}`); } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts index cb79263ef6b3c..ac1de377124f0 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts @@ -230,7 +230,7 @@ describe('query builder', () => { expect(query).toEqual({ body: { - query: { match: { 'HostDetails.host.id': mockID } }, + query: { match: { 'HostDetails.agent.id': mockID } }, sort: [{ 'HostDetails.event.created': { order: 'desc' } }], size: 1, }, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts index 0b166e097af94..7980fc83358b1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts @@ -116,14 +116,14 @@ function buildQueryBody( } export function getESQueryHostMetadataByID( - hostID: string, + agentID: string, metadataQueryStrategy: MetadataQueryStrategy ) { return { body: { query: { match: { - [metadataQueryStrategy.hostIdProperty]: hostID, + [metadataQueryStrategy.hostIdProperty]: agentID, }, }, sort: metadataQueryStrategy.sortProperty, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders_v1.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders_v1.test.ts index 899fe4b880acd..ca65d18bb9f6b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders_v1.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders_v1.test.ts @@ -31,7 +31,7 @@ describe('query builder v1', () => { match_all: {}, }, collapse: { - field: 'host.id', + field: 'agent.id', inner_hits: { name: 'most_recent', size: 1, @@ -41,7 +41,7 @@ describe('query builder v1', () => { aggs: { total: { cardinality: { - field: 'host.id', + field: 'agent.id', }, }, }, @@ -92,7 +92,7 @@ describe('query builder v1', () => { }, }, collapse: { - field: 'host.id', + field: 'agent.id', inner_hits: { name: 'most_recent', size: 1, @@ -102,7 +102,7 @@ describe('query builder v1', () => { aggs: { total: { cardinality: { - field: 'host.id', + field: 'agent.id', }, }, }, @@ -165,7 +165,7 @@ describe('query builder v1', () => { }, }, collapse: { - field: 'host.id', + field: 'agent.id', inner_hits: { name: 'most_recent', size: 1, @@ -175,7 +175,7 @@ describe('query builder v1', () => { aggs: { total: { cardinality: { - field: 'host.id', + field: 'agent.id', }, }, }, @@ -251,7 +251,7 @@ describe('query builder v1', () => { }, }, collapse: { - field: 'host.id', + field: 'agent.id', inner_hits: { name: 'most_recent', size: 1, @@ -261,7 +261,7 @@ describe('query builder v1', () => { aggs: { total: { cardinality: { - field: 'host.id', + field: 'agent.id', }, }, }, @@ -289,7 +289,7 @@ describe('query builder v1', () => { expect(query).toEqual({ body: { - query: { match: { 'host.id': mockID } }, + query: { match: { 'agent.id': mockID } }, sort: [{ 'event.created': { order: 'desc' } }], size: 1, }, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/query_strategies.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/query_strategies.ts index df4c377262466..f1614cc19e8c8 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/query_strategies.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/query_strategies.ts @@ -23,7 +23,7 @@ export function metadataQueryStrategyV1(): MetadataQueryStrategy { return { index: metadataIndexPattern, elasticAgentIdProperty: 'elastic.agent.id', - hostIdProperty: 'host.id', + hostIdProperty: 'agent.id', sortProperty: [ { 'event.created': { @@ -33,7 +33,7 @@ export function metadataQueryStrategyV1(): MetadataQueryStrategy { ], extraBodyProperties: { collapse: { - field: 'host.id', + field: 'agent.id', inner_hits: { name: 'most_recent', size: 1, @@ -43,7 +43,7 @@ export function metadataQueryStrategyV1(): MetadataQueryStrategy { aggs: { total: { cardinality: { - field: 'host.id', + field: 'agent.id', }, }, }, @@ -78,7 +78,7 @@ export function metadataQueryStrategyV2(): MetadataQueryStrategy { return { index: metadataCurrentIndexPattern, elasticAgentIdProperty: 'HostDetails.elastic.agent.id', - hostIdProperty: 'HostDetails.host.id', + hostIdProperty: 'HostDetails.agent.id', sortProperty: [ { 'HostDetails.event.created': { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts index 8d4524e06c49f..7dddc357fe53d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts @@ -51,7 +51,7 @@ describe('test policy response handler', () => { mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); const mockRequest = httpServerMock.createKibanaRequest({ - params: { hostId: 'id' }, + params: { agentId: 'id' }, }); await hostPolicyResponseHandler( @@ -62,7 +62,7 @@ describe('test policy response handler', () => { expect(mockResponse.ok).toBeCalled(); const result = mockResponse.ok.mock.calls[0][0]?.body as GetHostPolicyResponse; - expect(result.policy_response.host.id).toEqual(response.hits.hits[0]._source.host.id); + expect(result.policy_response.agent.id).toEqual(response.hits.hits[0]._source.agent.id); }); it('should return not found when there is no response policy for host', async () => { @@ -77,7 +77,7 @@ describe('test policy response handler', () => { ); const mockRequest = httpServerMock.createKibanaRequest({ - params: { hostId: 'id' }, + params: { agentId: 'id' }, }); await hostPolicyResponseHandler( diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts index fd685efb94aaa..f3a7b08a4cd44 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts @@ -8,16 +8,16 @@ import { TypeOf } from '@kbn/config-schema'; import { policyIndexPattern } from '../../../../common/endpoint/constants'; import { GetPolicyResponseSchema } from '../../../../common/endpoint/schema/policy'; import { EndpointAppContext } from '../../types'; -import { getPolicyResponseByHostId } from './service'; +import { getPolicyResponseByAgentId } from './service'; export const getHostPolicyResponseHandler = function ( endpointAppContext: EndpointAppContext ): RequestHandler, undefined> { return async (context, request, response) => { try { - const doc = await getPolicyResponseByHostId( + const doc = await getPolicyResponseByAgentId( policyIndexPattern, - request.query.hostId, + request.query.agentId, context.core.elasticsearch.legacy.client ); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.test.ts index f05d9ef5b821a..40a691c1ddbdf 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.test.ts @@ -5,13 +5,13 @@ */ import { GetPolicyResponseSchema } from '../../../../common/endpoint/schema/policy'; -import { getESQueryPolicyResponseByHostID } from './service'; +import { getESQueryPolicyResponseByAgentID } from './service'; describe('test policy handlers schema', () => { it('validate that get policy response query schema', async () => { expect( GetPolicyResponseSchema.query.validate({ - hostId: 'id', + agentId: 'id', }) ).toBeTruthy(); @@ -21,13 +21,13 @@ describe('test policy handlers schema', () => { describe('test policy query', () => { it('queries for the correct host', async () => { - const hostID = 'f757d3c0-e874-11ea-9ad9-015510b487f4'; - const query = getESQueryPolicyResponseByHostID(hostID, 'anyindex'); - expect(query.body.query.bool.filter.term).toEqual({ 'host.id': hostID }); + const agentId = 'f757d3c0-e874-11ea-9ad9-015510b487f4'; + const query = getESQueryPolicyResponseByAgentID(agentId, 'anyindex'); + expect(query.body.query.bool.filter.term).toEqual({ 'agent.id': agentId }); }); it('filters out initial policy by ID', async () => { - const query = getESQueryPolicyResponseByHostID( + const query = getESQueryPolicyResponseByAgentID( 'f757d3c0-e874-11ea-9ad9-015510b487f4', 'anyindex' ); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts index 1b3d232f9421c..0019c97a6cced 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts @@ -9,14 +9,14 @@ import { ILegacyScopedClusterClient } from 'kibana/server'; import { GetHostPolicyResponse, HostPolicyResponse } from '../../../../common/endpoint/types'; import { INITIAL_POLICY_ID } from './index'; -export function getESQueryPolicyResponseByHostID(hostID: string, index: string) { +export function getESQueryPolicyResponseByAgentID(agentID: string, index: string) { return { body: { query: { bool: { filter: { term: { - 'host.id': hostID, + 'agent.id': agentID, }, }, must_not: { @@ -39,12 +39,12 @@ export function getESQueryPolicyResponseByHostID(hostID: string, index: string) }; } -export async function getPolicyResponseByHostId( +export async function getPolicyResponseByAgentId( index: string, - hostId: string, + agentID: string, dataClient: ILegacyScopedClusterClient ): Promise { - const query = getESQueryPolicyResponseByHostID(hostId, index); + const query = getESQueryPolicyResponseByAgentID(agentID, index); const response = (await dataClient.callAsCurrentUser('search', query)) as SearchResponse< HostPolicyResponse >; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts index 55965e5a9c70e..d5297072388e7 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.test.ts @@ -20,7 +20,7 @@ describe('Pagination', () => { }; describe('cursor', () => { const root = generator.generateEvent(); - const events = Array.from(generator.relatedEventsGenerator(root, 5)); + const events = Array.from(generator.relatedEventsGenerator({ node: root, relatedEvents: 5 })); it('does build a cursor when received the same number of events as was requested', () => { expect(PaginationBuilder.buildCursorRequestLimit(4, events)).not.toBeNull(); diff --git a/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts index 48bb0cbe37afd..4623fa6514e75 100644 --- a/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts @@ -26,6 +26,10 @@ export const hostsSchema = gql` type: String } + type AgentFields { + id: String + } + type CloudInstance { id: [String] } @@ -55,6 +59,7 @@ export const hostsSchema = gql` type HostItem { _id: String + agent: AgentFields cloud: CloudFields endpoint: EndpointFields host: HostEcsFields diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index 7730cea2b9845..bda0fed494a6f 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -492,6 +492,8 @@ export interface HostsEdges { export interface HostItem { _id?: Maybe; + agent?: Maybe; + cloud?: Maybe; endpoint?: Maybe; @@ -503,6 +505,10 @@ export interface HostItem { lastSeen?: Maybe; } +export interface AgentFields { + id?: Maybe; +} + export interface CloudFields { instance?: Maybe; @@ -2268,6 +2274,8 @@ export namespace HostItemResolvers { export interface Resolvers { _id?: _IdResolver, TypeParent, TContext>; + agent?: AgentResolver, TypeParent, TContext>; + cloud?: CloudResolver, TypeParent, TContext>; endpoint?: EndpointResolver, TypeParent, TContext>; @@ -2284,6 +2292,11 @@ export namespace HostItemResolvers { Parent, TContext >; + export type AgentResolver< + R = Maybe, + Parent = HostItem, + TContext = SiemContext + > = Resolver; export type CloudResolver< R = Maybe, Parent = HostItem, @@ -2311,6 +2324,19 @@ export namespace HostItemResolvers { > = Resolver; } +export namespace AgentFieldsResolvers { + export interface Resolvers { + id?: IdResolver, TypeParent, TContext>; + } + + export type IdResolver< + R = Maybe, + Parent = AgentFields, + TContext = SiemContext + > = Resolver; +} + + export namespace CloudFieldsResolvers { export interface Resolvers { instance?: InstanceResolver, TypeParent, TContext>; @@ -6043,6 +6069,7 @@ export type IResolvers = { HostsData?: HostsDataResolvers.Resolvers; HostsEdges?: HostsEdgesResolvers.Resolvers; HostItem?: HostItemResolvers.Resolvers; + AgentFields?: AgentFieldsResolvers.Resolvers; CloudFields?: CloudFieldsResolvers.Resolvers; CloudInstance?: CloudInstanceResolvers.Resolvers; CloudMachine?: CloudMachineResolvers.Resolvers; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/check_template_version.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/check_template_version.ts new file mode 100644 index 0000000000000..473a2dad37f19 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/check_template_version.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash'; +import { LegacyAPICaller } from '../../../../../../../../src/core/server'; +import { getSignalsTemplate } from './get_signals_template'; +import { getTemplateExists } from '../../index/get_template_exists'; + +export const templateNeedsUpdate = async (callCluster: LegacyAPICaller, index: string) => { + const templateExists = await getTemplateExists(callCluster, index); + let existingTemplateVersion: number | undefined; + if (templateExists) { + const existingTemplate: unknown = await callCluster('indices.getTemplate', { + name: index, + }); + existingTemplateVersion = get(existingTemplate, [index, 'version']); + } + const newTemplate = getSignalsTemplate(index); + if (existingTemplateVersion === undefined || existingTemplateVersion < newTemplate.version) { + return true; + } + return false; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts index a09fd9e0c9bd9..a801bc18db439 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts @@ -12,9 +12,9 @@ import { getPolicyExists } from '../../index/get_policy_exists'; import { setPolicy } from '../../index/set_policy'; import { setTemplate } from '../../index/set_template'; import { getSignalsTemplate } from './get_signals_template'; -import { getTemplateExists } from '../../index/get_template_exists'; import { createBootstrapIndex } from '../../index/create_bootstrap_index'; import signalsPolicy from './signals_policy.json'; +import { templateNeedsUpdate } from './check_template_version'; export const createIndexRoute = (router: IRouter) => { router.post( @@ -39,24 +39,20 @@ export const createIndexRoute = (router: IRouter) => { const index = siemClient.getSignalsIndex(); const indexExists = await getIndexExists(callCluster, index); - if (indexExists) { - return siemResponse.error({ - statusCode: 409, - body: `index: "${index}" already exists`, - }); - } else { + if (await templateNeedsUpdate(callCluster, index)) { const policyExists = await getPolicyExists(callCluster, index); if (!policyExists) { await setPolicy(callCluster, index, signalsPolicy); } - const templateExists = await getTemplateExists(callCluster, index); - if (!templateExists) { - const template = getSignalsTemplate(index); - await setTemplate(callCluster, index, template); + await setTemplate(callCluster, index, getSignalsTemplate(index)); + if (indexExists) { + await callCluster('indices.rollover', { alias: index }); } + } + if (!indexExists) { await createBootstrapIndex(callCluster, index); - return response.ok({ body: { acknowledged: true } }); } + return response.ok({ body: { acknowledged: true } }); } catch (err) { const error = transformError(err); return siemResponse.error({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts index 7debe0931abd6..b9ae8b546b8bd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts @@ -8,6 +8,7 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_INDEX_URL } from '../../../../../common/constants'; import { transformError, buildSiemResponse } from '../utils'; import { getIndexExists } from '../../index/get_index_exists'; +import { templateNeedsUpdate } from './check_template_version'; export const readIndexRoute = (router: IRouter) => { router.get( @@ -31,9 +32,10 @@ export const readIndexRoute = (router: IRouter) => { const index = siemClient.getSignalsIndex(); const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, index); + const templateOutdated = await templateNeedsUpdate(clusterClient.callAsCurrentUser, index); if (indexExists) { - return response.ok({ body: { name: index } }); + return response.ok({ body: { name: index, template_outdated: templateOutdated } }); } else { return siemResponse.error({ statusCode: 404, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.test.ts index 5b2eb24bcdcd2..a21f861c1f348 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.test.ts @@ -4,102 +4,470 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getRulesToUpdate } from './get_rules_to_update'; +import { filterInstalledRules, getRulesToUpdate, mergeExceptionLists } from './get_rules_to_update'; import { getResult } from '../routes/__mocks__/request_responses'; import { getAddPrepackagedRulesSchemaDecodedMock } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock'; describe('get_rules_to_update', () => { - test('should return empty array if both rule sets are empty', () => { - const update = getRulesToUpdate([], []); - expect(update).toEqual([]); - }); + describe('get_rules_to_update', () => { + test('should return empty array if both rule sets are empty', () => { + const update = getRulesToUpdate([], []); + expect(update).toEqual([]); + }); - test('should return empty array if the id of the two rules do not match', () => { - const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); - ruleFromFileSystem.rule_id = 'rule-1'; - ruleFromFileSystem.version = 2; + test('should return empty array if the rule_id of the two rules do not match', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 2; - const installedRule = getResult(); - installedRule.params.ruleId = 'rule-2'; - installedRule.params.version = 1; - const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]); - expect(update).toEqual([]); - }); + const installedRule = getResult(); + installedRule.params.ruleId = 'rule-2'; + installedRule.params.version = 1; + const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]); + expect(update).toEqual([]); + }); - test('should return empty array if the id of file system rule is less than the installed version', () => { - const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); - ruleFromFileSystem.rule_id = 'rule-1'; - ruleFromFileSystem.version = 1; + test('should return empty array if the version of file system rule is less than the installed version', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 1; - const installedRule = getResult(); - installedRule.params.ruleId = 'rule-1'; - installedRule.params.version = 2; - const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]); - expect(update).toEqual([]); - }); + const installedRule = getResult(); + installedRule.params.ruleId = 'rule-1'; + installedRule.params.version = 2; + const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]); + expect(update).toEqual([]); + }); - test('should return empty array if the id of file system rule is the same as the installed version', () => { - const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); - ruleFromFileSystem.rule_id = 'rule-1'; - ruleFromFileSystem.version = 1; + test('should return empty array if the version of file system rule is the same as the installed version', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 1; - const installedRule = getResult(); - installedRule.params.ruleId = 'rule-1'; - installedRule.params.version = 1; - const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]); - expect(update).toEqual([]); - }); + const installedRule = getResult(); + installedRule.params.ruleId = 'rule-1'; + installedRule.params.version = 1; + const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]); + expect(update).toEqual([]); + }); + + test('should return the rule to update if the version of file system rule is greater than the installed version', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 2; + + const installedRule = getResult(); + installedRule.params.ruleId = 'rule-1'; + installedRule.params.version = 1; + installedRule.params.exceptionsList = []; + + const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]); + expect(update).toEqual([ruleFromFileSystem]); + }); + + test('should return 1 rule out of 2 to update if the version of file system rule is greater than the installed version of just one', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = []; + + const installedRule2 = getResult(); + installedRule2.params.ruleId = 'rule-2'; + installedRule2.params.version = 1; + installedRule2.params.exceptionsList = []; + + const update = getRulesToUpdate([ruleFromFileSystem], [installedRule1, installedRule2]); + expect(update).toEqual([ruleFromFileSystem]); + }); + + test('should return 2 rules out of 2 to update if the version of file system rule is greater than the installed version of both', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const ruleFromFileSystem2 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem2.rule_id = 'rule-2'; + ruleFromFileSystem2.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = []; + + const installedRule2 = getResult(); + installedRule2.params.ruleId = 'rule-2'; + installedRule2.params.version = 1; + installedRule2.params.exceptionsList = []; + + const update = getRulesToUpdate( + [ruleFromFileSystem1, ruleFromFileSystem2], + [installedRule1, installedRule2] + ); + expect(update).toEqual([ruleFromFileSystem1, ruleFromFileSystem2]); + }); + + test('should add back an exception_list if it was removed by the end user on an immutable rule during an upgrade', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = []; + + const [update] = getRulesToUpdate([ruleFromFileSystem1], [installedRule1]); + expect(update.exceptions_list).toEqual(ruleFromFileSystem1.exceptions_list); + }); + + test('should not remove an additional exception_list if an additional one was added by the end user on an immutable rule during an upgrade', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = [ + { + id: 'second_exception_list', + list_id: 'some-other-id', + namespace_type: 'single', + type: 'detection', + }, + ]; + + const [update] = getRulesToUpdate([ruleFromFileSystem1], [installedRule1]); + expect(update.exceptions_list).toEqual([ + ...ruleFromFileSystem1.exceptions_list, + ...installedRule1.params.exceptionsList, + ]); + }); + + test('should not remove an existing exception_list if they are the same between the current installed one and the upgraded one', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; - test('should return the rule to update if the id of file system rule is greater than the installed version', () => { - const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); - ruleFromFileSystem.rule_id = 'rule-1'; - ruleFromFileSystem.version = 2; + const [update] = getRulesToUpdate([ruleFromFileSystem1], [installedRule1]); + expect(update.exceptions_list).toEqual(ruleFromFileSystem1.exceptions_list); + }); - const installedRule = getResult(); - installedRule.params.ruleId = 'rule-1'; - installedRule.params.version = 1; - const update = getRulesToUpdate([ruleFromFileSystem], [installedRule]); - expect(update).toEqual([ruleFromFileSystem]); + test('should not remove an existing exception_list if the rule has an empty exceptions list', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = []; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + + const [update] = getRulesToUpdate([ruleFromFileSystem1], [installedRule1]); + expect(update.exceptions_list).toEqual(installedRule1.params.exceptionsList); + }); + + test('should not remove an existing exception_list if the rule has an empty exceptions list for multiple rules', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = []; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const ruleFromFileSystem2 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem2.exceptions_list = []; + ruleFromFileSystem2.rule_id = 'rule-2'; + ruleFromFileSystem2.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + const installedRule2 = getResult(); + installedRule2.params.ruleId = 'rule-2'; + installedRule2.params.version = 1; + installedRule2.params.exceptionsList = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + + const [update1, update2] = getRulesToUpdate( + [ruleFromFileSystem1, ruleFromFileSystem2], + [installedRule1, installedRule2] + ); + expect(update1.exceptions_list).toEqual(installedRule1.params.exceptionsList); + expect(update2.exceptions_list).toEqual(installedRule2.params.exceptionsList); + }); + + test('should not remove an existing exception_list if the rule has an empty exceptions list for mixed rules', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = []; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const ruleFromFileSystem2 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem2.exceptions_list = []; + ruleFromFileSystem2.rule_id = 'rule-2'; + ruleFromFileSystem2.version = 2; + ruleFromFileSystem2.exceptions_list = [ + { + id: 'second_list', + list_id: 'second_list', + namespace_type: 'single', + type: 'detection', + }, + ]; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + + const installedRule2 = getResult(); + installedRule2.params.ruleId = 'rule-2'; + installedRule2.params.version = 1; + installedRule2.params.exceptionsList = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + + const [update1, update2] = getRulesToUpdate( + [ruleFromFileSystem1, ruleFromFileSystem2], + [installedRule1, installedRule2] + ); + expect(update1.exceptions_list).toEqual(installedRule1.params.exceptionsList); + expect(update2.exceptions_list).toEqual([ + ...ruleFromFileSystem2.exceptions_list, + ...installedRule2.params.exceptionsList, + ]); + }); }); - test('should return 1 rule out of 2 to update if the id of file system rule is greater than the installed version of just one', () => { - const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); - ruleFromFileSystem.rule_id = 'rule-1'; - ruleFromFileSystem.version = 2; + describe('filterInstalledRules', () => { + test('should return "false" if the id of the two rules do not match', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 2; + + const installedRule = getResult(); + installedRule.params.ruleId = 'rule-2'; + installedRule.params.version = 1; + const shouldUpdate = filterInstalledRules(ruleFromFileSystem, [installedRule]); + expect(shouldUpdate).toEqual(false); + }); - const installedRule1 = getResult(); - installedRule1.params.ruleId = 'rule-1'; - installedRule1.params.version = 1; + test('should return "false" if the version of file system rule is less than the installed version', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 1; - const installedRule2 = getResult(); - installedRule2.params.ruleId = 'rule-2'; - installedRule2.params.version = 1; + const installedRule = getResult(); + installedRule.params.ruleId = 'rule-1'; + installedRule.params.version = 2; + const shouldUpdate = filterInstalledRules(ruleFromFileSystem, [installedRule]); + expect(shouldUpdate).toEqual(false); + }); - const update = getRulesToUpdate([ruleFromFileSystem], [installedRule1, installedRule2]); - expect(update).toEqual([ruleFromFileSystem]); + test('should return "false" if the version of file system rule is the same as the installed version', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 1; + + const installedRule = getResult(); + installedRule.params.ruleId = 'rule-1'; + installedRule.params.version = 1; + const shouldUpdate = filterInstalledRules(ruleFromFileSystem, [installedRule]); + expect(shouldUpdate).toEqual(false); + }); + + test('should return "true" to update if the version of file system rule is greater than the installed version', () => { + const ruleFromFileSystem = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem.rule_id = 'rule-1'; + ruleFromFileSystem.version = 2; + + const installedRule = getResult(); + installedRule.params.ruleId = 'rule-1'; + installedRule.params.version = 1; + installedRule.params.exceptionsList = []; + + const shouldUpdate = filterInstalledRules(ruleFromFileSystem, [installedRule]); + expect(shouldUpdate).toEqual(true); + }); }); - test('should return 2 rules out of 2 to update if the id of file system rule is greater than the installed version of both', () => { - const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); - ruleFromFileSystem1.rule_id = 'rule-1'; - ruleFromFileSystem1.version = 2; + describe('mergeExceptionLists', () => { + test('should add back an exception_list if it was removed by the end user on an immutable rule during an upgrade', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = []; + + const update = mergeExceptionLists(ruleFromFileSystem1, [installedRule1]); + expect(update.exceptions_list).toEqual(ruleFromFileSystem1.exceptions_list); + }); + + test('should not remove an additional exception_list if an additional one was added by the end user on an immutable rule during an upgrade', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = [ + { + id: 'second_exception_list', + list_id: 'some-other-id', + namespace_type: 'single', + type: 'detection', + }, + ]; + + const update = mergeExceptionLists(ruleFromFileSystem1, [installedRule1]); + expect(update.exceptions_list).toEqual([ + ...ruleFromFileSystem1.exceptions_list, + ...installedRule1.params.exceptionsList, + ]); + }); + + test('should not remove an existing exception_list if they are the same between the current installed one and the upgraded one', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; + + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; - const ruleFromFileSystem2 = getAddPrepackagedRulesSchemaDecodedMock(); - ruleFromFileSystem2.rule_id = 'rule-2'; - ruleFromFileSystem2.version = 2; + const update = mergeExceptionLists(ruleFromFileSystem1, [installedRule1]); + expect(update.exceptions_list).toEqual(ruleFromFileSystem1.exceptions_list); + }); - const installedRule1 = getResult(); - installedRule1.params.ruleId = 'rule-1'; - installedRule1.params.version = 1; + test('should not remove an existing exception_list if the rule has an empty exceptions list', () => { + const ruleFromFileSystem1 = getAddPrepackagedRulesSchemaDecodedMock(); + ruleFromFileSystem1.exceptions_list = []; + ruleFromFileSystem1.rule_id = 'rule-1'; + ruleFromFileSystem1.version = 2; - const installedRule2 = getResult(); - installedRule2.params.ruleId = 'rule-2'; - installedRule2.params.version = 1; + const installedRule1 = getResult(); + installedRule1.params.ruleId = 'rule-1'; + installedRule1.params.version = 1; + installedRule1.params.exceptionsList = [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + namespace_type: 'agnostic', + type: 'endpoint', + }, + ]; - const update = getRulesToUpdate( - [ruleFromFileSystem1, ruleFromFileSystem2], - [installedRule1, installedRule2] - ); - expect(update).toEqual([ruleFromFileSystem1, ruleFromFileSystem2]); + const update = mergeExceptionLists(ruleFromFileSystem1, [installedRule1]); + expect(update.exceptions_list).toEqual(installedRule1.params.exceptionsList); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.ts index 577ad44789bdc..28a58ea49b903 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_rules_to_update.ts @@ -7,15 +7,67 @@ import { AddPrepackagedRulesSchemaDecoded } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema'; import { RuleAlertType } from './types'; +/** + * Returns the rules to update by doing a compare to the rules from the file system against + * the installed rules already. This also merges exception list items between the two since + * exception list items can exist on both rules to update and already installed rules. + * @param rulesFromFileSystem The rules on the file system to check against installed + * @param installedRules The installed rules + */ export const getRulesToUpdate = ( rulesFromFileSystem: AddPrepackagedRulesSchemaDecoded[], installedRules: RuleAlertType[] ): AddPrepackagedRulesSchemaDecoded[] => { - return rulesFromFileSystem.filter((rule) => - installedRules.some((installedRule) => { - return ( - rule.rule_id === installedRule.params.ruleId && rule.version > installedRule.params.version + return rulesFromFileSystem + .filter((ruleFromFileSystem) => filterInstalledRules(ruleFromFileSystem, installedRules)) + .map((ruleFromFileSystem) => mergeExceptionLists(ruleFromFileSystem, installedRules)); +}; + +/** + * Filters rules from the file system that do not match the installed rules so you only + * get back rules that are going to be updated + * @param ruleFromFileSystem The rules from the file system to check if any are updates + * @param installedRules The installed rules to compare against for updates + */ +export const filterInstalledRules = ( + ruleFromFileSystem: AddPrepackagedRulesSchemaDecoded, + installedRules: RuleAlertType[] +): boolean => { + return installedRules.some((installedRule) => { + return ( + ruleFromFileSystem.rule_id === installedRule.params.ruleId && + ruleFromFileSystem.version > installedRule.params.version + ); + }); +}; + +/** + * Given a rule from the file system and the set of installed rules this will merge the exception lists + * from the installed rules onto the rules from the file system. + * @param ruleFromFileSystem The rules from the file system that might have exceptions_lists + * @param installedRules The installed rules which might have user driven exceptions_lists + */ +export const mergeExceptionLists = ( + ruleFromFileSystem: AddPrepackagedRulesSchemaDecoded, + installedRules: RuleAlertType[] +): AddPrepackagedRulesSchemaDecoded => { + if (ruleFromFileSystem.exceptions_list != null) { + const installedRule = installedRules.find( + (ruleToFind) => ruleToFind.params.ruleId === ruleFromFileSystem.rule_id + ); + if (installedRule != null && installedRule.params.exceptionsList != null) { + const installedExceptionList = installedRule.params.exceptionsList; + const fileSystemExceptions = ruleFromFileSystem.exceptions_list.filter((potentialDuplicate) => + installedExceptionList.every((item) => item.list_id !== potentialDuplicate.list_id) ); - }) - ); + return { + ...ruleFromFileSystem, + exceptions_list: [...fileSystemExceptions, ...installedRule.params.exceptionsList], + }; + } else { + return ruleFromFileSystem; + } + } else { + return ruleFromFileSystem; + } }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts index 09ddfb342496d..037f91240edfa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts @@ -4,15 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchResponse } from 'elasticsearch'; import { getThreatList } from './get_threat_list'; import { buildThreatMappingFilter } from './build_threat_mapping_filter'; import { getFilter } from '../get_filter'; import { searchAfterAndBulkCreate } from '../search_after_bulk_create'; -import { CreateThreatSignalOptions, ThreatListItem } from './types'; +import { CreateThreatSignalOptions, ThreatSignalResults } from './types'; import { combineResults } from './utils'; -import { SearchAfterAndBulkCreateReturnType } from '../types'; export const createThreatSignal = async ({ threatMapping, @@ -51,57 +49,7 @@ export const createThreatSignal = async ({ name, currentThreatList, currentResult, -}: CreateThreatSignalOptions): Promise<{ - threatList: SearchResponse; - results: SearchAfterAndBulkCreateReturnType; -}> => { - const threatFilter = buildThreatMappingFilter({ - threatMapping, - threatList: currentThreatList, - }); - - const esFilter = await getFilter({ - type, - filters: [...filters, threatFilter], - language, - query, - savedId, - services, - index: inputIndex, - lists: exceptionItems, - }); - - const newResult = await searchAfterAndBulkCreate({ - gap, - previousStartedAt, - listClient, - exceptionsList: exceptionItems, - ruleParams: params, - services, - logger, - eventsTelemetry, - id: alertId, - inputIndexPattern: inputIndex, - signalsIndex: outputIndex, - filter: esFilter, - actions, - name, - createdBy, - createdAt, - updatedBy, - updatedAt, - interval, - enabled, - pageSize: searchAfterSize, - refresh, - tags, - throttle, - buildRuleMessage, - }); - - const results = combineResults(currentResult, newResult); - const searchAfter = currentThreatList.hits.hits[currentThreatList.hits.hits.length - 1].sort; - +}: CreateThreatSignalOptions): Promise => { const threatList = await getThreatList({ callCluster: services.callCluster, exceptionItems, @@ -109,10 +57,60 @@ export const createThreatSignal = async ({ language: threatLanguage, threatFilters, index: threatIndex, - searchAfter, + searchAfter: currentThreatList.hits.hits[currentThreatList.hits.hits.length - 1].sort, sortField: undefined, sortOrder: undefined, + listClient, + }); + + const threatFilter = buildThreatMappingFilter({ + threatMapping, + threatList: currentThreatList, }); - return { threatList, results }; + if (threatFilter.query.bool.should.length === 0) { + // empty threat list and we do not want to return everything as being + // a hit so opt to return the existing result. + return { threatList, results: currentResult }; + } else { + const esFilter = await getFilter({ + type, + filters: [...filters, threatFilter], + language, + query, + savedId, + services, + index: inputIndex, + lists: exceptionItems, + }); + const newResult = await searchAfterAndBulkCreate({ + gap, + previousStartedAt, + listClient, + exceptionsList: exceptionItems, + ruleParams: params, + services, + logger, + eventsTelemetry, + id: alertId, + inputIndexPattern: inputIndex, + signalsIndex: outputIndex, + filter: esFilter, + actions, + name, + createdBy, + createdAt, + updatedBy, + updatedAt, + interval, + enabled, + pageSize: searchAfterSize, + refresh, + tags, + throttle, + buildRuleMessage, + }); + const results = combineResults(currentResult, newResult); + return { threatList, results }; + } }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts index eeace508c9bfe..8be76dc8caf0f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts @@ -62,6 +62,7 @@ export const createThreatSignals = async ({ query: threatQuery, language: threatLanguage, index: threatIndex, + listClient, searchAfter: undefined, sortField: undefined, sortOrder: undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.test.ts index f600463c213c2..8a689f455c31d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.test.ts @@ -9,23 +9,83 @@ import { getSortWithTieBreaker } from './get_threat_list'; describe('get_threat_signals', () => { describe('getSortWithTieBreaker', () => { test('it should return sort field of just timestamp if given no sort order', () => { - const sortOrder = getSortWithTieBreaker({ sortField: undefined, sortOrder: undefined }); + const sortOrder = getSortWithTieBreaker({ + sortField: undefined, + sortOrder: undefined, + index: ['index-123'], + listItemIndex: 'list-index-123', + }); expect(sortOrder).toEqual([{ '@timestamp': 'asc' }]); }); + test('it should return sort field of just tie_breaker_id if given no sort order for a list item index', () => { + const sortOrder = getSortWithTieBreaker({ + sortField: undefined, + sortOrder: undefined, + index: ['list-item-index-123'], + listItemIndex: 'list-item-index-123', + }); + expect(sortOrder).toEqual([{ tie_breaker_id: 'asc' }]); + }); + test('it should return sort field of timestamp with asc even if sortOrder is changed as it is hard wired in', () => { - const sortOrder = getSortWithTieBreaker({ sortField: undefined, sortOrder: 'desc' }); + const sortOrder = getSortWithTieBreaker({ + sortField: undefined, + sortOrder: 'desc', + index: ['index-123'], + listItemIndex: 'list-index-123', + }); expect(sortOrder).toEqual([{ '@timestamp': 'asc' }]); }); + test('it should return sort field of tie_breaker_id with asc even if sortOrder is changed as it is hard wired in for a list item index', () => { + const sortOrder = getSortWithTieBreaker({ + sortField: undefined, + sortOrder: 'desc', + index: ['list-index-123'], + listItemIndex: 'list-index-123', + }); + expect(sortOrder).toEqual([{ tie_breaker_id: 'asc' }]); + }); + test('it should return sort field of an extra field if given one', () => { - const sortOrder = getSortWithTieBreaker({ sortField: 'some-field', sortOrder: undefined }); + const sortOrder = getSortWithTieBreaker({ + sortField: 'some-field', + sortOrder: undefined, + index: ['index-123'], + listItemIndex: 'list-index-123', + }); expect(sortOrder).toEqual([{ 'some-field': 'asc', '@timestamp': 'asc' }]); }); + test('it should return sort field of an extra field if given one for a list item index', () => { + const sortOrder = getSortWithTieBreaker({ + sortField: 'some-field', + sortOrder: undefined, + index: ['list-index-123'], + listItemIndex: 'list-index-123', + }); + expect(sortOrder).toEqual([{ 'some-field': 'asc', tie_breaker_id: 'asc' }]); + }); + test('it should return sort field of desc if given one', () => { - const sortOrder = getSortWithTieBreaker({ sortField: 'some-field', sortOrder: 'desc' }); + const sortOrder = getSortWithTieBreaker({ + sortField: 'some-field', + sortOrder: 'desc', + index: ['index-123'], + listItemIndex: 'list-index-123', + }); expect(sortOrder).toEqual([{ 'some-field': 'desc', '@timestamp': 'asc' }]); }); + + test('it should return sort field of desc if given one for a list item index', () => { + const sortOrder = getSortWithTieBreaker({ + sortField: 'some-field', + sortOrder: 'desc', + index: ['list-index-123'], + listItemIndex: 'list-index-123', + }); + expect(sortOrder).toEqual([{ 'some-field': 'desc', tie_breaker_id: 'asc' }]); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts index 3c3f5b544bb17..3147eb1705168 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts @@ -29,6 +29,7 @@ export const getThreatList = async ({ sortOrder, exceptionItems, threatFilters, + listClient, }: GetThreatListOptions): Promise> => { const calculatedPerPage = perPage ?? MAX_PER_PAGE; if (calculatedPerPage > 10000) { @@ -41,11 +42,17 @@ export const getThreatList = async ({ index, exceptionItems ); + const response: SearchResponse = await callCluster('search', { body: { query: queryFilter, search_after: searchAfter, - sort: getSortWithTieBreaker({ sortField, sortOrder }), + sort: getSortWithTieBreaker({ + sortField, + sortOrder, + index, + listItemIndex: listClient.getListItemIndex(), + }), }, ignoreUnavailable: true, index, @@ -54,14 +61,31 @@ export const getThreatList = async ({ return response; }; +/** + * This returns the sort with a tiebreaker if we find out we are only + * querying against the list items index. If we are querying against any + * other index we are assuming we are 1 or more ECS compatible indexes and + * will query against those indexes using just timestamp since we don't have + * a tiebreaker. + */ export const getSortWithTieBreaker = ({ sortField, sortOrder, + index, + listItemIndex, }: GetSortWithTieBreakerOptions): SortWithTieBreaker[] => { const ascOrDesc = sortOrder ?? 'asc'; - if (sortField != null) { - return [{ [sortField]: ascOrDesc, '@timestamp': 'asc' }]; + if (index.length === 1 && index[0] === listItemIndex) { + if (sortField != null) { + return [{ [sortField]: ascOrDesc, tie_breaker_id: 'asc' }]; + } else { + return [{ tie_breaker_id: 'asc' }]; + } } else { - return [{ '@timestamp': 'asc' }]; + if (sortField != null) { + return [{ [sortField]: ascOrDesc, '@timestamp': 'asc' }]; + } else { + return [{ '@timestamp': 'asc' }]; + } } }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts index 06c9c4c13c5f3..0078cf1b3c64f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts @@ -103,6 +103,11 @@ export interface CreateThreatSignalOptions { currentResult: SearchAfterAndBulkCreateReturnType; } +export interface ThreatSignalResults { + threatList: SearchResponse; + results: SearchAfterAndBulkCreateReturnType; +} + export interface BuildThreatMappingFilterOptions { threatMapping: ThreatMapping; threatList: SearchResponse; @@ -150,11 +155,14 @@ export interface GetThreatListOptions { sortOrder: 'asc' | 'desc' | undefined; threatFilters: PartialFilter[]; exceptionItems: ExceptionListItemSchema[]; + listClient: ListClient; } export interface GetSortWithTieBreakerOptions { sortField: string | undefined; sortOrder: 'asc' | 'desc' | undefined; + index: string[]; + listItemIndex: string; } /** @@ -166,6 +174,5 @@ export interface ThreatListItem { } export interface SortWithTieBreaker { - '@timestamp': 'asc'; [key: string]: string; } diff --git a/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts b/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts index d1c8290b3462d..099160b7e4d60 100644 --- a/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts +++ b/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts @@ -85,6 +85,7 @@ export const processFieldsMap: Readonly> = { export const agentFieldsMap: Readonly> = { 'agent.type': 'agent.type', + 'agent.id': 'agent.id', }; export const userFieldsMap: Readonly> = { diff --git a/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.ts b/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.ts index ff2796e6852d0..36244ecbff72d 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.ts @@ -95,19 +95,19 @@ export class ElasticsearchHostsAdapter implements HostsAdapter { response: [inspectStringifyObject(response)], }; const formattedHostItem = formatHostItem(options.fields, aggregations); - const hostId = - formattedHostItem.host && formattedHostItem.host.id - ? Array.isArray(formattedHostItem.host.id) - ? formattedHostItem.host.id[0] - : formattedHostItem.host.id + const ident = // endpoint-generated ID, NOT elastic-agent-id + formattedHostItem.agent && formattedHostItem.agent.id + ? Array.isArray(formattedHostItem.agent.id) + ? formattedHostItem.agent.id[0] + : formattedHostItem.agent.id : null; - const endpoint: EndpointFields | null = await this.getHostEndpoint(request, hostId); + const endpoint: EndpointFields | null = await this.getHostEndpoint(request, ident); return { inspect, _id: options.hostName, ...formattedHostItem, endpoint }; } public async getHostEndpoint( request: FrameworkRequest, - hostId: string | null + id: string | null ): Promise { const logger = this.endpointContext.logFactory.get('metadata'); try { @@ -121,8 +121,8 @@ export class ElasticsearchHostsAdapter implements HostsAdapter { requestHandlerContext: request.context, }; const endpointData = - hostId != null && metadataRequestContext.endpointAppContextService.getAgentService() != null - ? await getHostData(metadataRequestContext, hostId) + id != null && metadataRequestContext.endpointAppContextService.getAgentService() != null + ? await getHostData(metadataRequestContext, id) : null; return endpointData != null && endpointData.metadata ? { diff --git a/x-pack/plugins/security_solution/server/lib/hosts/mock.ts b/x-pack/plugins/security_solution/server/lib/hosts/mock.ts index 97aa68c0f9bbf..e9dcee35005d6 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/mock.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/mock.ts @@ -299,6 +299,7 @@ export const mockGetHostOverviewOptions: HostOverviewRequestOptions = { defaultIndex: DEFAULT_INDEX_PATTERN, fields: [ '_id', + 'agent.id', 'host.architecture', 'host.id', 'host.ip', @@ -328,7 +329,7 @@ export const mockGetHostOverviewRequest = { operationName: 'GetHostOverviewQuery', variables: { sourceId: 'default', hostName: 'siem-es' }, query: - 'query GetHostOverviewQuery($sourceId: ID!, $hostName: String!, $timerange: TimerangeInput!) {\n source(id: $sourceId) {\n id\n HostOverview(hostName: $hostName, timerange: $timerange) {\n _id\n host {\n architecture\n id\n ip\n mac\n name\n os {\n family\n name\n platform\n version\n __typename\n }\n type\n __typename\n }\n cloud {\n instance {\n id\n __typename\n }\n machine {\n type\n __typename\n }\n provider\n region\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', + 'query GetHostOverviewQuery($sourceId: ID!, $hostName: String!, $timerange: TimerangeInput!) {\n source(id: $sourceId) {\n id\n HostOverview(hostName: $hostName, timerange: $timerange) {\n _id\n agent {\n id\n }\n host {\n architecture\n id\n ip\n mac\n name\n os {\n family\n name\n platform\n version\n __typename\n }\n type\n __typename\n }\n cloud {\n instance {\n id\n __typename\n }\n machine {\n type\n __typename\n }\n provider\n region\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', }, }; @@ -461,6 +462,17 @@ export const mockGetHostOverviewResponse = { }, ], }, + agent_id: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: '9f48a9ab-749a-4ff0-b4e2-7e53910a985', + doc_count: 611894, + timestamp: { value: 1554826117972, value_as_string: '2019-04-09T16:08:37.972Z' }, + }, + ], + }, }, }; @@ -474,6 +486,9 @@ export const mockGetHostOverviewResult = { response: [JSON.stringify(mockGetHostOverviewResponse, null, 2)], }, _id: 'siem-es', + agent: { + id: '9f48a9ab-749a-4ff0-b4e2-7e53910a985', + }, host: { architecture: 'x86_64', id: 'b6d5264e4b9c8880ad1053841067a4a6', diff --git a/x-pack/plugins/security_solution/server/lib/hosts/query.detail_host.dsl.ts b/x-pack/plugins/security_solution/server/lib/hosts/query.detail_host.dsl.ts index 10dcb7ee7e743..00769b75a8ce6 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/query.detail_host.dsl.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/query.detail_host.dsl.ts @@ -5,7 +5,7 @@ */ import { reduceFields } from '../../utils/build_query/reduce_fields'; -import { cloudFieldsMap, hostFieldsMap } from '../ecs_fields'; +import { cloudFieldsMap, hostFieldsMap, agentFieldsMap } from '../ecs_fields'; import { buildFieldsTermAggregation } from './helpers'; import { HostOverviewRequestOptions } from './types'; @@ -19,7 +19,7 @@ export const buildHostOverviewQuery = ({ }, timerange: { from, to }, }: HostOverviewRequestOptions) => { - const esFields = reduceFields(fields, { ...hostFieldsMap, ...cloudFieldsMap }); + const esFields = reduceFields(fields, { ...hostFieldsMap, ...cloudFieldsMap, ...agentFieldsMap }); const filter = [ { term: { 'host.name': hostName } }, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts index acee75abddcd9..88ce963757f6d 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -150,7 +150,13 @@ export class TelemetryEventsSender { })); this.queue = []; - await this.sendEvents(toSend, telemetryUrl, clusterInfo.cluster_uuid, licenseInfo?.uid); + await this.sendEvents( + toSend, + telemetryUrl, + clusterInfo.cluster_uuid, + clusterInfo.version?.number, + licenseInfo?.uid + ); } catch (err) { this.logger.warn(`Error sending telemetry events data: ${err}`); this.queue = []; @@ -202,6 +208,7 @@ export class TelemetryEventsSender { events: unknown[], telemetryUrl: string, clusterUuid: string, + clusterVersionNumber: string | undefined, licenseId: string | undefined ) { // this.logger.debug(`Sending events: ${JSON.stringify(events, null, 2)}`); @@ -213,8 +220,8 @@ export class TelemetryEventsSender { headers: { 'Content-Type': 'application/x-ndjson', 'X-Elastic-Cluster-ID': clusterUuid, + 'X-Elastic-Stack-Version': clusterVersionNumber ? clusterVersionNumber : '7.10.0', ...(licenseId ? { 'X-Elastic-License-ID': licenseId } : {}), - 'X-Elastic-Telemetry': '1', // TODO: no longer needed? }, }); this.logger.debug(`Events sent!. Response: ${resp.status} ${JSON.stringify(resp.data)}`); diff --git a/x-pack/plugins/security_solution/server/search_strategy/helpers/format_response_object_values.ts b/x-pack/plugins/security_solution/server/search_strategy/helpers/format_response_object_values.ts new file mode 100644 index 0000000000000..482734c73a257 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/helpers/format_response_object_values.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mapValues, isObject, isArray } from 'lodash/fp'; + +import { toArray } from './to_array'; + +export const mapObjectValuesToStringArray = (object: object): object => + mapValues((o) => { + if (isObject(o) && !isArray(o)) { + return mapObjectValuesToStringArray(o); + } + + return toArray(o); + }, object); + +export const formatResponseObjectValues = (object: T | T[] | null) => { + if (object && typeof object === 'object') { + return mapObjectValuesToStringArray(object as object); + } + + return object; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts b/x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts index f7d9f408c5e2d..1aba6660677cd 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/helpers/to_array.ts @@ -4,5 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export const toArray = (value: T | T[] | null) => +export const toArray = (value: T | T[] | null): T[] => + Array.isArray(value) ? value : value == null ? [] : [value]; + +export const toStringArray = (value: T | T[] | null): T[] | string[] => Array.isArray(value) ? value : value == null ? [] : [`${value}`]; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts index b06c36fd24e1a..55b54c8975214 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts @@ -9,7 +9,7 @@ import { hostFieldsMap } from '../../../../../../common/ecs/ecs_fields'; import { HostsEdges } from '../../../../../../common/search_strategy/security_solution/hosts'; import { HostAggEsItem, HostBuckets, HostValue } from '../../../../../lib/hosts/types'; -import { toArray } from '../../../../helpers/to_array'; +import { toStringArray } from '../../../../helpers/to_array'; export const HOSTS_FIELDS: readonly string[] = [ '_id', @@ -31,7 +31,7 @@ export const formatHostEdgesData = ( flattenedFields.cursor.value = hostId || ''; const fieldValue = getHostFieldValue(fieldName, bucket); if (fieldValue != null) { - return set(`node.${fieldName}`, toArray(fieldValue), flattenedFields); + return set(`node.${fieldName}`, toStringArray(fieldValue), flattenedFields); } return flattenedFields; }, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts index ce8900a578102..e1924d6c27940 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts @@ -6,7 +6,7 @@ import { get, getOr, isEmpty } from 'lodash/fp'; import { set } from '@elastic/safer-lodash-set/fp'; import { mergeFieldsWithHit } from '../../../../../utils/build_query'; -import { toArray } from '../../../../helpers/to_array'; +import { toStringArray } from '../../../../helpers/to_array'; import { AuthenticationsEdges, AuthenticationHit, @@ -53,7 +53,7 @@ export const formatAuthenticationData = ( const fieldPath = `node.${fieldName}`; const fieldValue = get(fieldPath, mergedResult); if (!isEmpty(fieldValue)) { - return set(fieldPath, toArray(fieldValue), mergedResult); + return set(fieldPath, toStringArray(fieldValue), mergedResult); } else { return mergedResult; } diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts index 644278963742d..36cf025304e76 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts @@ -7,7 +7,7 @@ import { set } from '@elastic/safer-lodash-set/fp'; import { get, has, head } from 'lodash/fp'; import { hostFieldsMap } from '../../../../../../common/ecs/ecs_fields'; import { HostItem } from '../../../../../../common/search_strategy/security_solution/hosts'; -import { toArray } from '../../../../helpers/to_array'; +import { toStringArray } from '../../../../helpers/to_array'; import { HostAggEsItem, HostBuckets, HostValue } from '../../../../../lib/hosts/types'; @@ -40,7 +40,7 @@ export const formatHostItem = (bucket: HostAggEsItem): HostItem => if (fieldName === '_id') { return set('_id', fieldValue, flattenedFields); } - return set(fieldName, toArray(fieldValue), flattenedFields); + return set(fieldName, toStringArray(fieldValue), flattenedFields); } return flattenedFields; }, {}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts index 20b3f5b05bc87..7d9351993bc85 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts @@ -12,7 +12,7 @@ import { HostsUncommonProcessesEdges, HostsUncommonProcessHit, } from '../../../../../../common/search_strategy/security_solution/hosts/uncommon_processes'; -import { toArray } from '../../../../helpers/to_array'; +import { toStringArray } from '../../../../helpers/to_array'; import { HostHits } from '../../../../../../common/search_strategy'; export const uncommonProcessesFields = [ @@ -79,7 +79,7 @@ export const formatUncommonProcessesData = ( fieldPath = `node.hosts.0.name`; fieldValue = get(fieldPath, mergedResult); } - return set(fieldPath, toArray(fieldValue), mergedResult); + return set(fieldPath, toStringArray(fieldValue), mergedResult); }, { node: { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/__mocks__/index.ts index b1470b17eea5d..3e4070a28a9f8 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/__mocks__/index.ts @@ -11,6 +11,7 @@ import { FlowTargetSourceDest, NetworkQueries, NetworkTopNFlowRequestOptions, + NetworkTopNFlowStrategyResponse, NetworkTopTablesFields, } from '../../../../../../../common/search_strategy'; @@ -554,7 +555,7 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { loaded: 21, }; -export const formattedSearchStrategyResponse = { +export const formattedSearchStrategyResponse: NetworkTopNFlowStrategyResponse = { edges: [ { node: { @@ -579,13 +580,16 @@ export const formattedSearchStrategyResponse = { ip: '35.232.239.42', location: { geo: { - continent_name: 'North America', - region_iso_code: 'US-VA', - country_iso_code: 'US', - region_name: 'Virginia', - location: { lon: -77.2481, lat: 38.6583 }, + continent_name: ['North America'], + region_iso_code: ['US-VA'], + country_iso_code: ['US'], + region_name: ['Virginia'], + location: { + lon: [-77.2481], + lat: [38.6583], + }, }, - flowTarget: 'source', + flowTarget: FlowTargetSourceDest.source, }, autonomous_system: { number: 15169, name: 'Google LLC' }, flows: 922, @@ -603,14 +607,17 @@ export const formattedSearchStrategyResponse = { ip: '151.101.200.204', location: { geo: { - continent_name: 'North America', - region_iso_code: 'US-VA', - city_name: 'Ashburn', - country_iso_code: 'US', - region_name: 'Virginia', - location: { lon: -77.4728, lat: 39.0481 }, - }, - flowTarget: 'source', + continent_name: ['North America'], + region_iso_code: ['US-VA'], + city_name: ['Ashburn'], + country_iso_code: ['US'], + region_name: ['Virginia'], + location: { + lon: [-77.4728], + lat: [39.0481], + }, + }, + flowTarget: FlowTargetSourceDest.source, }, autonomous_system: { number: 54113, name: 'Fastly' }, flows: 2, @@ -628,14 +635,17 @@ export const formattedSearchStrategyResponse = { ip: '91.189.92.39', location: { geo: { - continent_name: 'Europe', - region_iso_code: 'GB-ENG', - city_name: 'London', - country_iso_code: 'GB', - region_name: 'England', - location: { lon: -0.0961, lat: 51.5132 }, - }, - flowTarget: 'source', + continent_name: ['Europe'], + region_iso_code: ['GB-ENG'], + city_name: ['London'], + country_iso_code: ['GB'], + region_name: ['England'], + location: { + lon: [-0.0961], + lat: [51.5132], + }, + }, + flowTarget: FlowTargetSourceDest.source, }, autonomous_system: { number: 41231, name: 'Canonical Group Limited' }, flows: 1, @@ -668,14 +678,17 @@ export const formattedSearchStrategyResponse = { ip: '151.101.248.204', location: { geo: { - continent_name: 'North America', - region_iso_code: 'US-VA', - city_name: 'Ashburn', - country_iso_code: 'US', - region_name: 'Virginia', - location: { lon: -77.539, lat: 39.018 }, - }, - flowTarget: 'source', + continent_name: ['North America'], + region_iso_code: ['US-VA'], + city_name: ['Ashburn'], + country_iso_code: ['US'], + region_name: ['Virginia'], + location: { + lon: [-77.539], + lat: [39.018], + }, + }, + flowTarget: FlowTargetSourceDest.source, }, autonomous_system: { number: 54113, name: 'Fastly' }, flows: 6, @@ -693,13 +706,16 @@ export const formattedSearchStrategyResponse = { ip: '35.196.129.83', location: { geo: { - continent_name: 'North America', - region_iso_code: 'US-VA', - country_iso_code: 'US', - region_name: 'Virginia', - location: { lon: -77.2481, lat: 38.6583 }, + continent_name: ['North America'], + region_iso_code: ['US-VA'], + country_iso_code: ['US'], + region_name: ['Virginia'], + location: { + lon: [-77.2481], + lat: [38.6583], + }, }, - flowTarget: 'source', + flowTarget: FlowTargetSourceDest.source, }, autonomous_system: { number: 15169, name: 'Google LLC' }, flows: 1, @@ -717,11 +733,14 @@ export const formattedSearchStrategyResponse = { ip: '151.101.2.217', location: { geo: { - continent_name: 'North America', - country_iso_code: 'US', - location: { lon: -97.822, lat: 37.751 }, + continent_name: ['North America'], + country_iso_code: ['US'], + location: { + lon: [-97.822], + lat: [37.751], + }, }, - flowTarget: 'source', + flowTarget: FlowTargetSourceDest.source, }, autonomous_system: { number: 54113, name: 'Fastly' }, flows: 24, @@ -739,14 +758,17 @@ export const formattedSearchStrategyResponse = { ip: '91.189.91.38', location: { geo: { - continent_name: 'North America', - region_iso_code: 'US-MA', - city_name: 'Boston', - country_iso_code: 'US', - region_name: 'Massachusetts', - location: { lon: -71.0631, lat: 42.3562 }, - }, - flowTarget: 'source', + continent_name: ['North America'], + region_iso_code: ['US-MA'], + city_name: ['Boston'], + country_iso_code: ['US'], + region_name: ['Massachusetts'], + location: { + lon: [-71.0631], + lat: [42.3562], + }, + }, + flowTarget: FlowTargetSourceDest.source, }, autonomous_system: { number: 41231, name: 'Canonical Group Limited' }, flows: 1, @@ -764,11 +786,14 @@ export const formattedSearchStrategyResponse = { ip: '193.228.91.123', location: { geo: { - continent_name: 'North America', - country_iso_code: 'US', - location: { lon: -97.822, lat: 37.751 }, + continent_name: ['North America'], + country_iso_code: ['US'], + location: { + lon: [-97.822], + lat: [37.751], + }, }, - flowTarget: 'source', + flowTarget: FlowTargetSourceDest.source, }, autonomous_system: { number: 133766, name: 'YHSRV.LLC' }, flows: 33, @@ -846,6 +871,7 @@ export const formattedSearchStrategyResponse = { }, pageInfo: { activePage: 0, fakeTotalCount: 50, showMorePagesIndicator: true }, totalCount: 738, + rawResponse: {} as NetworkTopNFlowStrategyResponse['rawResponse'], }; export const expectedDsl = { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/helpers.ts index 720661e12bd96..0bf99aeea8a2d 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/top_n_flow/helpers.ts @@ -20,6 +20,7 @@ import { AutonomousSystemItem, } from '../../../../../../common/search_strategy'; import { getOppositeField } from '../helpers'; +import { formatResponseObjectValues } from '../../../../helpers/format_response_object_values'; export const getTopNFlowEdges = ( response: IEsSearchResponse, @@ -66,12 +67,14 @@ const getFlowTargetFromString = (flowAsString: string) => const getGeoItem = (result: NetworkTopNFlowBuckets): GeoItem | null => result.location.top_geo.hits.hits.length > 0 && result.location.top_geo.hits.hits[0]._source ? { - geo: getOr( - '', - `location.top_geo.hits.hits[0]._source.${ - Object.keys(result.location.top_geo.hits.hits[0]._source)[0] - }.geo`, - result + geo: formatResponseObjectValues( + getOr( + '', + `location.top_geo.hits.hits[0]._source.${ + Object.keys(result.location.top_geo.hits.hits[0]._source)[0] + }.geo`, + result + ) ), flowTarget: getFlowTargetFromString( Object.keys(result.location.top_geo.hits.hits[0]._source)[0] diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts index b2e3989f99d4f..8e2bfb5426610 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts @@ -6,7 +6,7 @@ import { get, has, merge, uniq } from 'lodash/fp'; import { EventHit, TimelineEdges } from '../../../../../../common/search_strategy'; -import { toArray } from '../../../../helpers/to_array'; +import { toStringArray } from '../../../../helpers/to_array'; export const formatTimelineData = ( dataFields: readonly string[], @@ -56,8 +56,8 @@ const mergeTimelineFieldsWithHit = ( { field: fieldName, value: specialFields.includes(esField) - ? toArray(get(esField, hit)) - : toArray(get(esField, hit._source)), + ? toStringArray(get(esField, hit)) + : toStringArray(get(esField, hit._source)), }, ] : get('node.data', flattenedFields), @@ -68,7 +68,7 @@ const mergeTimelineFieldsWithHit = ( ...fieldName.split('.').reduceRight( // @ts-expect-error (obj, next) => ({ [next]: obj }), - toArray(get(esField, hit._source)) + toStringArray(get(esField, hit._source)) ), } : get('node.ecs', flattenedFields), diff --git a/x-pack/plugins/spaces/common/lib/spaces_url_parser.test.ts b/x-pack/plugins/spaces/common/lib/spaces_url_parser.test.ts index b25d79c0a6907..2b34bc77ec686 100644 --- a/x-pack/plugins/spaces/common/lib/spaces_url_parser.test.ts +++ b/x-pack/plugins/spaces/common/lib/spaces_url_parser.test.ts @@ -10,41 +10,76 @@ describe('getSpaceIdFromPath', () => { describe('without a serverBasePath defined', () => { test('it identifies the space url context', () => { const basePath = `/s/my-awesome-space-lives-here`; - expect(getSpaceIdFromPath(basePath)).toEqual('my-awesome-space-lives-here'); + expect(getSpaceIdFromPath(basePath)).toEqual({ + spaceId: 'my-awesome-space-lives-here', + pathHasExplicitSpaceIdentifier: true, + }); }); test('ignores space identifiers in the middle of the path', () => { const basePath = `/this/is/a/crazy/path/s/my-awesome-space-lives-here`; - expect(getSpaceIdFromPath(basePath)).toEqual(DEFAULT_SPACE_ID); + expect(getSpaceIdFromPath(basePath)).toEqual({ + spaceId: DEFAULT_SPACE_ID, + pathHasExplicitSpaceIdentifier: false, + }); }); test('it handles base url without a space url context', () => { const basePath = `/this/is/a/crazy/path/s`; - expect(getSpaceIdFromPath(basePath)).toEqual(DEFAULT_SPACE_ID); + expect(getSpaceIdFromPath(basePath)).toEqual({ + spaceId: DEFAULT_SPACE_ID, + pathHasExplicitSpaceIdentifier: false, + }); + }); + + test('it identifies the space url context with the default space', () => { + const basePath = `/s/${DEFAULT_SPACE_ID}`; + expect(getSpaceIdFromPath(basePath)).toEqual({ + spaceId: DEFAULT_SPACE_ID, + pathHasExplicitSpaceIdentifier: true, + }); }); }); describe('with a serverBasePath defined', () => { test('it identifies the space url context', () => { const basePath = `/s/my-awesome-space-lives-here`; - expect(getSpaceIdFromPath(basePath, '/')).toEqual('my-awesome-space-lives-here'); + expect(getSpaceIdFromPath(basePath, '/')).toEqual({ + spaceId: 'my-awesome-space-lives-here', + pathHasExplicitSpaceIdentifier: true, + }); }); test('it identifies the space url context following the server base path', () => { const basePath = `/server-base-path-here/s/my-awesome-space-lives-here`; - expect(getSpaceIdFromPath(basePath, '/server-base-path-here')).toEqual( - 'my-awesome-space-lives-here' - ); + expect(getSpaceIdFromPath(basePath, '/server-base-path-here')).toEqual({ + spaceId: 'my-awesome-space-lives-here', + pathHasExplicitSpaceIdentifier: true, + }); }); test('ignores space identifiers in the middle of the path', () => { const basePath = `/this/is/a/crazy/path/s/my-awesome-space-lives-here`; - expect(getSpaceIdFromPath(basePath, '/this/is/a')).toEqual(DEFAULT_SPACE_ID); + expect(getSpaceIdFromPath(basePath, '/this/is/a')).toEqual({ + spaceId: DEFAULT_SPACE_ID, + pathHasExplicitSpaceIdentifier: false, + }); + }); + + test('it identifies the space url context with the default space following the server base path', () => { + const basePath = `/server-base-path-here/s/${DEFAULT_SPACE_ID}`; + expect(getSpaceIdFromPath(basePath, '/server-base-path-here')).toEqual({ + spaceId: DEFAULT_SPACE_ID, + pathHasExplicitSpaceIdentifier: true, + }); }); test('it handles base url without a space url context', () => { const basePath = `/this/is/a/crazy/path/s`; - expect(getSpaceIdFromPath(basePath, basePath)).toEqual(DEFAULT_SPACE_ID); + expect(getSpaceIdFromPath(basePath, basePath)).toEqual({ + spaceId: DEFAULT_SPACE_ID, + pathHasExplicitSpaceIdentifier: false, + }); }); }); }); diff --git a/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts b/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts index 994ec7c59cb6e..be950e6a651e6 100644 --- a/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts +++ b/x-pack/plugins/spaces/common/lib/spaces_url_parser.ts @@ -5,20 +5,22 @@ */ import { DEFAULT_SPACE_ID } from '../constants'; +const spaceContextRegex = /^\/s\/([a-z0-9_\-]+)/; + export function getSpaceIdFromPath( requestBasePath: string = '/', serverBasePath: string = '/' -): string { - let pathToCheck: string = requestBasePath; +): { spaceId: string; pathHasExplicitSpaceIdentifier: boolean } { + const pathToCheck: string = stripServerBasePath(requestBasePath, serverBasePath); - if (serverBasePath && serverBasePath !== '/' && requestBasePath.startsWith(serverBasePath)) { - pathToCheck = requestBasePath.substr(serverBasePath.length); - } // Look for `/s/space-url-context` in the base path - const matchResult = pathToCheck.match(/^\/s\/([a-z0-9_\-]+)/); + const matchResult = pathToCheck.match(spaceContextRegex); if (!matchResult || matchResult.length === 0) { - return DEFAULT_SPACE_ID; + return { + spaceId: DEFAULT_SPACE_ID, + pathHasExplicitSpaceIdentifier: false, + }; } // Ignoring first result, we only want the capture group result at index 1 @@ -28,7 +30,10 @@ export function getSpaceIdFromPath( throw new Error(`Unable to determine Space ID from request path: ${requestBasePath}`); } - return spaceId; + return { + spaceId, + pathHasExplicitSpaceIdentifier: true, + }; } export function addSpaceIdToPath( @@ -45,3 +50,10 @@ export function addSpaceIdToPath( } return `${basePath}${requestedPath}`; } + +function stripServerBasePath(requestBasePath: string, serverBasePath: string) { + if (serverBasePath && serverBasePath !== '/' && requestBasePath.startsWith(serverBasePath)) { + return requestBasePath.substr(serverBasePath.length); + } + return requestBasePath; +} diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts index 4b3a5d662f12d..6408803c2114b 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts @@ -10,7 +10,6 @@ import { CoreSetup, } from 'src/core/server'; import { format } from 'url'; -import { DEFAULT_SPACE_ID } from '../../../common/constants'; import { modifyUrl } from '../utils/url'; import { getSpaceIdFromPath } from '../../../common'; @@ -28,9 +27,9 @@ export function initSpacesOnRequestInterceptor({ http }: OnRequestInterceptorDep // If navigating within the context of a space, then we store the Space's URL Context on the request, // and rewrite the request to not include the space identifier in the URL. - const spaceId = getSpaceIdFromPath(path, serverBasePath); + const { spaceId, pathHasExplicitSpaceIdentifier } = getSpaceIdFromPath(path, serverBasePath); - if (spaceId !== DEFAULT_SPACE_ID) { + if (pathHasExplicitSpaceIdentifier) { const reqBasePath = `/s/${spaceId}`; http.basePath.set(request, reqBasePath); diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts index b341d76c86649..b48bf971d0c1b 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.test.ts @@ -58,7 +58,7 @@ const createService = async (serverBasePath: string = '') => { serverBasePath, } as HttpServiceSetup['basePath']; httpSetup.basePath.get = jest.fn().mockImplementation((request: KibanaRequest) => { - const spaceId = getSpaceIdFromPath(request.url.path); + const { spaceId } = getSpaceIdFromPath(request.url.path); if (spaceId !== DEFAULT_SPACE_ID) { return `/s/${spaceId}`; diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts index cf181a78efcb8..3630675a7ed3f 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts @@ -63,7 +63,7 @@ export class SpacesService { ? (request as Record).getBasePath() : http.basePath.get(request); - const spaceId = getSpaceIdFromPath(basePath, http.basePath.serverBasePath); + const { spaceId } = getSpaceIdFromPath(basePath, http.basePath.serverBasePath); return spaceId; }; diff --git a/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts b/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts index 6ef44e325b0a7..524b4c5616c73 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts @@ -10,6 +10,7 @@ import { CoreStart, Plugin, IClusterClient, + SavedObjectsServiceStart, } from 'kibana/server'; import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; import { getClusterUuids, getLocalLicense } from '../../../../src/plugins/telemetry/server'; @@ -21,12 +22,14 @@ interface TelemetryCollectionXpackDepsSetup { export class TelemetryCollectionXpackPlugin implements Plugin { private elasticsearchClient?: IClusterClient; + private savedObjectsService?: SavedObjectsServiceStart; constructor(initializerContext: PluginInitializerContext) {} public setup(core: CoreSetup, { telemetryCollectionManager }: TelemetryCollectionXpackDepsSetup) { telemetryCollectionManager.setCollection({ esCluster: core.elasticsearch.legacy.client, esClientGetter: () => this.elasticsearchClient, + soServiceGetter: () => this.savedObjectsService, title: 'local_xpack', priority: 1, statsGetter: getStatsWithXpack, @@ -37,5 +40,6 @@ export class TelemetryCollectionXpackPlugin implements Plugin { public start(core: CoreStart) { this.elasticsearchClient = core.elasticsearch.client; + this.savedObjectsService = core.savedObjects; } } diff --git a/x-pack/plugins/transform/readme.md b/x-pack/plugins/transform/readme.md new file mode 100644 index 0000000000000..2ee2a7b70c5f1 --- /dev/null +++ b/x-pack/plugins/transform/readme.md @@ -0,0 +1,117 @@ +# Documentation for Transforms UI developers + +This plugin provides access to the transforms features provided by Elastic. + +## Requirements + +To use the transforms feature, you must have at least a Basic license. For more +info, refer to +[Set up transforms](https://www.elastic.co/guide/en/elasticsearch/reference/current/transform-setup.html). + + +## Setup local environment + +### Kibana + +1. Fork and clone the [Kibana repo](https://github.com/elastic/kibana). + +1. Install `nvm`, `node`, `yarn` (for example, by using Homebrew). See + [Install dependencies](https://www.elastic.co/guide/en/kibana/master/development-getting-started.html#_install_dependencies). + +1. Make sure that Elasticsearch is deployed and running on `localhost:9200`. + +1. Navigate to the directory of the `kibana` repository on your machine. + +1. Fetch the latest changes from the repository. + +1. Checkout the branch of the version you want to use. For example, if you want + to use a 7.9 version, run `git checkout 7.9`. (Your Elasticsearch and Kibana + instances need to be the same version.) + +1. Run `nvm use`. The response shows the Node version that the environment uses. + If you need to update your Node version, the response message contains the + command you need to run to do it. + +1. Run `yarn kbn bootstrap`. It takes all the dependencies in the code and + installs/checks them. It is recommended to use it every time when you switch + between branches. + +1. Make a copy of `kibana.yml` and save as `kibana.dev.yml`. (Git will not track + the changes in `kibana.dev.yml` but yarn will use it.) + +1. Provide the appropriate password and user name in `kibana.dev.yml`. + +1. Run `yarn start` to start Kibana. + +1. Go to http://localhost:560x/xxx (check the terminal message for the exact + path). + +For more details, refer to this [getting started](https://www.elastic.co/guide/en/kibana/master/development-getting-started.html) page. + +### Adding sample data to Kibana + +Kibana has sample data sets that you can add to your setup so that you can test +different configurations on sample data. + +1. Click the Elastic logo in the upper left hand corner of your browser to + navigate to the Kibana home page. + +1. Click *Load a data set and a Kibana dashboard*. + +1. Pick a data set or feel free to click *Add* on all of the available sample + data sets. + +These data sets are now ready to be used for creating transforms in Kibana. + +## Running tests + +### Jest tests + +Run the test following jest tests from `kibana/x-pack`. + +New snapshots, all plugins: + +``` +node scripts/jest +``` + +Update snapshots for the transform plugin: + +``` +node scripts/jest plugins/transform -u +``` + +Update snapshots for a specific directory only: + +``` +node scripts/jest x-pack/plugins/transform/public/app/sections +``` + +Run tests with verbose output: + +``` +node scripts/jest plugins/transform --verbose +``` + +### Functional tests + +Before running the test server, make sure to quit all other instances of +Elasticsearch. + +1. From one terminal, in the x-pack directory, run: + + node scripts/functional_tests_server.js --config test/functional/config.js + + This command starts an Elasticsearch and Kibana instance that the tests will be run against. + +1. In another tab, run the following command to perform API integration tests (from the x-pack directory): + + node scripts/functional_test_runner.js --include-tag transform --config test/api_integration/config + + The transform API integration tests are located in `x-pack/test/api_integration/apis/transform`. + +1. In another tab, run the following command to perform UI functional tests (from the x-pack directory): + + node scripts/functional_test_runner.js --include-tag transform + + The transform functional tests are located in `x-pack/test/functional/apps/transform`. diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index bcdc9e0133666..d0c24b0239b62 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1567,6 +1567,26 @@ "expressions.functions.varset.name.help": "変数の名前を指定", "expressions.functions.varset.val.help": "変数の値を指定指定がない場合、インプットコンテキストが使用されます", "expressions.types.number.fromStringConversionErrorMessage": "\"{string}\" ストリンクを数字に変換できません", + "flot.pie.unableToDrawLabelsInsideCanvasErrorMessage": "キャンバス内のラベルではパイを作成できません", + "flot.time.aprLabel": "4月", + "flot.time.augLabel": "8月", + "flot.time.decLabel": "12月", + "flot.time.febLabel": "2月", + "flot.time.friLabel": "金", + "flot.time.janLabel": "1月", + "flot.time.julLabel": "7月", + "flot.time.junLabel": "6月", + "flot.time.marLabel": "3月", + "flot.time.mayLabel": "5月", + "flot.time.monLabel": "月", + "flot.time.novLabel": "11月", + "flot.time.octLabel": "10月", + "flot.time.satLabel": "土", + "flot.time.sepLabel": "9月", + "flot.time.sunLabel": "日", + "flot.time.thuLabel": "木", + "flot.time.tueLabel": "火", + "flot.time.wedLabel": "水", "home.breadcrumbs.addDataTitle": "データの追加", "home.breadcrumbs.homeTitle": "ホーム", "home.dataManagementDisableCollection": " 収集を停止するには、] ", @@ -12308,8 +12328,6 @@ "xpack.monitoring.apm.instances.versionTitle": "バージョン", "xpack.monitoring.apmNavigation.instancesLinkText": "インスタンス", "xpack.monitoring.apmNavigation.overviewLinkText": "概要", - "xpack.monitoring.aprLabel": "4月", - "xpack.monitoring.augLabel": "8月", "xpack.monitoring.beats.filterBeatsPlaceholder": "ビートをフィルタリング…", "xpack.monitoring.beats.instance.bytesSentLabel": "送信バイト", "xpack.monitoring.beats.instance.configReloadsLabel": "構成の再読み込み", @@ -12476,7 +12494,6 @@ "xpack.monitoring.clustersNavigation.clustersLinkText": "クラスター", "xpack.monitoring.clusterStats.uuidNotFoundErrorMessage": "選択された時間範囲にクラスターが見つかりませんでした。UUID: {clusterUuid}", "xpack.monitoring.clusterStats.uuidNotSpecifiedErrorMessage": "{clusterUuid} が指定されていません", - "xpack.monitoring.decLabel": "12月", "xpack.monitoring.elasticsearch.ccr.ccrListingTable.errorColumnTitle": "エラー", "xpack.monitoring.elasticsearch.ccr.ccrListingTable.followsColumnTitle": "フォロー", "xpack.monitoring.elasticsearch.ccr.ccrListingTable.indexColumnTitle": "インデックス", @@ -12642,15 +12659,10 @@ "xpack.monitoring.expiredLicenseStatusTitle": "ご使用の{typeTitleCase}ライセンスは期限切れです", "xpack.monitoring.feature.reserved.description": "ユーザーアクセスを許可するには、monitoring_user ロールも割り当てる必要があります。", "xpack.monitoring.featureRegistry.monitoringFeatureName": "スタック監視", - "xpack.monitoring.febLabel": "2月", "xpack.monitoring.formatNumbers.notAvailableLabel": "N/A", - "xpack.monitoring.friLabel": "金", "xpack.monitoring.healthCheck.encryptionErrorAction": "方法を確認してください。", "xpack.monitoring.healthCheck.tlsAndEncryptionError": "アラート機能を使用するには、KibanaとElasticsearchとの間のトランスポート層セキュリティを有効化して、 \n kibana.ymlファイルで暗号化鍵を構成する必要があります。", "xpack.monitoring.healthCheck.tlsAndEncryptionErrorTitle": "追加の設定が必要です", - "xpack.monitoring.janLabel": "1月", - "xpack.monitoring.julLabel": "7月", - "xpack.monitoring.junLabel": "6月", "xpack.monitoring.kibana.clusterStatus.connectionsLabel": "接続", "xpack.monitoring.kibana.clusterStatus.instancesLabel": "インスタンス", "xpack.monitoring.kibana.clusterStatus.maxResponseTimeLabel": "最高応答時間", @@ -12780,8 +12792,6 @@ "xpack.monitoring.logstashNavigation.overviewLinkText": "概要", "xpack.monitoring.logstashNavigation.pipelinesLinkText": "パイプライン", "xpack.monitoring.logstashNavigation.pipelineVersionDescription": "バージョンは {relativeLastSeen} 時点でアクティブ、初回検知 {relativeFirstSeen}", - "xpack.monitoring.marLabel": "3月", - "xpack.monitoring.mayLabel": "5月", "xpack.monitoring.metricbeatMigration.apmInstructions.configureMetricbeatDescription": "{file} にこれらの変更を加えます。", "xpack.monitoring.metricbeatMigration.apmInstructions.configureMetricbeatTitle": "Metricbeat を構成して監視クラスターに送ります", "xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.description": "APM サーバーの構成ファイル ({file}) に次の設定を追加します:", @@ -13405,7 +13415,6 @@ "xpack.monitoring.metrics.logstashInstance.systemLoad.last1MinuteLabel": "1m", "xpack.monitoring.metrics.logstashInstance.systemLoad.last5MinutesDescription": "過去 5 分間の平均負荷です。", "xpack.monitoring.metrics.logstashInstance.systemLoad.last5MinutesLabel": "5m", - "xpack.monitoring.monLabel": "月", "xpack.monitoring.noData.blurbs.changesNeededDescription": "監視を実行するには、次の手順に従います", "xpack.monitoring.noData.blurbs.changesNeededTitle": "調整が必要です", "xpack.monitoring.noData.blurbs.cloudDeploymentDescription": "次の場所に戻ってください: ", @@ -13444,15 +13453,10 @@ "xpack.monitoring.noData.routeTitle": "監視の設定", "xpack.monitoring.noData.setupInternalInstead": "または、自己監視で設定", "xpack.monitoring.noData.setupMetricbeatInstead": "または、Metricbeat で設定 (推奨)", - "xpack.monitoring.novLabel": "11月", - "xpack.monitoring.octLabel": "10月", "xpack.monitoring.overview.heading": "スタック監視概要", "xpack.monitoring.pageLoadingTitle": "読み込み中…", "xpack.monitoring.permanentActiveLicenseStatusDescription": "ご使用のライセンスには有効期限がありません。", - "xpack.monitoring.pie.unableToDrawLabelsInsideCanvasErrorMessage": "キャンバス内のラベルではパイを作成できません", "xpack.monitoring.requestedClusters.uuidNotFoundErrorMessage": "選択された時間範囲にクラスターが見つかりませんでした。UUID: {clusterUuid}", - "xpack.monitoring.satLabel": "土", - "xpack.monitoring.sepLabel": "9月", "xpack.monitoring.setupMode.clickToDisableInternalCollection": "自己監視を無効にする", "xpack.monitoring.setupMode.clickToMonitorWithMetricbeat": "Metricbeat で監視", "xpack.monitoring.setupMode.description": "現在設定モードです。({flagIcon}) アイコンは構成オプションを意味します。", @@ -13492,13 +13496,9 @@ "xpack.monitoring.summaryStatus.statusDescription": "ステータス", "xpack.monitoring.summaryStatus.statusIconLabel": "ステータス: {status}", "xpack.monitoring.summaryStatus.statusIconTitle": "ステータス: {statusIcon}", - "xpack.monitoring.sunLabel": "日", - "xpack.monitoring.thuLabel": "木", - "xpack.monitoring.tueLabel": "火", "xpack.monitoring.updateLicenseButtonLabel": "ライセンスを更新", "xpack.monitoring.updateLicenseTitle": "ライセンスの更新", "xpack.monitoring.useAvailableLicenseDescription": "既に新しいライセンスがある場合は、今すぐアップロードしてください。", - "xpack.monitoring.wedLabel": "水", "xpack.observability.emptySection.apps.alert.description": "503エラーが累積していますか?サービスは応答していますか?CPUとRAMの使用量が跳ね上がっていますか?このような警告を、事後にではなく、発生と同時に把握しましょう。", "xpack.observability.emptySection.apps.alert.link": "アラートの作成", "xpack.observability.emptySection.apps.alert.title": "アラートが見つかりません。", @@ -14822,8 +14822,6 @@ "xpack.securitySolution.case.caseView.noTags": "現在、このケースにタグは割り当てられていません。", "xpack.securitySolution.case.caseView.openedOn": "開始日", "xpack.securitySolution.case.caseView.optional": "オプション", - "xpack.securitySolution.case.caseView.pageBadgeLabel": "ベータ", - "xpack.securitySolution.case.caseView.pageBadgeTooltip": "ケースワークフローはまだベータです。Kibana repoで問題や不具合を報告して製品の改善にご協力ください。", "xpack.securitySolution.case.caseView.particpantsLabel": "参加者", "xpack.securitySolution.case.caseView.pushNamedIncident": "{ thirdParty }インシデントとしてプッシュ", "xpack.securitySolution.case.caseView.pushThirdPartyIncident": "外部インシデントとしてプッシュ", @@ -15199,8 +15197,6 @@ "xpack.securitySolution.detectionEngine.emptyActionSecondary": "ドキュメントに移動", "xpack.securitySolution.detectionEngine.emptyTitle": "セキュリティアプリケーションの検出エンジンに関連したインデックスがないようです", "xpack.securitySolution.detectionEngine.goToDocumentationButton": "ドキュメンテーションを表示", - "xpack.securitySolution.detectionEngine.headerPage.pageBadgeLabel": "ベータ", - "xpack.securitySolution.detectionEngine.headerPage.pageBadgeTooltip": "アラートはまだベータ段階です。Kibana repoで問題や不具合を報告して製品の改善にご協力ください。", "xpack.securitySolution.detectionEngine.lastSignalTitle": "前回のアラート", "xpack.securitySolution.detectionEngine.mitreAttack.addTitle": "MITRE ATT&CK\\u2122脅威を追加", "xpack.securitySolution.detectionEngine.mitreAttack.tacticPlaceHolderDescription": "Tacticを追加...", @@ -15578,13 +15574,12 @@ "xpack.securitySolution.detectionEngine.rules.deleteDescription": "削除", "xpack.securitySolution.detectionEngine.rules.editPageTitle": "編集", "xpack.securitySolution.detectionEngine.rules.importRuleTitle": "ルールのインポート...", - "xpack.securitySolution.detectionEngine.rules.loadPrePackagedRulesButton": "Elastic事前構築済みルールおよびタイムラインテンプレートを読み込む", + "xpack.securitySolution.detectionEngine.rules.loadPrePackagedRulesAndTemplatesButton": "Elastic事前構築済みルールおよびタイムラインテンプレートを読み込む", "xpack.securitySolution.detectionEngine.rules.optionalFieldDescription": "オプション", "xpack.securitySolution.detectionEngine.rules.pageTitle": "検出ルール", "xpack.securitySolution.detectionEngine.rules.prePackagedRules.createOwnRuletButton": "独自のルールの作成", "xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptMessage": "Elasticセキュリティには、バックグラウンドで実行され、条件が合うとアラートを作成する事前構築済み検出ルールがあります。デフォルトでは、Elastic Endpoint Securityルールを除くすべての事前構築済みルールが無効になっています。有効にする追加のルールを選択することができます。", "xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptTitle": "Elastic事前構築済み検出ルールを読み込む", - "xpack.securitySolution.detectionEngine.rules.prePackagedRules.loadPreBuiltButton": "事前構築済み検出ルールおよびタイムラインテンプレートを読み込む", "xpack.securitySolution.detectionEngine.rules.releaseNotesHelp": "リリースノート", "xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesAndTimelinesButton": "{missingRules} Elastic事前構築済み{missingRules, plural, =1 {ルール} other {ルール}}と{missingTimelines} Elastic事前構築済み{missingTimelines, plural, =1 {タイムライン} other {タイムライン}}をインストール ", "xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesButton": "{missingRules} Elasticの事前構築済みの{missingRules, plural, =1 {個のルール} other {個のルール}}をインストール ", @@ -16405,7 +16400,6 @@ "xpack.securitySolution.timeline.flyout.pane.timelinePropertiesAriaLabel": "タイムラインのプロパティ", "xpack.securitySolution.timeline.flyoutTimelineTemplateLabel": "タイムラインテンプレート", "xpack.securitySolution.timeline.fullScreenButton": "全画面", - "xpack.securitySolution.timeline.graphOverlay.backToEventsButton": "< イベントに戻る", "xpack.securitySolution.timeline.properties.attachTimelineToCaseTooltip": "ケースに関連付けるには、タイムラインのタイトルを入力してください", "xpack.securitySolution.timeline.properties.attachToExistingCaseButtonLabel": "既存のケースに添付...", "xpack.securitySolution.timeline.properties.attachToNewCaseButtonLabel": "新しいケースに添付", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 97caed949d4e5..db73cd8043e7e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1568,6 +1568,26 @@ "expressions.functions.varset.name.help": "指定变量的名称", "expressions.functions.varset.val.help": "为变量指定值。如果未提供,将使用输入上下文", "expressions.types.number.fromStringConversionErrorMessage": "无法将“{string}”字符串的类型转换为数字", + "flot.pie.unableToDrawLabelsInsideCanvasErrorMessage": "无法用画布内包含的标签绘制饼图", + "flot.time.aprLabel": "四月", + "flot.time.augLabel": "八月", + "flot.time.decLabel": "十二月", + "flot.time.febLabel": "二月", + "flot.time.friLabel": "周五", + "flot.time.janLabel": "一月", + "flot.time.julLabel": "七月", + "flot.time.junLabel": "六月", + "flot.time.marLabel": "三月", + "flot.time.mayLabel": "五月", + "flot.time.monLabel": "周一", + "flot.time.novLabel": "十一月", + "flot.time.octLabel": "十月", + "flot.time.satLabel": "周六", + "flot.time.sepLabel": "九月", + "flot.time.sunLabel": "周日", + "flot.time.thuLabel": "周四", + "flot.time.tueLabel": "周二", + "flot.time.wedLabel": "周三", "home.breadcrumbs.addDataTitle": "添加数据", "home.breadcrumbs.homeTitle": "主页", "home.dataManagementDisableCollection": " 要停止收集, ", @@ -12317,8 +12337,6 @@ "xpack.monitoring.apm.instances.versionTitle": "版本", "xpack.monitoring.apmNavigation.instancesLinkText": "实例", "xpack.monitoring.apmNavigation.overviewLinkText": "概览", - "xpack.monitoring.aprLabel": "四月", - "xpack.monitoring.augLabel": "八月", "xpack.monitoring.beats.filterBeatsPlaceholder": "筛选 Beats……", "xpack.monitoring.beats.instance.bytesSentLabel": "已发送字节", "xpack.monitoring.beats.instance.configReloadsLabel": "配置重载", @@ -12485,7 +12503,6 @@ "xpack.monitoring.clustersNavigation.clustersLinkText": "集群", "xpack.monitoring.clusterStats.uuidNotFoundErrorMessage": "在选定时间范围内找不到该集群。UUID:{clusterUuid}", "xpack.monitoring.clusterStats.uuidNotSpecifiedErrorMessage": "{clusterUuid} 未指定", - "xpack.monitoring.decLabel": "十二月", "xpack.monitoring.elasticsearch.ccr.ccrListingTable.errorColumnTitle": "错误", "xpack.monitoring.elasticsearch.ccr.ccrListingTable.followsColumnTitle": "跟随", "xpack.monitoring.elasticsearch.ccr.ccrListingTable.indexColumnTitle": "索引", @@ -12651,15 +12668,10 @@ "xpack.monitoring.expiredLicenseStatusTitle": "您的{typeTitleCase}许可证已过期", "xpack.monitoring.feature.reserved.description": "要向用户授予访问权限,还应分配 monitoring_user 角色。", "xpack.monitoring.featureRegistry.monitoringFeatureName": "堆栈监测", - "xpack.monitoring.febLabel": "二月", "xpack.monitoring.formatNumbers.notAvailableLabel": "不适用", - "xpack.monitoring.friLabel": "周五", "xpack.monitoring.healthCheck.encryptionErrorAction": "了解操作方法。", "xpack.monitoring.healthCheck.tlsAndEncryptionError": "必须在 Kibana 与 Elasticsearch 之间启用传输层安全, \n 并在 kibana.yml 文件中配置加密密钥,才能使用 Alerting 功能。", "xpack.monitoring.healthCheck.tlsAndEncryptionErrorTitle": "需要其他设置", - "xpack.monitoring.janLabel": "一月", - "xpack.monitoring.julLabel": "七月", - "xpack.monitoring.junLabel": "六月", "xpack.monitoring.kibana.clusterStatus.connectionsLabel": "连接", "xpack.monitoring.kibana.clusterStatus.instancesLabel": "实例", "xpack.monitoring.kibana.clusterStatus.maxResponseTimeLabel": "最大响应时间", @@ -12789,8 +12801,6 @@ "xpack.monitoring.logstashNavigation.overviewLinkText": "概览", "xpack.monitoring.logstashNavigation.pipelinesLinkText": "管道", "xpack.monitoring.logstashNavigation.pipelineVersionDescription": "活动版本 {relativeLastSeen} 和首次看到 {relativeFirstSeen}", - "xpack.monitoring.marLabel": "三月", - "xpack.monitoring.mayLabel": "五月", "xpack.monitoring.metricbeatMigration.apmInstructions.configureMetricbeatDescription": "在 {file} 文件中进行这些更改。", "xpack.monitoring.metricbeatMigration.apmInstructions.configureMetricbeatTitle": "配置 Metricbeat 以发送至监测集群", "xpack.monitoring.metricbeatMigration.apmInstructions.disableInternalCollection.description": "在 APM Server 的配置文件 ({file}) 中添加以下设置:", @@ -13414,7 +13424,6 @@ "xpack.monitoring.metrics.logstashInstance.systemLoad.last1MinuteLabel": "1 分钟", "xpack.monitoring.metrics.logstashInstance.systemLoad.last5MinutesDescription": "过去 5 分钟的负载平均值。", "xpack.monitoring.metrics.logstashInstance.systemLoad.last5MinutesLabel": "5 分钟", - "xpack.monitoring.monLabel": "周一", "xpack.monitoring.noData.blurbs.changesNeededDescription": "要运行 Monitoring,请执行以下步骤", "xpack.monitoring.noData.blurbs.changesNeededTitle": "您需要做些调整", "xpack.monitoring.noData.blurbs.cloudDeploymentDescription": "请返回到您的 ", @@ -13453,15 +13462,10 @@ "xpack.monitoring.noData.routeTitle": "设置监测", "xpack.monitoring.noData.setupInternalInstead": "或,使用内部收集设置", "xpack.monitoring.noData.setupMetricbeatInstead": "或,使用 Metricbeat 设置(推荐)", - "xpack.monitoring.novLabel": "十一月", - "xpack.monitoring.octLabel": "十月", "xpack.monitoring.overview.heading": "堆栈监测概览", "xpack.monitoring.pageLoadingTitle": "正在加载……", "xpack.monitoring.permanentActiveLicenseStatusDescription": "您的许可证永不会过期。", - "xpack.monitoring.pie.unableToDrawLabelsInsideCanvasErrorMessage": "无法用画布内包含的标签绘制饼图", "xpack.monitoring.requestedClusters.uuidNotFoundErrorMessage": "在选定时间范围内找不到该集群。UUID:{clusterUuid}", - "xpack.monitoring.satLabel": "周六", - "xpack.monitoring.sepLabel": "九月", "xpack.monitoring.setupMode.clickToDisableInternalCollection": "禁用内部收集(self monitoring)", "xpack.monitoring.setupMode.clickToMonitorWithMetricbeat": "使用 Metricbeat 监测", "xpack.monitoring.setupMode.description": "您处于设置模式。图标 ({flagIcon}) 表示配置选项。", @@ -13501,13 +13505,9 @@ "xpack.monitoring.summaryStatus.statusDescription": "状态", "xpack.monitoring.summaryStatus.statusIconLabel": "状态:{status}", "xpack.monitoring.summaryStatus.statusIconTitle": "状态:{statusIcon}", - "xpack.monitoring.sunLabel": "周日", - "xpack.monitoring.thuLabel": "周四", - "xpack.monitoring.tueLabel": "周二", "xpack.monitoring.updateLicenseButtonLabel": "更新许可证", "xpack.monitoring.updateLicenseTitle": "更新您的许可证", "xpack.monitoring.useAvailableLicenseDescription": "如果已有新的许可证,请立即上传。", - "xpack.monitoring.wedLabel": "周三", "xpack.observability.emptySection.apps.alert.description": "503 错误是否越来越多?服务是否响应?CPU 和 RAM 利用率是否激增?实时查看警告,而不是事后再进行剖析。", "xpack.observability.emptySection.apps.alert.link": "创建告警", "xpack.observability.emptySection.apps.alert.title": "未找到告警。", @@ -14831,8 +14831,6 @@ "xpack.securitySolution.case.caseView.noTags": "当前没有为此案例分配标记。", "xpack.securitySolution.case.caseView.openedOn": "打开时间", "xpack.securitySolution.case.caseView.optional": "可选", - "xpack.securitySolution.case.caseView.pageBadgeLabel": "公测版", - "xpack.securitySolution.case.caseView.pageBadgeTooltip": "案例工作流仍为公测版。请通过在 Kibana 存储库中报告问题或错误,帮助我们改进产品。", "xpack.securitySolution.case.caseView.particpantsLabel": "参与者", "xpack.securitySolution.case.caseView.pushNamedIncident": "推送为 { thirdParty } 事件", "xpack.securitySolution.case.caseView.pushThirdPartyIncident": "推送为外部事件", @@ -15208,8 +15206,6 @@ "xpack.securitySolution.detectionEngine.emptyActionSecondary": "前往文档", "xpack.securitySolution.detectionEngine.emptyTitle": "似乎在 Security 应用程序中没有与检测引擎相关的索引", "xpack.securitySolution.detectionEngine.goToDocumentationButton": "查看文档", - "xpack.securitySolution.detectionEngine.headerPage.pageBadgeLabel": "公测版", - "xpack.securitySolution.detectionEngine.headerPage.pageBadgeTooltip": "告警仍为公测版。请通过在 Kibana 存储库中报告问题或错误,帮助我们改进产品。", "xpack.securitySolution.detectionEngine.lastSignalTitle": "上一告警", "xpack.securitySolution.detectionEngine.mitreAttack.addTitle": "添加 MITRE ATT&CK\\u2122 威胁", "xpack.securitySolution.detectionEngine.mitreAttack.tacticPlaceHolderDescription": "选择策略......", @@ -15587,13 +15583,12 @@ "xpack.securitySolution.detectionEngine.rules.deleteDescription": "删除", "xpack.securitySolution.detectionEngine.rules.editPageTitle": "编辑", "xpack.securitySolution.detectionEngine.rules.importRuleTitle": "导入规则……", - "xpack.securitySolution.detectionEngine.rules.loadPrePackagedRulesButton": "加载 Elastic 预构建规则和时间线模板", + "xpack.securitySolution.detectionEngine.rules.loadPrePackagedRulesAndTemplatesButton": "加载 Elastic 预构建规则和时间线模板", "xpack.securitySolution.detectionEngine.rules.optionalFieldDescription": "可选", "xpack.securitySolution.detectionEngine.rules.pageTitle": "检测规则", "xpack.securitySolution.detectionEngine.rules.prePackagedRules.createOwnRuletButton": "创建自己的规则", "xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptMessage": "Elastic Security 附带预置检测规则,这些规则在后台运行,并在条件满足时创建告警。默认情况下,除 Elastic Endpoint Security 规则外,所有预置规则都处于禁用状态。您可以选择其他要激活的规则。", "xpack.securitySolution.detectionEngine.rules.prePackagedRules.emptyPromptTitle": "加载 Elastic 预构建检测规则", - "xpack.securitySolution.detectionEngine.rules.prePackagedRules.loadPreBuiltButton": "加载预置检测规则和时间线模板", "xpack.securitySolution.detectionEngine.rules.releaseNotesHelp": "发行说明", "xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesAndTimelinesButton": "安装 {missingRules} 个 Elastic 预构建{missingRules, plural, =1 {规则} other {规则}}以及 {missingTimelines} 个 Elastic 预构建{missingTimelines, plural, =1 {时间线} other {时间线}} ", "xpack.securitySolution.detectionEngine.rules.reloadMissingPrePackagedRulesButton": "安装 {missingRules} 个 Elastic 预构建{missingRules, plural, =1 {规则} other {规则}} ", @@ -16415,7 +16410,6 @@ "xpack.securitySolution.timeline.flyout.pane.timelinePropertiesAriaLabel": "时间线属性", "xpack.securitySolution.timeline.flyoutTimelineTemplateLabel": "时间线模板", "xpack.securitySolution.timeline.fullScreenButton": "全屏", - "xpack.securitySolution.timeline.graphOverlay.backToEventsButton": "< 返回至事件", "xpack.securitySolution.timeline.properties.attachTimelineToCaseTooltip": "请为您的时间线提供标题,以便将其附加到案例", "xpack.securitySolution.timeline.properties.attachToExistingCaseButtonLabel": "附加到现有案例......", "xpack.securitySolution.timeline.properties.attachToNewCaseButtonLabel": "附加到新案例", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts index 2517552304d8d..019133b03d55f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts @@ -151,14 +151,14 @@ export const GET_ISSUE_API_ERROR = (id: string) => export const SEARCH_ISSUES_COMBO_BOX_ARIA_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxAriaLabel', { - defaultMessage: 'Select parent issue', + defaultMessage: 'Type to search', } ); export const SEARCH_ISSUES_PLACEHOLDER = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxPlaceholder', { - defaultMessage: 'Select parent issue', + defaultMessage: 'Type to search', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 7ee1e0d3f3fa6..34569dcc75240 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -114,7 +114,7 @@ describe('action_form', () => { describe('action_form in alert', () => { let wrapper: ReactWrapper; - async function setup() { + async function setup(customActions?: AlertAction[]) { const { loadAllActions } = jest.requireMock('../../lib/action_connector_api'); loadAllActions.mockResolvedValueOnce([ { @@ -177,6 +177,7 @@ describe('action_form', () => { show: true, }, }, + setHasActionsWithBrokenConnector: jest.fn(), actionTypeRegistry: actionTypeRegistry as any, docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, }; @@ -198,16 +199,18 @@ describe('action_form', () => { schedule: { interval: '1m', }, - actions: [ - { - group: 'default', - id: 'test', - actionTypeId: actionType.id, - params: { - message: '', - }, - }, - ], + actions: customActions + ? customActions + : [ + { + group: 'default', + id: 'test', + actionTypeId: actionType.id, + params: { + message: '', + }, + }, + ], tags: [], muteAll: false, enabled: false, @@ -229,6 +232,7 @@ describe('action_form', () => { setActionParamsProperty={(key: string, value: any, index: number) => (initialAlert.actions[index] = { ...initialAlert.actions[index], [key]: value }) } + setHasActionsWithBrokenConnector={deps!.setHasActionsWithBrokenConnector} http={deps!.http} actionTypeRegistry={deps!.actionTypeRegistry} defaultActionMessage={'Alert [{{ctx.metadata.name}}] has exceeded the threshold'} @@ -306,6 +310,7 @@ describe('action_form', () => { .find(`EuiToolTip [data-test-subj="${actionType.id}-ActionTypeSelectOption"]`) .exists() ).toBeFalsy(); + expect(deps.setHasActionsWithBrokenConnector).toHaveBeenLastCalledWith(false); }); it('does not render action types disabled by config', async () => { @@ -392,5 +397,27 @@ describe('action_form', () => { ); expect(actionOption.exists()).toBeFalsy(); }); + + it('recognizes actions with broken connectors', async () => { + await setup([ + { + group: 'default', + id: 'test', + actionTypeId: actionType.id, + params: { + message: '', + }, + }, + { + group: 'default', + id: 'connector-doesnt-exist', + actionTypeId: actionType.id, + params: { + message: 'broken', + }, + }, + ]); + expect(deps.setHasActionsWithBrokenConnector).toHaveBeenLastCalledWith(true); + }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 1b176e0f63dbd..94571c4eb1e5b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -62,6 +62,7 @@ interface ActionAccordionFormProps { messageVariables?: ActionVariable[]; defaultActionMessage?: string; setHasActionsDisabled?: (value: boolean) => void; + setHasActionsWithBrokenConnector?: (value: boolean) => void; capabilities: ApplicationStart['capabilities']; } @@ -83,6 +84,7 @@ export const ActionForm = ({ defaultActionMessage, toastNotifications, setHasActionsDisabled, + setHasActionsWithBrokenConnector, capabilities, docLinks, }: ActionAccordionFormProps) => { @@ -171,6 +173,16 @@ export const ActionForm = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [connectors, actionTypesIndex]); + useEffect(() => { + const hasActionWithBrokenConnector = actions.some( + (action) => !connectors.find((connector) => connector.id === action.id) + ); + if (setHasActionsWithBrokenConnector) { + setHasActionsWithBrokenConnector(hasActionWithBrokenConnector); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actions, connectors]); + const preconfiguredMessage = i18n.translate( 'xpack.triggersActionsUI.sections.actionForm.preconfiguredTitleMessage', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index 999873a650f07..b60aa04ee9f27 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -38,6 +38,9 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { const [{ alert }, dispatch] = useReducer(alertReducer, { alert: initialAlert }); const [isSaving, setIsSaving] = useState(false); const [hasActionsDisabled, setHasActionsDisabled] = useState(false); + const [hasActionsWithBrokenConnector, setHasActionsWithBrokenConnector] = useState( + false + ); const setAlert = (key: string, value: any) => { dispatch({ command: { type: 'setAlert' }, payload: { key, value } }); }; @@ -155,6 +158,7 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { errors={errors} canChangeTrigger={false} setHasActionsDisabled={setHasActionsDisabled} + setHasActionsWithBrokenConnector={setHasActionsWithBrokenConnector} operation="i18n.translate('xpack.triggersActionsUI.sections.alertEdit.operationName', { defaultMessage: 'edit', })" @@ -176,7 +180,7 @@ export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { data-test-subj="saveEditedAlertButton" type="submit" iconType="check" - isDisabled={hasErrors || hasActionErrors} + isDisabled={hasErrors || hasActionErrors || hasActionsWithBrokenConnector} isLoading={isSaving} onClick={async () => { setIsSaving(true); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index c69c33c0fe22e..8800f149c033b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -81,6 +81,7 @@ interface AlertFormProps { errors: IErrorObject; canChangeTrigger?: boolean; // to hide Change trigger button setHasActionsDisabled?: (value: boolean) => void; + setHasActionsWithBrokenConnector?: (value: boolean) => void; operation: string; } @@ -90,6 +91,7 @@ export const AlertForm = ({ dispatch, errors, setHasActionsDisabled, + setHasActionsWithBrokenConnector, operation, }: AlertFormProps) => { const alertsContext = useAlertsContext(); @@ -260,6 +262,7 @@ export const AlertForm = ({ diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json index 58c50d0dac7bd..fc9db4a8b6b22 100644 --- a/x-pack/plugins/uptime/kibana.json +++ b/x-pack/plugins/uptime/kibana.json @@ -2,7 +2,7 @@ "configPath": ["xpack", "uptime"], "id": "uptime", "kibanaVersion": "kibana", - "optionalPlugins": ["capabilities", "data", "home", "observability", "ml"], + "optionalPlugins": ["data", "home", "observability", "ml"], "requiredPlugins": [ "alerts", "embeddable", diff --git a/x-pack/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts index cd2dc5018e110..730bb2277227e 100644 --- a/x-pack/plugins/uptime/server/kibana.index.ts +++ b/x-pack/plugins/uptime/server/kibana.index.ts @@ -67,7 +67,7 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor read: [umDynamicSettings.name], }, alerting: { - all: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'], + read: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'], }, management: { insightsAndAlerting: ['triggersActions'], diff --git a/x-pack/plugins/xpack_legacy/server/routes/settings.test.ts b/x-pack/plugins/xpack_legacy/server/routes/settings.test.ts index e4afbb0902830..5d22e22ee0eb6 100644 --- a/x-pack/plugins/xpack_legacy/server/routes/settings.test.ts +++ b/x-pack/plugins/xpack_legacy/server/routes/settings.test.ts @@ -13,7 +13,11 @@ import { ServiceStatus, ServiceStatusLevels, } from '../../../../../src/core/server'; -import { contextServiceMock, elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; +import { + contextServiceMock, + elasticsearchServiceMock, + savedObjectsServiceMock, +} from '../../../../../src/core/server/mocks'; import { createHttpServer } from '../../../../../src/core/server/test_utils'; import { registerSettingsRoute } from './settings'; @@ -42,6 +46,9 @@ describe('/api/settings', () => { asCurrentUser: elasticsearchServiceMock.createScopedClusterClient().asCurrentUser, }, }, + savedObjects: { + client: savedObjectsServiceMock.create(), + }, }, }), }); diff --git a/x-pack/plugins/xpack_legacy/server/routes/settings.ts b/x-pack/plugins/xpack_legacy/server/routes/settings.ts index 091344ae4e894..9a30ca30616b7 100644 --- a/x-pack/plugins/xpack_legacy/server/routes/settings.ts +++ b/x-pack/plugins/xpack_legacy/server/routes/settings.ts @@ -45,6 +45,7 @@ export function registerSettingsRoute({ const collectorFetchContext = { callCluster: callAsCurrentUser, esClient: context.core.elasticsearch.client.asCurrentUser, + soClient: context.core.savedObjects.client, }; const settingsCollector = usageCollection.getCollectorByType(KIBANA_SETTINGS_TYPE) as diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts index f3542c728845d..2fa9fbe18730c 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/create.ts @@ -54,5 +54,31 @@ export default function createActionTests({ getService }: FtrProviderContext) { id: response.body.id, }); }); + + it('should notify feature usage when creating a gold action type', async () => { + const testStart = new Date(); + const response = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'Noop action type', + actionTypeId: 'test.noop', + secrets: {}, + config: {}, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, response.body.id, 'action', 'actions'); + + const { + body: { features }, + } = await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/licensing/feature_usage`); + expect(features).to.be.an(Array); + const noopFeature = features.find( + (feature: { name: string }) => feature.name === 'Connector: Test: Noop' + ); + expect(noopFeature).to.be.ok(); + expect(noopFeature.last_used).to.be.a('string'); + expect(new Date(noopFeature.last_used).getTime()).to.be.greaterThan(testStart.getTime()); + }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts index f74c6eaa3298a..2316585d2d0f4 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts @@ -216,6 +216,40 @@ export default function ({ getService }: FtrProviderContext) { }, }); }); + + it('should notify feature usage when executing a gold action type', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'Noop action type', + actionTypeId: 'test.noop', + secrets: {}, + config: {}, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); + + const executionStart = new Date(); + await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/${createdAction.id}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: {}, + }) + .expect(200); + + const { + body: { features }, + } = await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/licensing/feature_usage`); + expect(features).to.be.an(Array); + const noopFeature = features.find( + (feature: { name: string }) => feature.name === 'Connector: Test: Noop' + ); + expect(noopFeature).to.be.ok(); + expect(noopFeature.last_used).to.be.a('string'); + expect(new Date(noopFeature.last_used).getTime()).to.be.greaterThan(executionStart.getTime()); + }); }); interface ValidateEventLogParams { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts index 81db8177b2c11..e06aec72f1874 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/update.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import expect from '@kbn/expect'; import { Spaces } from '../../scenarios'; import { checkAAD, getUrlPrefix, ObjectRemover } from '../../../common/lib'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; @@ -120,5 +121,41 @@ export default function updateActionTests({ getService }: FtrProviderContext) { message: `Preconfigured action custom-system-abc-connector is not allowed to update.`, }); }); + + it('should notify feature usage when editing a gold action type', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'Noop action type', + actionTypeId: 'test.noop', + secrets: {}, + config: {}, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); + + const updateStart = new Date(); + await supertest + .put(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action/${createdAction.id}`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'Noop action type updated', + secrets: {}, + config: {}, + }) + .expect(200); + + const { + body: { features }, + } = await supertest.get(`${getUrlPrefix(Spaces.space1.id)}/api/licensing/feature_usage`); + expect(features).to.be.an(Array); + const noopFeature = features.find( + (feature: { name: string }) => feature.name === 'Connector: Test: Noop' + ); + expect(noopFeature).to.be.ok(); + expect(noopFeature.last_used).to.be.a('string'); + expect(new Date(noopFeature.last_used).getTime()).to.be.greaterThan(updateStart.getTime()); + }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts index b94a547452377..40d88a6bface5 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts @@ -343,5 +343,30 @@ instanceStateValue: true }, }); }); + + it('should notify feature usage when using a gold action type', async () => { + const testStart = new Date(); + const reference = alertUtils.generateReference(); + const response = await alertUtils.createAlwaysFiringAction({ reference }); + expect(response.statusCode).to.eql(200); + + // Wait for alert to run + await esTestIndexTool.waitForDocs('action:test.index-record', reference); + + const { + body: { features }, + } = await supertestWithoutAuth.get(`${getUrlPrefix(space.id)}/api/licensing/feature_usage`); + expect(features).to.be.an(Array); + const indexRecordFeature = features.find( + (feature: { name: string }) => feature.name === 'Connector: Test: Index Record' + ); + expect(indexRecordFeature).to.be.ok(); + expect(indexRecordFeature.last_used).to.be.a('string'); + expect(new Date(indexRecordFeature.last_used).getTime()).to.be.greaterThan( + testStart.getTime() + ); + + await taskManagerUtils.waitForActionTaskParamsToBeCleanedUp(testStart); + }); }); } diff --git a/x-pack/test/api_integration/apis/ml/job_validation/cardinality.ts b/x-pack/test/api_integration/apis/ml/job_validation/cardinality.ts index 00c1ae12e182a..f7657e482d87d 100644 --- a/x-pack/test/api_integration/apis/ml/job_validation/cardinality.ts +++ b/x-pack/test/api_integration/apis/ml/job_validation/cardinality.ts @@ -13,6 +13,8 @@ export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertestWithoutAuth'); const ml = getService('ml'); + const VALIDATED_SEPARATELY = 'this value is not validated directly'; + describe('ValidateCardinality', function () { before(async () => { await esArchiver.loadIfNeeded('ml/ecommerce'); @@ -60,9 +62,7 @@ export default ({ getService }: FtrProviderContext) => { expect(body).to.eql([{ id: 'success_cardinality' }]); }); - // Failing ES promotion due to changes in the cardinality agg, - // see https://github.com/elastic/kibana/issues/80418 - it.skip(`should recognize a high model plot cardinality`, async () => { + it(`should recognize a high model plot cardinality`, async () => { const requestBody = { job_id: '', description: '', @@ -96,10 +96,32 @@ export default ({ getService }: FtrProviderContext) => { .send(requestBody) .expect(200); - expect(body).to.eql([ - { id: 'cardinality_model_plot_high', modelPlotCardinality: 4711 }, + const expectedResponse = [ + { + id: 'cardinality_model_plot_high', + modelPlotCardinality: VALIDATED_SEPARATELY, + }, { id: 'cardinality_partition_field', fieldName: 'order_id' }, - ]); + ]; + + expect(body.length).to.eql( + expectedResponse.length, + `Response body should have ${expectedResponse.length} entries (got ${body})` + ); + for (const entry of expectedResponse) { + const responseEntry = body.find((obj: any) => obj.id === entry.id); + expect(responseEntry).to.not.eql( + undefined, + `Response entry with id '${entry.id}' should exist` + ); + + if (entry.id === 'cardinality_model_plot_high') { + // don't check the exact value of modelPlotCardinality as this is an approximation + expect(responseEntry).to.have.property('modelPlotCardinality'); + } else { + expect(responseEntry).to.eql(entry); + } + } }); it('should not validate cardinality in case request payload is invalid', async () => { diff --git a/x-pack/test/api_integration/apis/ml/job_validation/validate.ts b/x-pack/test/api_integration/apis/ml/job_validation/validate.ts index 01a34f110ed1e..8f78cdf015601 100644 --- a/x-pack/test/api_integration/apis/ml/job_validation/validate.ts +++ b/x-pack/test/api_integration/apis/ml/job_validation/validate.ts @@ -14,6 +14,8 @@ export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertestWithoutAuth'); const ml = getService('ml'); + const VALIDATED_SEPARATELY = 'this value is not validated directly'; + describe('Validate job', function () { before(async () => { await esArchiver.loadIfNeeded('ml/ecommerce'); @@ -234,7 +236,7 @@ export default ({ getService }: FtrProviderContext) => { } }); - expect(body).to.eql([ + const expectedResponse = [ { id: 'job_id_valid', heading: 'Job ID format is valid', @@ -252,10 +254,9 @@ export default ({ getService }: FtrProviderContext) => { }, { id: 'cardinality_model_plot_high', - modelPlotCardinality: 4711, - text: - 'The estimated cardinality of 4711 of fields relevant to creating model plots might result in resource intensive jobs.', - status: 'warning', + modelPlotCardinality: VALIDATED_SEPARATELY, + text: VALIDATED_SEPARATELY, + status: VALIDATED_SEPARATELY, }, { id: 'cardinality_partition_field', @@ -296,7 +297,32 @@ export default ({ getService }: FtrProviderContext) => { url: `https://www.elastic.co/guide/en/machine-learning/${pkg.branch}/create-jobs.html#model-memory-limits`, status: 'warning', }, - ]); + ]; + + expect(body.length).to.eql( + expectedResponse.length, + `Response body should have ${expectedResponse.length} entries (got ${body})` + ); + for (const entry of expectedResponse) { + const responseEntry = body.find((obj: any) => obj.id === entry.id); + expect(responseEntry).to.not.eql( + undefined, + `Response entry with id '${entry.id}' should exist` + ); + + if (entry.id === 'cardinality_model_plot_high') { + // don't check the exact value of modelPlotCardinality as this is an approximation + expect(responseEntry).to.have.property('modelPlotCardinality'); + expect(responseEntry) + .to.have.property('text') + .match( + /^The estimated cardinality of [0-9]+ of fields relevant to creating model plots might result in resource intensive jobs./ + ); + expect(responseEntry).to.have.property('status', 'warning'); + } else { + expect(responseEntry).to.eql(entry); + } + } }); it('should not validate configuration in case request payload is invalid', async () => { diff --git a/x-pack/test/api_integration/apis/security_solution/authentications.ts b/x-pack/test/api_integration/apis/security_solution/authentications.ts index c0a3570c9d8e2..7073658ab3ccc 100644 --- a/x-pack/test/api_integration/apis/security_solution/authentications.ts +++ b/x-pack/test/api_integration/apis/security_solution/authentications.ts @@ -14,6 +14,7 @@ const TO = '3000-01-01T00:00:00.000Z'; // typical values that have to change after an update from "scripts/es_archiver" const HOST_NAME = 'zeek-newyork-sha-aa8df15'; +const LAST_SUCCESS_SOURCE_IP = '8.42.77.171'; const TOTAL_COUNT = 3; const EDGE_LENGTH = 1; @@ -78,6 +79,9 @@ export default function ({ getService }: FtrProviderContext) { expect(authentications.edges.length).to.be(EDGE_LENGTH); expect(authentications.totalCount).to.be(TOTAL_COUNT); + expect(authentications.edges[0]!.node.lastSuccess!.source!.ip).to.eql([ + LAST_SUCCESS_SOURCE_IP, + ]); expect(authentications.edges[0]!.node.lastSuccess!.host!.name).to.eql([HOST_NAME]); }); }); diff --git a/x-pack/test/api_integration/apis/spaces/get_active_space.ts b/x-pack/test/api_integration/apis/spaces/get_active_space.ts index b925df3918825..16cb03fe8a316 100644 --- a/x-pack/test/api_integration/apis/spaces/get_active_space.ts +++ b/x-pack/test/api_integration/apis/spaces/get_active_space.ts @@ -35,6 +35,20 @@ export default function ({ getService }: FtrProviderContext) { }); }); + it('returns the default space when explicitly referenced', async () => { + await supertest + .get('/s/default/internal/spaces/_active_space') + .set('kbn-xsrf', 'xxx') + .expect(200, { + id: 'default', + name: 'Default', + description: 'This is your default space!', + color: '#00bfb3', + disabledFeatures: [], + _reserved: true, + }); + }); + it('returns the foo space', async () => { await supertest .get('/s/foo-space/internal/spaces/_active_space') diff --git a/x-pack/test/apm_api_integration/common/authentication.ts b/x-pack/test/apm_api_integration/common/authentication.ts index 6179c88916639..501a844311334 100644 --- a/x-pack/test/apm_api_integration/common/authentication.ts +++ b/x-pack/test/apm_api_integration/common/authentication.ts @@ -29,6 +29,15 @@ const roles = { ], }, [ApmUser.apmReadUserWithoutMlAccess]: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['apm-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, kibana: [ { base: [], @@ -74,7 +83,7 @@ const users = { roles: ['apm_user', ApmUser.apmReadUser], }, [ApmUser.apmReadUserWithoutMlAccess]: { - roles: ['apm_user', ApmUser.apmReadUserWithoutMlAccess], + roles: [ApmUser.apmReadUserWithoutMlAccess], }, [ApmUser.apmWriteUser]: { roles: ['apm_user', ApmUser.apmWriteUser], diff --git a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts index 6fd5e7e0c3ea7..9a6c6f94dbb60 100644 --- a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts @@ -99,6 +99,25 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(definedHealthStatuses.length).to.be(0); }); }); + + describe('and fetching a list of services with a filter', () => { + let response: PromiseReturnType; + before(async () => { + response = await supertest.get( + `/api/apm/services?start=${start}&end=${end}&uiFilters=${encodeURIComponent( + `{"kuery":"service.name:opbeans-java","environment":"ENVIRONMENT_ALL"}` + )}` + ); + }); + + it('does not return health statuses for services that are not found in APM data', () => { + expect(response.status).to.be(200); + + expect(response.body.items.length).to.be(1); + + expect(response.body.items[0].serviceName).to.be('opbeans-java'); + }); + }); }); }); } diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts new file mode 100644 index 0000000000000..42d4b86119bb0 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_exceptions.ts @@ -0,0 +1,704 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import expect from '@kbn/expect'; +import { SearchResponse } from 'elasticsearch'; +import { Signal } from '../../../../plugins/security_solution/server/lib/detection_engine/signals/types'; +import { getCreateExceptionListItemMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock'; +import { deleteAllExceptions } from '../../../lists_api_integration/utils'; +import { RulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/response'; +import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; +import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; +import { CreateExceptionListItemSchema } from '../../../../plugins/lists/common'; +import { + EXCEPTION_LIST_ITEM_URL, + EXCEPTION_LIST_URL, +} from '../../../../plugins/lists/common/constants'; + +import { + DETECTION_ENGINE_RULES_URL, + DETECTION_ENGINE_RULES_STATUS_URL, + DETECTION_ENGINE_QUERY_SIGNALS_URL, + DETECTION_ENGINE_PREPACKAGED_URL, +} from '../../../../plugins/security_solution/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getSimpleRule, + getSimpleRuleOutput, + removeServerGeneratedProperties, + waitFor, + getQueryAllSignals, + downgradeImmutableRule, +} from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + describe('create_rules_with_exceptions', () => { + describe('creating rules with exceptions', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + await deleteAllExceptions(es); + }); + + it('should create a single rule with a rule_id and add an exception list to the rule', async () => { + const { + body: { id, list_id, namespace_type, type }, + } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + const ruleWithException: CreateRulesSchema = { + ...getSimpleRule(), + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }; + + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(ruleWithException) + .expect(200); + + const expected: Partial = { + ...getSimpleRuleOutput(), + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }; + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(expected); + }); + + it('should create a single rule with an exception list and validate it ran successfully', async () => { + const { + body: { id, list_id, namespace_type, type }, + } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + const ruleWithException: CreateRulesSchema = { + ...getSimpleRule(), + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }; + + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(ruleWithException) + .expect(200); + + // wait for Task Manager to execute the rule and update status + await waitFor(async () => { + const { body: statusBody } = await supertest + .post(DETECTION_ENGINE_RULES_STATUS_URL) + .set('kbn-xsrf', 'true') + .send({ ids: [body.id] }) + .expect(200); + + return statusBody[body.id]?.current_status?.status === 'succeeded'; + }); + + const { body: statusBody } = await supertest + .post(DETECTION_ENGINE_RULES_STATUS_URL) + .set('kbn-xsrf', 'true') + .send({ ids: [body.id] }) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + const expected: Partial = { + ...getSimpleRuleOutput(), + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }; + expect(bodyToCompare).to.eql(expected); + expect(statusBody[body.id].current_status.status).to.eql('succeeded'); + }); + + it('should allow removing an exception list from an immutable rule through patch', async () => { + // add all the immutable rules from the pre-packaged url + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + // This rule has an existing exceptions_list that we are going to use + const { body: immutableRule } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + expect(immutableRule.exceptions_list.length).greaterThan(0); // make sure we have at least one + + // remove the exceptions list as a user is allowed to remove it from an immutable rule + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', exceptions_list: [] }) + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + expect(body.exceptions_list.length).to.eql(0); + }); + + it('should allow adding a second exception list to an immutable rule through patch', async () => { + // add all the immutable rules from the pre-packaged url + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Create a new exception list + const { + body: { id, list_id, namespace_type, type }, + } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + // This rule has an existing exceptions_list that we are going to use + const { body: immutableRule } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + expect(immutableRule.exceptions_list.length).greaterThan(0); // make sure we have at least one + + // add a second exceptions list as a user is allowed to add a second list to an immutable rule + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + exceptions_list: [ + ...immutableRule.exceptions_list, + { + id, + list_id, + namespace_type, + type, + }, + ], + }) + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + expect(body.exceptions_list.length).to.eql(2); + }); + + it('should override any updates to pre-packaged rules if the user removes the exception list through the API but the new version of a rule has an exception list again', async () => { + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + // This rule has an existing exceptions_list that we are going to use + const { body: immutableRule } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + expect(immutableRule.exceptions_list.length).greaterThan(0); // make sure we have at least one exception list + + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + // This rule has an existing exceptions_list that we are going to ensure does not stomp on our existing rule + // remove the exceptions list as a user is allowed to remove it + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', exceptions_list: [] }) + .expect(200); + + // downgrade the version number of the rule + await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + + // re-add the pre-packaged rule to get the single upgrade to happen + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // get the pre-packaged rule after we upgraded it + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // We should have a length of 1 and it should be the same as our original before we tried to remove it using patch + expect(body.exceptions_list.length).to.eql(1); + expect(body.exceptions_list).to.eql(immutableRule.exceptions_list); + }); + + it('should merge back an exceptions_list if it was removed from the immutable rule through PATCH', async () => { + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Create a new exception list + const { + body: { id, list_id, namespace_type, type }, + } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + // This rule has an existing exceptions_list that we are going to ensure does not stomp on our existing rule + const { body: immutableRule } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // remove the exception list and only have a single list that is not an endpoint_list + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }) + .expect(200); + + // downgrade the version number of the rule + await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + + // re-add the pre-packaged rule to get the single upgrade to happen + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // get the immutable rule after we installed it a second time + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // The installed rule should have both the original immutable exceptions list back and the + // new list the user added. + expect(body.exceptions_list).to.eql([ + ...immutableRule.exceptions_list, + { + id, + list_id, + namespace_type, + type, + }, + ]); + }); + + it('should NOT add an extra exceptions_list that already exists on a rule during an upgrade', async () => { + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + // This rule has an existing exceptions_list that we are going to ensure does not stomp on our existing rule + const { body: immutableRule } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // downgrade the version number of the rule + await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + + // re-add the pre-packaged rule to get the single upgrade to happen + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // get the immutable rule after we installed it a second time + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // The installed rule should have both the original immutable exceptions list back and the + // new list the user added. + expect(body.exceptions_list).to.eql([...immutableRule.exceptions_list]); + }); + + it('should NOT allow updates to pre-packaged rules to overwrite existing exception based rules when the user adds an additional exception list', async () => { + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Create a new exception list + const { + body: { id, list_id, namespace_type, type }, + } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json + // This rule has an existing exceptions_list that we are going to ensure does not stomp on our existing rule + const { body: immutableRule } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // add a second exceptions list as a user is allowed to add a second list to an immutable rule + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + exceptions_list: [ + ...immutableRule.exceptions_list, + { + id, + list_id, + namespace_type, + type, + }, + ], + }) + .expect(200); + + // downgrade the version number of the rule + await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + + // re-add the pre-packaged rule to get the single upgrade to happen + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=9a1a2dae-0b5f-4c3d-8305-a268d404c306`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + // It should be the same as what the user added originally + expect(body.exceptions_list).to.eql([ + ...immutableRule.exceptions_list, + { + id, + list_id, + namespace_type, + type, + }, + ]); + }); + + it('should not remove any exceptions added to a pre-packaged/immutable rule during an update if that rule has no existing exception lists', async () => { + // add all the immutable rules from the pre-packaged url + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Create a new exception list + const { + body: { id, list_id, namespace_type, type }, + } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // Rule id of "6d3456a5-4a42-49d1-aaf2-7b1fd475b2c6" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/c2_reg_beacon.json + // since this rule does not have existing exceptions_list that we are going to use for tests + const { body: immutableRule } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=6d3456a5-4a42-49d1-aaf2-7b1fd475b2c6`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + expect(immutableRule.exceptions_list.length).eql(0); // make sure we have no exceptions_list + + // add a second exceptions list as a user is allowed to add a second list to an immutable rule + await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ + rule_id: '6d3456a5-4a42-49d1-aaf2-7b1fd475b2c6', + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }) + .expect(200); + + // downgrade the version number of the rule + await downgradeImmutableRule(es, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + + // re-add the pre-packaged rule to get the single upgrade of the rule to happen + await supertest + .put(DETECTION_ENGINE_PREPACKAGED_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // ensure that the same exception is still on the rule + const { body } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}?rule_id=6d3456a5-4a42-49d1-aaf2-7b1fd475b2c6`) + .set('kbn-xsrf', 'true') + .send(getSimpleRule()) + .expect(200); + + expect(body.exceptions_list).to.eql([ + { + id, + list_id, + namespace_type, + type, + }, + ]); + }); + + describe('tests with auditbeat data', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + await esArchiver.load('auditbeat/hosts'); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + await deleteAllExceptions(es); + await esArchiver.unload('auditbeat/hosts'); + }); + + it('should be able to execute against an exception list that does not include valid entries and get back 10 signals', async () => { + const { + body: { id, list_id, namespace_type, type }, + } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + const exceptionListItem: CreateExceptionListItemSchema = { + ...getCreateExceptionListItemMinimalSchemaMock(), + entries: [ + { + field: 'some.none.existent.field', // non-existent field where we should not exclude anything + operator: 'included', + type: 'match', + value: 'some value', + }, + ], + }; + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(exceptionListItem) + .expect(200); + + const ruleWithException: CreateRulesSchema = { + ...getSimpleRule(), + from: '1900-01-01T00:00:00.000Z', + query: 'host.name: "suricata-sensor-amsterdam"', + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }; + + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(ruleWithException) + .expect(200); + + // wait until rules show up and are present + await waitFor(async () => { + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + return signalsOpen.hits.hits.length > 0; + }); + + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + + // expect there to be 10 + expect(signalsOpen.hits.hits.length).equal(10); + }); + + it('should be able to execute against an exception list that does include valid entries and get back 0 signals', async () => { + const { + body: { id, list_id, namespace_type, type }, + } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + const exceptionListItem: CreateExceptionListItemSchema = { + ...getCreateExceptionListItemMinimalSchemaMock(), + entries: [ + { + field: 'host.name', // This matches the query below which will exclude everything + operator: 'included', + type: 'match', + value: 'suricata-sensor-amsterdam', + }, + ], + }; + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(exceptionListItem) + .expect(200); + + const ruleWithException: CreateRulesSchema = { + ...getSimpleRule(), + from: '1900-01-01T00:00:00.000Z', + query: 'host.name: "suricata-sensor-amsterdam"', // this matches all the exceptions we should exclude + exceptions_list: [ + { + id, + list_id, + namespace_type, + type, + }, + ], + }; + + const { body: resBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(ruleWithException) + .expect(200); + + // wait for Task Manager to finish executing the rule + await waitFor(async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [resBody.id] }) + .expect(200); + return body[resBody.id]?.current_status?.status === 'succeeded'; + }); + + // Get the signals now that we are done running and expect the result to always be zero + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + + // expect there to be 10 + expect(signalsOpen.hits.hits.length).equal(0); + }); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts new file mode 100644 index 0000000000000..6d3a0ce683cda --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -0,0 +1,225 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { SearchResponse } from 'elasticsearch'; +import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; +import { + DETECTION_ENGINE_RULES_URL, + DETECTION_ENGINE_RULES_STATUS_URL, + DETECTION_ENGINE_QUERY_SIGNALS_URL, +} from '../../../../plugins/security_solution/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getQueryAllSignals, + removeServerGeneratedProperties, + waitFor, +} from '../../utils'; + +import { getCreateThreatMatchRulesSchemaMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock'; +import { getThreatMatchingSchemaPartialMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks'; +import { Signal } from '../../../../plugins/security_solution/server/lib/detection_engine/signals/types'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + /** + * Specific api integration tests for threat matching rule type + */ + describe('create_threat_matching', () => { + describe('validation errors', () => { + it('should give an error that the index must exist first if it does not exist before creating a rule', async () => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getCreateThreatMatchRulesSchemaMock()) + .expect(400); + + expect(body).to.eql({ + message: + 'To create a rule, the index must exist first. Index .siem-signals-default does not exist', + status_code: 400, + }); + }); + }); + + describe('creating threat match rule', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + }); + + it('should create a single rule with a rule_id', async () => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getCreateThreatMatchRulesSchemaMock()) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getThreatMatchingSchemaPartialMock()); + }); + + it('should create a single rule with a rule_id and validate it ran successfully', async () => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getCreateThreatMatchRulesSchemaMock()) + .expect(200); + + // wait for Task Manager to execute the rule and update status + await waitFor(async () => { + const { body: statusBody } = await supertest + .post(DETECTION_ENGINE_RULES_STATUS_URL) + .set('kbn-xsrf', 'true') + .send({ ids: [body.id] }) + .expect(200); + + return statusBody[body.id]?.current_status?.status === 'succeeded'; + }); + + const { body: statusBody } = await supertest + .post(DETECTION_ENGINE_RULES_STATUS_URL) + .set('kbn-xsrf', 'true') + .send({ ids: [body.id] }) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getThreatMatchingSchemaPartialMock()); + expect(statusBody[body.id].current_status.status).to.eql('succeeded'); + }); + }); + + describe('tests with auditbeat data', () => { + beforeEach(async () => { + await deleteAllAlerts(es); + await createSignalsIndex(supertest); + await esArchiver.load('auditbeat/hosts'); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(es); + await esArchiver.unload('auditbeat/hosts'); + }); + + it('should be able to execute and get 10 signals when doing a specific query', async () => { + const rule: CreateRulesSchema = { + ...getCreateThreatMatchRulesSchemaMock(), + from: '1900-01-01T00:00:00.000Z', + query: '*:*', + threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip + threat_index: ['auditbeat-*'], // We use auditbeat as both the matching index and the threat list for simplicity + threat_mapping: [ + // We match host.name against host.name + { + entries: [ + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + }; + + // create a simple rule + await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rule) + .expect(200); + + // wait until rules show up and are present + await waitFor(async () => { + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + return signalsOpen.hits.hits.length > 0; + }); + + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + + // expect there to be 10 + expect(signalsOpen.hits.hits.length).equal(10); + }); + + it('should return zero matches if the mapping does not match against anything in the mapping', async () => { + const rule: CreateRulesSchema = { + ...getCreateThreatMatchRulesSchemaMock(), + from: '1900-01-01T00:00:00.000Z', + query: '*:*', + threat_query: 'source.ip: "188.166.120.93"', // narrow things down with a query to a specific source ip + threat_index: ['auditbeat-*'], // We use auditbeat as both the matching index and the threat list for simplicity + threat_mapping: [ + // We match host.name against host.name + { + entries: [ + { + field: 'host.name', + value: 'invalid.mapping.value', // invalid mapping value + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + }; + + // create the threat match rule + const { body: resBody } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(rule) + .expect(200); + + // wait for Task Manager to finish executing the rule + await waitFor(async () => { + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [resBody.id] }) + .expect(200); + return body[resBody.id]?.current_status?.status === 'succeeded'; + }); + + // Get the signals now that we are done running and expect the result to always be zero + const { + body: signalsOpen, + }: { body: SearchResponse<{ signal: Signal }> } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQueryAllSignals()) + .expect(200); + + expect(signalsOpen.hits.hits.length).equal(0); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts index 779205377621d..24b76853164f2 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts @@ -15,6 +15,8 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./add_prepackaged_rules')); loadTestFile(require.resolve('./create_rules')); loadTestFile(require.resolve('./create_rules_bulk')); + loadTestFile(require.resolve('./create_threat_matching')); + loadTestFile(require.resolve('./create_exceptions')); loadTestFile(require.resolve('./delete_rules')); loadTestFile(require.resolve('./delete_rules_bulk')); loadTestFile(require.resolve('./export_rules')); diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 5d82eed41d3c5..db91529b8a2c3 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Client } from '@elastic/elasticsearch'; +import { ApiResponse, Client } from '@elastic/elasticsearch'; import { SuperTest } from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; +import { Context } from '@elastic/elasticsearch/lib/Transport'; import { Status, SignalIds, @@ -14,7 +15,10 @@ import { import { CreateRulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema'; import { UpdateRulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema'; import { RulesSchema } from '../../plugins/security_solution/common/detection_engine/schemas/response/rules_schema'; -import { DETECTION_ENGINE_INDEX_URL } from '../../plugins/security_solution/common/constants'; +import { + DETECTION_ENGINE_INDEX_URL, + INTERNAL_RULE_ID_KEY, +} from '../../plugins/security_solution/common/constants'; /** * This will remove server generated properties such as date times, etc... @@ -245,34 +249,38 @@ export const getSimpleMlRuleOutput = (ruleId = 'rule-1'): Partial = * This will retry 20 times before giving up and hopefully still not interfere with other tests * @param es The ElasticSearch handle */ -export const deleteAllAlerts = async (es: Client, retryCount = 20): Promise => { - if (retryCount > 0) { - try { - const result = await es.deleteByQuery({ - index: '.kibana', - q: 'type:alert', - wait_for_completion: true, - refresh: true, - conflicts: 'proceed', - body: {}, - }); - // deleteByQuery will cause version conflicts as alerts are being updated - // by background processes; the code below accounts for that - if (result.body.version_conflicts !== 0) { - throw new Error(`Version conflicts for ${result.body.version_conflicts} alerts`); - } - } catch (err) { - // eslint-disable-next-line no-console - console.log(`Error in deleteAllAlerts(), retries left: ${retryCount - 1}`, err); +export const deleteAllAlerts = async (es: Client): Promise => { + return countDownES(async () => { + return es.deleteByQuery({ + index: '.kibana', + q: 'type:alert', + wait_for_completion: true, + refresh: true, + conflicts: 'proceed', + body: {}, + }); + }, 'deleteAllAlerts'); +}; - // retry, counting down, and delay a bit before - await new Promise((resolve) => setTimeout(resolve, 250)); - await deleteAllAlerts(es, retryCount - 1); - } - } else { - // eslint-disable-next-line no-console - console.log('Could not deleteAllAlerts, no retries are left'); - } +export const downgradeImmutableRule = async (es: Client, ruleId: string): Promise => { + return countDownES(async () => { + return es.updateByQuery({ + index: '.kibana', + refresh: true, + wait_for_completion: true, + body: { + script: { + lang: 'painless', + source: 'ctx._source.alert.params.version--', + }, + query: { + term: { + 'alert.tags': `${INTERNAL_RULE_ID_KEY}:${ruleId}`, + }, + }, + }, + }); + }, 'downgradeImmutableRule'); }; /** @@ -295,27 +303,15 @@ export const deleteAllTimelines = async (es: Client): Promise => { * @param es The ElasticSearch handle */ export const deleteAllRulesStatuses = async (es: Client, retryCount = 20): Promise => { - if (retryCount > 0) { - try { - await es.deleteByQuery({ - index: '.kibana', - q: 'type:siem-detection-engine-rule-status', - wait_for_completion: true, - refresh: true, - body: {}, - }); - } catch (err) { - // eslint-disable-next-line no-console - console.log( - `Failure trying to deleteAllRulesStatuses, retries left are: ${retryCount - 1}`, - err - ); - await deleteAllRulesStatuses(es, retryCount - 1); - } - } else { - // eslint-disable-next-line no-console - console.log('Could not deleteAllRulesStatuses, no retries are left'); - } + return countDownES(async () => { + return es.deleteByQuery({ + index: '.kibana', + q: 'type:siem-detection-engine-rule-status', + wait_for_completion: true, + refresh: true, + body: {}, + }); + }, 'deleteAllRulesStatuses'); }; /** @@ -324,24 +320,12 @@ export const deleteAllRulesStatuses = async (es: Client, retryCount = 20): Promi * @param supertest The supertest client library */ export const createSignalsIndex = async ( - supertest: SuperTest, - retryCount = 20 + supertest: SuperTest ): Promise => { - if (retryCount > 0) { - try { - await supertest.post(DETECTION_ENGINE_INDEX_URL).set('kbn-xsrf', 'true').send(); - } catch (err) { - // eslint-disable-next-line no-console - console.log( - `Failure trying to create the signals index, retries left are: ${retryCount - 1}`, - err - ); - await createSignalsIndex(supertest, retryCount - 1); - } - } else { - // eslint-disable-next-line no-console - console.log('Could not createSignalsIndex, no retries are left'); - } + await countDownTest(async () => { + await supertest.post(DETECTION_ENGINE_INDEX_URL).set('kbn-xsrf', 'true').send(); + return true; + }, 'createSignalsIndex'); }; /** @@ -349,21 +333,12 @@ export const createSignalsIndex = async ( * @param supertest The supertest client library */ export const deleteSignalsIndex = async ( - supertest: SuperTest, - retryCount = 20 + supertest: SuperTest ): Promise => { - if (retryCount > 0) { - try { - await supertest.delete(DETECTION_ENGINE_INDEX_URL).set('kbn-xsrf', 'true').send(); - } catch (err) { - // eslint-disable-next-line no-console - console.log(`Failure trying to deleteSignalsIndex, retries left are: ${retryCount - 1}`, err); - await deleteSignalsIndex(supertest, retryCount - 1); - } - } else { - // eslint-disable-next-line no-console - console.log('Could not deleteSignalsIndex, no retries are left'); - } + await countDownTest(async () => { + await supertest.delete(DETECTION_ENGINE_INDEX_URL).set('kbn-xsrf', 'true').send(); + return true; + }, 'deleteSignalsIndex'); }; /** @@ -616,7 +591,7 @@ export const waitFor = async ( functionToTest: () => Promise, maxTimeout: number = 5000, timeoutWait: number = 10 -) => { +): Promise => { await new Promise(async (resolve, reject) => { let found = false; let numberOfTries = 0; @@ -636,3 +611,82 @@ export const waitFor = async ( } }); }; + +/** + * Does a plain countdown and checks against es queries for either conflicts in the error + * or for any over the wire issues such as timeouts or temp 404's to make the tests more + * reliant. + * @param esFunction The function to test against + * @param esFunctionName The name of the function to print if we encounter errors + * @param retryCount The number of times to retry before giving up (has default) + * @param timeoutWait Time to wait before trying again (has default) + */ +export const countDownES = async ( + esFunction: () => Promise, Context>>, + esFunctionName: string, + retryCount: number = 20, + timeoutWait = 250 +): Promise => { + await countDownTest( + async () => { + const result = await esFunction(); + if (result.body.version_conflicts !== 0) { + // eslint-disable-next-line no-console + console.log(`Version conflicts for ${result.body.version_conflicts}`); + return false; + } else { + return true; + } + }, + esFunctionName, + retryCount, + timeoutWait + ); +}; + +/** + * Does a plain countdown and checks against a boolean to determine if to wait and try again. + * This is useful for over the wire things that can cause issues such as conflict or timeouts + * for testing resiliency. + * @param functionToTest The function to test against + * @param name The name of the function to print if we encounter errors + * @param retryCount The number of times to retry before giving up (has default) + * @param timeoutWait Time to wait before trying again (has default) + */ +export const countDownTest = async ( + functionToTest: () => Promise, + name: string, + retryCount: number = 20, + timeoutWait = 250, + ignoreThrow: boolean = false +) => { + if (retryCount > 0) { + try { + const passed = await functionToTest(); + if (!passed) { + // eslint-disable-next-line no-console + console.log(`Failure trying to ${name}, retries left are: ${retryCount - 1}`); + // retry, counting down, and delay a bit before + await new Promise((resolve) => setTimeout(resolve, timeoutWait)); + await countDownTest(functionToTest, name, retryCount - 1, timeoutWait, ignoreThrow); + } + } catch (err) { + if (ignoreThrow) { + throw err; + } else { + // eslint-disable-next-line no-console + console.log( + `Failure trying to ${name}, with exception message of:`, + err.message, + `retries left are: ${retryCount - 1}` + ); + // retry, counting down, and delay a bit before + await new Promise((resolve) => setTimeout(resolve, timeoutWait)); + await countDownTest(functionToTest, name, retryCount - 1, timeoutWait, ignoreThrow); + } + } + } else { + // eslint-disable-next-line no-console + console.log(`Could not ${name}, no retries are left`); + } +}; diff --git a/x-pack/test/functional/apps/security/role_mappings.ts b/x-pack/test/functional/apps/security/role_mappings.ts index 60c166d837933..96f16aebd11b9 100644 --- a/x-pack/test/functional/apps/security/role_mappings.ts +++ b/x-pack/test/functional/apps/security/role_mappings.ts @@ -17,6 +17,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Role Mappings', function () { before(async () => { + // Delete any existing role mappings. ESS commonly sets up a role mapping automatically. + const existingMappings = await security.roleMappings.getAll(); + await Promise.all(existingMappings.map((m) => security.roleMappings.delete(m.name))); + await pageObjects.common.navigateToApp('roleMappings'); }); diff --git a/x-pack/test/ingest_manager_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/ingest_manager_api_integration/apis/agent_policy/agent_policy.ts index c6cf72f697aa0..c0132a5822b58 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/agent_policy/agent_policy.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/agent_policy/agent_policy.ts @@ -225,7 +225,9 @@ export default function ({ getService }: FtrProviderContext) { .post(`/api/fleet/agent_policies`) .set('kbn-xsrf', 'xxxx') .send(sharedBody) - .expect(200); + .expect(409); + + expect(body.message).to.match(/already exists?/); }); }); }); diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts index b9558240ca007..d1d909f773a2b 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/data_stream.ts @@ -12,6 +12,8 @@ export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; const supertest = getService('supertest'); const es = getService('es'); + const dockerServers = getService('dockerServers'); + const server = dockerServers.get('registry'); const pkgName = 'datastreams'; const pkgVersion = '0.1.0'; const pkgUpdateVersion = '0.2.0'; @@ -63,6 +65,7 @@ export default function (providerContext: FtrProviderContext) { }); }); afterEach(async () => { + if (!server) return; await es.transport.request({ method: 'DELETE', path: `/_data_stream/${logsTemplateName}-default`, diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/package_install_complete.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/package_install_complete.ts index 6b43c9d74c6bf..6fd4b64f0ee5e 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/package_install_complete.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/package_install_complete.ts @@ -9,6 +9,7 @@ import { PACKAGES_SAVED_OBJECT_TYPE, MAX_TIME_COMPLETE_INSTALL, } from '../../../../plugins/ingest_manager/common'; +import { skipIfNoDockerRegistry } from '../../helpers'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; export default function (providerContext: FtrProviderContext) { @@ -19,12 +20,14 @@ export default function (providerContext: FtrProviderContext) { const pkgVersion = '0.1.0'; const pkgUpdateVersion = '0.2.0'; describe('setup checks packages completed install', async () => { + skipIfNoDockerRegistry(providerContext); describe('package install', async () => { before(async () => { await supertest .post(`/api/fleet/epm/packages/${pkgName}-0.1.0`) .set('kbn-xsrf', 'xxxx') - .send({ force: true }); + .send({ force: true }) + .expect(200); }); it('should have not reinstalled if package install completed', async function () { const packageBeforeSetup = await kibanaServer.savedObjects.get({ @@ -32,7 +35,7 @@ export default function (providerContext: FtrProviderContext) { id: pkgName, }); const installStartedAtBeforeSetup = packageBeforeSetup.attributes.install_started_at; - await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').send(); + await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').expect(200); const packageAfterSetup = await kibanaServer.savedObjects.get({ type: PACKAGES_SAVED_OBJECT_TYPE, id: pkgName, @@ -51,7 +54,7 @@ export default function (providerContext: FtrProviderContext) { install_started_at: previousInstallDate, }, }); - await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').send(); + await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').expect(200); const packageAfterSetup = await kibanaServer.savedObjects.get({ type: PACKAGES_SAVED_OBJECT_TYPE, id: pkgName, @@ -71,7 +74,7 @@ export default function (providerContext: FtrProviderContext) { install_started_at: previousInstallDate, }, }); - await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').send(); + await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').expect(200); const packageAfterSetup = await kibanaServer.savedObjects.get({ type: PACKAGES_SAVED_OBJECT_TYPE, id: pkgName, @@ -83,7 +86,8 @@ export default function (providerContext: FtrProviderContext) { after(async () => { await supertest .delete(`/api/fleet/epm/packages/multiple_versions-0.1.0`) - .set('kbn-xsrf', 'xxxx'); + .set('kbn-xsrf', 'xxxx') + .expect(200); }); }); describe('package update', async () => { @@ -91,11 +95,13 @@ export default function (providerContext: FtrProviderContext) { await supertest .post(`/api/fleet/epm/packages/${pkgName}-0.1.0`) .set('kbn-xsrf', 'xxxx') - .send({ force: true }); + .send({ force: true }) + .expect(200); await supertest .post(`/api/fleet/epm/packages/${pkgName}-0.2.0`) .set('kbn-xsrf', 'xxxx') - .send({ force: true }); + .send({ force: true }) + .expect(200); }); it('should have not reinstalled if package update completed', async function () { const packageBeforeSetup = await kibanaServer.savedObjects.get({ @@ -103,7 +109,7 @@ export default function (providerContext: FtrProviderContext) { id: pkgName, }); const installStartedAtBeforeSetup = packageBeforeSetup.attributes.install_started_at; - await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').send(); + await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').expect(200); const packageAfterSetup = await kibanaServer.savedObjects.get({ type: PACKAGES_SAVED_OBJECT_TYPE, id: pkgName, @@ -124,7 +130,7 @@ export default function (providerContext: FtrProviderContext) { install_version: pkgUpdateVersion, // set version back }, }); - await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').send(); + await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').expect(200); const packageAfterSetup = await kibanaServer.savedObjects.get({ type: PACKAGES_SAVED_OBJECT_TYPE, id: pkgName, @@ -147,7 +153,7 @@ export default function (providerContext: FtrProviderContext) { version: pkgVersion, // set version back }, }); - await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').send(); + await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').expect(200); const packageAfterSetup = await kibanaServer.savedObjects.get({ type: PACKAGES_SAVED_OBJECT_TYPE, id: pkgName, @@ -160,7 +166,8 @@ export default function (providerContext: FtrProviderContext) { after(async () => { await supertest .delete(`/api/fleet/epm/packages/multiple_versions-0.1.0`) - .set('kbn-xsrf', 'xxxx'); + .set('kbn-xsrf', 'xxxx') + .expect(200); }); }); }); diff --git a/x-pack/test/lists_api_integration/utils.ts b/x-pack/test/lists_api_integration/utils.ts index 54a13fc027c99..5870239b73ed1 100644 --- a/x-pack/test/lists_api_integration/utils.ts +++ b/x-pack/test/lists_api_integration/utils.ts @@ -15,6 +15,7 @@ import { } from '../../plugins/lists/common/schemas'; import { ListSchema } from '../../plugins/lists/common'; import { LIST_INDEX } from '../../plugins/lists/common/constants'; +import { countDownES, countDownTest } from '../detection_engine_api_integration/utils'; /** * Creates the lists and lists items index for use inside of beforeEach blocks of tests @@ -22,24 +23,12 @@ import { LIST_INDEX } from '../../plugins/lists/common/constants'; * @param supertest The supertest client library */ export const createListsIndex = async ( - supertest: SuperTest, - retryCount = 20 + supertest: SuperTest ): Promise => { - if (retryCount > 0) { - try { - await supertest.post(LIST_INDEX).set('kbn-xsrf', 'true').send(); - } catch (err) { - // eslint-disable-next-line no-console - console.log( - `Failure trying to create the lists index, retries left are: ${retryCount - 1}`, - err - ); - await createListsIndex(supertest, retryCount - 1); - } - } else { - // eslint-disable-next-line no-console - console.log('Could not createListsIndex, no retries are left'); - } + return countDownTest(async () => { + await supertest.post(LIST_INDEX).set('kbn-xsrf', 'true').send(); + return true; + }, 'createListsIndex'); }; /** @@ -47,21 +36,26 @@ export const createListsIndex = async ( * @param supertest The supertest client library */ export const deleteListsIndex = async ( - supertest: SuperTest, - retryCount = 20 + supertest: SuperTest ): Promise => { - if (retryCount > 0) { - try { - await supertest.delete(LIST_INDEX).set('kbn-xsrf', 'true').send(); - } catch (err) { - // eslint-disable-next-line no-console - console.log(`Failure trying to deleteListsIndex, retries left are: ${retryCount - 1}`, err); - await deleteListsIndex(supertest, retryCount - 1); - } - } else { - // eslint-disable-next-line no-console - console.log('Could not deleteListsIndex, no retries are left'); - } + return countDownTest(async () => { + await supertest.delete(LIST_INDEX).set('kbn-xsrf', 'true').send(); + return true; + }, 'deleteListsIndex'); +}; + +/** + * Creates the exception lists and lists items index for use inside of beforeEach blocks of tests + * This will retry 20 times before giving up and hopefully still not interfere with other tests + * @param supertest The supertest client library + */ +export const createExceptionListsIndex = async ( + supertest: SuperTest +): Promise => { + return countDownTest(async () => { + await supertest.post(LIST_INDEX).set('kbn-xsrf', 'true').send(); + return true; + }, 'createListsIndex'); }; /** @@ -159,26 +153,14 @@ export const binaryToString = (res: any, callback: any): void => { * This will retry 20 times before giving up and hopefully still not interfere with other tests * @param es The ElasticSearch handle */ -export const deleteAllExceptions = async (es: Client, retryCount = 20): Promise => { - if (retryCount > 0) { - try { - await es.deleteByQuery({ - index: '.kibana', - q: 'type:exception-list or type:exception-list-agnostic', - wait_for_completion: true, - refresh: true, - body: {}, - }); - } catch (err) { - // eslint-disable-next-line no-console - console.log( - `Failure trying to deleteAllExceptions, retries left are: ${retryCount - 1}`, - err - ); - await deleteAllExceptions(es, retryCount - 1); - } - } else { - // eslint-disable-next-line no-console - console.log('Could not deleteAllExceptions, no retries are left'); - } +export const deleteAllExceptions = async (es: Client): Promise => { + return countDownES(async () => { + return es.deleteByQuery({ + index: '.kibana', + q: 'type:exception-list or type:exception-list-agnostic', + wait_for_completion: true, + refresh: true, + body: {}, + }); + }, 'deleteAllExceptions'); }; diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts index 5b5949821580f..77f32063d41be 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts @@ -103,7 +103,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('finds page title', async () => { const title = await testSubjects.getVisibleText('header-page-title'); - expect(title).to.equal('Endpoints BETA'); + expect(title).to.equal('Endpoints'); }); it('displays table data', async () => { @@ -162,7 +162,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe.skip("has a url with an endpoint host's id", () => { before(async () => { await pageObjects.endpoint.navigateToEndpointList( - 'selected_host=fc0ff548-feba-41b6-8367-65e8790d0eaf' + 'selected_endpoint=3838df35-a095-4af4-8fce-0b6d78793f2e' ); }); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts index 49a7a2155a700..70958d7ca7631 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts @@ -30,7 +30,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('displays page title', async () => { const policyTitle = await testSubjects.getVisibleText('header-page-title'); - expect(policyTitle).to.equal('Policies BETA'); + expect(policyTitle).to.equal('Policies'); }); it('shows header create policy button', async () => { const createButtonTitle = await testSubjects.getVisibleText('headerCreateNewPolicyButton'); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts index 78ef1bc894e0b..3a0f0b91bddb3 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts @@ -20,7 +20,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should show page title', async () => { expect(await testSubjects.getVisibleText('header-page-title')).to.equal( - 'Trusted Applications BETA' + 'Trusted Applications' ); }); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/package.ts b/x-pack/test/security_solution_endpoint_api_int/apis/package.ts index afbf0dcd7bd13..8dc78ed71d0b6 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/package.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/package.ts @@ -74,7 +74,9 @@ export default function ({ getService }: FtrProviderContext) { }); it('handles events without the `network.protocol` field being defined', async () => { - const eventWithoutNetworkObject = generator.generateEvent(); + const eventWithoutNetworkObject = generator.generateEvent({ + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(networkIndex), + }); // ensure that `network.protocol` does not exist in the event to test that the pipeline handles those type of events delete eventWithoutNetworkObject.network; @@ -137,8 +139,10 @@ export default function ({ getService }: FtrProviderContext) { let genData: InsertedEvents; before(async () => { - event = generator.generateEvent(); - genData = await resolver.insertEvents([event]); + event = generator.generateEvent({ + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), + }); + genData = await resolver.insertEvents([event], processEventsIndex); }); after(async () => { @@ -158,20 +162,29 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { // 46.239.193.5 should be in Iceland // 8.8.8.8 should be in the US - const eventWithBothIPs = generator.generateEvent({ + const eventWithBothIPsNetwork = generator.generateEvent({ extensions: { source: { ip: '8.8.8.8' }, destination: { ip: '46.239.193.5' } }, + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(networkIndex), }); - const eventWithSourceOnly = generator.generateEvent({ + const eventWithSourceOnlyNetwork = generator.generateEvent({ extensions: { source: { ip: '8.8.8.8' } }, + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(networkIndex), }); networkIndexData = await resolver.insertEvents( - [eventWithBothIPs, eventWithSourceOnly], + [eventWithBothIPsNetwork, eventWithSourceOnlyNetwork], networkIndex ); - processIndexData = await resolver.insertEvents([eventWithBothIPs], processEventsIndex); + const eventWithBothIPsProcess = generator.generateEvent({ + extensions: { source: { ip: '8.8.8.8' }, destination: { ip: '46.239.193.5' } }, + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), + }); + processIndexData = await resolver.insertEvents( + [eventWithBothIPsProcess], + processEventsIndex + ); }); after(async () => { diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/policy.ts b/x-pack/test/security_solution_endpoint_api_int/apis/policy.ts index 66bcc0e759916..1a4e69267f9c6 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/policy.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/policy.ts @@ -19,20 +19,20 @@ export default function ({ getService }: FtrProviderContext) { // to do it manually after(async () => await deletePolicyStream(getService)); - it('should return one policy response for host', async () => { - const expectedHostId = '4f3b9858-a96d-49d8-a326-230d7763d767'; + it('should return one policy response for an id', async () => { + const expectedAgentId = 'a10ac658-a3bc-4ac6-944a-68d9bd1c5a5e'; const { body } = await supertest - .get(`/api/endpoint/policy_response?hostId=${expectedHostId}`) + .get(`/api/endpoint/policy_response?agentId=${expectedAgentId}`) .send() .expect(200); - expect(body.policy_response.host.id).to.eql(expectedHostId); + expect(body.policy_response.agent.id).to.eql(expectedAgentId); expect(body.policy_response.Endpoint.policy).to.not.be(undefined); }); it('should return not found if host has no policy response', async () => { const { body } = await supertest - .get(`/api/endpoint/policy_response?hostId=bad_host_id`) + .get(`/api/endpoint/policy_response?agentId=bad_id`) .send() .expect(404); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts index 49e24ff67fa77..b56dea94ab569 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts @@ -22,7 +22,7 @@ import { Event, EndpointDocGenerator, } from '../../../../plugins/security_solution/common/endpoint/generate_data'; -import { InsertedEvents } from '../../services/resolver'; +import { InsertedEvents, processEventsIndex } from '../../services/resolver'; import { createAncestryArray } from './common'; export default function resolverAPIIntegrationTests({ getService }: FtrProviderContext) { @@ -42,25 +42,33 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC before(async () => { // Construct the following tree: // Origin -> infoEvent -> startEvent -> execEvent - origin = generator.generateEvent(); + origin = generator.generateEvent({ + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), + }); infoEvent = generator.generateEvent({ parentEntityID: entityIDSafeVersion(origin), ancestry: createAncestryArray([origin]), eventType: ['info'], + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); startEvent = generator.generateEvent({ parentEntityID: entityIDSafeVersion(infoEvent), ancestry: createAncestryArray([infoEvent, origin]), eventType: ['start'], + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); execEvent = generator.generateEvent({ parentEntityID: entityIDSafeVersion(startEvent), ancestry: createAncestryArray([startEvent, infoEvent]), eventType: ['change'], + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); - genData = await resolver.insertEvents([origin, infoEvent, startEvent, execEvent]); + genData = await resolver.insertEvents( + [origin, infoEvent, startEvent, execEvent], + processEventsIndex + ); }); after(async () => { @@ -88,11 +96,14 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC before(async () => { // Construct the following tree: // Origin -> (infoEvent, startEvent, execEvent are all for the same node) - origin = generator.generateEvent(); + origin = generator.generateEvent({ + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), + }); startEvent = generator.generateEvent({ parentEntityID: entityIDSafeVersion(origin), ancestry: createAncestryArray([origin]), eventType: ['start'], + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); infoEvent = generator.generateEvent({ @@ -100,6 +111,7 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC ancestry: createAncestryArray([origin]), entityID: entityIDSafeVersion(startEvent), eventType: ['info'], + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); execEvent = generator.generateEvent({ @@ -107,8 +119,12 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC ancestry: createAncestryArray([origin]), eventType: ['change'], entityID: entityIDSafeVersion(startEvent), + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); - genData = await resolver.insertEvents([origin, infoEvent, startEvent, execEvent]); + genData = await resolver.insertEvents( + [origin, infoEvent, startEvent, execEvent], + processEventsIndex + ); }); after(async () => { @@ -141,11 +157,14 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC before(async () => { // Construct the following tree: // Origin -> (infoEvent, startEvent, execEvent are all for the same node) - origin = generator.generateEvent(); + origin = generator.generateEvent({ + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), + }); startEvent = generator.generateEvent({ parentEntityID: entityIDSafeVersion(origin), ancestry: createAncestryArray([origin]), eventType: ['start'], + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); infoEvent = generator.generateEvent({ @@ -154,6 +173,7 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC ancestry: createAncestryArray([origin]), entityID: entityIDSafeVersion(startEvent), eventType: ['info'], + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); execEvent = generator.generateEvent({ @@ -162,8 +182,12 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC ancestry: createAncestryArray([origin]), eventType: ['change'], entityID: entityIDSafeVersion(startEvent), + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); - genData = await resolver.insertEvents([origin, infoEvent, startEvent, execEvent]); + genData = await resolver.insertEvents( + [origin, infoEvent, startEvent, execEvent], + processEventsIndex + ); }); after(async () => { diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts index e6d5e8fccd00d..f9492e6291684 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts @@ -15,7 +15,7 @@ import { EndpointDocGenerator, Event, } from '../../../../plugins/security_solution/common/endpoint/generate_data'; -import { InsertedEvents } from '../../services/resolver'; +import { InsertedEvents, processEventsIndex } from '../../services/resolver'; import { createAncestryArray } from './common'; export default function ({ getService }: FtrProviderContext) { @@ -34,9 +34,12 @@ export default function ({ getService }: FtrProviderContext) { let origin: Event; let genData: InsertedEvents; before(async () => { - origin = generator.generateEvent({ parentEntityID: 'a' }); + origin = generator.generateEvent({ + parentEntityID: 'a', + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), + }); setEntityIDEmptyString(origin); - genData = await resolver.insertEvents([origin]); + genData = await resolver.insertEvents([origin], processEventsIndex); }); after(async () => { @@ -63,10 +66,14 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { // construct a tree with an origin and two direct children. One child will not have an entity_id. That child // should not be returned by the backend. - origin = generator.generateEvent({ entityID: 'a' }); + origin = generator.generateEvent({ + entityID: 'a', + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), + }); childNoEntityID = generator.generateEvent({ parentEntityID: entityIDSafeVersion(origin), ancestry: createAncestryArray([origin]), + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); // force it to be empty setEntityIDEmptyString(childNoEntityID); @@ -75,9 +82,10 @@ export default function ({ getService }: FtrProviderContext) { entityID: 'b', parentEntityID: entityIDSafeVersion(origin), ancestry: createAncestryArray([origin]), + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); events = [origin, childNoEntityID, childWithEntityID]; - genData = await resolver.insertEvents(events); + genData = await resolver.insertEvents(events, processEventsIndex); }); after(async () => { @@ -106,17 +114,20 @@ export default function ({ getService }: FtrProviderContext) { // entity_ids in the ancestry array. This is to make sure that the backend will not query for that event. ancestor2 = generator.generateEvent({ entityID: '2', + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); ancestor1 = generator.generateEvent({ entityID: '1', parentEntityID: entityIDSafeVersion(ancestor2), ancestry: createAncestryArray([ancestor2]), + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); // we'll insert an event that doesn't have an entity id so if the backend does search for it, it should be // returned and our test should fail ancestorNoEntityID = generator.generateEvent({ ancestry: createAncestryArray([ancestor2]), + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); setEntityIDEmptyString(ancestorNoEntityID); @@ -124,10 +135,11 @@ export default function ({ getService }: FtrProviderContext) { entityID: 'a', parentEntityID: entityIDSafeVersion(ancestor1), ancestry: ['', ...createAncestryArray([ancestor2])], + eventsDataStream: EndpointDocGenerator.createDataStreamFromIndex(processEventsIndex), }); events = [origin, ancestor1, ancestor2, ancestorNoEntityID]; - genData = await resolver.insertEvents(events); + genData = await resolver.insertEvents(events, processEventsIndex); }); after(async () => { diff --git a/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts b/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts index f233bc1d11d7c..c8e13f6bada7a 100644 --- a/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts +++ b/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts @@ -13,3 +13,24 @@ export function getUrlPrefix(spaceId?: string) { export function getIdPrefix(spaceId?: string) { return spaceId === DEFAULT_SPACE_ID ? '' : `${spaceId}-`; } + +export function getTestScenariosForSpace(spaceId: string) { + const explicitScenario = { + spaceId, + urlPrefix: `/s/${spaceId}`, + scenario: `when referencing the ${spaceId} space explicitly in the URL`, + }; + + if (spaceId === DEFAULT_SPACE_ID) { + return [ + { + spaceId, + urlPrefix: ``, + scenario: 'when referencing the default space implicitly', + }, + explicitScenario, + ]; + } + + return [explicitScenario]; +} diff --git a/x-pack/test/spaces_api_integration/common/suites/create.ts b/x-pack/test/spaces_api_integration/common/suites/create.ts index 4de638c784147..7c2120ce6eeaf 100644 --- a/x-pack/test/spaces_api_integration/common/suites/create.ts +++ b/x-pack/test/spaces_api_integration/common/suites/create.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { getUrlPrefix } from '../lib/space_test_utils'; +import { getTestScenariosForSpace } from '../lib/space_test_utils'; import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; interface CreateTest { @@ -67,56 +67,58 @@ export function createTestSuiteFactory(esArchiver: any, supertest: SuperTest { describeFn(description, () => { - before(() => esArchiver.load('saved_objects/spaces')); - after(() => esArchiver.unload('saved_objects/spaces')); + beforeEach(() => esArchiver.load('saved_objects/spaces')); + afterEach(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.newSpace.statusCode}`, async () => { - return supertest - .post(`${getUrlPrefix(spaceId)}/api/spaces/space`) - .auth(user.username, user.password) - .send({ - name: 'marketing', - id: 'marketing', - description: 'a description', - color: '#5c5959', - disabledFeatures: [], - }) - .expect(tests.newSpace.statusCode) - .then(tests.newSpace.response); - }); - - describe('when it already exists', () => { - it(`should return ${tests.alreadyExists.statusCode}`, async () => { + getTestScenariosForSpace(spaceId).forEach(({ urlPrefix, scenario }) => { + it(`should return ${tests.newSpace.statusCode} ${scenario}`, async () => { return supertest - .post(`${getUrlPrefix(spaceId)}/api/spaces/space`) + .post(`${urlPrefix}/api/spaces/space`) .auth(user.username, user.password) .send({ - name: 'space_1', - id: 'space_1', - color: '#ffffff', + name: 'marketing', + id: 'marketing', description: 'a description', + color: '#5c5959', disabledFeatures: [], }) - .expect(tests.alreadyExists.statusCode) - .then(tests.alreadyExists.response); + .expect(tests.newSpace.statusCode) + .then(tests.newSpace.response); }); - }); - describe('when _reserved is specified', () => { - it(`should return ${tests.reservedSpecified.statusCode} and ignore _reserved`, async () => { - return supertest - .post(`${getUrlPrefix(spaceId)}/api/spaces/space`) - .auth(user.username, user.password) - .send({ - name: 'reserved space', - id: 'reserved', - description: 'a description', - color: '#5c5959', - _reserved: true, - disabledFeatures: [], - }) - .expect(tests.reservedSpecified.statusCode) - .then(tests.reservedSpecified.response); + describe('when it already exists', () => { + it(`should return ${tests.alreadyExists.statusCode} ${scenario}`, async () => { + return supertest + .post(`${urlPrefix}/api/spaces/space`) + .auth(user.username, user.password) + .send({ + name: 'space_1', + id: 'space_1', + color: '#ffffff', + description: 'a description', + disabledFeatures: [], + }) + .expect(tests.alreadyExists.statusCode) + .then(tests.alreadyExists.response); + }); + }); + + describe('when _reserved is specified', () => { + it(`should return ${tests.reservedSpecified.statusCode} and ignore _reserved ${scenario}`, async () => { + return supertest + .post(`${urlPrefix}/api/spaces/space`) + .auth(user.username, user.password) + .send({ + name: 'reserved space', + id: 'reserved', + description: 'a description', + color: '#5c5959', + _reserved: true, + disabledFeatures: [], + }) + .expect(tests.reservedSpecified.statusCode) + .then(tests.reservedSpecified.response); + }); }); }); }); diff --git a/x-pack/test/spaces_api_integration/common/suites/delete.ts b/x-pack/test/spaces_api_integration/common/suites/delete.ts index 69b5697d8a9a8..2a6b2c0e69d1d 100644 --- a/x-pack/test/spaces_api_integration/common/suites/delete.ts +++ b/x-pack/test/spaces_api_integration/common/suites/delete.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { getUrlPrefix } from '../lib/space_test_utils'; +import { getTestScenariosForSpace } from '../lib/space_test_utils'; import { MULTI_NAMESPACE_SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; @@ -176,7 +176,7 @@ export function deleteTestSuiteFactory(es: any, esArchiver: any, supertest: Supe { user = {}, spaceId, tests }: DeleteTestDefinition ) => { describeFn(description, () => { - before(async () => { + beforeEach(async () => { await esArchiver.load('saved_objects/spaces'); // since we want to verify that we only delete the right things @@ -189,33 +189,35 @@ export function deleteTestSuiteFactory(es: any, esArchiver: any, supertest: Supe .auth(user.username, user.password) .expect(200); }); - after(() => esArchiver.unload('saved_objects/spaces')); + afterEach(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.exists.statusCode}`, async () => { - return supertest - .delete(`${getUrlPrefix(spaceId)}/api/spaces/space/space_2`) - .auth(user.username, user.password) - .expect(tests.exists.statusCode) - .then(tests.exists.response); - }); - - describe(`when the space is reserved`, () => { - it(`should return ${tests.reservedSpace.statusCode}`, async () => { + getTestScenariosForSpace(spaceId).forEach(({ urlPrefix, scenario }) => { + it(`should return ${tests.exists.statusCode} ${scenario}`, async () => { return supertest - .delete(`${getUrlPrefix(spaceId)}/api/spaces/space/default`) + .delete(`${urlPrefix}/api/spaces/space/space_2`) .auth(user.username, user.password) - .expect(tests.reservedSpace.statusCode) - .then(tests.reservedSpace.response); + .expect(tests.exists.statusCode) + .then(tests.exists.response); }); - }); - describe(`when the space doesn't exist`, () => { - it(`should return ${tests.doesntExist.statusCode}`, async () => { - return supertest - .delete(`${getUrlPrefix(spaceId)}/api/spaces/space/space_3`) - .auth(user.username, user.password) - .expect(tests.doesntExist.statusCode) - .then(tests.doesntExist.response); + describe(`when the space is reserved`, () => { + it(`should return ${tests.reservedSpace.statusCode} ${scenario}`, async () => { + return supertest + .delete(`${urlPrefix}/api/spaces/space/default`) + .auth(user.username, user.password) + .expect(tests.reservedSpace.statusCode) + .then(tests.reservedSpace.response); + }); + }); + + describe(`when the space doesn't exist`, () => { + it(`should return ${tests.doesntExist.statusCode} ${scenario}`, async () => { + return supertest + .delete(`${urlPrefix}/api/spaces/space/space_3`) + .auth(user.username, user.password) + .expect(tests.doesntExist.statusCode) + .then(tests.doesntExist.response); + }); }); }); }); diff --git a/x-pack/test/spaces_api_integration/common/suites/get.ts b/x-pack/test/spaces_api_integration/common/suites/get.ts index bd0e2a18d5c50..6bf5b0f180237 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; import { SuperAgent } from 'superagent'; -import { getUrlPrefix } from '../lib/space_test_utils'; +import { getTestScenariosForSpace } from '../lib/space_test_utils'; import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; interface GetTest { @@ -80,12 +80,14 @@ export function getTestSuiteFactory(esArchiver: any, supertest: SuperAgent) before(() => esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.default.statusCode}`, async () => { - return supertest - .get(`${getUrlPrefix(currentSpaceId)}/api/spaces/space/${spaceId}`) - .auth(user.username, user.password) - .expect(tests.default.statusCode) - .then(tests.default.response); + getTestScenariosForSpace(currentSpaceId).forEach(({ urlPrefix, scenario }) => { + it(`should return ${tests.default.statusCode} ${scenario}`, async () => { + return supertest + .get(`${urlPrefix}/api/spaces/space/${spaceId}`) + .auth(user.username, user.password) + .expect(tests.default.statusCode) + .then(tests.default.response); + }); }); }); }; diff --git a/x-pack/test/spaces_api_integration/common/suites/get_all.ts b/x-pack/test/spaces_api_integration/common/suites/get_all.ts index d41d73bba90bc..fce48e4938baa 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get_all.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get_all.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; -import { getUrlPrefix } from '../lib/space_test_utils'; +import { getTestScenariosForSpace } from '../lib/space_test_utils'; import { DescribeFn, TestDefinitionAuthentication } from '../lib/types'; interface GetAllTest { @@ -71,33 +71,35 @@ export function getAllTestSuiteFactory(esArchiver: any, supertest: SuperTest esArchiver.load('saved_objects/spaces')); after(() => esArchiver.unload('saved_objects/spaces')); - it(`should return ${tests.exists.statusCode}`, async () => { - return supertest - .get(`${getUrlPrefix(spaceId)}/api/spaces/space`) - .auth(user.username, user.password) - .expect(tests.exists.statusCode) - .then(tests.exists.response); - }); - - describe('copySavedObjects purpose', () => { - it(`should return ${tests.copySavedObjectsPurpose.statusCode}`, async () => { + getTestScenariosForSpace(spaceId).forEach(({ scenario, urlPrefix }) => { + it(`should return ${tests.exists.statusCode} ${scenario}`, async () => { return supertest - .get(`${getUrlPrefix(spaceId)}/api/spaces/space`) - .query({ purpose: 'copySavedObjectsIntoSpace' }) + .get(`${urlPrefix}/api/spaces/space`) .auth(user.username, user.password) - .expect(tests.copySavedObjectsPurpose.statusCode) - .then(tests.copySavedObjectsPurpose.response); + .expect(tests.exists.statusCode) + .then(tests.exists.response); }); - }); - describe('copySavedObjects purpose', () => { - it(`should return ${tests.shareSavedObjectsPurpose.statusCode}`, async () => { - return supertest - .get(`${getUrlPrefix(spaceId)}/api/spaces/space`) - .query({ purpose: 'shareSavedObjectsIntoSpace' }) - .auth(user.username, user.password) - .expect(tests.copySavedObjectsPurpose.statusCode) - .then(tests.copySavedObjectsPurpose.response); + describe('copySavedObjects purpose', () => { + it(`should return ${tests.copySavedObjectsPurpose.statusCode} ${scenario}`, async () => { + return supertest + .get(`${urlPrefix}/api/spaces/space`) + .query({ purpose: 'copySavedObjectsIntoSpace' }) + .auth(user.username, user.password) + .expect(tests.copySavedObjectsPurpose.statusCode) + .then(tests.copySavedObjectsPurpose.response); + }); + }); + + describe('copySavedObjects purpose', () => { + it(`should return ${tests.shareSavedObjectsPurpose.statusCode} ${scenario}`, async () => { + return supertest + .get(`${urlPrefix}/api/spaces/space`) + .query({ purpose: 'shareSavedObjectsIntoSpace' }) + .auth(user.username, user.password) + .expect(tests.copySavedObjectsPurpose.statusCode) + .then(tests.copySavedObjectsPurpose.response); + }); }); }); }); diff --git a/yarn.lock b/yarn.lock index 200896a9ce1a6..4ed8f513da7da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2759,17 +2759,39 @@ "@types/node" ">=8.9.0" axios "^0.18.0" -"@storybook/addon-actions@6.0.16", "@storybook/addon-actions@^6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-6.0.16.tgz#869c90291fdfec4a0644e8415f5004cc57e59145" - integrity sha512-kyPGMP2frdhUgJAm6ChqvndaUawwQE9Vx7pN1pk/Q4qnyVlWCneZVojQf0iAgL45q0az0XI1tOPr4ooroaniYg== - dependencies: - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/client-api" "6.0.16" - "@storybook/components" "6.0.16" - "@storybook/core-events" "6.0.16" - "@storybook/theming" "6.0.16" +"@storybook/addon-a11y@^6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-a11y/-/addon-a11y-6.0.26.tgz#b71761d9b8f8b340894eb9826d51ce319ce65116" + integrity sha512-sx1Ethl9W3Kfns0qB1v0CoAymKTC+odB+rCjUKM1h/ILS/n8ZzwkzAj0L7DU/6wA0nZwZDQ+1wL2ZN7r+vxr8A== + dependencies: + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/channels" "6.0.26" + "@storybook/client-api" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/core-events" "6.0.26" + "@storybook/theming" "6.0.26" + axe-core "^3.5.2" + core-js "^3.0.1" + global "^4.3.2" + lodash "^4.17.15" + react-sizeme "^2.5.2" + regenerator-runtime "^0.13.3" + ts-dedent "^1.1.1" + util-deprecate "^1.0.2" + +"@storybook/addon-actions@6.0.26", "@storybook/addon-actions@^6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-6.0.26.tgz#d0de9e4d78a8f8f5bf8730c04d0b6d1741c29273" + integrity sha512-9tWbAqSwzWWVz8zwAndZFusZYjIcRYgZUC0LqC8QlH79DgF3ASjw9y97+w1VTTAzdb6LYnsMuSpX6+8m5hrR4g== + dependencies: + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/client-api" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/core-events" "6.0.26" + "@storybook/theming" "6.0.26" core-js "^3.0.1" fast-deep-equal "^3.1.1" global "^4.3.2" @@ -2783,40 +2805,40 @@ util-deprecate "^1.0.2" uuid "^8.0.0" -"@storybook/addon-backgrounds@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-backgrounds/-/addon-backgrounds-6.0.16.tgz#cbf909992a86dbbdfea172d3950300e8c2a7de01" - integrity sha512-0sH7hlZh4bHt6zV6QyG3ryNGJsxD42iXVwWdwAShzfWJKGfLy5XwdvHUKkMEBbY9bOPeoI9oMli2RAfsD6juLQ== - dependencies: - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/components" "6.0.16" - "@storybook/core-events" "6.0.16" - "@storybook/theming" "6.0.16" +"@storybook/addon-backgrounds@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-backgrounds/-/addon-backgrounds-6.0.26.tgz#97cea86cc4fe88b6c0ad8addb2d01712e535aa10" + integrity sha512-Y9t1s4N2PgTxLhO85w+WFnnclZNRdxGpsoiLDPkb63HajZvUa5/ogmIOrfFl5DX2zCpkgNLlskmDcEP6n835cA== + dependencies: + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/core-events" "6.0.26" + "@storybook/theming" "6.0.26" core-js "^3.0.1" memoizerific "^1.11.3" react "^16.8.3" regenerator-runtime "^0.13.3" -"@storybook/addon-controls@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-controls/-/addon-controls-6.0.16.tgz#c7fc765a01cc3a0de397f8b55bfeda3f328e5495" - integrity sha512-RgBOply9o3PYoWI7TNKef2AQixw7l620pT1fCJbXykp/lu17eqKaIa5KYHRE9vEajun5RuEQxGnSzQOV3OZAsA== - dependencies: - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/client-api" "6.0.16" - "@storybook/components" "6.0.16" - "@storybook/node-logger" "6.0.16" - "@storybook/theming" "6.0.16" +"@storybook/addon-controls@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-controls/-/addon-controls-6.0.26.tgz#4cc4c30ee7bf89ab873158ead4d25d6f7e07ffba" + integrity sha512-K3Oik9ClpShv8Qc6JeNwtmd4yJJcnO2nyaAYYFiyNt+Vsg7zMaDtE2wfvViThNKjX7nUXIeh+OscseIkdWgLuA== + dependencies: + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/client-api" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/node-logger" "6.0.26" + "@storybook/theming" "6.0.26" core-js "^3.0.1" ts-dedent "^1.1.1" -"@storybook/addon-docs@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-6.0.16.tgz#b24983a63c6c9469a418bb1478606626aff42dff" - integrity sha512-7gM/0lQ3mSybpOpQbgR8fjAU+u3zgAWyOM1a+LR7zVn5lNjgBhZD2pfHuwViTeAGG/IIpvmOsd57BKlFJw5TPA== +"@storybook/addon-docs@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-6.0.26.tgz#bd7fc1fcdc47bb7992fa8d3254367e8c3bba373d" + integrity sha512-3t8AOPkp8ZW74h7FnzxF3wAeb1wRyYjMmgJZxqzgi/x7K0i1inbCq8MuJnytuTcZ7+EK4HR6Ih7o9tJuAtIBLw== dependencies: "@babel/generator" "^7.9.6" "@babel/parser" "^7.9.6" @@ -2826,18 +2848,18 @@ "@mdx-js/loader" "^1.5.1" "@mdx-js/mdx" "^1.5.1" "@mdx-js/react" "^1.5.1" - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/client-api" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/components" "6.0.16" - "@storybook/core" "6.0.16" - "@storybook/core-events" "6.0.16" + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/client-api" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/core" "6.0.26" + "@storybook/core-events" "6.0.26" "@storybook/csf" "0.0.1" - "@storybook/node-logger" "6.0.16" - "@storybook/postinstall" "6.0.16" - "@storybook/source-loader" "6.0.16" - "@storybook/theming" "6.0.16" + "@storybook/node-logger" "6.0.26" + "@storybook/postinstall" "6.0.26" + "@storybook/source-loader" "6.0.26" + "@storybook/theming" "6.0.26" acorn "^7.1.0" acorn-jsx "^5.1.0" acorn-walk "^7.0.0" @@ -2857,36 +2879,36 @@ ts-dedent "^1.1.1" util-deprecate "^1.0.2" -"@storybook/addon-essentials@^6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-essentials/-/addon-essentials-6.0.16.tgz#031b05f6a9947fd93a86f28767b1c354e8ea4237" - integrity sha512-tHH2B4cGYihaPytzIlcFlc/jDSu1PUMgaQM4uzIDOn6SCYZJMp5vygK97zF7hf41x/TXv+8i9ZMN5iUJ7l1+fw== - dependencies: - "@storybook/addon-actions" "6.0.16" - "@storybook/addon-backgrounds" "6.0.16" - "@storybook/addon-controls" "6.0.16" - "@storybook/addon-docs" "6.0.16" - "@storybook/addon-toolbars" "6.0.16" - "@storybook/addon-viewport" "6.0.16" - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/node-logger" "6.0.16" +"@storybook/addon-essentials@^6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-essentials/-/addon-essentials-6.0.26.tgz#1962f4fd19a9d9a1d1fad152bbfc3bba90f45216" + integrity sha512-AsKcPrVFksYNWq07jKXX/GRcdTa6Uo3sTEwuV5ZazWltlbOIcI0YdQs6mCFaCElB5Dqg1jqyxZ3vcd+VHiRSkA== + dependencies: + "@storybook/addon-actions" "6.0.26" + "@storybook/addon-backgrounds" "6.0.26" + "@storybook/addon-controls" "6.0.26" + "@storybook/addon-docs" "6.0.26" + "@storybook/addon-toolbars" "6.0.26" + "@storybook/addon-viewport" "6.0.26" + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/node-logger" "6.0.26" core-js "^3.0.1" regenerator-runtime "^0.13.3" ts-dedent "^1.1.1" -"@storybook/addon-knobs@^6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-knobs/-/addon-knobs-6.0.16.tgz#ef7b9a67c5f3f75579af1d3c2c1f36205f77f505" - integrity sha512-//4Fq70M7LLOghM6+eugL53QHVmlbBm5240u+Aq2nWQLUtaszrPW6/7Vj0XRwLyp/DQtEHetTE/fFfCLoGK+dw== - dependencies: - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/channels" "6.0.16" - "@storybook/client-api" "6.0.16" - "@storybook/components" "6.0.16" - "@storybook/core-events" "6.0.16" - "@storybook/theming" "6.0.16" +"@storybook/addon-knobs@^6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-knobs/-/addon-knobs-6.0.26.tgz#c574a817c8d791aced89a272eb0c6baaee9a0bdf" + integrity sha512-a25hOepctnsqX1nym80HLKrn8fvXFqsbcL3ZkAZWAIXZhf+zPYTJFrw25FxvbyhktmjQv6l89jteAfGfC0g8DA== + dependencies: + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/channels" "6.0.26" + "@storybook/client-api" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/core-events" "6.0.26" + "@storybook/theming" "6.0.26" copy-to-clipboard "^3.0.8" core-js "^3.0.1" escape-html "^1.0.3" @@ -2900,15 +2922,15 @@ react-select "^3.0.8" regenerator-runtime "^0.13.3" -"@storybook/addon-storyshots@^6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-storyshots/-/addon-storyshots-6.0.16.tgz#e912273966d4c7cba1a9053d6a76e8856e3b834f" - integrity sha512-wQhM6pnjUCLTr/6BMXTptGeqiMPnnTrvLeaRwG1cDChGK/qs3YqTsa2QqLXQ17IvNUDTHLUNQlYk5af+HrCGhg== +"@storybook/addon-storyshots@^6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-storyshots/-/addon-storyshots-6.0.26.tgz#529a557b4a8e4558da22a8ce847b88f9fb3ab5fa" + integrity sha512-XLt7aqjp3lH9ye5zfgbcZIDe8B9riu9shOsJfhZ1DpzfXxb8NVgAcvsXyMW/7dJZ/paAadXAeZZtWnOBuqNnmw== dependencies: "@jest/transform" "^26.0.0" - "@storybook/addons" "6.0.16" - "@storybook/client-api" "6.0.16" - "@storybook/core" "6.0.16" + "@storybook/addons" "6.0.26" + "@storybook/client-api" "6.0.26" + "@storybook/core" "6.0.26" "@types/glob" "^7.1.1" "@types/jest" "^25.1.1" "@types/jest-specific-snapshot" "^0.5.3" @@ -2922,62 +2944,62 @@ regenerator-runtime "^0.13.3" ts-dedent "^1.1.1" -"@storybook/addon-toolbars@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-toolbars/-/addon-toolbars-6.0.16.tgz#704a5d506b8d952eca6e5dca96c00b22aedf495f" - integrity sha512-6ulvPqe38NJRbQp0zajeNsDJQKZzGqbCMsSw3gtkFOMt8D/V625MF8YY/Y9UZ+xHWor17GUgE1k9hljdyZe1Nw== +"@storybook/addon-toolbars@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-toolbars/-/addon-toolbars-6.0.26.tgz#650a1793caac6616f4481116f4dfb79f2d3c336b" + integrity sha512-f9OI7ix0lQWg4eEHheWYB3dz7kYO6qCGkzp+oIQkPpjnYmY8ZghyRM+ui6vfq+G8BwxrAKGR0CB8ttNxVsd/9A== dependencies: - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/client-api" "6.0.16" - "@storybook/components" "6.0.16" + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/client-api" "6.0.26" + "@storybook/components" "6.0.26" core-js "^3.0.1" -"@storybook/addon-viewport@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addon-viewport/-/addon-viewport-6.0.16.tgz#574cc0a3f991ce405ba4a3540132fb05edf488f6" - integrity sha512-3vk6lBZrKJrK9rwxglLT1p579WkLvoJxgW5ddpvSsu31NPAKfDufkDqOZOQGyMmcgIFzZJEc9eKjoTcLiHxppw== - dependencies: - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/components" "6.0.16" - "@storybook/core-events" "6.0.16" - "@storybook/theming" "6.0.16" +"@storybook/addon-viewport@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addon-viewport/-/addon-viewport-6.0.26.tgz#c913dadcb55b31d2df21a580e932b85b1a200a8b" + integrity sha512-LdVW61iZhUf2npNk3qPH9DTunVMhKcyiFq2CRlgxcA5FwjdkAbcPiYMc18rfyTvp/Zd2idartvwYalBYpJhAhw== + dependencies: + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/core-events" "6.0.26" + "@storybook/theming" "6.0.26" core-js "^3.0.1" global "^4.3.2" memoizerific "^1.11.3" prop-types "^15.7.2" regenerator-runtime "^0.13.3" -"@storybook/addons@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.0.16.tgz#a20a219bd5b1474ad02b92e79a74652898a684d9" - integrity sha512-jGMaOJYTM2yZeX1tI6whEn+4xpI1aAybZBrc+OD21CcGoQrbF/jplZMq7xKI0Y6vOMguuTGulpUNCezD3LbBjA== - dependencies: - "@storybook/api" "6.0.16" - "@storybook/channels" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/core-events" "6.0.16" - "@storybook/router" "6.0.16" - "@storybook/theming" "6.0.16" +"@storybook/addons@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.0.26.tgz#343cbea3eee2d39413b80bc2d66535a7f61488fc" + integrity sha512-OhAApFKgsj9an7FLYfHI4cJQuZ4Zm6yoGOpaxhOvKQMw7dXUPsLvbCyw/6dZOLvaFhjJjQiXtbxtZG+UjR8nvA== + dependencies: + "@storybook/api" "6.0.26" + "@storybook/channels" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/core-events" "6.0.26" + "@storybook/router" "6.0.26" + "@storybook/theming" "6.0.26" core-js "^3.0.1" global "^4.3.2" regenerator-runtime "^0.13.3" -"@storybook/api@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.0.16.tgz#56cdfc6f7a21d62d1a4ab06b4741c1560160d320" - integrity sha512-RTC4BKmH5i8bJUQejOHEtjebVKtOaHkmEagI2HQRalsokBc1GLAf84EGrO2TaZiRrItAPL5zZQgEnKUblsGJGw== +"@storybook/api@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.0.26.tgz#c45222c132eb8bc2e383536adfebbeb7a89867d0" + integrity sha512-aszDoz1c6T+eRtTUwWvySoyd3gRXmQxsingD084NnEp4VfFLA5H7VS/0sre0ZvU5GWh8d9COxY0DS2Ry/QSKvw== dependencies: "@reach/router" "^1.3.3" - "@storybook/channels" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/core-events" "6.0.16" + "@storybook/channels" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/core-events" "6.0.26" "@storybook/csf" "0.0.1" - "@storybook/router" "6.0.16" + "@storybook/router" "6.0.26" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.0.16" + "@storybook/theming" "6.0.26" "@types/reach__router" "^1.3.5" core-js "^3.0.1" fast-deep-equal "^3.1.1" @@ -2991,38 +3013,38 @@ ts-dedent "^1.1.1" util-deprecate "^1.0.2" -"@storybook/channel-postmessage@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-6.0.16.tgz#a617578c49543b0de9f53eb28daae2bd3c9e1754" - integrity sha512-66B4FH5R7k9i7LBhGsr/hYOxwE4UBM1JMPGV0rhAnFY8m91GiUWl4YWTRdbYIkeaZxf/0oT4sgPScqz44hnw6Q== +"@storybook/channel-postmessage@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-6.0.26.tgz#a98a0132d6bdf06741afac2607e9feabe34ab98b" + integrity sha512-FT6lC8M5JlNBxPT0rYfmF1yl9mBv04nfYs82TZpp1CzpLxf7wxdCBZ8SSRmvWIVBoNwGZPDhIk5+6JWyDEISBg== dependencies: - "@storybook/channels" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/core-events" "6.0.16" + "@storybook/channels" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/core-events" "6.0.26" core-js "^3.0.1" global "^4.3.2" qs "^6.6.0" telejson "^5.0.2" -"@storybook/channels@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.0.16.tgz#94e521b9eae535da80afb23feae593aa69bfe75d" - integrity sha512-TsI4GA7lKD4L2w6IjODMRfnEOkmvEp4eJDgf3MKm7+sMbxwi1y1d6yrW1UQbnmwoNJWk60ArMN2yqDBV+5MNJQ== +"@storybook/channels@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.0.26.tgz#3e8678b4b40085081257a39b9e85fab13a19943c" + integrity sha512-H0iUorayYqS+zfhbjd+cYRzAdRLGLWUeWFu2Aa+oJ4/zeAQNL+DafWboHc567RQ4Vb5KqE5QZoCFskWUUYqJYA== dependencies: core-js "^3.0.1" ts-dedent "^1.1.1" util-deprecate "^1.0.2" -"@storybook/client-api@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-6.0.16.tgz#4af47caccf92a31326ab77c5094dd4f90f888b91" - integrity sha512-fFsp53lt9W2QHSumqdfFRbh+DI9fvd7li0GDxqLeNESXaUVw48yg8lQiyRNK+j5Pl4VBS3AqytLugJ+0MGm2cA== +"@storybook/client-api@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-6.0.26.tgz#ac9334ba86834e5cb23fc4fb577de60bda66164d" + integrity sha512-Qd5wR5b5lio/EchuJMhAmmJAE1pfvnEyu+JnyFGwMZLV9mN9NSspz+YsqbSCCDZsYcP5ewvPEnumIWqmj/wagQ== dependencies: - "@storybook/addons" "6.0.16" - "@storybook/channel-postmessage" "6.0.16" - "@storybook/channels" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/core-events" "6.0.16" + "@storybook/addons" "6.0.26" + "@storybook/channel-postmessage" "6.0.26" + "@storybook/channels" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/core-events" "6.0.26" "@storybook/csf" "0.0.1" "@types/qs" "^6.9.0" "@types/webpack-env" "^1.15.2" @@ -3036,22 +3058,22 @@ ts-dedent "^1.1.1" util-deprecate "^1.0.2" -"@storybook/client-logger@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.0.16.tgz#6265d2b869a82be64538eaac39470e3845c9e069" - integrity sha512-xM61Aewxqoo8500UxV7iPpfqwikITojiCX3+w8ZiCJ2NizSaXkis95TEFAeHqyozfNym5CqG+6v2NWvGYV3ncQ== +"@storybook/client-logger@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.0.26.tgz#e3d28bd8dc02ec2c53a9d69773a68189590b746f" + integrity sha512-VNoL6/oehVhn3hZi9vrTNT+C/3oAZKV+smfZFnPtsCR/Fq7CKbmsBd0pGPL57f81RU8e8WygwrIlAGJTDSNIjw== dependencies: core-js "^3.0.1" global "^4.3.2" -"@storybook/components@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/components/-/components-6.0.16.tgz#d4c797f7897cefa11bbdb8dfd07bb3d4fa66b3e9" - integrity sha512-zpYGt3tWiN0yT7V0VhBl2T5Mr0COiNnTQUGCpA9Gl3pUBmAov2jCVf1sUxsIcBcMMZmDRcfo6NbJ/LqCFeUg+Q== +"@storybook/components@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/components/-/components-6.0.26.tgz#e1f6e16aae850a71c9ac7bdd1d44a068ec9cfdc1" + integrity sha512-8wigI1pDFJO1m1IQWPguOK+nOsaAVRWkVdu+2te/rDcIR9QNvMzzou0+Lhfp3zKSVT4E6mEoGB/TWXXF5Iq0sQ== dependencies: - "@storybook/client-logger" "6.0.16" + "@storybook/client-logger" "6.0.26" "@storybook/csf" "0.0.1" - "@storybook/theming" "6.0.16" + "@storybook/theming" "6.0.26" "@types/overlayscrollbars" "^1.9.0" "@types/react-color" "^3.0.1" "@types/react-syntax-highlighter" "11.0.4" @@ -3072,17 +3094,17 @@ react-textarea-autosize "^8.1.1" ts-dedent "^1.1.1" -"@storybook/core-events@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.0.16.tgz#3f8cd525c15fd80c9f327389851cce82a4b96850" - integrity sha512-ib+58N4OY8AOix2qcBH9ICRmVHUocpGaGRVlIo79WxJrpnB/HNQ8pEaniD+OAavDRq1B7uJqFlMkTXCC0GoFiQ== +"@storybook/core-events@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.0.26.tgz#61181c9a8610d26cc85d47f133a563879044ca2d" + integrity sha512-nWjS/+kMiw31OPgeJQaiFsJk9ZJJo3/d4c+kc6GOl2iC1H3Q4/5cm3NvJBn/7bUtKHmSFwfbDouj+XjUk5rZbQ== dependencies: core-js "^3.0.1" -"@storybook/core@6.0.16", "@storybook/core@^6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/core/-/core-6.0.16.tgz#ec9aa8c0fd1c23d29bf8401b650c0876c41d1b5f" - integrity sha512-dVgw03bB8rSMrYDw+v07Yiqyy4yas1olnXpytscWCWdbBuflSAQU+mtqcHMIH9YlhucIT2dYiErDDDNmqP+6tw== +"@storybook/core@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/core/-/core-6.0.26.tgz#ff587929d0f55cefa8405686e831e79aeeb6870e" + integrity sha512-2kmkxbzDJVrjzCjlseffoQJwZRH9bHZUumo5m8gpbN9kVnADER7yd6RUf2Zle5BK3ExC+0PPI1Whfg0qkiXvqw== dependencies: "@babel/plugin-proposal-class-properties" "^7.8.3" "@babel/plugin-proposal-decorators" "^7.8.3" @@ -3105,20 +3127,20 @@ "@babel/preset-react" "^7.8.3" "@babel/preset-typescript" "^7.9.0" "@babel/register" "^7.10.5" - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/channel-postmessage" "6.0.16" - "@storybook/channels" "6.0.16" - "@storybook/client-api" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/components" "6.0.16" - "@storybook/core-events" "6.0.16" + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/channel-postmessage" "6.0.26" + "@storybook/channels" "6.0.26" + "@storybook/client-api" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/core-events" "6.0.26" "@storybook/csf" "0.0.1" - "@storybook/node-logger" "6.0.16" - "@storybook/router" "6.0.16" + "@storybook/node-logger" "6.0.26" + "@storybook/router" "6.0.26" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.0.16" - "@storybook/ui" "6.0.16" + "@storybook/theming" "6.0.26" + "@storybook/ui" "6.0.26" "@types/glob-base" "^0.3.0" "@types/micromatch" "^4.0.1" "@types/node-fetch" "^2.5.4" @@ -3189,10 +3211,10 @@ dependencies: lodash "^4.17.15" -"@storybook/node-logger@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-6.0.16.tgz#805e0748355d13535c3295455f568ea94e57d1ad" - integrity sha512-mD6so/puFV5oByBkDp9rv2mV/WyGy21QdrwXpXdtLDKNgqPuJjHZuF1RA/+MmDK4P1CjvP1no2H5WDKg+aW4QQ== +"@storybook/node-logger@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-6.0.26.tgz#2ef95ea1e2defd4efcba6b23431ea5c5cbaa110b" + integrity sha512-mdILu91d/2ZgYfICoAMBjwBAYOgjk2URsPudrs5+23lFoPPIwf4CPWcfgs0f4GdfoICk3kV0W7+8bIARhRKp3g== dependencies: "@types/npmlog" "^4.1.2" chalk "^4.0.0" @@ -3200,23 +3222,23 @@ npmlog "^4.1.2" pretty-hrtime "^1.0.3" -"@storybook/postinstall@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/postinstall/-/postinstall-6.0.16.tgz#77c428534dd10074778dc669f7ffce9f387acc93" - integrity sha512-gZgPNJK/58VepIBodK0pSlD1jPQgIVTEFWot5/iDjxv9cnSl9V+LbIEW5jZp/lzoAONSj8AS646ZZjAM87S4RQ== +"@storybook/postinstall@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/postinstall/-/postinstall-6.0.26.tgz#3ba9f6fa598d92daf5823361186c4b1369f16ebe" + integrity sha512-B9Dh66MfserWw1J4KbLqfxpnanN//yeDjrrkowzqa3OFLqEPQCekv0ALocovnCkQ13+TcVGjPprxnWXfGhEMpg== dependencies: core-js "^3.0.1" -"@storybook/react@^6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/react/-/react-6.0.16.tgz#21464749f7bd90dc6026235b2ee47acf168d974a" - integrity sha512-cxnBwewx37rL1BjXo3TQFIvvCv9z26r3yuRRWh527/0QODfwGz8TT+/sJHeqBA5JIQzLwAHNqNJhLp6xzfr5Dw== +"@storybook/react@^6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/react/-/react-6.0.26.tgz#5d4b8f2c6d8003912d371298a6e5a945e24680b4" + integrity sha512-X02VpIEhpVc4avYiff861c015++tvMVSXJSrDP5J1xTAglVEiRFcU0Kn5h96o9N8FTup2n2xyj6Y7e8oC9yLXQ== dependencies: "@babel/preset-flow" "^7.0.0" "@babel/preset-react" "^7.0.0" - "@storybook/addons" "6.0.16" - "@storybook/core" "6.0.16" - "@storybook/node-logger" "6.0.16" + "@storybook/addons" "6.0.26" + "@storybook/core" "6.0.26" + "@storybook/node-logger" "6.0.26" "@storybook/semver" "^7.3.2" "@svgr/webpack" "^5.4.0" "@types/webpack-env" "^1.15.2" @@ -3233,10 +3255,10 @@ ts-dedent "^1.1.1" webpack "^4.43.0" -"@storybook/router@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.0.16.tgz#b18cc0b1bba477f16f9f2ae8f0eaa0d5ba4b0a0e" - integrity sha512-zijPJ3CR4ytHE0v+pGdaWT3H+es+mLHRkR6hkqcD0ABT5HVfwMlmXJ9FkQGCVpnnNeBOz7+QKCdE13HMelQpqg== +"@storybook/router@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.0.26.tgz#5b991001afa7d7eb5e40c53cd4c58266b6f9edfd" + integrity sha512-kQ1LF/2gX3IkjS1wX7CsoeBc9ptHQzOsyax16rUyJa769DT5vMNtFtQxjNXMqSiSapPg2yrXJFKQNaoWvKgQEQ== dependencies: "@reach/router" "^1.3.3" "@types/reach__router" "^1.3.5" @@ -3253,31 +3275,31 @@ core-js "^3.6.5" find-up "^4.1.0" -"@storybook/source-loader@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/source-loader/-/source-loader-6.0.16.tgz#a3eb2b0cbede7d9121387738a530d71df645db5d" - integrity sha512-Ub6bU7o2JJUigzu9MSrFH1RD2SmpZZnym+WEidWI9A1gseKp1Rd4KDq36AqJo/oL3hAzoAOirrv3ZixIwXLFMg== +"@storybook/source-loader@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/source-loader/-/source-loader-6.0.26.tgz#0c9a20b9e018c49d559c56e1bdae8350b8175371" + integrity sha512-axNYEHEj7c9oHUFTMKZ6xRyKZCEEP7Aa9sFPzV5Q3Vrq6/3qhih5fOPXhst6/s4XZC1eIoKKHb/Gk4hmjYOEYA== dependencies: - "@storybook/addons" "6.0.16" - "@storybook/client-logger" "6.0.16" + "@storybook/addons" "6.0.26" + "@storybook/client-logger" "6.0.26" "@storybook/csf" "0.0.1" core-js "^3.0.1" estraverse "^4.2.0" global "^4.3.2" loader-utils "^2.0.0" lodash "^4.17.15" - prettier "^2.0.5" + prettier "~2.0.5" regenerator-runtime "^0.13.3" -"@storybook/theming@6.0.16", "@storybook/theming@^6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.0.16.tgz#dd6de4f29316a6a2380018978b7b4a0ef9ea33c8" - integrity sha512-6D7oMEbeABYZdDY8e3i+O39XLrk6fvG3GBaSGp31BE30d269NcPkGPxMKY/nzc6MY30a+/LbBbM7b6gRKe6b4Q== +"@storybook/theming@6.0.26", "@storybook/theming@^6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.0.26.tgz#e5b545fb2653dfd1b043b567197d490b1c3c0da3" + integrity sha512-9yon2ofb9a+RT1pdvn8Njydy7XRw0qXcIsMqGsJRKoZecmRRozqB6DxH9Gbdf1vRSbM9gYUUDjbiMDFz7+4RiQ== dependencies: "@emotion/core" "^10.0.20" "@emotion/is-prop-valid" "^0.8.6" "@emotion/styled" "^10.0.17" - "@storybook/client-logger" "6.0.16" + "@storybook/client-logger" "6.0.26" core-js "^3.0.1" deep-object-diff "^1.1.0" emotion-theming "^10.0.19" @@ -3287,21 +3309,21 @@ resolve-from "^5.0.0" ts-dedent "^1.1.1" -"@storybook/ui@6.0.16": - version "6.0.16" - resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-6.0.16.tgz#448d2286404554afb13e27fecd9efb0861fa9286" - integrity sha512-4F21kwQVaMwgqoJmO+566j7MXmvPp+7jfWBMPAvyGsf5uIZ4q6V29h5mMLvTOFA4qHw0lHZk2k8V0g5gk/tjCA== +"@storybook/ui@6.0.26": + version "6.0.26" + resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-6.0.26.tgz#60e97d2044a3f63b489d7ad0b0529d93373b71ee" + integrity sha512-Jb7oUJs6uyW+rM4zA8xDn9T0/0XtUAOC/zBl6ofdhYU9rVjYKAQUJqmYgUHNOggq1NGS7BVp1RJIzDWGYEagsA== dependencies: "@emotion/core" "^10.0.20" - "@storybook/addons" "6.0.16" - "@storybook/api" "6.0.16" - "@storybook/channels" "6.0.16" - "@storybook/client-logger" "6.0.16" - "@storybook/components" "6.0.16" - "@storybook/core-events" "6.0.16" - "@storybook/router" "6.0.16" + "@storybook/addons" "6.0.26" + "@storybook/api" "6.0.26" + "@storybook/channels" "6.0.26" + "@storybook/client-logger" "6.0.26" + "@storybook/components" "6.0.26" + "@storybook/core-events" "6.0.26" + "@storybook/router" "6.0.26" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.0.16" + "@storybook/theming" "6.0.26" "@types/markdown-to-jsx" "^6.11.0" copy-to-clipboard "^3.0.8" core-js "^3.0.1" @@ -6735,6 +6757,11 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== +axe-core@^3.5.2: + version "3.5.5" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.5.5.tgz#84315073b53fa3c0c51676c588d59da09a192227" + integrity sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q== + axe-core@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.0.2.tgz#c7cf7378378a51fcd272d3c09668002a4990b1cb" @@ -7649,7 +7676,7 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browser-resolve@^1.11.3, browser-resolve@^1.8.1: +browser-resolve@^1.8.1: version "1.11.3" resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6" integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ== @@ -11199,7 +11226,7 @@ elegant-spinner@^1.0.1: resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= -element-resize-detector@^1.1.12: +element-resize-detector@^1.1.12, element-resize-detector@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/element-resize-detector/-/element-resize-detector-1.2.1.tgz#b0305194447a4863155e58f13323a0aef30851d1" integrity sha512-BdFsPepnQr9fznNPF9nF4vQ457U/ZJXQDSNF1zBe7yaga8v9AdZf3/NElYxFdUh7SitSGt040QygiTo6dtatIw== @@ -12262,7 +12289,7 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" -expect@^24.8.0, expect@^24.9.0: +expect@^24.8.0: version "24.9.0" resolved "https://registry.yarnpkg.com/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca" integrity sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q== @@ -12462,7 +12489,7 @@ fast-equals@^2.0.0: resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-2.0.0.tgz#bef2c423af3939f2c54310df54c57e64cd2adefc" integrity sha512-u6RBd8cSiLLxAiC04wVsLV6GBFDOXcTCgWkd3wEoFXgidPSoAJENqC9m7Jb2vewSvjBIfXV6icKeh3GTKfIaXA== -fast-glob@2.2.7, fast-glob@^2.0.2, fast-glob@^2.2.6: +fast-glob@^2.0.2, fast-glob@^2.2.6: version "2.2.7" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d" integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw== @@ -13672,7 +13699,7 @@ glob-to-regexp@^0.4.0: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.0.tgz#49bd677b1671022bd10921c3788f23cdebf9c7e6" integrity sha512-fyPCII4vn9Gvjq2U/oDAfP433aiE64cyP/CJjRJcpVGjqqNdioUYn9+r0cSzT1XPwmGAHuTT7iv+rQT8u/YHKQ== -glob-watcher@5.0.3, glob-watcher@^5.0.3: +glob-watcher@^5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/glob-watcher/-/glob-watcher-5.0.3.tgz#88a8abf1c4d131eb93928994bc4a593c2e5dd626" integrity sha512-8tWsULNEPHKQ2MR4zXuzSmqbdyV5PtwwCaWSGQ1WwHsJ07ilNeN1JB8ntxhckbnpSHaf9dXFUHzIWvm1I13dsg== @@ -17092,7 +17119,7 @@ jest-mock@^26.3.0: "@jest/types" "^26.3.0" "@types/node" "*" -jest-pnp-resolver@^1.2.1, jest-pnp-resolver@^1.2.2: +jest-pnp-resolver@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== @@ -17121,17 +17148,6 @@ jest-resolve-dependencies@^26.4.2: jest-regex-util "^26.0.0" jest-snapshot "^26.4.2" -jest-resolve@^24.9.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-24.9.0.tgz#dff04c7687af34c4dd7e524892d9cf77e5d17321" - integrity sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ== - dependencies: - "@jest/types" "^24.9.0" - browser-resolve "^1.11.3" - chalk "^2.0.1" - jest-pnp-resolver "^1.2.1" - realpath-native "^1.1.0" - jest-resolve@^26.4.0, jest-resolve@^26.5.2: version "26.5.2" resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.5.2.tgz#0d719144f61944a428657b755a0e5c6af4fc8602" @@ -17212,25 +17228,6 @@ jest-serializer@^26.5.0: "@types/node" "*" graceful-fs "^4.2.4" -jest-snapshot@^24.1.0: - version "24.9.0" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-24.9.0.tgz#ec8e9ca4f2ec0c5c87ae8f925cf97497b0e951ba" - integrity sha512-uI/rszGSs73xCM0l+up7O7a40o90cnrk429LOiK3aeTvfC0HHmldbd81/B7Ix81KSFe1lwkbl7GnBGG4UfuDew== - dependencies: - "@babel/types" "^7.0.0" - "@jest/types" "^24.9.0" - chalk "^2.0.1" - expect "^24.9.0" - jest-diff "^24.9.0" - jest-get-type "^24.9.0" - jest-matcher-utils "^24.9.0" - jest-message-util "^24.9.0" - jest-resolve "^24.9.0" - mkdirp "^0.5.1" - natural-compare "^1.4.0" - pretty-format "^24.9.0" - semver "^6.2.0" - jest-snapshot@^26.3.0, jest-snapshot@^26.4.2: version "26.4.2" resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.4.2.tgz#87d3ac2f2bd87ea8003602fbebd8fcb9e94104f6" @@ -17252,13 +17249,6 @@ jest-snapshot@^26.3.0, jest-snapshot@^26.4.2: pretty-format "^26.4.2" semver "^7.3.2" -jest-specific-snapshot@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/jest-specific-snapshot/-/jest-specific-snapshot-2.0.0.tgz#425fe524b25df154aa39f97fa6fe9726faaac273" - integrity sha512-aXaNqBg/svwEpY5iQEzEHc5I85cUBKgfeVka9KmpznxLnatpjiqjr7QLb/BYNYlsrZjZzgRHTjQJ+Svx+dbdvg== - dependencies: - jest-snapshot "^24.1.0" - jest-specific-snapshot@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jest-specific-snapshot/-/jest-specific-snapshot-4.0.0.tgz#a52a2e223e7576e610dbeaf341207c557ac20554" @@ -21921,11 +21911,16 @@ prettier@1.16.4: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.4.tgz#73e37e73e018ad2db9c76742e2647e21790c9717" integrity sha512-ZzWuos7TI5CKUeQAtFd6Zhm2s6EpAD/ZLApIhsF9pRvRtM1RFo61dM/4MSRUA0SuLugA/zgrZD8m0BaY46Og7g== -prettier@^2.0.5, prettier@^2.1.1: +prettier@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.1.tgz#d9485dd5e499daa6cb547023b87a6cf51bee37d6" integrity sha512-9bY+5ZWCfqj3ghYBLxApy2zf6m+NJo5GzmLTpr9FsApsfjriNnS2dahWReHMi7qNPhhHl9SYHJs2cHZLgexNIw== +prettier@~2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4" + integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg== + pretty-bytes@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9" @@ -23003,6 +22998,16 @@ react-sizeme@^2.3.6: invariant "^2.2.2" lodash "^4.17.4" +react-sizeme@^2.5.2: + version "2.6.12" + resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-2.6.12.tgz#ed207be5476f4a85bf364e92042520499455453e" + integrity sha512-tL4sCgfmvapYRZ1FO2VmBmjPVzzqgHA7kI8lSJ6JS6L78jXFNRdOZFpXyK6P1NBZvKPPCZxReNgzZNUajAerZw== + dependencies: + element-resize-detector "^1.2.1" + invariant "^2.2.4" + shallowequal "^1.1.0" + throttle-debounce "^2.1.0" + react-sizeme@^2.6.7: version "2.6.10" resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-2.6.10.tgz#9993dcb5e67fab94a8e5d078a0d3820609010f17" @@ -23373,13 +23378,6 @@ readline2@^1.0.1: is-fullwidth-code-point "^1.0.0" mute-stream "0.0.5" -realpath-native@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" - integrity sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA== - dependencies: - util.promisify "^1.0.0" - recast@^0.14.7: version "0.14.7" resolved "https://registry.yarnpkg.com/recast/-/recast-0.14.7.tgz#4f1497c2b5826d42a66e8e3c9d80c512983ff61d" @@ -27845,7 +27843,7 @@ util-extend@^1.0.1: resolved "https://registry.yarnpkg.com/util-extend/-/util-extend-1.0.3.tgz#a7c216d267545169637b3b6edc6ca9119e2ff93f" integrity sha1-p8IW0mdUUWljeztu3GypEZ4v+T8= -util.promisify@1.0.0, util.promisify@^1.0.0, util.promisify@~1.0.0: +util.promisify@1.0.0, util.promisify@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==