diff --git a/.eslintignore b/.eslintignore index 93c69b4f9b20..7f3e3ef597cb 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 27dacd51be6f..24ae50791d91 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 5dd41581914e..8f2c27ac7c3c 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 000000000000..0c24eb2f973f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/security_solution_bug_report.md @@ -0,0 +1,38 @@ +--- +name: Security Solution Bug Report +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 e0281b0a5bc2..68e38d3976a6 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 24940e232e88..0504b7f7d6db 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 3321aae3c099..a2cda1e0b1e8 100644 --- a/docs/developer/best-practices/typescript.asciidoc +++ b/docs/developer/best-practices/typescript.asciidoc @@ -19,7 +19,7 @@ More details are available in the https://www.typescriptlang.org/docs/handbook/p ==== Caveats This architecture imposes several limitations to which we must comply: -- Projects cannot have circular dependencies. Even though the Kibana platform doesn't support circular dependencies between Kibana plugins, TypeScript (and ES6 modules) does allow circular imports between files. So in theory, you may face a problem when migrating to the TS project references and you will have to resolve this circular dependency. +- Projects cannot have circular dependencies. Even though the Kibana platform doesn't support circular dependencies between Kibana plugins, TypeScript (and ES6 modules) does allow circular imports between files. So in theory, you may face a problem when migrating to the TS project references and you will have to resolve this circular dependency. https://github.com/elastic/kibana/issues/78162 is going to provide a tool to find such problem places. - A project must emit its type declaration. It's not always possible to generate a type declaration if the compiler cannot infer a type. There are two basic cases: 1. Your plugin exports a type inferring an internal type declared in Kibana codebase. In this case, you'll have to either export an internal type or to declare an exported type explicitly. @@ -27,7 +27,8 @@ This architecture imposes several limitations to which we must comply: [discrete] ==== Prerequisites -Since `tsc` doesn't support circular project references, 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. +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. [discrete] ==== Implementation diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 21c51f8cabd3..b5a810852b94 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] diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getvaluebucketpath.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getvaluebucketpath.md new file mode 100644 index 000000000000..5616064ddaa0 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getvaluebucketpath.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [getValueBucketPath](./kibana-plugin-plugins-data-public.aggconfig.getvaluebucketpath.md) + +## AggConfig.getValueBucketPath() method + +Returns the bucket path containing the main value the agg will produce (e.g. for sum of bytes it will point to the sum, for median it will point to the 50 percentile in the percentile multi value bucket) + +Signature: + +```typescript +getValueBucketPath(): string; +``` +Returns: + +`string` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.md index ceb90cffbf6c..d4a8eddf51cf 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.md @@ -47,6 +47,7 @@ export declare class AggConfig | [getResponseAggs()](./kibana-plugin-plugins-data-public.aggconfig.getresponseaggs.md) | | | | [getTimeRange()](./kibana-plugin-plugins-data-public.aggconfig.gettimerange.md) | | | | [getValue(bucket)](./kibana-plugin-plugins-data-public.aggconfig.getvalue.md) | | | +| [getValueBucketPath()](./kibana-plugin-plugins-data-public.aggconfig.getvaluebucketpath.md) | | Returns the bucket path containing the main value the agg will produce (e.g. for sum of bytes it will point to the sum, for median it will point to the 50 percentile in the percentile multi value bucket) | | [isFilterable()](./kibana-plugin-plugins-data-public.aggconfig.isfilterable.md) | | | | [makeLabel(percentageMode)](./kibana-plugin-plugins-data-public.aggconfig.makelabel.md) | | | | [nextId(list)](./kibana-plugin-plugins-data-public.aggconfig.nextid.md) | static | Calculate the next id based on the ids in this list {array} list - a list of objects with id properties | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md index 9c47ea1a166d..b99c5f0f10a9 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.md @@ -16,6 +16,6 @@ export interface ISearchStartAggsStart | | | [getSearchStrategy](./kibana-plugin-plugins-data-server.isearchstart.getsearchstrategy.md) | (name: string) => ISearchStrategy<SearchStrategyRequest, SearchStrategyResponse> | Get other registered search strategies. For example, if a new strategy needs to use the already-registered ES search strategy, it can use this function to accomplish that. | -| [search](./kibana-plugin-plugins-data-server.isearchstart.search.md) | (context: RequestHandlerContext, request: SearchStrategyRequest, options: ISearchOptions) => Promise<SearchStrategyResponse> | | +| [search](./kibana-plugin-plugins-data-server.isearchstart.search.md) | ISearchStrategy['search'] | | | [searchSource](./kibana-plugin-plugins-data-server.isearchstart.searchsource.md) | {
asScoped: (request: KibanaRequest) => Promise<ISearchStartSearchSource>;
} | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.search.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.search.md index fdcd4d6768db..98ea175aaaea 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.search.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstart.search.md @@ -7,5 +7,5 @@ Signature: ```typescript -search: (context: RequestHandlerContext, request: SearchStrategyRequest, options: ISearchOptions) => Promise; +search: ISearchStrategy['search']; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md index 3d2caf417f3c..6dd95da2be3c 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.md @@ -17,5 +17,5 @@ export interface ISearchStrategy(context: RequestHandlerContext, id: string) => Promise<void> | | -| [search](./kibana-plugin-plugins-data-server.isearchstrategy.search.md) | (context: RequestHandlerContext, request: SearchStrategyRequest, options?: ISearchOptions) => Promise<SearchStrategyResponse> | | +| [search](./kibana-plugin-plugins-data-server.isearchstrategy.search.md) | (request: SearchStrategyRequest, options: ISearchOptions, context: RequestHandlerContext) => Observable<SearchStrategyResponse> | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.search.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.search.md index 45f43648ab60..84b90ae23f91 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.search.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchstrategy.search.md @@ -7,5 +7,5 @@ Signature: ```typescript -search: (context: RequestHandlerContext, request: SearchStrategyRequest, options?: ISearchOptions) => Promise; +search: (request: SearchStrategyRequest, options: ISearchOptions, context: RequestHandlerContext) => Observable; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrendererror.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrendererror.md index 3b3c1644adbe..9a2507056eb8 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrendererror.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrendererror.md @@ -14,5 +14,6 @@ export interface ExpressionRenderError extends Error | Property | Type | Description | | --- | --- | --- | +| [original](./kibana-plugin-plugins-expressions-public.expressionrendererror.original.md) | Error | | | [type](./kibana-plugin-plugins-expressions-public.expressionrendererror.type.md) | string | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrendererror.original.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrendererror.original.md new file mode 100644 index 000000000000..45f74a52e6b6 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrendererror.original.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionRenderError](./kibana-plugin-plugins-expressions-public.expressionrendererror.md) > [original](./kibana-plugin-plugins-expressions-public.expressionrendererror.original.md) + +## ExpressionRenderError.original property + +Signature: + +```typescript +original?: Error; +``` 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 000000000000..26d1e7810f9e --- /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 cf0cf4cb50b7..83d4b9bd3509 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/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md index bd6c8cba5f78..5622516530ed 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md @@ -20,5 +20,5 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams | [onEvent](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.onevent.md) | (event: ExpressionRendererEvent) => void | | | [padding](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.padding.md) | 'xs' | 's' | 'm' | 'l' | 'xl' | | | [reload$](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.reload_.md) | Observable<unknown> | An observable which can be used to re-run the expression without destroying the component | -| [renderError](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.rendererror.md) | (error?: string | null) => React.ReactElement | React.ReactElement[] | | +| [renderError](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.rendererror.md) | (message?: string | null, error?: ExpressionRenderError | null) => React.ReactElement | React.ReactElement[] | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.rendererror.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.rendererror.md index 48bfe1ee5c7c..162d0da04ae7 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.rendererror.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.rendererror.md @@ -7,5 +7,5 @@ Signature: ```typescript -renderError?: (error?: string | null) => React.ReactElement | React.ReactElement[]; +renderError?: (message?: string | null, error?: ExpressionRenderError | null) => React.ReactElement | React.ReactElement[]; ``` 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 000000000000..767f6011290a --- /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 d369d882757f..4e6ae12217f2 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 7039468f4b18..06b2b96c0035 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/docs/infrastructure/images/infra-sysmon.png b/docs/infrastructure/images/infra-sysmon.png deleted file mode 100644 index dd653bb046f4..000000000000 Binary files a/docs/infrastructure/images/infra-sysmon.png and /dev/null differ diff --git a/docs/infrastructure/index.asciidoc b/docs/infrastructure/index.asciidoc deleted file mode 100644 index 81a3022436a7..000000000000 --- a/docs/infrastructure/index.asciidoc +++ /dev/null @@ -1,32 +0,0 @@ -[chapter] -[role="xpack"] -[[xpack-infra]] -= Metrics - -The {metrics-app} in {kib} enables you to monitor your infrastructure metrics and identify problems in real time. -You start with a visual summary of your infrastructure where you can view basic metrics for common servers, containers, and services. -Then you can drill down to view more detailed metrics or other information for that component. - -You can: - -* View your infrastructure metrics by hosts, Kubernetes pods, or Docker containers. -You can group and filter the data in various ways to help you identify the items that interest you. - -* View current and historic values for metrics such as CPU usage, memory usage, and network traffic for each component. -The available metrics depend on the kind of component being inspected. - -* Use *Metrics Explorer* to group and visualize multiple customizable metrics for one or more components in a graphical format. -You can optionally save these views and add them to {kibana-ref}/dashboard.html[dashboards]. - -* Seamlessly switch to view the corresponding logs, application traces or uptime information for a component. - -* Create alerts based on metric thresholds for one or more components. - -[role="screenshot"] -image::infrastructure/images/infra-sysmon.png[Infrastructure Overview in Kibana] - -[float] -=== Get started - -To get started with Metrics, refer to {metrics-guide}/install-metrics-monitoring.html[Install Metrics]. - diff --git a/docs/logs/images/logs-console.png b/docs/logs/images/logs-console.png deleted file mode 100644 index ddd3346475da..000000000000 Binary files a/docs/logs/images/logs-console.png and /dev/null differ diff --git a/docs/logs/index.asciidoc b/docs/logs/index.asciidoc deleted file mode 100644 index 45d4321f4055..000000000000 --- a/docs/logs/index.asciidoc +++ /dev/null @@ -1,21 +0,0 @@ -[chapter] -[role="xpack"] -[[xpack-logs]] -= Logs - -The Logs app in Kibana enables you to explore logs for common servers, containers, and services. - -The Logs app has a compact, console-like display that you can customize. -You can filter the logs by various fields, start and stop live streaming, and highlight text of interest. - -You can open the Logs app from the *Logs* tab in Kibana. -You can also open the Logs app directly from a component in the Metrics app. -In this case, you will only see the logs for the selected component. - -[role="screenshot"] -image::logs/images/logs-console.png[Logs Console in Kibana] - -[float] -=== Get started - -To get started with Elastic Logs, refer to {logs-guide}/install-logs-monitoring.html[Install Logs]. diff --git a/docs/management/alerting/alert-management.asciidoc b/docs/management/alerting/alert-management.asciidoc index 73cf40c4d7c4..f34881255097 100644 --- a/docs/management/alerting/alert-management.asciidoc +++ b/docs/management/alerting/alert-management.asciidoc @@ -4,7 +4,7 @@ beta[] -The *Alerts* tab provides a cross-app view of alerting. Different {kib} apps like <>, <>, <>, and <> can offer their own alerts, and the *Alerts* tab provides a central place to: +The *Alerts* tab provides a cross-app view of alerting. Different {kib} apps like <>, <>, <>, and <> can offer their own alerts, and the *Alerts* tab provides a central place to: * <> alerts * <> including enabling/disabling, muting/unmuting, and deleting @@ -39,7 +39,7 @@ image::images/alerts-filter-by-action-type.png[Filtering the alert list by type [[create-edit-alerts]] ==== Creating and editing alerts -Many alerts must be created within the context of a {kib} app like <>, <>, or <>, but others are generic. Generic alert types can be created in the *Alerts* management UI by clicking the *Create* button. This will launch a flyout that guides you through selecting an alert type and configuring it's properties. Refer to <> for details on what types of alerts are available and how to configure them. +Many alerts must be created within the context of a {kib} app like <>, <>, or <>, but others are generic. Generic alert types can be created in the *Alerts* management UI by clicking the *Create* button. This will launch a flyout that guides you through selecting an alert type and configuring it's properties. Refer to <> for details on what types of alerts are available and how to configure them. After an alert is created, you can re-open the flyout and change an alerts properties by clicking the *Edit* button shown on each row of the alert listing. diff --git a/docs/management/ingest-pipelines/images/ingest-pipeline-processor.png b/docs/management/ingest-pipelines/images/ingest-pipeline-processor.png old mode 100755 new mode 100644 index 8d8b8aa4b42e..2de7449affd0 Binary files a/docs/management/ingest-pipelines/images/ingest-pipeline-processor.png and b/docs/management/ingest-pipelines/images/ingest-pipeline-processor.png differ diff --git a/docs/management/ingest-pipelines/ingest-pipelines.asciidoc b/docs/management/ingest-pipelines/ingest-pipelines.asciidoc index da2d3b8accac..7986e4e56279 100644 --- a/docs/management/ingest-pipelines/ingest-pipelines.asciidoc +++ b/docs/management/ingest-pipelines/ingest-pipelines.asciidoc @@ -62,11 +62,40 @@ You also want to know where the request is coming from. . In *Ingest Node Pipelines*, click *Create a pipeline*. . Provide a name and description for the pipeline. -. Define the processors: +. Add a grok processor to parse the log message: + +.. Click *Add a processor* and select the *Grok* processor type. +.. Set the field input to `message` and enter the following grok pattern: + [source,js] ---------------------------------- -[ +%{IPORHOST:clientip} %{USER:ident} %{USER:auth} \[%{HTTPDATE:timestamp}\] "%{WORD:verb} %{DATA:request} HTTP/%{NUMBER:httpversion}" %{NUMBER:response:int} (?:-|%{NUMBER:bytes:int}) %{QS:referrer} %{QS:agent} +---------------------------------- ++ +.. Click *Update* to save the processor. + +. Add processors to map the date, IP, and user agent fields. + +.. Map the appropriate field to each processor type: ++ +-- +* **Date**: `timestamp` +* **GeoIP**: `clientip` +* **User agent**: `agent` + +For the **Date** processor, you also need to specify the date format you want to use: `dd/MMM/YYYY:HH:mm:ss Z`. +-- +Your form should look similar to this: ++ +[role="screenshot"] +image:management/ingest-pipelines/images/ingest-pipeline-processor.png["Processors for Ingest Node Pipelines"] ++ +Alternatively, you can click the **Import processors** link and define the processors as JSON: ++ +[source,js] +---------------------------------- +{ + "processors": [ { "grok": { "field": "message", @@ -90,19 +119,16 @@ You also want to know where the request is coming from. } } ] +} ---------------------------------- + -This code defines four {ref}/ingest-processors.html[processors] that run sequentially: +The four {ref}/ingest-processors.html[processors] will run sequentially: {ref}/grok-processor.html[grok], {ref}/date-processor.html[date], -{ref}/geoip-processor.html[geoip], and {ref}/user-agent-processor.html[user_agent]. -Your form should look similar to this: -+ -[role="screenshot"] -image:management/ingest-pipelines/images/ingest-pipeline-processor.png["Processors for Ingest Node Pipelines"] +{ref}/geoip-processor.html[geoip], and {ref}/user-agent-processor.html[user_agent]. You can reorder processors using the arrow icon next to each processor. -. To verify that the pipeline gives the expected outcome, click *Test pipeline*. +. To test the pipeline to verify that it produces the expected results, click *Add documents*. -. In the *Document* tab, provide the following sample document for testing: +. In the *Documents* tab, provide a sample document for testing: + [source,js] ---------------------------------- diff --git a/docs/observability/images/apm-app.png b/docs/observability/images/apm-app.png new file mode 100644 index 000000000000..acbaa70c7f2f Binary files /dev/null and b/docs/observability/images/apm-app.png differ diff --git a/docs/observability/images/logs-app.png b/docs/observability/images/logs-app.png new file mode 100644 index 000000000000..1138ec175e5b Binary files /dev/null and b/docs/observability/images/logs-app.png differ diff --git a/docs/observability/images/metrics-app.png b/docs/observability/images/metrics-app.png new file mode 100644 index 000000000000..8c00a31974a7 Binary files /dev/null and b/docs/observability/images/metrics-app.png differ diff --git a/docs/observability/images/uptime-app.png b/docs/observability/images/uptime-app.png new file mode 100644 index 000000000000..522a696adf50 Binary files /dev/null and b/docs/observability/images/uptime-app.png differ diff --git a/docs/observability/index.asciidoc b/docs/observability/index.asciidoc index d63402e8df2f..c924cea3712d 100644 --- a/docs/observability/index.asciidoc +++ b/docs/observability/index.asciidoc @@ -13,12 +13,69 @@ With *Observability*, you have: * *View in app* options to drill down and analyze data in the Logs, Metrics, Uptime, and APM apps. * An alerts chart to keep you informed of any issues that you may need to resolve quickly. +{kib} provides step-by-step instructions to help you add and configure your data +sources. The {observability-guide}/index.html[Observability Guide] is a good source for more detailed information +and instructions. + [role="screenshot"] image::observability/images/observability-overview.png[Observability Overview in {kib}] [float] -== Get started +[[logs-app]] +== Logs -{kib} provides step-by-step instructions to help you add and configure your data -sources. The {observability-guide}/index.html[Observability Guide] is a good source for more detailed information -and instructions. +The {logs-app} in {kib} enables you to search, filter, and tail all your logs +ingested into {es}. Instead of having to log into different servers, change +directories, and tail individual files, all your logs are available in the {logs-app}. + +There is live streaming of logs, filtering using auto-complete, and a logs histogram +for quick navigation. You can also use machine learning to detect specific log +anomalies automatically and categorize log messages to quickly identify patterns in your +log events. + +To get started with the {logs-app}, see {observability-guide}/ingest-logs.html[Ingest logs]. + +[role="screenshot"] +image::observability/images/logs-app.png[Logs app in {kib}] + +[float] +[[metrics-app]] +== Metrics + +The {metrics-app} in {kib} enables you to visualize infrastructure metrics +to help diagnose problematic spikes, identify high resource utilization, +automatically discover and track pods, and unify your metrics +with logs and APM data in {es}. + +To get started with the {metrics-app}, see {observability-guide}/ingest-metrics.html[Ingest metrics]. + +[role="screenshot"] +image::observability/images/metrics-app.png[Metrics app in {kib}] + +[float] +[[uptime-app]] +== Uptime + +The {uptime-app} in {kib} enables you to monitor the availability and response times +of applications and services in real time, and detect problems before they affect users. +You can monitor the status of network endpoints via HTTP/S, TCP, and ICMP, explore +endpoint status over time, drill down into specific monitors, and view a high-level +snapshot of your environment at any point in time. + +To get started with the {uptime-app}, see {observability-guide}/ingest-uptime.html[Ingest uptime data]. + +[role="screenshot"] +image::observability/images/uptime-app.png[Uptime app in {kib}] + +[float] +[[apm-app]] +== APM + +The APM app in {kib} enables you to monitors software services and applications in real time, +collect unhandled errors and exceptions, and automatically pick up basic host-level metrics +and agent specific metrics. + +To get started with the APM app, see <>. + +[role="screenshot"] +image::observability/images/apm-app.png[APM app in {kib}] diff --git a/docs/uptime/images/uptime-overview.png b/docs/uptime/images/uptime-overview.png deleted file mode 100644 index 25c88b2d1428..000000000000 Binary files a/docs/uptime/images/uptime-overview.png and /dev/null differ diff --git a/docs/uptime/index.asciidoc b/docs/uptime/index.asciidoc deleted file mode 100644 index 66c9e9357420..000000000000 --- a/docs/uptime/index.asciidoc +++ /dev/null @@ -1,19 +0,0 @@ -[chapter] -[role="xpack"] -[[xpack-uptime]] -= Uptime - -The Uptime app in {kib} enables you to monitor the status of network endpoints via HTTP/S, TCP, and ICMP. -You can explore endpoint status over time, drill down into specific monitors, -and view a high-level snapshot of your environment at any point in time. - -[role="screenshot"] -image::images/uptime-overview.png[Uptime app overview] - -[float] -=== Get started - -To get started with Elastic Uptime, refer to {uptime-guide}/install-uptime.html[Install Uptime]. - - - diff --git a/docs/user/alerting/alert-types.asciidoc b/docs/user/alerting/alert-types.asciidoc index 4a99c70f9d96..f71e43c5defc 100644 --- a/docs/user/alerting/alert-types.asciidoc +++ b/docs/user/alerting/alert-types.asciidoc @@ -2,7 +2,7 @@ [[alert-types]] == Alert types -{kib} supplies alerts types in two ways: some are built into {kib}, while domain-specific alert types are registered by {kib} apps such as <>, <>, and <>. +{kib} supplies alerts types in two ways: some are built into {kib}, while domain-specific alert types are registered by {kib} apps such as <>, <>, and <>. This section covers built-in alert types. For domain-specific alert types, refer to the documentation for that app. diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc index bdb72b1658cd..f8656b87cbe0 100644 --- a/docs/user/alerting/alerting-getting-started.asciidoc +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -6,7 +6,7 @@ beta[] -- -Alerting allows you to detect complex conditions within different {kib} apps and trigger actions when those conditions are met. Alerting is integrated with <>, <>, <>, <>, can be centrally managed from the <> UI, and provides a set of built-in <> and <> for you to use. +Alerting allows you to detect complex conditions within different {kib} apps and trigger actions when those conditions are met. Alerting is integrated with <>, <>, <>, <>, can be centrally managed from the <> UI, and provides a set of built-in <> and <> for you to use. image::images/alerting-overview.png[Alerts and actions UI] @@ -148,7 +148,7 @@ Functionally, {kib} alerting differs in that: * {kib} alerts tracks and persists the state of each detected condition through *alert instances*. This makes it possible to mute and throttle individual instances, and detect changes in state such as resolution. * Actions are linked to *alert instances* in {kib} alerting. Actions are fired for each occurrence of a detected condition, rather than for the entire alert. -At a higher level, {kib} alerts allow rich integrations across use cases like <>, <>, <>, and <>. +At a higher level, {kib} alerts allow rich integrations across use cases like <>, <>, <>, and <>. Pre-packaged *alert types* simplify setup, hide the details complex domain-specific detections, while providing a consistent interface across {kib}. [float] @@ -170,9 +170,9 @@ If you are using an *on-premises* Elastic Stack deployment with <> -* <> +* <> * <> -* <> +* <> See <> for more information on configuring roles that provide access to these features. diff --git a/docs/user/alerting/defining-alerts.asciidoc b/docs/user/alerting/defining-alerts.asciidoc index 7f201d2c39e8..89a487ca8fb3 100644 --- a/docs/user/alerting/defining-alerts.asciidoc +++ b/docs/user/alerting/defining-alerts.asciidoc @@ -2,7 +2,7 @@ [[defining-alerts]] == Defining alerts -{kib} alerts can be created in a variety of apps including <>, <>, <>, <> and from <> UI. While alerting details may differ from app to app, they share a common interface for defining and configuring alerts that this section describes in more detail. +{kib} alerts can be created in a variety of apps including <>, <>, <>, <> and from <> UI. While alerting details may differ from app to app, they share a common interface for defining and configuring alerts that this section describes in more detail. [float] === Alert flyout diff --git a/docs/user/index.asciidoc b/docs/user/index.asciidoc index c186704dd2f1..d375b6f425e5 100644 --- a/docs/user/index.asciidoc +++ b/docs/user/index.asciidoc @@ -27,14 +27,8 @@ include::graph/index.asciidoc[] include::{kib-repo-dir}/observability/index.asciidoc[] -include::{kib-repo-dir}/logs/index.asciidoc[] - -include::{kib-repo-dir}/infrastructure/index.asciidoc[] - include::{kib-repo-dir}/apm/index.asciidoc[] -include::{kib-repo-dir}/uptime/index.asciidoc[] - include::{kib-repo-dir}/siem/index.asciidoc[] include::dev-tools.asciidoc[] diff --git a/examples/search_examples/server/my_strategy.ts b/examples/search_examples/server/my_strategy.ts index 169982544e6e..26e7056cdd78 100644 --- a/examples/search_examples/server/my_strategy.ts +++ b/examples/search_examples/server/my_strategy.ts @@ -17,6 +17,7 @@ * under the License. */ +import { map } from 'rxjs/operators'; import { ISearchStrategy, PluginStart } from '../../../src/plugins/data/server'; import { IMyStrategyResponse, IMyStrategyRequest } from '../common'; @@ -25,13 +26,13 @@ export const mySearchStrategyProvider = ( ): ISearchStrategy => { const es = data.search.getSearchStrategy('es'); return { - search: async (context, request, options): Promise => { - const esSearchRes = await es.search(context, request, options); - return { - ...esSearchRes, - cool: request.get_cool ? 'YES' : 'NOPE', - }; - }, + search: (request, options, context) => + es.search(request, options, context).pipe( + map((esSearchRes) => ({ + ...esSearchRes, + cool: request.get_cool ? 'YES' : 'NOPE', + })) + ), cancel: async (context, id) => { if (es.cancel) { es.cancel(context, id); diff --git a/examples/search_examples/server/routes/server_search_route.ts b/examples/search_examples/server/routes/server_search_route.ts index 6eb21cf34b4a..21ae38b99f3d 100644 --- a/examples/search_examples/server/routes/server_search_route.ts +++ b/examples/search_examples/server/routes/server_search_route.ts @@ -39,26 +39,28 @@ export function registerServerSearchRoute(router: IRouter, data: DataPluginStart // Run a synchronous search server side, by enforcing a high keepalive and waiting for completion. // If you wish to run the search with polling (in basic+), you'd have to poll on the search API. // Please reach out to the @app-arch-team if you need this to be implemented. - const res = await data.search.search( - context, - { - params: { - index, - body: { - aggs: { - '1': { - avg: { - field, + const res = await data.search + .search( + { + params: { + index, + body: { + aggs: { + '1': { + avg: { + field, + }, }, }, }, + waitForCompletionTimeout: '5m', + keepAlive: '5m', }, - waitForCompletionTimeout: '5m', - keepAlive: '5m', - }, - } as IEsSearchRequest, - {} - ); + } as IEsSearchRequest, + {}, + context + ) + .toPromise(); return response.ok({ body: { diff --git a/package.json b/package.json index 1e334f75d41b..732ee1fd3038 100644 --- a/package.json +++ b/package.json @@ -480,7 +480,7 @@ "typescript": "4.0.2", "ui-select": "0.19.8", "vega": "^5.17.0", - "vega-lite": "^4.16.8", + "vega-lite": "^4.17.0", "vega-schema-url-parser": "^2.1.0", "vega-tooltip": "^0.24.2", "vinyl-fs": "^3.0.3", diff --git a/packages/kbn-optimizer/src/cli.ts b/packages/kbn-optimizer/src/cli.ts index a822773052ca..28b3e37380b4 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/node/cache.ts b/packages/kbn-optimizer/src/node/cache.ts index 7fbf009e38a7..1ce3b9eeeafd 100644 --- a/packages/kbn-optimizer/src/node/cache.ts +++ b/packages/kbn-optimizer/src/node/cache.ts @@ -64,6 +64,7 @@ export class Cache { this.codes = LmdbStore.open({ name: 'codes', path: CACHE_DIR, + maxReaders: 500, }); this.atimes = this.codes.openDB({ 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 000000000000..0e31899e6e42 --- /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 000000000000..67c6d0236466 --- /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/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts index 948ba520931e..c3f350519703 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 b685d6ea0159..8091f6aa9050 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -32,6 +32,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 +105,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 +138,7 @@ export interface ParsedOptions { pluginPaths: string[]; pluginScanDirs: string[]; filters: string[]; + focus: string[]; inspectWorkers: boolean; includeCoreBundle: boolean; themeTags: ThemeTags; @@ -148,6 +155,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)) { @@ -210,6 +218,7 @@ export class OptimizerConfig { pluginScanDirs, pluginPaths, filters, + focus, inspectWorkers, includeCoreBundle, themeTags, @@ -236,7 +245,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-std/src/rxjs_7.test.ts b/packages/kbn-std/src/rxjs_7.test.ts index dcc73602613f..ff1026e23b7e 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 f0a1be9125cc..cb10f9de880f 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-ui-shared-deps/entry.js b/packages/kbn-ui-shared-deps/entry.js index d74b45f973eb..4700479941ee 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 613939256cfc..6d9872d3ec52 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 000000000000..c1301a0659bd --- /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 991e87d364e8..767088d1410e 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/src/core/public/apm_system.test.ts b/src/core/public/apm_system.test.ts new file mode 100644 index 000000000000..f88cdd899ef8 --- /dev/null +++ b/src/core/public/apm_system.test.ts @@ -0,0 +1,176 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('@elastic/apm-rum'); +import { init, apm } from '@elastic/apm-rum'; +import { ApmSystem } from './apm_system'; + +const initMock = init as jest.Mocked; +const apmMock = apm as DeeplyMockedKeys; + +describe('ApmSystem', () => { + afterEach(() => { + jest.resetAllMocks(); + jest.resetAllMocks(); + }); + + describe('setup', () => { + it('does not init apm if no config provided', async () => { + const apmSystem = new ApmSystem(undefined); + await apmSystem.setup(); + expect(initMock).not.toHaveBeenCalled(); + }); + + it('calls init with configuration', async () => { + const apmSystem = new ApmSystem({ active: true }); + await apmSystem.setup(); + expect(initMock).toHaveBeenCalledWith({ active: true }); + }); + + it('adds globalLabels if provided', async () => { + const apmSystem = new ApmSystem({ active: true, globalLabels: { alpha: 'one' } }); + await apmSystem.setup(); + expect(apm.addLabels).toHaveBeenCalledWith({ alpha: 'one' }); + }); + + describe('http request normalization', () => { + let windowSpy: any; + + beforeEach(() => { + windowSpy = jest.spyOn(global as any, 'window', 'get').mockImplementation(() => ({ + location: { + protocol: 'http:', + hostname: 'mykibanadomain.com', + port: '5601', + }, + })); + }); + + afterEach(() => { + windowSpy.mockRestore(); + }); + + it('adds an observe function', async () => { + const apmSystem = new ApmSystem({ active: true }); + await apmSystem.setup(); + expect(apm.observe).toHaveBeenCalledWith('transaction:end', expect.any(Function)); + }); + + /** + * Utility function to wrap functions that mutate their input but don't return the mutated value. + * Makes expects easier below. + */ + const returnArg = (func: (input: T) => any): ((input: T) => T) => { + return (input) => { + func(input); + return input; + }; + }; + + it('removes the hostname, port, and protocol only when all match window.location', async () => { + const apmSystem = new ApmSystem({ active: true }); + await apmSystem.setup(); + const observer = apmMock.observe.mock.calls[0][1]; + const wrappedObserver = returnArg(observer); + + // Strips the hostname, protocol, and port from URLs that are on the same origin + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET http://mykibanadomain.com:5601/asdf/qwerty', + } as Transaction) + ).toEqual({ type: 'http-request', name: 'GET /asdf/qwerty' }); + + // Does not modify URLs that are not on the same origin + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET https://mykibanadomain.com:5601/asdf/qwerty', + } as Transaction) + ).toEqual({ + type: 'http-request', + name: 'GET https://mykibanadomain.com:5601/asdf/qwerty', + }); + + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET http://mykibanadomain.com:9200/asdf/qwerty', + } as Transaction) + ).toEqual({ + type: 'http-request', + name: 'GET http://mykibanadomain.com:9200/asdf/qwerty', + }); + + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET http://myotherdomain.com:5601/asdf/qwerty', + } as Transaction) + ).toEqual({ + type: 'http-request', + name: 'GET http://myotherdomain.com:5601/asdf/qwerty', + }); + }); + + it('strips the basePath', async () => { + const apmSystem = new ApmSystem({ active: true }, '/alpha'); + await apmSystem.setup(); + const observer = apmMock.observe.mock.calls[0][1]; + const wrappedObserver = returnArg(observer); + + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET http://mykibanadomain.com:5601/alpha', + } as Transaction) + ).toEqual({ type: 'http-request', name: 'GET /' }); + + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET http://mykibanadomain.com:5601/alpha/', + } as Transaction) + ).toEqual({ type: 'http-request', name: 'GET /' }); + + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET http://mykibanadomain.com:5601/alpha/beta', + } as Transaction) + ).toEqual({ type: 'http-request', name: 'GET /beta' }); + + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET http://mykibanadomain.com:5601/alpha/beta/', + } as Transaction) + ).toEqual({ type: 'http-request', name: 'GET /beta/' }); + + // Works with relative URLs as well + expect( + wrappedObserver({ + type: 'http-request', + name: 'GET /alpha/beta/', + } as Transaction) + ).toEqual({ type: 'http-request', name: 'GET /beta/' }); + }); + }); + }); +}); diff --git a/src/core/public/apm_system.ts b/src/core/public/apm_system.ts index 5e4953b96dc5..3b3c1da01a92 100644 --- a/src/core/public/apm_system.ts +++ b/src/core/public/apm_system.ts @@ -17,14 +17,17 @@ * under the License. */ +import type { ApmBase } from '@elastic/apm-rum'; +import { modifyUrl } from '@kbn/std'; +import type { InternalApplicationStart } from './application'; + +/** "GET protocol://hostname:port/pathname" */ +const HTTP_REQUEST_TRANSACTION_NAME_REGEX = /^(GET|POST|PUT|HEAD|PATCH|DELETE|OPTIONS|CONNECT|TRACE)\s(.*)$/; + /** * This is the entry point used to boot the frontend when serving a application * that lives in the Kibana Platform. - * - * Any changes to this file should be kept in sync with - * src/legacy/ui/ui_bundles/app_entry_template.js */ -import type { InternalApplicationStart } from './application'; interface ApmConfig { // AgentConfigOptions is not exported from @elastic/apm-rum @@ -42,7 +45,7 @@ export class ApmSystem { * `apmConfig` would be populated with relevant APM RUM agent * configuration if server is started with elastic.apm.* config. */ - constructor(private readonly apmConfig?: ApmConfig) { + constructor(private readonly apmConfig?: ApmConfig, private readonly basePath = '') { this.enabled = apmConfig != null && !!apmConfig.active; } @@ -54,6 +57,8 @@ export class ApmSystem { apm.addLabels(globalLabels); } + this.addHttpRequestNormalization(apm); + init(apmConfig); } @@ -73,4 +78,52 @@ export class ApmSystem { } }); } + + /** + * Adds an observer to the APM configuration for normalizing transactions of the 'http-request' type to remove the + * hostname, protocol, port, and base path. Allows for coorelating data cross different deployments. + */ + private addHttpRequestNormalization(apm: ApmBase) { + apm.observe('transaction:end', (t) => { + if (t.type !== 'http-request') { + return; + } + + /** Split URLs of the from "GET protocol://hostname:port/pathname" into method & hostname */ + const matches = t.name.match(HTTP_REQUEST_TRANSACTION_NAME_REGEX); + if (!matches) { + return; + } + + const [, method, originalUrl] = matches; + // Normalize the URL + const normalizedUrl = modifyUrl(originalUrl, (parts) => { + const isAbsolute = parts.hostname && parts.protocol && parts.port; + // If the request was to a different host, port, or protocol then don't change anything + if ( + isAbsolute && + (parts.hostname !== window.location.hostname || + parts.protocol !== window.location.protocol || + parts.port !== window.location.port) + ) { + return; + } + + // Strip the protocol, hostnname, port, and protocol slashes to normalize + parts.protocol = null; + parts.hostname = null; + parts.port = null; + parts.slashes = false; + + // Replace the basePath if present in the pathname + if (parts.pathname === this.basePath) { + parts.pathname = '/'; + } else if (parts.pathname?.startsWith(`${this.basePath}/`)) { + parts.pathname = parts.pathname?.slice(this.basePath.length); + } + }); + + t.name = `${method} ${normalizedUrl}`; + }); + } } diff --git a/src/core/public/kbn_bootstrap.ts b/src/core/public/kbn_bootstrap.ts index a083196004cf..4536826a4a26 100644 --- a/src/core/public/kbn_bootstrap.ts +++ b/src/core/public/kbn_bootstrap.ts @@ -28,7 +28,7 @@ export async function __kbnBootstrap__() { ); let i18nError: Error | undefined; - const apmSystem = new ApmSystem(injectedMetadata.vars.apmConfig); + const apmSystem = new ApmSystem(injectedMetadata.vars.apmConfig, injectedMetadata.basePath); await Promise.all([ // eslint-disable-next-line no-console 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 86a02d74dea1..d6a4224d9fab 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/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 755269d1a31b..3f7d05e8692c 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 3cc1a8a1dffe..d89c38f297e8 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 bff0236c802f..6a0b71d8250b 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 000000000000..c6f223fa45c2 --- /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 000000000000..8bc81b3296c3 --- /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 b4178fd40c76..0f61a74cd703 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 e2a6ec7dd394..f5cf8b4e866a 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 SavedObjectDashboard { - const SavedObjectClass = createSavedObjectClass(services); - class SavedDashboard extends SavedObjectClass { + class SavedDashboard extends savedObjectStart.SavedObjectClass { // save these objects with the 'dashboard' type public static type = 'dashboard'; diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts index 3bd4d66a693b..750fec4d4d1f 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts +++ b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts @@ -17,23 +17,19 @@ * under the License. */ -import { SavedObjectsClientContract, ChromeStart, OverlayStart } from 'kibana/public'; -import { DataPublicPluginStart, IndexPatternsContract } from '../../../../plugins/data/public'; -import { SavedObjectLoader } from '../../../../plugins/saved_objects/public'; +import { SavedObjectsClientContract } from 'kibana/public'; +import { SavedObjectLoader, SavedObjectsStart } from '../../../../plugins/saved_objects/public'; import { createSavedDashboardClass } from './saved_dashboard'; interface Services { savedObjectsClient: SavedObjectsClientContract; - indexPatterns: IndexPatternsContract; - search: DataPublicPluginStart['search']; - chrome: ChromeStart; - overlays: OverlayStart; + savedObjects: SavedObjectsStart; } /** * @param services */ -export function createSavedDashboardLoader(services: Services) { - const SavedDashboard = createSavedDashboardClass(services); - return new SavedObjectLoader(SavedDashboard, services.savedObjectsClient); +export function createSavedDashboardLoader({ savedObjects, savedObjectsClient }: Services) { + const SavedDashboard = createSavedDashboardClass(savedObjects); + return new SavedObjectLoader(SavedDashboard, savedObjectsClient); } diff --git a/src/plugins/data/common/es_query/filters/get_filter_params.ts b/src/plugins/data/common/es_query/filters/get_filter_params.ts index 2e90ff0fe069..040bb5b70f7a 100644 --- a/src/plugins/data/common/es_query/filters/get_filter_params.ts +++ b/src/plugins/data/common/es_query/filters/get_filter_params.ts @@ -26,9 +26,10 @@ export function getFilterParams(filter: Filter) { case FILTERS.PHRASES: return (filter as PhrasesFilter).meta.params; case FILTERS.RANGE: + const { gte, gt, lte, lt } = (filter as RangeFilter).meta.params; return { - from: (filter as RangeFilter).meta.params.gte, - to: (filter as RangeFilter).meta.params.lt, + from: gte ?? gt, + to: lt ?? lte, }; } } diff --git a/src/plugins/data/common/search/aggs/agg_config.ts b/src/plugins/data/common/search/aggs/agg_config.ts index 201e9f1ec402..910c79f5dd0d 100644 --- a/src/plugins/data/common/search/aggs/agg_config.ts +++ b/src/plugins/data/common/search/aggs/agg_config.ts @@ -400,6 +400,15 @@ export class AggConfig { return this.params.field; } + /** + * Returns the bucket path containing the main value the agg will produce + * (e.g. for sum of bytes it will point to the sum, for median it will point + * to the 50 percentile in the percentile multi value bucket) + */ + getValueBucketPath() { + return this.type.getValueBucketPath(this); + } + makeLabel(percentageMode = false) { if (this.params.customLabel) { return this.params.customLabel; diff --git a/src/plugins/data/common/search/aggs/agg_type.ts b/src/plugins/data/common/search/aggs/agg_type.ts index 1e3839038b0f..3ffac0c12eb2 100644 --- a/src/plugins/data/common/search/aggs/agg_type.ts +++ b/src/plugins/data/common/search/aggs/agg_type.ts @@ -60,6 +60,7 @@ export interface AggTypeConfig< getSerializedFormat?: (agg: TAggConfig) => SerializedFieldFormat; getValue?: (agg: TAggConfig, bucket: any) => any; getKey?: (bucket: any, key: any, agg: TAggConfig) => any; + getValueBucketPath?: (agg: TAggConfig) => string; } // TODO need to make a more explicit interface for this @@ -210,6 +211,10 @@ export class AggType< return this.params.find((p: TParam) => p.name === name); }; + getValueBucketPath = (agg: TAggConfig) => { + return agg.id; + }; + /** * Generic AggType Constructor * @@ -233,6 +238,10 @@ export class AggType< this.createFilter = config.createFilter; } + if (config.getValueBucketPath) { + this.getValueBucketPath = config.getValueBucketPath; + } + if (config.params && config.params.length && config.params[0] instanceof BaseParamType) { this.params = config.params as TParam[]; } else { 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 b53ae44c0507..ead88f924731 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 8dea33a450c5..bea8e577b21f 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 169b23484527..bdb6ea7cd4b9 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 cd781f7e082a..43fdc20e53f5 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/buckets/terms.test.ts b/src/plugins/data/common/search/aggs/buckets/terms.test.ts index 2c5be00c8afe..8f645b4712c7 100644 --- a/src/plugins/data/common/search/aggs/buckets/terms.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/terms.test.ts @@ -18,6 +18,7 @@ */ import { AggConfigs } from '../agg_configs'; +import { METRIC_TYPES } from '../metrics'; import { mockAggTypesRegistry } from '../test_helpers'; import { BUCKET_TYPES } from './bucket_agg_types'; @@ -133,5 +134,49 @@ describe('Terms Agg', () => { expect(params.include).toStrictEqual([1.1, 2, 3.33]); expect(params.exclude).toStrictEqual([4, 5.555, 6]); }); + + test('uses correct bucket path for sorting by median', () => { + const indexPattern = { + id: '1234', + title: 'logstash-*', + fields: { + getByName: () => field, + filter: () => [field], + }, + } as any; + + const field = { + name: 'field', + indexPattern, + }; + + const aggConfigs = new AggConfigs( + indexPattern, + [ + { + id: 'test', + params: { + field: { + name: 'string_field', + type: 'string', + }, + orderAgg: { + type: METRIC_TYPES.MEDIAN, + params: { + field: { + name: 'number_field', + type: 'number', + }, + }, + }, + }, + type: BUCKET_TYPES.TERMS, + }, + ], + { typesRegistry: mockAggTypesRegistry() } + ); + const { [BUCKET_TYPES.TERMS]: params } = aggConfigs.aggs[0].toDsl(); + expect(params.order).toEqual({ 'test-orderAgg.50': 'desc' }); + }); }); }); diff --git a/src/plugins/data/common/search/aggs/buckets/terms.ts b/src/plugins/data/common/search/aggs/buckets/terms.ts index 1363d38748c8..3d543e6c5f57 100644 --- a/src/plugins/data/common/search/aggs/buckets/terms.ts +++ b/src/plugins/data/common/search/aggs/buckets/terms.ts @@ -41,7 +41,6 @@ import { export const termsAggFilter = [ '!top_hits', '!percentiles', - '!median', '!std_dev', '!derivative', '!moving_avg', @@ -198,14 +197,14 @@ export const getTermsBucketAgg = () => return; } - const orderAggId = orderAgg.id; + const orderAggPath = orderAgg.getValueBucketPath(); if (orderAgg.parentId && aggs) { orderAgg = aggs.byId(orderAgg.parentId); } output.subAggs = (output.subAggs || []).concat(orderAgg); - order[orderAggId] = dir; + order[orderAggPath] = dir; }, }, { diff --git a/src/plugins/data/common/search/aggs/metrics/median.test.ts b/src/plugins/data/common/search/aggs/metrics/median.test.ts index f3f2d157ebaf..42298586cb68 100644 --- a/src/plugins/data/common/search/aggs/metrics/median.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/median.test.ts @@ -63,6 +63,12 @@ describe('AggTypeMetricMedianProvider class', () => { expect(dsl.median.percentiles.percents).toEqual([50]); }); + it('points to right value within multi metric for value bucket path', () => { + expect(aggConfigs.byId(METRIC_TYPES.MEDIAN)!.getValueBucketPath()).toEqual( + `${METRIC_TYPES.MEDIAN}.50` + ); + }); + it('converts the response', () => { const agg = aggConfigs.getResponseAggs()[0]; diff --git a/src/plugins/data/common/search/aggs/metrics/median.ts b/src/plugins/data/common/search/aggs/metrics/median.ts index 7b48a664b5fb..a18946102091 100644 --- a/src/plugins/data/common/search/aggs/metrics/median.ts +++ b/src/plugins/data/common/search/aggs/metrics/median.ts @@ -42,6 +42,9 @@ export const getMedianMetricAgg = () => { values: { field: aggConfig.getFieldDisplayName() }, }); }, + getValueBucketPath(aggConfig) { + return `${aggConfig.id}.50`; + }, params: [ { name: 'field', 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 20d8cfc105e4..28646c092c01 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 01369206ab3c..a8134619fec0 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/public/public.api.md b/src/plugins/data/public/public.api.md index 050f1d917f2c..2ed3e440040d 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -128,6 +128,7 @@ export class AggConfig { getTimeRange(): import("../../../public").TimeRange | undefined; // (undocumented) getValue(bucket: any): any; + getValueBucketPath(): string; // (undocumented) id: string; // (undocumented) diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 2d582b30bcd1..734e88e08566 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -127,7 +127,7 @@ export class SearchService implements Plugin { request: SearchStrategyRequest, options: ISearchOptions ) => { - return search(request, options).toPromise() as Promise; + return search(request, options).toPromise(); }, onResponse: handleResponse, legacy: { diff --git a/src/plugins/data/public/ui/filter_bar/_variables.scss b/src/plugins/data/public/ui/filter_bar/_variables.scss index 3a9a0df4332c..efe2e28ac3b8 100644 --- a/src/plugins/data/public/ui/filter_bar/_variables.scss +++ b/src/plugins/data/public/ui/filter_bar/_variables.scss @@ -1,3 +1,4 @@ $kbnGlobalFilterItemBorderColor: tintOrShade($euiColorMediumShade, 35%, 20%); $kbnGlobalFilterItemBorderColorExcluded: tintOrShade($euiColorDanger, 70%, 50%); $kbnGlobalFilterItemPinnedColorExcluded: tintOrShade($euiColorDanger, 30%, 20%); +$kbnGlobalFilterItemEditorWidth: 420px; // if changing this make sure to also change `FILTER_EDITOR_WIDTH` in ./filter_item.tsx diff --git a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx index fdd952e2207d..0d544ac9ad16 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx @@ -23,7 +23,7 @@ import classNames from 'classnames'; import React, { useState } from 'react'; import { FilterEditor } from './filter_editor'; -import { FilterItem } from './filter_item'; +import { FILTER_EDITOR_WIDTH, FilterItem } from './filter_item'; import { FilterOptions } from './filter_options'; import { useKibana } from '../../../../kibana_react/public'; import { IIndexPattern } from '../..'; @@ -112,7 +112,7 @@ function FilterBarUI(props: Props) { repositionOnScroll > -
+
{ private renderRegularEditor() { return (
- + {this.renderFieldInput()} {this.renderOperatorInput()} diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx index cbff20115f8e..018f41ab82bf 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -62,6 +62,13 @@ export type FilterLabelStatus = | typeof FILTER_ITEM_WARNING | typeof FILTER_ITEM_ERROR; +/** + * @remarks + * if changing this make sure to also change + * $kbnGlobalFilterItemEditorWidth + */ +export const FILTER_EDITOR_WIDTH = 420; + export function FilterItem(props: Props) { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [indexPatternExists, setIndexPatternExists] = useState(undefined); @@ -228,7 +235,7 @@ export function FilterItem(props: Props) { }, { id: 1, - width: 420, + width: FILTER_EDITOR_WIDTH, content: (
({ DEFAULT_QUERY_LANGUAGE: 'lucene', @@ -29,6 +31,8 @@ jest.mock('../../../common', () => ({ let fetch: ReturnType; let callCluster: LegacyAPICaller; +let collectorFetchContext: CollectorFetchContext; +const collectorFetchContextMock = createCollectorFetchContextMock(); function setupMockCallCluster( optCount: { optInCount?: number; optOutCount?: number } | null, @@ -89,40 +93,64 @@ describe('makeKQLUsageCollector', () => { it('should return opt in data from the .kibana/kql-telemetry doc', async () => { setupMockCallCluster({ optInCount: 1 }, 'kuery'); - const fetchResponse = await fetch(callCluster); + collectorFetchContext = { + ...collectorFetchContextMock, + callCluster, + }; + const fetchResponse = await fetch(collectorFetchContext); expect(fetchResponse.optInCount).toBe(1); expect(fetchResponse.optOutCount).toBe(0); }); it('should return the default query language set in advanced settings', async () => { setupMockCallCluster({ optInCount: 1 }, 'kuery'); - const fetchResponse = await fetch(callCluster); + collectorFetchContext = { + ...collectorFetchContextMock, + callCluster, + }; + const fetchResponse = await fetch(collectorFetchContext); expect(fetchResponse.defaultQueryLanguage).toBe('kuery'); }); // Indicates the user has modified the setting at some point but the value is currently the default it('should return the kibana default query language if the config value is null', async () => { setupMockCallCluster({ optInCount: 1 }, null); - const fetchResponse = await fetch(callCluster); + collectorFetchContext = { + ...collectorFetchContextMock, + callCluster, + }; + const fetchResponse = await fetch(collectorFetchContext); expect(fetchResponse.defaultQueryLanguage).toBe('lucene'); }); it('should indicate when the default language has never been modified by the user', async () => { setupMockCallCluster({ optInCount: 1 }, undefined); - const fetchResponse = await fetch(callCluster); + collectorFetchContext = { + ...collectorFetchContextMock, + callCluster, + }; + const fetchResponse = await fetch(collectorFetchContext); expect(fetchResponse.defaultQueryLanguage).toBe('default-lucene'); }); it('should default to 0 opt in counts if the .kibana/kql-telemetry doc does not exist', async () => { setupMockCallCluster(null, 'kuery'); - const fetchResponse = await fetch(callCluster); + collectorFetchContext = { + ...collectorFetchContextMock, + callCluster, + }; + const fetchResponse = await fetch(collectorFetchContext); expect(fetchResponse.optInCount).toBe(0); expect(fetchResponse.optOutCount).toBe(0); }); it('should default to the kibana default language if the config document does not exist', async () => { setupMockCallCluster(null, 'missingConfigDoc'); - const fetchResponse = await fetch(callCluster); + collectorFetchContext = { + ...collectorFetchContextMock, + callCluster, + }; + const fetchResponse = await fetch(collectorFetchContext); expect(fetchResponse.defaultQueryLanguage).toBe('default-lucene'); }); }); diff --git a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts index 109d6f812334..21a1843d1ec8 100644 --- a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts +++ b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts @@ -18,7 +18,7 @@ */ import { get } from 'lodash'; -import { LegacyAPICaller } from 'kibana/server'; +import { CollectorFetchContext } from 'src/plugins/usage_collection/server'; import { DEFAULT_QUERY_LANGUAGE, UI_SETTINGS } from '../../../common'; const defaultSearchQueryLanguageSetting = DEFAULT_QUERY_LANGUAGE; @@ -30,7 +30,7 @@ export interface Usage { } export function fetchProvider(index: string) { - return async (callCluster: LegacyAPICaller): Promise => { + return async ({ callCluster }: CollectorFetchContext): Promise => { const [response, config] = await Promise.all([ callCluster('get', { index, diff --git a/src/plugins/data/server/search/collectors/fetch.ts b/src/plugins/data/server/search/collectors/fetch.ts index 3551767eab01..344bc18c7b4b 100644 --- a/src/plugins/data/server/search/collectors/fetch.ts +++ b/src/plugins/data/server/search/collectors/fetch.ts @@ -19,7 +19,8 @@ import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; -import { LegacyAPICaller, SharedGlobalConfig } from 'kibana/server'; +import { SharedGlobalConfig } from 'kibana/server'; +import { CollectorFetchContext } from 'src/plugins/usage_collection/server'; import { Usage } from './register'; interface SearchTelemetrySavedObject { @@ -27,7 +28,7 @@ interface SearchTelemetrySavedObject { } export function fetchProvider(config$: Observable) { - return async (callCluster: LegacyAPICaller): Promise => { + return async ({ callCluster }: CollectorFetchContext): Promise => { const config = await config$.pipe(first()).toPromise(); const response = await callCluster('search', { diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts index 504ce728481f..2dbcc3196aa7 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts @@ -35,7 +35,8 @@ describe('ES search strategy', () => { }, }, }); - const mockContext = { + + const mockContext = ({ core: { uiSettings: { client: { @@ -44,7 +45,8 @@ describe('ES search strategy', () => { }, elasticsearch: { client: { asCurrentUser: { search: mockApiCaller } } }, }, - }; + } as unknown) as RequestHandlerContext; + const mockConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$; beforeEach(() => { @@ -57,44 +59,51 @@ describe('ES search strategy', () => { expect(typeof esSearch.search).toBe('function'); }); - it('calls the API caller with the params with defaults', async () => { + it('calls the API caller with the params with defaults', async (done) => { const params = { index: 'logstash-*' }; - const esSearch = await esSearchStrategyProvider(mockConfig$, mockLogger); - await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params }); - - expect(mockApiCaller).toBeCalled(); - expect(mockApiCaller.mock.calls[0][0]).toEqual({ - ...params, - ignore_unavailable: true, - track_total_hits: true, - }); + await esSearchStrategyProvider(mockConfig$, mockLogger) + .search({ params }, {}, mockContext) + .subscribe(() => { + expect(mockApiCaller).toBeCalled(); + expect(mockApiCaller.mock.calls[0][0]).toEqual({ + ...params, + ignore_unavailable: true, + track_total_hits: true, + }); + done(); + }); }); - it('calls the API caller with overridden defaults', async () => { + it('calls the API caller with overridden defaults', async (done) => { const params = { index: 'logstash-*', ignore_unavailable: false, timeout: '1000ms' }; - const esSearch = await esSearchStrategyProvider(mockConfig$, mockLogger); - - await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params }); - expect(mockApiCaller).toBeCalled(); - expect(mockApiCaller.mock.calls[0][0]).toEqual({ - ...params, - track_total_hits: true, - }); + await esSearchStrategyProvider(mockConfig$, mockLogger) + .search({ params }, {}, mockContext) + .subscribe(() => { + expect(mockApiCaller).toBeCalled(); + expect(mockApiCaller.mock.calls[0][0]).toEqual({ + ...params, + track_total_hits: true, + }); + done(); + }); }); - it('has all response parameters', async () => { - const params = { index: 'logstash-*' }; - const esSearch = await esSearchStrategyProvider(mockConfig$, mockLogger); - - const response = await esSearch.search((mockContext as unknown) as RequestHandlerContext, { - params, - }); - - expect(response.isRunning).toBe(false); - expect(response.isPartial).toBe(false); - expect(response).toHaveProperty('loaded'); - expect(response).toHaveProperty('rawResponse'); - }); + it('has all response parameters', async (done) => + await esSearchStrategyProvider(mockConfig$, mockLogger) + .search( + { + params: { index: 'logstash-*' }, + }, + {}, + mockContext + ) + .subscribe((data) => { + expect(data.isRunning).toBe(false); + expect(data.isPartial).toBe(false); + expect(data).toHaveProperty('loaded'); + expect(data).toHaveProperty('rawResponse'); + done(); + })); }); diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.ts b/src/plugins/data/server/search/es_search/es_search_strategy.ts index 6e185d30ad56..92cc941e1485 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.ts @@ -16,10 +16,10 @@ * specific language governing permissions and limitations * under the License. */ +import { Observable, from } from 'rxjs'; import { first } from 'rxjs/operators'; import { SharedGlobalConfig, Logger } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; -import { Observable } from 'rxjs'; import { ApiResponse } from '@elastic/elasticsearch'; import { SearchUsage } from '../collectors/usage'; import { toSnakeCase } from './to_snake_case'; @@ -29,6 +29,7 @@ import { getTotalLoaded, getShardTimeout, shimAbortSignal, + IEsSearchResponse, } from '..'; export const esSearchStrategyProvider = ( @@ -37,47 +38,52 @@ export const esSearchStrategyProvider = ( usage?: SearchUsage ): ISearchStrategy => { return { - search: async (context, request, options) => { - logger.debug(`search ${request.params?.index}`); - const config = await config$.pipe(first()).toPromise(); - const uiSettingsClient = await context.core.uiSettings.client; + search: (request, options, context) => + from( + new Promise(async (resolve, reject) => { + logger.debug(`search ${request.params?.index}`); + const config = await config$.pipe(first()).toPromise(); + const uiSettingsClient = await context.core.uiSettings.client; - // Only default index pattern type is supported here. - // See data_enhanced for other type support. - if (!!request.indexType) { - throw new Error(`Unsupported index pattern type ${request.indexType}`); - } + // Only default index pattern type is supported here. + // See data_enhanced for other type support. + if (!!request.indexType) { + throw new Error(`Unsupported index pattern type ${request.indexType}`); + } - // ignoreThrottled is not supported in OSS - const { ignoreThrottled, ...defaultParams } = await getDefaultSearchParams(uiSettingsClient); + // ignoreThrottled is not supported in OSS + const { ignoreThrottled, ...defaultParams } = await getDefaultSearchParams( + uiSettingsClient + ); - const params = toSnakeCase({ - ...defaultParams, - ...getShardTimeout(config), - ...request.params, - }); + const params = toSnakeCase({ + ...defaultParams, + ...getShardTimeout(config), + ...request.params, + }); - try { - const promise = shimAbortSignal( - context.core.elasticsearch.client.asCurrentUser.search(params), - options?.abortSignal - ); - const { body: rawResponse } = (await promise) as ApiResponse>; + try { + const promise = shimAbortSignal( + context.core.elasticsearch.client.asCurrentUser.search(params), + options?.abortSignal + ); + const { body: rawResponse } = (await promise) as ApiResponse>; - if (usage) usage.trackSuccess(rawResponse.took); + if (usage) usage.trackSuccess(rawResponse.took); - // The above query will either complete or timeout and throw an error. - // There is no progress indication on this api. - return { - isPartial: false, - isRunning: false, - rawResponse, - ...getTotalLoaded(rawResponse._shards), - }; - } catch (e) { - if (usage) usage.trackError(); - throw e; - } - }, + // The above query will either complete or timeout and throw an error. + // There is no progress indication on this api. + resolve({ + isPartial: false, + isRunning: false, + rawResponse, + ...getTotalLoaded(rawResponse._shards), + }); + } catch (e) { + if (usage) usage.trackError(); + reject(e); + } + }) + ), }; }; diff --git a/src/plugins/data/server/search/routes/search.test.ts b/src/plugins/data/server/search/routes/search.test.ts index d4404c318ab4..834e5de5c312 100644 --- a/src/plugins/data/server/search/routes/search.test.ts +++ b/src/plugins/data/server/search/routes/search.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Observable } from 'rxjs'; +import { Observable, from } from 'rxjs'; import { CoreSetup, @@ -66,7 +66,8 @@ describe('Search service', () => { }, }, }; - mockDataStart.search.search.mockResolvedValue(response); + + mockDataStart.search.search.mockReturnValue(from(Promise.resolve(response))); const mockContext = {}; const mockBody = { id: undefined, params: {} }; const mockParams = { strategy: 'foo' }; @@ -83,7 +84,7 @@ describe('Search service', () => { await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); expect(mockDataStart.search.search).toBeCalled(); - expect(mockDataStart.search.search.mock.calls[0][1]).toStrictEqual(mockBody); + expect(mockDataStart.search.search.mock.calls[0][0]).toStrictEqual(mockBody); expect(mockResponse.ok).toBeCalled(); expect(mockResponse.ok.mock.calls[0][0]).toEqual({ body: response, @@ -91,12 +92,16 @@ describe('Search service', () => { }); it('handler throws an error if the search throws an error', async () => { - mockDataStart.search.search.mockRejectedValue({ - message: 'oh no', - body: { - error: 'oops', - }, - }); + const rejectedValue = from( + Promise.reject({ + message: 'oh no', + body: { + error: 'oops', + }, + }) + ); + + mockDataStart.search.search.mockReturnValue(rejectedValue); const mockContext = {}; const mockBody = { id: undefined, params: {} }; @@ -114,7 +119,7 @@ describe('Search service', () => { await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); expect(mockDataStart.search.search).toBeCalled(); - expect(mockDataStart.search.search.mock.calls[0][1]).toStrictEqual(mockBody); + expect(mockDataStart.search.search.mock.calls[0][0]).toStrictEqual(mockBody); expect(mockResponse.customError).toBeCalled(); const error: any = mockResponse.customError.mock.calls[0][0]; expect(error.body.message).toBe('oh no'); diff --git a/src/plugins/data/server/search/routes/search.ts b/src/plugins/data/server/search/routes/search.ts index 492ad4395b32..1e8433d9685e 100644 --- a/src/plugins/data/server/search/routes/search.ts +++ b/src/plugins/data/server/search/routes/search.ts @@ -49,14 +49,16 @@ export function registerSearchRoute( const [, , selfStart] = await getStartServices(); try { - const response = await selfStart.search.search( - context, - { ...searchRequest, id }, - { - abortSignal, - strategy, - } - ); + const response = await selfStart.search + .search( + { ...searchRequest, id }, + { + abortSignal, + strategy, + }, + context + ) + .toPromise(); return res.ok({ body: { diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 6e66f8027207..0130d3aacc91 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -49,10 +49,10 @@ import { IKibanaSearchResponse, IEsSearchRequest, IEsSearchResponse, - ISearchOptions, SearchSourceDependencies, SearchSourceService, searchSourceRequiredUiSettings, + ISearchOptions, } from '../../common/search'; import { getShardDelayBucketAgg, @@ -151,13 +151,7 @@ export class SearchService implements Plugin { return { aggs: this.aggsService.start({ fieldFormats, uiSettings }), getSearchStrategy: this.getSearchStrategy, - search: ( - context: RequestHandlerContext, - searchRequest: IKibanaSearchRequest, - options: Record - ) => { - return this.search(context, searchRequest, options); - }, + search: this.search.bind(this), searchSource: { asScoped: async (request: KibanaRequest) => { const esClient = elasticsearch.client.asScoped(request); @@ -175,7 +169,13 @@ export class SearchService implements Plugin { const searchSourceDependencies: SearchSourceDependencies = { getConfig: (key: string): T => uiSettingsCache[key], - search: (searchRequest, options) => { + search: < + SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, + SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse + >( + searchStrategyRequest: SearchStrategyRequest, + options: ISearchOptions + ) => { /** * Unless we want all SearchSource users to provide both a KibanaRequest * (needed for index patterns) AND the RequestHandlerContext (needed for @@ -195,7 +195,12 @@ export class SearchService implements Plugin { }, }, } as RequestHandlerContext; - return this.search(fakeRequestHandlerContext, searchRequest, options); + + return this.search( + searchStrategyRequest, + options, + fakeRequestHandlerContext + ).toPromise(); }, // onResponse isn't used on the server, so we just return the original value onResponse: (req, res) => res, @@ -234,13 +239,15 @@ export class SearchService implements Plugin { SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse >( - context: RequestHandlerContext, searchRequest: SearchStrategyRequest, - options: ISearchOptions - ): Promise => { - return this.getSearchStrategy( + options: ISearchOptions, + context: RequestHandlerContext + ) => { + const strategy = this.getSearchStrategy( options.strategy || this.defaultSearchStrategyName - ).search(context, searchRequest, options); + ); + + return strategy.search(searchRequest, options, context); }; private getSearchStrategy = < diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index 0de4ef529e89..9ba06d88dc4b 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -17,6 +17,7 @@ * under the License. */ +import { Observable } from 'rxjs'; import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { ISearchOptions, @@ -57,6 +58,22 @@ export interface ISearchSetup { __enhance: (enhancements: SearchEnhancements) => void; } +/** + * Search strategy interface contains a search method that takes in a request and returns a promise + * that resolves to a response. + */ +export interface ISearchStrategy< + SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, + SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse +> { + search: ( + request: SearchStrategyRequest, + options: ISearchOptions, + context: RequestHandlerContext + ) => Observable; + cancel?: (context: RequestHandlerContext, id: string) => Promise; +} + export interface ISearchStart< SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse @@ -69,28 +86,8 @@ export interface ISearchStart< getSearchStrategy: ( name: string ) => ISearchStrategy; - search: ( - context: RequestHandlerContext, - request: SearchStrategyRequest, - options: ISearchOptions - ) => Promise; + search: ISearchStrategy['search']; searchSource: { asScoped: (request: KibanaRequest) => Promise; }; } - -/** - * Search strategy interface contains a search method that takes in a request and returns a promise - * that resolves to a response. - */ -export interface ISearchStrategy< - SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, - SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse -> { - search: ( - context: RequestHandlerContext, - request: SearchStrategyRequest, - options?: ISearchOptions - ) => Promise; - cancel?: (context: RequestHandlerContext, id: string) => Promise; -} diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 07afad1c96a0..0828460830f2 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'; @@ -713,7 +714,7 @@ export interface ISearchStart ISearchStrategy; // (undocumented) - search: (context: RequestHandlerContext, request: SearchStrategyRequest, options: ISearchOptions) => Promise; + search: ISearchStrategy['search']; // (undocumented) searchSource: { asScoped: (request: KibanaRequest) => Promise; @@ -727,7 +728,7 @@ export interface ISearchStrategy Promise; // (undocumented) - search: (context: RequestHandlerContext, request: SearchStrategyRequest, options?: ISearchOptions) => Promise; + search: (request: SearchStrategyRequest, options: ISearchOptions, context: RequestHandlerContext) => Observable; } // @public (undocumented) @@ -1140,7 +1141,7 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:254:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index_patterns/index_patterns_service.ts:50:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:88:66 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/search/types.ts:78:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/search/types.ts:91:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index 1a23f6deb5fa..67c93ad8a406 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -12,13 +12,9 @@ "urlForwarding", "navigation", "uiActions", - "visualizations" + "visualizations", + "savedObjects" ], "optionalPlugins": ["home", "share"], - "requiredBundles": [ - "kibanaUtils", - "home", - "savedObjects", - "kibanaReact" - ] + "requiredBundles": ["kibanaUtils", "home", "kibanaReact"] } diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index fdb14b3f1f63..27844cc2347b 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -37,7 +37,6 @@ import { Start as InspectorPublicPluginStart } from 'src/plugins/inspector/publi import { SharePluginStart } from 'src/plugins/share/public'; import { ChartsPluginStart } from 'src/plugins/charts/public'; import { VisualizationsStart } from 'src/plugins/visualizations/public'; -import { SavedObjectKibanaServices } from 'src/plugins/saved_objects/public'; import { DiscoverStartPlugins } from './plugin'; import { createSavedSearchesLoader, SavedSearch } from './saved_searches'; @@ -78,12 +77,9 @@ export async function buildServices( context: PluginInitializerContext, getEmbeddableInjector: any ): Promise { - const services: SavedObjectKibanaServices = { + const services = { savedObjectsClient: core.savedObjects.client, - indexPatterns: plugins.data.indexPatterns, - search: plugins.data.search, - chrome: core.chrome, - overlays: core.overlays, + savedObjects: plugins.savedObjects, }; const savedObjectService = createSavedSearchesLoader(services); diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index b1bbc89b62d9..11ec4f08d951 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -41,7 +41,7 @@ import { UrlForwardingSetup, UrlForwardingStart } from 'src/plugins/url_forwardi import { HomePublicPluginSetup } from 'src/plugins/home/public'; import { Start as InspectorPublicPluginStart } from 'src/plugins/inspector/public'; import { DataPublicPluginStart, DataPublicPluginSetup, esFilters } from '../../data/public'; -import { SavedObjectLoader } from '../../saved_objects/public'; +import { SavedObjectLoader, SavedObjectsStart } from '../../saved_objects/public'; import { createKbnUrlTracker } from '../../kibana_utils/public'; import { DEFAULT_APP_CATEGORIES } from '../../../core/public'; import { UrlGeneratorState } from '../../share/public'; @@ -141,6 +141,7 @@ export interface DiscoverStartPlugins { urlForwarding: UrlForwardingStart; inspector: InspectorPublicPluginStart; visualizations: VisualizationsStart; + savedObjects: SavedObjectsStart; } const innerAngularName = 'app/discover'; @@ -351,10 +352,7 @@ export class DiscoverPlugin urlGenerator: this.urlGenerator, savedSearchLoader: createSavedSearchesLoader({ savedObjectsClient: core.savedObjects.client, - indexPatterns: plugins.data.indexPatterns, - search: plugins.data.search, - chrome: core.chrome, - overlays: core.overlays, + savedObjects: plugins.savedObjects, }), }; } diff --git a/src/plugins/discover/public/saved_searches/_saved_search.ts b/src/plugins/discover/public/saved_searches/_saved_search.ts index 2b8574a8fa11..1ec4549f05d4 100644 --- a/src/plugins/discover/public/saved_searches/_saved_search.ts +++ b/src/plugins/discover/public/saved_searches/_saved_search.ts @@ -16,16 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { - createSavedObjectClass, - SavedObject, - SavedObjectKibanaServices, -} from '../../../saved_objects/public'; +import { SavedObject, SavedObjectsStart } from '../../../saved_objects/public'; -export function createSavedSearchClass(services: SavedObjectKibanaServices) { - const SavedObjectClass = createSavedObjectClass(services); - - class SavedSearch extends SavedObjectClass { +export function createSavedSearchClass(savedObjects: SavedObjectsStart) { + class SavedSearch extends savedObjects.SavedObjectClass { public static type: string = 'search'; public static mapping = { title: 'text', @@ -70,5 +64,5 @@ export function createSavedSearchClass(services: SavedObjectKibanaServices) { } } - return SavedSearch as new (id: string) => SavedObject; + return (SavedSearch as unknown) as new (id: string) => SavedObject; } diff --git a/src/plugins/discover/public/saved_searches/saved_searches.ts b/src/plugins/discover/public/saved_searches/saved_searches.ts index 0bc332ed8ec7..fd7a185f7012 100644 --- a/src/plugins/discover/public/saved_searches/saved_searches.ts +++ b/src/plugins/discover/public/saved_searches/saved_searches.ts @@ -17,12 +17,18 @@ * under the License. */ -import { SavedObjectLoader, SavedObjectKibanaServices } from '../../../saved_objects/public'; +import { SavedObjectsClientContract } from 'kibana/public'; +import { SavedObjectLoader, SavedObjectsStart } from '../../../saved_objects/public'; import { createSavedSearchClass } from './_saved_search'; -export function createSavedSearchesLoader(services: SavedObjectKibanaServices) { - const SavedSearchClass = createSavedSearchClass(services); - const savedSearchLoader = new SavedObjectLoader(SavedSearchClass, services.savedObjectsClient); +interface Services { + savedObjectsClient: SavedObjectsClientContract; + savedObjects: SavedObjectsStart; +} + +export function createSavedSearchesLoader({ savedObjectsClient, savedObjects }: Services) { + const SavedSearchClass = createSavedSearchClass(savedObjects); + const savedSearchLoader = new SavedObjectLoader(SavedSearchClass, savedObjectsClient); // Customize loader properties since adding an 's' on type doesn't work for type 'search' . savedSearchLoader.loaderProperties = { name: 'searches', diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx index 7c4724a66743..9bcef051a935 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx @@ -68,7 +68,13 @@ function renderNotifications( const context = { embeddable }; let badge = notification.MenuItem ? ( - React.createElement(uiToReactComponent(notification.MenuItem)) + React.createElement(uiToReactComponent(notification.MenuItem), { + key: notification.id, + context: { + embeddable, + trigger: panelNotificationTrigger, + }, + }) ) : ( = { @@ -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/common/util/create_error.ts b/src/plugins/expressions/common/util/create_error.ts index 46306d3fbbf6..293afd46d4de 100644 --- a/src/plugins/expressions/common/util/create_error.ts +++ b/src/plugins/expressions/common/util/create_error.ts @@ -40,6 +40,11 @@ export const createError = (err: string | ErrorLike): ExpressionValueError => ({ : undefined, message: typeof err === 'string' ? err : String(err.message), name: typeof err === 'object' ? err.name || 'Error' : 'Error', - original: err instanceof Error ? (err as SerializedError) : undefined, + original: + err instanceof Error + ? err + : typeof err === 'object' && 'original' in err && err.original instanceof Error + ? err.original + : undefined, }, }); diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 355bd502df3b..c7b6190b96ed 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -511,6 +511,8 @@ export class ExpressionRendererRegistry implements IRegistry // // @public (undocumented) export interface ExpressionRenderError extends Error { + // (undocumented) + original?: Error; // (undocumented) type?: string; } @@ -1067,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 // @@ -1095,7 +1099,7 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams { padding?: 'xs' | 's' | 'm' | 'l' | 'xl'; reload$?: Observable; // (undocumented) - renderError?: (error?: string | null) => React.ReactElement | React.ReactElement[]; + renderError?: (message?: string | null, error?: ExpressionRenderError | null) => React.ReactElement | React.ReactElement[]; } // Warning: (ae-missing-release-tag) "ReactExpressionRendererType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/src/plugins/expressions/public/react_expression_renderer.tsx b/src/plugins/expressions/public/react_expression_renderer.tsx index 12476c70044b..99d170c96666 100644 --- a/src/plugins/expressions/public/react_expression_renderer.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.tsx @@ -35,7 +35,10 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams { className?: string; dataAttrs?: string[]; expression: string | ExpressionAstExpression; - renderError?: (error?: string | null) => React.ReactElement | React.ReactElement[]; + renderError?: ( + message?: string | null, + error?: ExpressionRenderError | null + ) => React.ReactElement | React.ReactElement[]; padding?: 'xs' | 's' | 'm' | 'l' | 'xl'; onEvent?: (event: ExpressionRendererEvent) => void; /** @@ -186,7 +189,10 @@ export const ReactExpressionRenderer = ({
{state.isEmpty && } {state.isLoading && } - {!state.isLoading && state.error && renderError && renderError(state.error.message)} + {!state.isLoading && + state.error && + renderError && + renderError(state.error.message, state.error)}
{ - let callClusterMock: sinon.SinonStub; +const getMockFetchClients = (hits?: unknown[]) => { + const fetchParamsMock = createCollectorFetchContextMock(); + fetchParamsMock.callCluster.mockResolvedValue({ hits: { hits } }); + return fetchParamsMock; +}; - beforeEach(() => { - callClusterMock = sinon.stub(); - }); +describe('Sample Data Fetch', () => { + let collectorFetchContext: CollectorFetchContext; test('uninitialized .kibana', async () => { const fetch = fetchProvider('index'); - const telemetry = await fetch(callClusterMock); + collectorFetchContext = getMockFetchClients(); + const telemetry = await fetch(collectorFetchContext); expect(telemetry).toMatchInlineSnapshot(`undefined`); }); test('installed data set', async () => { const fetch = fetchProvider('index'); - callClusterMock.returns({ - hits: { - hits: [ - { - _id: 'sample-data-telemetry:test1', - _source: { - updated_at: '2019-03-13T22:02:09Z', - 'sample-data-telemetry': { installCount: 1 }, - }, - }, - ], + collectorFetchContext = getMockFetchClients([ + { + _id: 'sample-data-telemetry:test1', + _source: { + updated_at: '2019-03-13T22:02:09Z', + 'sample-data-telemetry': { installCount: 1 }, + }, }, - }); - const telemetry = await fetch(callClusterMock); + ]); + const telemetry = await fetch(collectorFetchContext); expect(telemetry).toMatchInlineSnapshot(` Object { @@ -67,27 +67,23 @@ Object { test('multiple installed data sets', async () => { const fetch = fetchProvider('index'); - callClusterMock.returns({ - hits: { - hits: [ - { - _id: 'sample-data-telemetry:test1', - _source: { - updated_at: '2019-03-13T22:02:09Z', - 'sample-data-telemetry': { installCount: 1 }, - }, - }, - { - _id: 'sample-data-telemetry:test2', - _source: { - updated_at: '2019-03-13T22:13:17Z', - 'sample-data-telemetry': { installCount: 1 }, - }, - }, - ], + collectorFetchContext = getMockFetchClients([ + { + _id: 'sample-data-telemetry:test1', + _source: { + updated_at: '2019-03-13T22:02:09Z', + 'sample-data-telemetry': { installCount: 1 }, + }, }, - }); - const telemetry = await fetch(callClusterMock); + { + _id: 'sample-data-telemetry:test2', + _source: { + updated_at: '2019-03-13T22:13:17Z', + 'sample-data-telemetry': { installCount: 1 }, + }, + }, + ]); + const telemetry = await fetch(collectorFetchContext); expect(telemetry).toMatchInlineSnapshot(` Object { @@ -106,17 +102,13 @@ Object { test('installed data set, missing counts', async () => { const fetch = fetchProvider('index'); - callClusterMock.returns({ - hits: { - hits: [ - { - _id: 'sample-data-telemetry:test1', - _source: { updated_at: '2019-03-13T22:02:09Z', 'sample-data-telemetry': {} }, - }, - ], + collectorFetchContext = getMockFetchClients([ + { + _id: 'sample-data-telemetry:test1', + _source: { updated_at: '2019-03-13T22:02:09Z', 'sample-data-telemetry': {} }, }, - }); - const telemetry = await fetch(callClusterMock); + ]); + const telemetry = await fetch(collectorFetchContext); expect(telemetry).toMatchInlineSnapshot(` Object { @@ -132,34 +124,30 @@ Object { test('installed and uninstalled data sets', async () => { const fetch = fetchProvider('index'); - callClusterMock.returns({ - hits: { - hits: [ - { - _id: 'sample-data-telemetry:test0', - _source: { - updated_at: '2019-03-13T22:29:32Z', - 'sample-data-telemetry': { installCount: 4, unInstallCount: 4 }, - }, - }, - { - _id: 'sample-data-telemetry:test1', - _source: { - updated_at: '2019-03-13T22:02:09Z', - 'sample-data-telemetry': { installCount: 1 }, - }, - }, - { - _id: 'sample-data-telemetry:test2', - _source: { - updated_at: '2019-03-13T22:13:17Z', - 'sample-data-telemetry': { installCount: 1 }, - }, - }, - ], + collectorFetchContext = getMockFetchClients([ + { + _id: 'sample-data-telemetry:test0', + _source: { + updated_at: '2019-03-13T22:29:32Z', + 'sample-data-telemetry': { installCount: 4, unInstallCount: 4 }, + }, + }, + { + _id: 'sample-data-telemetry:test1', + _source: { + updated_at: '2019-03-13T22:02:09Z', + 'sample-data-telemetry': { installCount: 1 }, + }, + }, + { + _id: 'sample-data-telemetry:test2', + _source: { + updated_at: '2019-03-13T22:13:17Z', + 'sample-data-telemetry': { installCount: 1 }, + }, }, - }); - const telemetry = await fetch(callClusterMock); + ]); + const telemetry = await fetch(collectorFetchContext); expect(telemetry).toMatchInlineSnapshot(` Object { diff --git a/src/plugins/home/server/services/sample_data/usage/collector_fetch.ts b/src/plugins/home/server/services/sample_data/usage/collector_fetch.ts index d43458cfc64d..7df9b14d2efb 100644 --- a/src/plugins/home/server/services/sample_data/usage/collector_fetch.ts +++ b/src/plugins/home/server/services/sample_data/usage/collector_fetch.ts @@ -19,6 +19,7 @@ import { get } from 'lodash'; import moment from 'moment'; +import { CollectorFetchContext } from '../../../../../usage_collection/server'; interface SearchHit { _id: string; @@ -41,7 +42,7 @@ export interface TelemetryResponse { } export function fetchProvider(index: string) { - return async (callCluster: any) => { + return async ({ callCluster }: CollectorFetchContext) => { const response = await callCluster('search', { index, body: { diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts index 23a77c2d4c28..c1457c64080a 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts @@ -17,16 +17,13 @@ * under the License. */ -import { - savedObjectsRepositoryMock, - loggingSystemMock, - elasticsearchServiceMock, -} from '../../../../../core/server/mocks'; +import { savedObjectsRepositoryMock, loggingSystemMock } from '../../../../../core/server/mocks'; import { CollectorOptions, createUsageCollectionSetupMock, } from '../../../../usage_collection/server/usage_collection.mock'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; import { ROLL_INDICES_START, ROLL_TOTAL_INDICES_INTERVAL, @@ -53,8 +50,7 @@ describe('telemetry_application_usage', () => { const getUsageCollector = jest.fn(); const registerType = jest.fn(); - const callCluster = jest.fn(); - const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const mockedFetchContext = createCollectorFetchContextMock(); beforeAll(() => registerApplicationUsageCollector(logger, usageCollectionMock, registerType, getUsageCollector) @@ -67,7 +63,7 @@ describe('telemetry_application_usage', () => { test('if no savedObjectClient initialised, return undefined', async () => { expect(collector.isReady()).toBe(false); - expect(await collector.fetch(callCluster, esClient)).toBeUndefined(); + expect(await collector.fetch(mockedFetchContext)).toBeUndefined(); jest.runTimersToTime(ROLL_INDICES_START); }); @@ -85,7 +81,7 @@ describe('telemetry_application_usage', () => { jest.runTimersToTime(ROLL_TOTAL_INDICES_INTERVAL); // Force rollTotals to run expect(collector.isReady()).toBe(true); - expect(await collector.fetch(callCluster, esClient)).toStrictEqual({}); + expect(await collector.fetch(mockedFetchContext)).toStrictEqual({}); expect(savedObjectClient.bulkCreate).not.toHaveBeenCalled(); }); @@ -142,7 +138,7 @@ describe('telemetry_application_usage', () => { jest.runTimersToTime(ROLL_TOTAL_INDICES_INTERVAL); // Force rollTotals to run - expect(await collector.fetch(callCluster, esClient)).toStrictEqual({ + expect(await collector.fetch(mockedFetchContext)).toStrictEqual({ appId: { clicks_total: total + 1 + 10, clicks_7_days: total + 1, @@ -202,7 +198,7 @@ describe('telemetry_application_usage', () => { getUsageCollector.mockImplementation(() => savedObjectClient); - expect(await collector.fetch(callCluster, esClient)).toStrictEqual({ + expect(await collector.fetch(mockedFetchContext)).toStrictEqual({ appId: { clicks_total: 1, clicks_7_days: 0, diff --git a/src/plugins/kibana_usage_collection/server/collectors/core/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/core/index.test.ts index b712e9ebbce4..e8efa9997c45 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/core/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/core/index.test.ts @@ -21,7 +21,7 @@ import { CollectorOptions, createUsageCollectionSetupMock, } from '../../../../usage_collection/server/usage_collection.mock'; - +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; import { registerCoreUsageCollector } from '.'; import { coreUsageDataServiceMock } from '../../../../../core/server/mocks'; import { CoreUsageData } from 'src/core/server/'; @@ -35,7 +35,7 @@ describe('telemetry_core', () => { return createUsageCollectionSetupMock().makeUsageCollector(config); }); - const callCluster = jest.fn().mockImplementation(() => ({})); + const collectorFetchContext = createCollectorFetchContextMock(); const coreUsageDataStart = coreUsageDataServiceMock.createStartContract(); const getCoreUsageDataReturnValue = (Symbol('core telemetry') as any) as CoreUsageData; coreUsageDataStart.getCoreUsageData.mockResolvedValue(getCoreUsageDataReturnValue); @@ -48,6 +48,6 @@ describe('telemetry_core', () => { }); test('fetch', async () => { - expect(await collector.fetch(callCluster)).toEqual(getCoreUsageDataReturnValue); + expect(await collector.fetch(collectorFetchContext)).toEqual(getCoreUsageDataReturnValue); }); }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.test.ts index 465b21e3578b..03184d738586 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/csp/csp_collector.test.ts @@ -20,10 +20,12 @@ import { CspConfig, ICspConfig } from '../../../../../core/server'; import { createCspCollector } from './csp_collector'; import { httpServiceMock } from '../../../../../core/server/mocks'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; describe('csp collector', () => { let httpMock: ReturnType; - const mockCallCluster = null as any; + // changed for consistency with expected implementation + const mockedFetchContext = createCollectorFetchContextMock(); function updateCsp(config: Partial) { httpMock.csp = new CspConfig(config); @@ -36,28 +38,28 @@ describe('csp collector', () => { test('fetches whether strict mode is enabled', async () => { const collector = createCspCollector(httpMock); - expect((await collector.fetch(mockCallCluster)).strict).toEqual(true); + expect((await collector.fetch(mockedFetchContext)).strict).toEqual(true); updateCsp({ strict: false }); - expect((await collector.fetch(mockCallCluster)).strict).toEqual(false); + expect((await collector.fetch(mockedFetchContext)).strict).toEqual(false); }); test('fetches whether the legacy browser warning is enabled', async () => { const collector = createCspCollector(httpMock); - expect((await collector.fetch(mockCallCluster)).warnLegacyBrowsers).toEqual(true); + expect((await collector.fetch(mockedFetchContext)).warnLegacyBrowsers).toEqual(true); updateCsp({ warnLegacyBrowsers: false }); - expect((await collector.fetch(mockCallCluster)).warnLegacyBrowsers).toEqual(false); + expect((await collector.fetch(mockedFetchContext)).warnLegacyBrowsers).toEqual(false); }); test('fetches whether the csp rules have been changed or not', async () => { const collector = createCspCollector(httpMock); - expect((await collector.fetch(mockCallCluster)).rulesChangedFromDefault).toEqual(false); + expect((await collector.fetch(mockedFetchContext)).rulesChangedFromDefault).toEqual(false); updateCsp({ rules: ['not', 'default'] }); - expect((await collector.fetch(mockCallCluster)).rulesChangedFromDefault).toEqual(true); + expect((await collector.fetch(mockedFetchContext)).rulesChangedFromDefault).toEqual(true); }); test('does not include raw csp rules under any property names', async () => { @@ -69,7 +71,7 @@ describe('csp collector', () => { // // We use a snapshot here to ensure csp.rules isn't finding its way into the // payload under some new and unexpected variable name (e.g. cspRules). - expect(await collector.fetch(mockCallCluster)).toMatchInlineSnapshot(` + expect(await collector.fetch(mockedFetchContext)).toMatchInlineSnapshot(` Object { "rulesChangedFromDefault": false, "strict": true, diff --git a/src/plugins/kibana_usage_collection/server/collectors/kibana/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/kibana/index.test.ts index 2bfe59d7dd4f..88ccb2016d42 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/kibana/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/kibana/index.test.ts @@ -22,7 +22,7 @@ import { CollectorOptions, createUsageCollectionSetupMock, } from '../../../../usage_collection/server/usage_collection.mock'; - +import { createCollectorFetchContextMock } from '../../../../usage_collection/server/mocks'; import { registerKibanaUsageCollector } from './'; describe('telemetry_kibana', () => { @@ -35,7 +35,12 @@ describe('telemetry_kibana', () => { }); const legacyConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$; - const callCluster = jest.fn().mockImplementation(() => ({})); + + const getMockFetchClients = (hits?: unknown[]) => { + const fetchParamsMock = createCollectorFetchContextMock(); + fetchParamsMock.callCluster.mockResolvedValue({ hits: { hits } }); + return fetchParamsMock; + }; beforeAll(() => registerKibanaUsageCollector(usageCollectionMock, legacyConfig$)); afterAll(() => jest.clearAllTimers()); @@ -46,7 +51,7 @@ describe('telemetry_kibana', () => { }); test('fetch', async () => { - expect(await collector.fetch(callCluster)).toStrictEqual({ + expect(await collector.fetch(getMockFetchClients())).toStrictEqual({ index: '.kibana-tests', dashboard: { total: 0 }, visualization: { total: 0 }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts index 5b56e1a9b596..d292b2d5ace0 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts @@ -44,7 +44,7 @@ export function getKibanaUsageCollector( graph_workspace: { total: { type: 'long' } }, timelion_sheet: { total: { type: 'long' } }, }, - async fetch(callCluster) { + async fetch({ callCluster }) { const { kibana: { index }, } = await legacyConfig$.pipe(take(1)).toPromise(); diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/management/index.test.ts index d4b635448d0a..e671f739ee08 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/index.test.ts @@ -21,6 +21,7 @@ import { uiSettingsServiceMock } from '../../../../../core/server/mocks'; import { CollectorOptions, createUsageCollectionSetupMock, + createCollectorFetchContextMock, } from '../../../../usage_collection/server/usage_collection.mock'; import { registerManagementUsageCollector } from './'; @@ -36,7 +37,7 @@ describe('telemetry_application_usage_collector', () => { const uiSettingsClient = uiSettingsServiceMock.createClient(); const getUiSettingsClient = jest.fn(() => uiSettingsClient); - const callCluster = jest.fn(); + const mockedFetchContext = createCollectorFetchContextMock(); beforeAll(() => { registerManagementUsageCollector(usageCollectionMock, getUiSettingsClient); @@ -59,11 +60,11 @@ describe('telemetry_application_usage_collector', () => { uiSettingsClient.getUserProvided.mockImplementationOnce(async () => ({ 'my-key': { userValue: 'my-value' }, })); - await expect(collector.fetch(callCluster)).resolves.toMatchSnapshot(); + await expect(collector.fetch(mockedFetchContext)).resolves.toMatchSnapshot(); }); test('fetch() should not fail if invoked when not ready', async () => { getUiSettingsClient.mockImplementationOnce(() => undefined as any); - await expect(collector.fetch(callCluster)).resolves.toBe(undefined); + await expect(collector.fetch(mockedFetchContext)).resolves.toBe(undefined); }); }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/ops_stats/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/ops_stats/index.test.ts index a527d4d03c6f..61990730812c 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/ops_stats/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/ops_stats/index.test.ts @@ -21,6 +21,7 @@ import { Subject } from 'rxjs'; import { CollectorOptions, createUsageCollectionSetupMock, + createCollectorFetchContextMock, } from '../../../../usage_collection/server/usage_collection.mock'; import { registerOpsStatsCollector } from './'; @@ -36,7 +37,7 @@ describe('telemetry_ops_stats', () => { }); const metrics$ = new Subject(); - const callCluster = jest.fn(); + const mockedFetchContext = createCollectorFetchContextMock(); const metric: OpsMetrics = { collected_at: new Date('2020-01-01 01:00:00'), @@ -92,7 +93,7 @@ describe('telemetry_ops_stats', () => { test('should return something when there is a metric', async () => { metrics$.next(metric); expect(collector.isReady()).toBe(true); - expect(await collector.fetch(callCluster)).toMatchSnapshot({ + expect(await collector.fetch(mockedFetchContext)).toMatchSnapshot({ concurrent_connections: 20, os: { load: { diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_metric/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_metric/index.test.ts index d6f40a2a6867..48e4e0d99d3c 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_metric/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/ui_metric/index.test.ts @@ -21,6 +21,7 @@ import { savedObjectsRepositoryMock } from '../../../../../core/server/mocks'; import { CollectorOptions, createUsageCollectionSetupMock, + createCollectorFetchContextMock, } from '../../../../usage_collection/server/usage_collection.mock'; import { registerUiMetricUsageCollector } from './'; @@ -36,7 +37,7 @@ describe('telemetry_ui_metric', () => { const getUsageCollector = jest.fn(); const registerType = jest.fn(); - const callCluster = jest.fn(); + const mockedFetchContext = createCollectorFetchContextMock(); beforeAll(() => registerUiMetricUsageCollector(usageCollectionMock, registerType, getUsageCollector) @@ -47,7 +48,7 @@ describe('telemetry_ui_metric', () => { }); test('if no savedObjectClient initialised, return undefined', async () => { - expect(await collector.fetch(callCluster)).toBeUndefined(); + expect(await collector.fetch(mockedFetchContext)).toBeUndefined(); }); test('when savedObjectClient is initialised, return something', async () => { @@ -61,7 +62,7 @@ describe('telemetry_ui_metric', () => { ); getUsageCollector.mockImplementation(() => savedObjectClient); - expect(await collector.fetch(callCluster)).toStrictEqual({}); + expect(await collector.fetch(mockedFetchContext)).toStrictEqual({}); expect(savedObjectClient.bulkCreate).not.toHaveBeenCalled(); }); @@ -85,7 +86,7 @@ describe('telemetry_ui_metric', () => { getUsageCollector.mockImplementation(() => savedObjectClient); - expect(await collector.fetch(callCluster)).toStrictEqual({ + expect(await collector.fetch(mockedFetchContext)).toStrictEqual({ testAppName: [ { key: 'testKeyName1', value: 3 }, { key: 'testKeyName2', value: 5 }, diff --git a/src/plugins/saved_objects/public/index.ts b/src/plugins/saved_objects/public/index.ts index 9140de316605..ecf6aa0569bf 100644 --- a/src/plugins/saved_objects/public/index.ts +++ b/src/plugins/saved_objects/public/index.ts @@ -30,7 +30,6 @@ export { export { getSavedObjectFinder, SavedObjectFinderUi, SavedObjectMetaData } from './finder'; export { SavedObjectLoader, - createSavedObjectClass, checkForDuplicateTitle, saveWithConfirmation, isErrorNonFatal, diff --git a/src/plugins/timelion/public/flot/index.js b/src/plugins/saved_objects/public/mocks.ts similarity index 72% rename from src/plugins/timelion/public/flot/index.js rename to src/plugins/saved_objects/public/mocks.ts index a066fd3ab860..d34a6ded7c8d 100644 --- a/src/plugins/timelion/public/flot/index.js +++ b/src/plugins/saved_objects/public/mocks.ts @@ -17,10 +17,18 @@ * 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'; +import { SavedObjectsStart } from './plugin'; + +const createStartContract = (): SavedObjectsStart => { + return { + SavedObjectClass: jest.fn(), + settings: { + getPerPage: () => 20, + getListingLimit: () => 100, + }, + }; +}; + +export const savedObjectsPluginMock = { + createStartContract, +}; diff --git a/src/plugins/saved_objects/public/plugin.ts b/src/plugins/saved_objects/public/plugin.ts index d430c8896484..0c50180e13d8 100644 --- a/src/plugins/saved_objects/public/plugin.ts +++ b/src/plugins/saved_objects/public/plugin.ts @@ -23,9 +23,10 @@ import './index.scss'; import { createSavedObjectClass } from './saved_object'; import { DataPublicPluginStart } from '../../data/public'; import { PER_PAGE_SETTING, LISTING_LIMIT_SETTING } from '../common'; +import { SavedObject } from './types'; export interface SavedObjectsStart { - SavedObjectClass: any; + SavedObjectClass: new (raw: Record) => SavedObject; settings: { getPerPage: () => number; getListingLimit: () => number; diff --git a/src/plugins/security_oss/public/insecure_cluster_service/components/default_alert.tsx b/src/plugins/security_oss/public/insecure_cluster_service/components/default_alert.tsx index f2eeedb5b737..fff266bf964b 100644 --- a/src/plugins/security_oss/public/insecure_cluster_service/components/default_alert.tsx +++ b/src/plugins/security_oss/public/insecure_cluster_service/components/default_alert.tsx @@ -32,7 +32,7 @@ import React, { useState } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; export const defaultAlertTitle = i18n.translate('security.checkup.insecureClusterTitle', { - defaultMessage: 'Please secure your installation', + defaultMessage: 'Your data is not secure', }); export const defaultAlertText: (onDismiss: (persist: boolean) => void) => MountPoint = ( @@ -47,7 +47,7 @@ export const defaultAlertText: (onDismiss: (persist: boolean) => void) => MountP @@ -66,7 +66,7 @@ export const defaultAlertText: (onDismiss: (persist: boolean) => void) => MountP size="s" color="primary" fill - href="https://www.elastic.co/what-is/elastic-stack-security" + href="https://www.elastic.co/what-is/elastic-stack-security?blade=kibanasecuritymessage" target="_blank" > {i18n.translate('security.checkup.learnMoreButtonText', { diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index b423cbb07ba3..037f97fb63ac 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 0c8b0b249f7d..fcecbca23038 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 6244c6fac51d..4aeefb1d81d6 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 9dac4900f5f1..27ca5ae74651 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 ff63262004cf..4900e75a1936 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 3b0936fb73a6..d6e4fdce2b18 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/kibana.json b/src/plugins/timelion/kibana.json index d8c709d867a3..3134cc265fba 100644 --- a/src/plugins/timelion/kibana.json +++ b/src/plugins/timelion/kibana.json @@ -6,7 +6,6 @@ "requiredBundles": [ "kibanaLegacy", "kibanaUtils", - "savedObjects", "visTypeTimelion" ], "requiredPlugins": [ @@ -14,6 +13,7 @@ "data", "navigation", "visTypeTimelion", + "savedObjects", "kibanaLegacy" ] } diff --git a/src/plugins/timelion/public/application.ts b/src/plugins/timelion/public/application.ts index a4963ee6b1b0..e0425ac94c59 100644 --- a/src/plugins/timelion/public/application.ts +++ b/src/plugins/timelion/public/application.ts @@ -41,7 +41,7 @@ import { createTopNavDirective, createTopNavHelper, } from '../../kibana_legacy/public'; -import { TimelionPluginDependencies } from './plugin'; +import { TimelionPluginStartDependencies } from './plugin'; import { DataPublicPluginStart } from '../../data/public'; // @ts-ignore import { initTimelionApp } from './app'; @@ -50,7 +50,7 @@ export interface RenderDeps { pluginInitializerContext: PluginInitializerContext; mountParams: AppMountParameters; core: CoreStart; - plugins: TimelionPluginDependencies; + plugins: TimelionPluginStartDependencies; timelionPanels: Map; } @@ -137,7 +137,7 @@ function createLocalIconModule() { .directive('icon', (reactDirective) => reactDirective(EuiIcon)); } -function createLocalTopNavModule(navigation: TimelionPluginDependencies['navigation']) { +function createLocalTopNavModule(navigation: TimelionPluginStartDependencies['navigation']) { angular .module('app/timelion/TopNav', ['react']) .directive('kbnTopNav', createTopNavDirective) 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 5d613037cf23..000000000000 --- 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 34c1d121259a..000000000000 --- 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 b56d8a66110c..d874f0d32c9d 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/timelion/public/plugin.ts b/src/plugins/timelion/public/plugin.ts index 7656a808dfb0..b435cc6fd399 100644 --- a/src/plugins/timelion/public/plugin.ts +++ b/src/plugins/timelion/public/plugin.ts @@ -36,20 +36,29 @@ import { createKbnUrlTracker } from '../../kibana_utils/public'; import { DataPublicPluginStart, esFilters, DataPublicPluginSetup } from '../../data/public'; import { NavigationPublicPluginStart } from '../../navigation/public'; import { VisualizationsStart } from '../../visualizations/public'; +import { SavedObjectsStart } from '../../saved_objects/public'; import { VisTypeTimelionPluginStart, VisTypeTimelionPluginSetup, } from '../../vis_type_timelion/public'; -export interface TimelionPluginDependencies { +export interface TimelionPluginSetupDependencies { + data: DataPublicPluginSetup; + visTypeTimelion: VisTypeTimelionPluginSetup; +} + +export interface TimelionPluginStartDependencies { data: DataPublicPluginStart; navigation: NavigationPublicPluginStart; visualizations: VisualizationsStart; visTypeTimelion: VisTypeTimelionPluginStart; + savedObjects: SavedObjectsStart; + kibanaLegacy: KibanaLegacyStart; } /** @internal */ -export class TimelionPlugin implements Plugin { +export class TimelionPlugin + implements Plugin { initializerContext: PluginInitializerContext; private appStateUpdater = new BehaviorSubject(() => ({})); private stopUrlTracking: (() => void) | undefined = undefined; @@ -60,7 +69,7 @@ export class TimelionPlugin implements Plugin { } public setup( - core: CoreSetup, + core: CoreSetup, { data, visTypeTimelion, @@ -122,7 +131,7 @@ export class TimelionPlugin implements Plugin { pluginInitializerContext: this.initializerContext, timelionPanels, core: coreStart, - plugins: pluginsStart as TimelionPluginDependencies, + plugins: pluginsStart, }); return () => { unlistenParentHistory(); diff --git a/src/plugins/timelion/public/services/_saved_sheet.ts b/src/plugins/timelion/public/services/_saved_sheet.ts index 0958cce86012..3fe66fabebe7 100644 --- a/src/plugins/timelion/public/services/_saved_sheet.ts +++ b/src/plugins/timelion/public/services/_saved_sheet.ts @@ -18,16 +18,11 @@ */ import { IUiSettingsClient } from 'kibana/public'; -import { createSavedObjectClass, SavedObjectKibanaServices } from '../../../saved_objects/public'; +import { SavedObjectsStart } from '../../../saved_objects/public'; // Used only by the savedSheets service, usually no reason to change this -export function createSavedSheetClass( - services: SavedObjectKibanaServices, - config: IUiSettingsClient -) { - const SavedObjectClass = createSavedObjectClass(services); - - class SavedSheet extends SavedObjectClass { +export function createSavedSheetClass(savedObjects: SavedObjectsStart, config: IUiSettingsClient) { + class SavedSheet extends savedObjects.SavedObjectClass { static type = 'timelion-sheet'; // if type:sheet has no mapping, we push this mapping into ES diff --git a/src/plugins/timelion/public/services/saved_sheets.ts b/src/plugins/timelion/public/services/saved_sheets.ts index 9ad529cb0134..4c360ad55823 100644 --- a/src/plugins/timelion/public/services/saved_sheets.ts +++ b/src/plugins/timelion/public/services/saved_sheets.ts @@ -23,15 +23,7 @@ import { RenderDeps } from '../application'; export function initSavedSheetService(app: angular.IModule, deps: RenderDeps) { const savedObjectsClient = deps.core.savedObjects.client; - const services = { - savedObjectsClient, - indexPatterns: deps.plugins.data.indexPatterns, - search: deps.plugins.data.search, - chrome: deps.core.chrome, - overlays: deps.core.overlays, - }; - - const SavedSheet = createSavedSheetClass(services, deps.core.uiSettings); + const SavedSheet = createSavedSheetClass(deps.plugins.savedObjects, deps.core.uiSettings); const savedSheetLoader = new SavedObjectLoader(SavedSheet, savedObjectsClient); savedSheetLoader.urlFor = (id) => `#/${encodeURIComponent(id)}`; diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md index aae633a956c4..430241cbe0a0 100644 --- a/src/plugins/usage_collection/README.md +++ b/src/plugins/usage_collection/README.md @@ -37,10 +37,9 @@ All you need to provide is a `type` for organizing your fields, `schema` field t ``` 3. Creating and registering a Usage Collector. Ideally collectors would be defined in a separate directory `server/collectors/register.ts`. - ```ts // server/collectors/register.ts - import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; + import { UsageCollectionSetup, CollectorFetchContext } from 'src/plugins/usage_collection/server'; import { APICluster } from 'kibana/server'; interface Usage { @@ -63,9 +62,9 @@ All you need to provide is a `type` for organizing your fields, `schema` field t total: 'long', }, }, - fetch: async (callCluster: APICluster, esClient: IClusterClient) => { + 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 @@ -86,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`, `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. -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: +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 @@ -99,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 8491bdb0c957..11a709c03778 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 }; @@ -45,11 +51,30 @@ export type MakeSchemaFrom = { : RecursiveMakeSchemaFrom[Key]>; }; +export interface CollectorFetchContext { + /** + * @depricated Scoped Legacy Elasticsearch client: use esClient instead + */ + callCluster: LegacyAPICaller; + /** + * Request-scoped Elasticsearch 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 + */ + 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; schema?: MakeSchemaFrom; - fetch: (callCluster: LegacyAPICaller, esClient?: ElasticsearchClient) => Promise | T; + fetch: (collectorFetchContext: CollectorFetchContext) => Promise | T; /* * A hook for allowing the fetched data payload to be organized into a typed * data model for internal bulk upload. See defaultFormatterForBulkUpload for 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 3f943ad8bf2f..45a3437777c5 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 }); @@ -81,12 +85,14 @@ describe('CollectorSet', () => { collectors.registerCollector( new Collector(logger, { type: 'MY_TEST_COLLECTOR', - fetch: (caller: any) => caller(), + fetch: (collectorFetchContext: any) => { + return collectorFetchContext.callCluster(); + }, isReady: () => true, }) ); - 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' @@ -111,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 } @@ -129,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', @@ -147,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', @@ -170,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 7bf4e19c72cc..4e64cbc1bf30 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'; @@ -122,12 +128,10 @@ export class CollectorSet { return allReady; }; - // all collections eventually pass through bulkFetch. - // the shape of the response is different when using the new ES client as is the error handling. - // We'll handle the refactor for using the new client in a follow up PR. public bulkFetch = async ( callCluster: LegacyAPICaller, esClient: ElasticsearchClient, + soClient: SavedObjectsClientContract | ISavedObjectsRepository, collectors: Map> = this.collectors ) => { const responses = await Promise.all( @@ -136,7 +140,7 @@ export class CollectorSet { try { return { type: collector.type, - result: await collector.fetch(callCluster, esClient), // each collector must ensure they handle the response appropriately. + result: await collector.fetch({ callCluster, esClient, soClient }), }; } catch (err) { this.logger.warn(err); @@ -158,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/collector/index.ts b/src/plugins/usage_collection/server/collector/index.ts index 1816e845b4d6..c294ba77d3cd 100644 --- a/src/plugins/usage_collection/server/collector/index.ts +++ b/src/plugins/usage_collection/server/collector/index.ts @@ -24,5 +24,6 @@ export { SchemaField, MakeSchemaFrom, CollectorOptions, + CollectorFetchContext, } from './collector'; export { UsageCollector } from './usage_collector'; diff --git a/src/plugins/usage_collection/server/index.ts b/src/plugins/usage_collection/server/index.ts index 87761bca9a50..80e34b1502cd 100644 --- a/src/plugins/usage_collection/server/index.ts +++ b/src/plugins/usage_collection/server/index.ts @@ -26,6 +26,7 @@ export { SchemaField, CollectorOptions, Collector, + CollectorFetchContext, } from './collector'; export { UsageCollectionSetup } from './plugin'; export { config } from './config'; diff --git a/src/plugins/usage_collection/server/mocks.ts b/src/plugins/usage_collection/server/mocks.ts index e1f13304165a..d08db1eaec0e 100644 --- a/src/plugins/usage_collection/server/mocks.ts +++ b/src/plugins/usage_collection/server/mocks.ts @@ -20,6 +20,7 @@ import { loggingSystemMock } from '../../../core/server/mocks'; import { UsageCollectionSetup } from './plugin'; import { CollectorSet } from './collector'; +export { createCollectorFetchContextMock } from './usage_collection.mock'; const createSetupContract = () => { return { diff --git a/src/plugins/usage_collection/server/routes/stats/stats.ts b/src/plugins/usage_collection/server/routes/stats/stats.ts index bee25fef669f..ef64d15fabc2 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 7a6d16d6950e..c31756c60e32 100644 --- a/src/plugins/usage_collection/server/usage_collection.mock.ts +++ b/src/plugins/usage_collection/server/usage_collection.mock.ts @@ -17,8 +17,13 @@ * under the License. */ +import { + elasticsearchServiceMock, + savedObjectsRepositoryMock, +} from '../../../../src/core/server/mocks'; + import { CollectorOptions } from './collector/collector'; -import { UsageCollectionSetup } from './index'; +import { UsageCollectionSetup, CollectorFetchContext } from './index'; export { CollectorOptions }; @@ -45,3 +50,12 @@ export const createUsageCollectionSetupMock = () => { usageCollectionSetupMock.areAllCollectorsReady.mockResolvedValue(true); return usageCollectionSetupMock; }; + +export function createCollectorFetchContextMock(): jest.Mocked { + const collectorFetchClientsMock: jest.Mocked = { + callCluster: elasticsearchServiceMock.createLegacyClusterClient().callAsInternalUser, + esClient: elasticsearchServiceMock.createClusterClient().asInternalUser, + soClient: savedObjectsRepositoryMock.create(), + }; + return collectorFetchClientsMock; +} diff --git a/src/plugins/vis_default_editor/public/components/controls/order_agg.test.tsx b/src/plugins/vis_default_editor/public/components/controls/order_agg.test.tsx index 4c843791153b..f1497631b66c 100644 --- a/src/plugins/vis_default_editor/public/components/controls/order_agg.test.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/order_agg.test.tsx @@ -106,7 +106,7 @@ describe('OrderAggParamEditor component', () => { mount(); - expect(setValue).toHaveBeenCalledWith('agg5'); + expect(setValue).toHaveBeenCalledWith('agg3'); }); it('defaults to the _key metric if no agg is compatible', () => { 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 a7b623ac8680..953ec5e819f4 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 cda8038953c7..000000000000 --- 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 5111695e3d12..000000000000 --- 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 5d613037cf23..000000000000 --- 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 c8707b30f4e6..000000000000 --- 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 0d91c0f3c016..000000000000 --- 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 79f634971b6f..000000000000 --- 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 34c1d121259a..000000000000 --- 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/vis_type_timelion/server/routes/validate_es.ts b/src/plugins/vis_type_timelion/server/routes/validate_es.ts index ea08310499a9..242be515e52b 100644 --- a/src/plugins/vis_type_timelion/server/routes/validate_es.ts +++ b/src/plugins/vis_type_timelion/server/routes/validate_es.ts @@ -57,10 +57,17 @@ export function validateEsRoute(router: IRouter, core: CoreSetup) { let resp; try { - resp = await deps.data.search.search(context, body, { - strategy: ES_SEARCH_STRATEGY, - }); - resp = resp.rawResponse; + resp = ( + await deps.data.search + .search( + body, + { + strategy: ES_SEARCH_STRATEGY, + }, + context + ) + .toPromise() + ).rawResponse; } catch (errResp) { resp = errResp; } diff --git a/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js b/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js index c5fc4b7b9326..8be3cf5171c6 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js +++ b/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js @@ -16,9 +16,9 @@ * specific language governing permissions and limitations * under the License. */ +import { from } from 'rxjs'; import es from './index'; - import tlConfigFn from '../fixtures/tl_config'; import * as aggResponse from './lib/agg_response_to_series_list'; import buildRequest from './lib/build_request'; @@ -36,7 +36,10 @@ function stubRequestAndServer(response, indexPatternSavedObjects = []) { getStartServices: sinon .stub() .returns( - Promise.resolve([{}, { data: { search: { search: () => Promise.resolve(response) } } }]) + Promise.resolve([ + {}, + { data: { search: { search: () => from(Promise.resolve(response)) } } }, + ]) ), savedObjectsClient: { find: function () { diff --git a/src/plugins/vis_type_timelion/server/series_functions/es/index.js b/src/plugins/vis_type_timelion/server/series_functions/es/index.js index bfa8d75900d1..fc3250f0d472 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/es/index.js +++ b/src/plugins/vis_type_timelion/server/series_functions/es/index.js @@ -132,9 +132,15 @@ export default new Datasource('es', { const deps = (await tlConfig.getStartServices())[1]; - const resp = await deps.data.search.search(tlConfig.context, body, { - strategy: ES_SEARCH_STRATEGY, - }); + const resp = await deps.data.search + .search( + body, + { + strategy: ES_SEARCH_STRATEGY, + }, + tlConfig.context + ) + .toPromise(); if (!resp.rawResponse._shards.total) { throw new Error( diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js index 34f339ce24c2..0f64c570088d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js @@ -21,6 +21,7 @@ import { set } from '@elastic/safer-lodash-set'; import _ from 'lodash'; import { getLastValue } from '../../../../../../plugins/vis_type_timeseries/common/get_last_value'; import { createTickFormatter } from './tick_formatter'; +import { labelDateFormatter } from './label_date_formatter'; import moment from 'moment'; export const convertSeriesToVars = (series, model, dateFormat = 'lll', getConfig = null) => { @@ -63,15 +64,7 @@ export const convertSeriesToVars = (series, model, dateFormat = 'lll', getConfig * If not, return a formatted value from elasticsearch */ if (row.labelFormatted) { - const momemntObj = moment(row.labelFormatted); - let val; - - if (momemntObj.isValid()) { - val = momemntObj.format(dateFormat); - } else { - val = row.labelFormatted; - } - + const val = labelDateFormatter(row.labelFormatted, dateFormat); set(variables, `${_.snakeCase(row.label)}.formatted`, val); } }); diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/label_date_formatter.test.ts b/src/plugins/vis_type_timeseries/public/application/components/lib/label_date_formatter.test.ts new file mode 100644 index 000000000000..c4a0f10c5748 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/label_date_formatter.test.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment-timezone'; +import { labelDateFormatter } from './label_date_formatter'; + +const dateString = '2020-09-24T18:59:02.000Z'; + +describe('Label Date Formatter Function', () => { + it('Should format the date string', () => { + const label = labelDateFormatter(dateString); + expect(label).toEqual(moment(dateString).format('lll')); + }); + + it('Should format the date string on the given formatter', () => { + const label = labelDateFormatter(dateString, 'MM/DD/YYYY'); + expect(label).toEqual(moment(dateString).format('MM/DD/YYYY')); + }); + + it('Returns the label if it is not date string', () => { + const label = labelDateFormatter('test date'); + expect(label).toEqual('test date'); + }); + + it('Returns the label if it is a number string', () => { + const label = labelDateFormatter('1'); + expect(label).toEqual('1'); + }); +}); diff --git a/src/plugins/vis_type_timelion/public/flot/index.js b/src/plugins/vis_type_timeseries/public/application/components/lib/label_date_formatter.ts similarity index 70% rename from src/plugins/vis_type_timelion/public/flot/index.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/label_date_formatter.ts index a066fd3ab860..f4de19b084c7 100644 --- a/src/plugins/vis_type_timelion/public/flot/index.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/label_date_formatter.ts @@ -17,10 +17,14 @@ * 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'; +import moment from 'moment'; + +export const labelDateFormatter = (label: string, dateformat = 'lll') => { + let formattedLabel = label; + // Use moment isValid function on strict mode + const isDate = moment(label, '', true).isValid(); + if (isDate) { + formattedLabel = moment(label).format(dateformat); + } + return formattedLabel; +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js b/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js index 8b63d1b5043f..ccf486bff562 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js @@ -19,6 +19,7 @@ import React from 'react'; import { getDisplayName } from './lib/get_display_name'; +import { labelDateFormatter } from './lib/label_date_formatter'; import { last, findIndex, first } from 'lodash'; import { calculateLabel } from '../../../../../plugins/vis_type_timeseries/common/calculate_label'; @@ -41,6 +42,7 @@ export function visWithSplits(WrappedComponent) { acc[splitId] = { series: [], label: series.label.toString(), + labelFormatted: series.labelFormatted, }; } @@ -67,7 +69,11 @@ export function visWithSplits(WrappedComponent) { const rows = Object.keys(splitsVisData).map((key) => { const splitData = splitsVisData[key]; - const { series, label } = splitData; + const { series, label, labelFormatted } = splitData; + let additionalLabel = label; + if (labelFormatted) { + additionalLabel = labelDateFormatter(labelFormatted); + } const newSeries = indexOfNonSplit != null && indexOfNonSplit > 0 ? [...series, nonSplitSeries] @@ -84,7 +90,7 @@ export function visWithSplits(WrappedComponent) { model={model} visData={newVisData} onBrush={props.onBrush} - additionalLabel={label} + additionalLabel={additionalLabel} backgroundColor={props.backgroundColor} getConfig={props.getConfig} /> diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js index 664751bbc0ec..278d7906dde9 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js @@ -20,6 +20,7 @@ import React, { useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; +import { labelDateFormatter } from '../../../components/lib/label_date_formatter'; import { Axis, @@ -165,6 +166,7 @@ export const TimeSeries = ({ { id, label, + labelFormatted, bars, lines, data, @@ -188,14 +190,17 @@ export const TimeSeries = ({ const key = `${id}-${label}`; // Only use color mapping if there is no color from the server const finalColor = color ?? colors.mappedColors.mapping[label]; - + let seriesName = label.toString(); + if (labelFormatted) { + seriesName = labelDateFormatter(labelFormatted); + } if (bars?.show) { return ( - {item.label} + {item.labelFormatted ? labelDateFormatter(item.labelFormatted) : item.label}
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js index 4dcc67dc4697..ceae784cf74a 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { from } from 'rxjs'; import { AbstractSearchStrategy } from './abstract_search_strategy'; describe('AbstractSearchStrategy', () => { @@ -55,7 +56,7 @@ describe('AbstractSearchStrategy', () => { test('should return response', async () => { const searches = [{ body: 'body', index: 'index' }]; - const searchFn = jest.fn().mockReturnValue(Promise.resolve({})); + const searchFn = jest.fn().mockReturnValue(from(Promise.resolve({}))); const responses = await abstractSearchStrategy.search( { @@ -82,7 +83,6 @@ describe('AbstractSearchStrategy', () => { expect(responses).toEqual([{}]); expect(searchFn).toHaveBeenCalledWith( - {}, { params: { body: 'body', @@ -92,7 +92,8 @@ describe('AbstractSearchStrategy', () => { }, { strategy: 'es', - } + }, + {} ); }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts index 2eb92b2b777e..7b62ad310a35 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts @@ -60,20 +60,22 @@ export class AbstractSearchStrategy { const requests: any[] = []; bodies.forEach((body) => { requests.push( - deps.data.search.search( - req.requestContext, - { - params: { - ...body, - ...this.additionalParams, + deps.data.search + .search( + { + params: { + ...body, + ...this.additionalParams, + }, + indexType: this.indexType, }, - indexType: this.indexType, - }, - { - ...options, - strategy: this.searchStrategyName, - } - ) + { + ...options, + strategy: this.searchStrategyName, + }, + req.requestContext + ) + .toPromise() ); }); return Promise.all(requests); diff --git a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts index a5f095a4c4f3..0969174c7143 100644 --- a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts +++ b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { LegacyAPICaller, CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; +import { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; import { UsageCollectionSetup } from '../../../usage_collection/server'; import { tsvbTelemetrySavedObjectType } from '../saved_objects'; @@ -49,7 +49,7 @@ export class ValidationTelemetryService implements Plugin({ type: 'tsvb-validation', isReady: () => this.kibanaIndex !== '', - fetch: async (callCluster: LegacyAPICaller) => { + fetch: async ({ callCluster }) => { try { const response = await callCluster('get', { index: this.kibanaIndex, diff --git a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts index 891ebf658267..6f17703bc9de 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts @@ -17,9 +17,9 @@ * under the License. */ -import { LegacyAPICaller } from 'src/core/server'; import { getStats } from './get_usage_collector'; import { HomeServerPluginSetup } from '../../../home/server'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; const mockedSavedObjects = [ // vega-lite lib spec @@ -70,8 +70,11 @@ const mockedSavedObjects = [ }, ]; -const getMockCallCluster = (hits?: unknown[]) => - jest.fn().mockReturnValue(Promise.resolve({ hits: { hits } }) as unknown) as LegacyAPICaller; +const getMockCollectorFetchContext = (hits?: unknown[]) => { + const fetchParamsMock = createCollectorFetchContextMock(); + fetchParamsMock.callCluster.mockResolvedValue({ hits: { hits } }); + return fetchParamsMock; +}; describe('Vega visualization usage collector', () => { const mockIndex = 'mock_index'; @@ -101,19 +104,23 @@ describe('Vega visualization usage collector', () => { }; test('Returns undefined when no results found (undefined)', async () => { - const result = await getStats(getMockCallCluster(), mockIndex, mockDeps); + const result = await getStats(getMockCollectorFetchContext().callCluster, mockIndex, mockDeps); expect(result).toBeUndefined(); }); test('Returns undefined when no results found (0 results)', async () => { - const result = await getStats(getMockCallCluster([]), mockIndex, mockDeps); + const result = await getStats( + getMockCollectorFetchContext([]).callCluster, + mockIndex, + mockDeps + ); expect(result).toBeUndefined(); }); test('Returns undefined when no vega saved objects found', async () => { - const mockCallCluster = getMockCallCluster([ + const mockCollectorFetchContext = getMockCollectorFetchContext([ { _id: 'visualization:myvis-123', _source: { @@ -122,13 +129,13 @@ describe('Vega visualization usage collector', () => { }, }, ]); - const result = await getStats(mockCallCluster, mockIndex, mockDeps); + const result = await getStats(mockCollectorFetchContext.callCluster, mockIndex, mockDeps); expect(result).toBeUndefined(); }); test('Should ingnore sample data visualizations', async () => { - const mockCallCluster = getMockCallCluster([ + const mockCollectorFetchContext = getMockCollectorFetchContext([ { _id: 'visualization:sampledata-123', _source: { @@ -146,14 +153,14 @@ describe('Vega visualization usage collector', () => { }, ]); - const result = await getStats(mockCallCluster, mockIndex, mockDeps); + const result = await getStats(mockCollectorFetchContext.callCluster, mockIndex, mockDeps); expect(result).toBeUndefined(); }); test('Summarizes visualizations response data', async () => { - const mockCallCluster = getMockCallCluster(mockedSavedObjects); - const result = await getStats(mockCallCluster, mockIndex, mockDeps); + const mockCollectorFetchContext = getMockCollectorFetchContext(mockedSavedObjects); + const result = await getStats(mockCollectorFetchContext.callCluster, mockIndex, mockDeps); expect(result).toMatchObject({ vega_lib_specs_total: 2, diff --git a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts index 433b786ed46a..e092fc8acfd7 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts @@ -20,6 +20,7 @@ import { of } from 'rxjs'; import { mockStats, mockGetStats } from './get_usage_collector.mock'; import { createUsageCollectionSetupMock } from 'src/plugins/usage_collection/server/usage_collection.mock'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; import { HomeServerPluginSetup } from '../../../home/server'; import { registerVegaUsageCollector } from './register_vega_collector'; @@ -59,10 +60,14 @@ describe('registerVegaUsageCollector', () => { const mockCollectorSet = createUsageCollectionSetupMock(); registerVegaUsageCollector(mockCollectorSet, mockConfig, mockDeps); const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0]; - const mockCallCluster = jest.fn(); - const fetchResult = await usageCollectorConfig.fetch(mockCallCluster); + const mockedCollectorFetchContext = createCollectorFetchContextMock(); + const fetchResult = await usageCollectorConfig.fetch(mockedCollectorFetchContext); expect(mockGetStats).toBeCalledTimes(1); - expect(mockGetStats).toBeCalledWith(mockCallCluster, mockIndex, mockDeps); + expect(mockGetStats).toBeCalledWith( + mockedCollectorFetchContext.callCluster, + mockIndex, + mockDeps + ); expect(fetchResult).toBe(mockStats); }); }); diff --git a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.ts b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.ts index af62821f7cdc..e4772dad99d4 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.ts @@ -35,7 +35,7 @@ export function registerVegaUsageCollector( vega_lite_lib_specs_total: { type: 'long' }, vega_use_map_total: { type: 'long' }, }, - fetch: async (callCluster) => { + fetch: async ({ callCluster }) => { const { index } = (await config.pipe(first()).toPromise()).kibana; return await getStats(callCluster, index, dependencies); diff --git a/src/plugins/visualizations/kibana.json b/src/plugins/visualizations/kibana.json index 688987b1104a..0ced74e2733d 100644 --- a/src/plugins/visualizations/kibana.json +++ b/src/plugins/visualizations/kibana.json @@ -3,7 +3,14 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["data", "expressions", "uiActions", "embeddable", "inspector" ], + "requiredPlugins": [ + "data", + "expressions", + "uiActions", + "embeddable", + "inspector", + "savedObjects" + ], "optionalPlugins": ["usageCollection"], - "requiredBundles": ["kibanaUtils", "discover", "savedObjects"] + "requiredBundles": ["kibanaUtils", "discover"] } diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts index 90e4936a58b4..f20e87dbd3b6 100644 --- a/src/plugins/visualizations/public/mocks.ts +++ b/src/plugins/visualizations/public/mocks.ts @@ -28,6 +28,7 @@ import { usageCollectionPluginMock } from '../../../plugins/usage_collection/pub import { uiActionsPluginMock } from '../../../plugins/ui_actions/public/mocks'; import { inspectorPluginMock } from '../../../plugins/inspector/public/mocks'; import { dashboardPluginMock } from '../../../plugins/dashboard/public/mocks'; +import { savedObjectsPluginMock } from '../../../plugins/saved_objects/public/mocks'; const createSetupContract = (): VisualizationsSetup => ({ createBaseVisualization: jest.fn(), @@ -73,6 +74,7 @@ const createInstance = async () => { dashboard: dashboardPluginMock.createStartContract(), getAttributeService: jest.fn(), savedObjectsClient: coreMock.createStart().savedObjects.client, + savedObjects: savedObjectsPluginMock.createStartContract(), }); return { diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index be7629ef4114..c1dbe39def64 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -78,6 +78,7 @@ import { } from './saved_visualizations/_saved_vis'; import { createSavedSearchesLoader } from '../../discover/public'; import { DashboardStart } from '../../dashboard/public'; +import { SavedObjectsStart } from '../../saved_objects/public'; /** * Interface for this plugin's returned setup/start contracts. @@ -113,6 +114,7 @@ export interface VisualizationsStartDeps { application: ApplicationStart; dashboard: DashboardStart; getAttributeService: EmbeddableStart['getAttributeService']; + savedObjects: SavedObjectsStart; savedObjectsClient: SavedObjectsClientContract; } @@ -160,7 +162,7 @@ export class VisualizationsPlugin public start( core: CoreStart, - { data, expressions, uiActions, embeddable, dashboard }: VisualizationsStartDeps + { data, expressions, uiActions, embeddable, dashboard, savedObjects }: VisualizationsStartDeps ): VisualizationsStart { const types = this.types.start(); setI18n(core.i18n); @@ -182,18 +184,13 @@ export class VisualizationsPlugin const savedVisualizationsLoader = createSavedVisLoader({ savedObjectsClient: core.savedObjects.client, indexPatterns: data.indexPatterns, - search: data.search, - chrome: core.chrome, - overlays: core.overlays, + savedObjects, visualizationTypes: types, }); setSavedVisualizationsLoader(savedVisualizationsLoader); const savedSearchLoader = createSavedSearchesLoader({ savedObjectsClient: core.savedObjects.client, - indexPatterns: data.indexPatterns, - search: data.search, - chrome: core.chrome, - overlays: core.overlays, + savedObjects, }); setSavedSearchLoader(savedSearchLoader); return { diff --git a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts b/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts index 8edf494ddc0e..59359fb00cc9 100644 --- a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts +++ b/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts @@ -24,17 +24,20 @@ * * NOTE: It's a type of SavedObject, but specific to visualizations. */ -import { - createSavedObjectClass, - SavedObject, - SavedObjectKibanaServices, -} from '../../../../plugins/saved_objects/public'; +import { SavedObjectsStart, SavedObject } from '../../../../plugins/saved_objects/public'; // @ts-ignore import { updateOldState } from '../legacy/vis_update_state'; import { extractReferences, injectReferences } from './saved_visualization_references'; -import { IIndexPattern } from '../../../../plugins/data/public'; +import { IIndexPattern, IndexPatternsContract } from '../../../../plugins/data/public'; import { ISavedVis, SerializedVis } from '../types'; import { createSavedSearchesLoader } from '../../../discover/public'; +import { SavedObjectsClientContract } from '../../../../core/public'; + +export interface SavedVisServices { + savedObjectsClient: SavedObjectsClientContract; + savedObjects: SavedObjectsStart; + indexPatterns: IndexPatternsContract; +} export const convertToSerializedVis = (savedVis: ISavedVis): SerializedVis => { const { id, title, description, visState, uiStateJSON, searchSourceFields } = savedVis; @@ -73,11 +76,10 @@ export const convertFromSerializedVis = (vis: SerializedVis): ISavedVis => { }; }; -export function createSavedVisClass(services: SavedObjectKibanaServices) { - const SavedObjectClass = createSavedObjectClass(services); +export function createSavedVisClass(services: SavedVisServices) { const savedSearch = createSavedSearchesLoader(services); - class SavedVis extends SavedObjectClass { + class SavedVis extends services.savedObjects.SavedObjectClass { public static type: string = 'visualization'; public static mapping: Record = { title: 'text', @@ -130,5 +132,5 @@ export function createSavedVisClass(services: SavedObjectKibanaServices) { } } - return SavedVis as new (opts: Record | string) => SavedObject; + return (SavedVis as unknown) as new (opts: Record | string) => SavedObject; } diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts b/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts index 0ec3c0dab2e9..760bf3cc7a36 100644 --- a/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts +++ b/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts @@ -16,19 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -import { - SavedObjectLoader, - SavedObjectKibanaServices, -} from '../../../../plugins/saved_objects/public'; +import { SavedObjectLoader } from '../../../../plugins/saved_objects/public'; import { findListItems } from './find_list_items'; -import { createSavedVisClass } from './_saved_vis'; +import { createSavedVisClass, SavedVisServices } from './_saved_vis'; import { TypesStart } from '../vis_types'; -export interface SavedObjectKibanaServicesWithVisualizations extends SavedObjectKibanaServices { +export interface SavedVisServicesWithVisualizations extends SavedVisServices { visualizationTypes: TypesStart; } export type SavedVisualizationsLoader = ReturnType; -export function createSavedVisLoader(services: SavedObjectKibanaServicesWithVisualizations) { +export function createSavedVisLoader(services: SavedVisServicesWithVisualizations) { const { savedObjectsClient, visualizationTypes } = services; class SavedObjectLoaderVisualize extends SavedObjectLoader { diff --git a/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.test.ts b/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.test.ts index 38d88dd65001..7789e3de13e5 100644 --- a/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.test.ts +++ b/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.test.ts @@ -20,6 +20,7 @@ import { of } from 'rxjs'; import { mockStats, mockGetStats } from './get_usage_collector.mock'; import { createUsageCollectionSetupMock } from 'src/plugins/usage_collection/server/usage_collection.mock'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; import { registerVisualizationsCollector } from './register_visualizations_collector'; @@ -58,10 +59,10 @@ describe('registerVisualizationsCollector', () => { const mockCollectorSet = createUsageCollectionSetupMock(); registerVisualizationsCollector(mockCollectorSet, mockConfig); const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0]; - const mockCallCluster = jest.fn(); - const fetchResult = await usageCollectorConfig.fetch(mockCallCluster); + const mockCollectorFetchContext = createCollectorFetchContextMock(); + const fetchResult = await usageCollectorConfig.fetch(mockCollectorFetchContext); expect(mockGetStats).toBeCalledTimes(1); - expect(mockGetStats).toBeCalledWith(mockCallCluster, mockIndex); + expect(mockGetStats).toBeCalledWith(mockCollectorFetchContext.callCluster, mockIndex); expect(fetchResult).toBe(mockStats); }); }); diff --git a/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.ts b/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.ts index 5919b3d20642..4188f564ed5f 100644 --- a/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.ts +++ b/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.ts @@ -41,7 +41,7 @@ export function registerVisualizationsCollector( saved_90_days_total: { type: 'long' }, }, }, - fetch: async (callCluster) => { + fetch: async ({ callCluster }) => { const index = (await config.pipe(first()).toPromise()).kibana.index; return await getStats(callCluster, index); }, diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts index c5cfa5a4c639..6010c4f8b163 100644 --- a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts +++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts @@ -35,7 +35,12 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async ( vis: Vis, visualizeServices: VisualizeServices ) => { - const { chrome, data, overlays, createVisEmbeddableFromObject, savedObjects } = visualizeServices; + const { + data, + createVisEmbeddableFromObject, + savedObjects, + savedObjectsPublic, + } = visualizeServices; const embeddableHandler = (await createVisEmbeddableFromObject(vis, { timeRange: data.query.timefilter.timefilter.getTime(), filters: data.query.filterManager.getFilters(), @@ -55,10 +60,7 @@ const createVisualizeEmbeddableAndLinkSavedSearch = async ( if (vis.data.savedSearchId) { savedSearch = await createSavedSearchesLoader({ savedObjectsClient: savedObjects.client, - indexPatterns: data.indexPatterns, - search: data.search, - chrome, - overlays, + savedObjects: savedObjectsPublic, }).get(vis.data.savedSearchId); } diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 86159a13379a..ef7d8ea18902 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/accessibility/apps/kibana_overview.ts b/test/accessibility/apps/kibana_overview.ts new file mode 100644 index 000000000000..1f703c64bbde --- /dev/null +++ b/test/accessibility/apps/kibana_overview.ts @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'home']); + const a11y = getService('a11y'); + + describe('Kibana overview', () => { + const esArchiver = getService('esArchiver'); + + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('kibanaOverview'); + }); + + after(async () => { + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await PageObjects.home.removeSampleDataSet('flights'); + await esArchiver.unload('empty_kibana'); + }); + + it('Getting started view', async () => { + await a11y.testAppSnapshot(); + }); + + it('Overview view', async () => { + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await PageObjects.home.addSampleDataSet('flights'); + await PageObjects.common.navigateToApp('kibanaOverview'); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/test/accessibility/config.ts b/test/accessibility/config.ts index 9068a7e06def..9730eae1e136 100644 --- a/test/accessibility/config.ts +++ b/test/accessibility/config.ts @@ -36,6 +36,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/console'), require.resolve('./apps/home'), require.resolve('./apps/filter_panel'), + require.resolve('./apps/kibana_overview'), ], pageObjects, services, diff --git a/test/functional/apps/discover/_field_visualize.ts b/test/functional/apps/discover/_field_visualize.ts index c95211e98cdb..1a1631b9db48 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 94409a94e925..56c648562404 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/services/filter_bar.ts b/test/functional/services/filter_bar.ts index 98ab1babd60f..de895918efbb 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/x-pack/package.json b/x-pack/package.json index 67efa9f474c0..484a64fdc262 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -198,7 +198,7 @@ "loader-utils": "^1.2.3", "lz-string": "^1.4.4", "madge": "3.4.4", - "mapbox-gl": "^1.10.0", + "mapbox-gl": "^1.12.0", "mapbox-gl-draw-rectangle-mode": "^1.0.4", "marge": "^1.0.1", "memoize-one": "^5.0.0", diff --git a/x-pack/plugins/alerts/server/alerts_client.test.ts b/x-pack/plugins/alerts/server/alerts_client.test.ts deleted file mode 100644 index 8617ea891edf..000000000000 --- a/x-pack/plugins/alerts/server/alerts_client.test.ts +++ /dev/null @@ -1,4567 +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 uuid from 'uuid'; -import { schema } from '@kbn/config-schema'; -import { AlertsClient, CreateOptions, ConstructorOptions } from './alerts_client'; -import { savedObjectsClientMock, loggingSystemMock } from '../../../../src/core/server/mocks'; -import { nodeTypes } from '../../../../src/plugins/data/common'; -import { esKuery } from '../../../../src/plugins/data/server'; -import { taskManagerMock } from '../../task_manager/server/mocks'; -import { alertTypeRegistryMock } from './alert_type_registry.mock'; -import { alertsAuthorizationMock } from './authorization/alerts_authorization.mock'; -import { TaskStatus } from '../../task_manager/server'; -import { IntervalSchedule, RawAlert } from './types'; -import { resolvable } from './test_utils'; -import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; -import { actionsClientMock, actionsAuthorizationMock } from '../../actions/server/mocks'; -import { AlertsAuthorization } from './authorization/alerts_authorization'; -import { ActionsAuthorization } from '../../actions/server'; -import { eventLogClientMock } from '../../event_log/server/mocks'; -import { QueryEventsBySavedObjectResult } from '../../event_log/server'; -import { SavedObject } from 'kibana/server'; -import { EventsFactory } from './lib/alert_instance_summary_from_event_log.test'; - -const taskManager = taskManagerMock.createStart(); -const alertTypeRegistry = alertTypeRegistryMock.create(); -const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); -const eventLogClient = eventLogClientMock.create(); - -const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); -const authorization = alertsAuthorizationMock.create(); -const actionsAuthorization = actionsAuthorizationMock.create(); - -const kibanaVersion = 'v7.10.0'; -const alertsClientParams: jest.Mocked = { - taskManager, - alertTypeRegistry, - unsecuredSavedObjectsClient, - authorization: (authorization as unknown) as AlertsAuthorization, - actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, - spaceId: 'default', - namespace: 'default', - getUserName: jest.fn(), - createAPIKey: jest.fn(), - invalidateAPIKey: jest.fn(), - logger: loggingSystemMock.create().get(), - encryptedSavedObjectsClient: encryptedSavedObjects, - getActionsClient: jest.fn(), - getEventLogClient: jest.fn(), - kibanaVersion, -}; - -beforeEach(() => { - jest.resetAllMocks(); - alertsClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false }); - alertsClientParams.invalidateAPIKey.mockResolvedValue({ - apiKeysEnabled: true, - result: { - invalidated_api_keys: [], - previously_invalidated_api_keys: [], - error_count: 0, - }, - }); - alertsClientParams.getUserName.mockResolvedValue('elastic'); - taskManager.runNow.mockResolvedValue({ id: '' }); - const actionsClient = actionsClientMock.create(); - actionsClient.getBulk.mockResolvedValueOnce([ - { - id: '1', - isPreconfigured: false, - actionTypeId: 'test', - name: 'test', - config: { - foo: 'bar', - }, - }, - { - id: '2', - isPreconfigured: false, - actionTypeId: 'test2', - name: 'test2', - config: { - foo: 'bar', - }, - }, - { - id: 'testPreconfigured', - actionTypeId: '.slack', - isPreconfigured: true, - name: 'test', - }, - ]); - alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); - - alertTypeRegistry.get.mockImplementation((id) => ({ - id: '123', - name: 'Test', - actionGroups: [{ id: 'default', name: 'Default' }], - defaultActionGroupId: 'default', - async executor() {}, - producer: 'alerts', - })); - alertsClientParams.getEventLogClient.mockResolvedValue(eventLogClient); -}); - -const mockedDateString = '2019-02-12T21:01:22.479Z'; -const mockedDate = new Date(mockedDateString); -const DateOriginal = Date; - -// A version of date that responds to `new Date(null|undefined)` and `Date.now()` -// by returning a fixed date, otherwise should be same as Date. -/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ -(global as any).Date = class Date { - constructor(...args: unknown[]) { - // sometimes the ctor has no args, sometimes has a single `null` arg - if (args[0] == null) { - // @ts-ignore - return mockedDate; - } else { - // @ts-ignore - return new DateOriginal(...args); - } - } - static now() { - return mockedDate.getTime(); - } - static parse(string: string) { - return DateOriginal.parse(string); - } -}; - -function getMockData(overwrites: Record = {}): CreateOptions['data'] { - return { - enabled: true, - name: 'abc', - tags: ['foo'], - alertTypeId: '123', - consumer: 'bar', - schedule: { interval: '10s' }, - throttle: null, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - ], - ...overwrites, - }; -} - -describe('create()', () => { - let alertsClient: AlertsClient; - - beforeEach(() => { - alertsClient = new AlertsClient(alertsClientParams); - }); - - describe('authorization', () => { - function tryToExecuteOperation(options: CreateOptions): Promise { - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - createdAt: '2019-02-12T21:01:22.479Z', - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - taskManager.schedule.mockResolvedValueOnce({ - id: 'task-123', - taskType: 'alerting:123', - scheduledAt: new Date(), - attempts: 1, - status: TaskStatus.Idle, - runAt: new Date(), - startedAt: null, - retryAt: null, - state: {}, - params: {}, - ownerId: null, - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [], - scheduledTaskId: 'task-123', - }, - references: [ - { - id: '1', - name: 'action_0', - type: 'action', - }, - ], - }); - - return alertsClient.create(options); - } - - test('ensures user is authorised to create this type of alert under the consumer', async () => { - const data = getMockData({ - alertTypeId: 'myType', - consumer: 'myApp', - }); - - await tryToExecuteOperation({ data }); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'create'); - }); - - test('throws when user is not authorised to create this type of alert', async () => { - const data = getMockData({ - alertTypeId: 'myType', - consumer: 'myApp', - }); - - authorization.ensureAuthorized.mockRejectedValue( - new Error(`Unauthorized to create a "myType" alert for "myApp"`) - ); - - await expect(tryToExecuteOperation({ data })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to create a "myType" alert for "myApp"]` - ); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'create'); - }); - }); - - test('creates an alert', async () => { - const data = getMockData(); - const createdAttributes = { - ...data, - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - createdAt: '2019-02-12T21:01:22.479Z', - createdBy: 'elastic', - updatedBy: 'elastic', - muteAll: false, - mutedInstanceIds: [], - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - }; - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: createdAttributes, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - taskManager.schedule.mockResolvedValueOnce({ - id: 'task-123', - taskType: 'alerting:123', - scheduledAt: new Date(), - attempts: 1, - status: TaskStatus.Idle, - runAt: new Date(), - startedAt: null, - retryAt: null, - state: {}, - params: {}, - ownerId: null, - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - ...createdAttributes, - scheduledTaskId: 'task-123', - }, - references: [ - { - id: '1', - name: 'action_0', - type: 'action', - }, - ], - }); - const result = await alertsClient.create({ data }); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('123', 'bar', 'create'); - expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionTypeId": "test", - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "consumer": "bar", - "createdAt": 2019-02-12T21:01:22.479Z, - "createdBy": "elastic", - "enabled": true, - "id": "1", - "muteAll": false, - "mutedInstanceIds": Array [], - "name": "abc", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "scheduledTaskId": "task-123", - "tags": Array [ - "foo", - ], - "throttle": null, - "updatedAt": 2019-02-12T21:01:22.479Z, - "updatedBy": "elastic", - } - `); - expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionRef": "action_0", - "actionTypeId": "test", - "group": "default", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "apiKey": null, - "apiKeyOwner": null, - "consumer": "bar", - "createdAt": "2019-02-12T21:01:22.479Z", - "createdBy": "elastic", - "enabled": true, - "executionStatus": Object { - "error": null, - "lastExecutionDate": "2019-02-12T21:01:22.479Z", - "status": "pending", - }, - "meta": Object { - "versionApiKeyLastmodified": "v7.10.0", - }, - "muteAll": false, - "mutedInstanceIds": Array [], - "name": "abc", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "tags": Array [ - "foo", - ], - "throttle": null, - "updatedBy": "elastic", - } - `); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "references": Array [ - Object { - "id": "1", - "name": "action_0", - "type": "action", - }, - ], - } - `); - expect(taskManager.schedule).toHaveBeenCalledTimes(1); - expect(taskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "params": Object { - "alertId": "1", - "spaceId": "default", - }, - "scope": Array [ - "alerting", - ], - "state": Object { - "alertInstances": Object {}, - "alertTypeState": Object {}, - "previousStartedAt": null, - }, - "taskType": "alerting:123", - }, - ] - `); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(3); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1'); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "scheduledTaskId": "task-123", - } - `); - }); - - test('creates an alert with multiple actions', async () => { - const data = getMockData({ - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - { - group: 'default', - id: '2', - params: { - foo: true, - }, - }, - ], - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - createdAt: new Date().toISOString(), - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - { - group: 'default', - actionRef: 'action_1', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - { - group: 'default', - actionRef: 'action_2', - actionTypeId: 'test2', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - { - name: 'action_1', - type: 'action', - id: '1', - }, - { - name: 'action_2', - type: 'action', - id: '2', - }, - ], - }); - taskManager.schedule.mockResolvedValueOnce({ - id: 'task-123', - taskType: 'alerting:123', - scheduledAt: new Date(), - attempts: 1, - status: TaskStatus.Idle, - runAt: new Date(), - startedAt: null, - retryAt: null, - state: {}, - params: {}, - ownerId: null, - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [], - scheduledTaskId: 'task-123', - }, - references: [], - }); - const result = await alertsClient.create({ data }); - expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionTypeId": "test", - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - Object { - "actionTypeId": "test", - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - Object { - "actionTypeId": "test2", - "group": "default", - "id": "2", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "createdAt": 2019-02-12T21:01:22.479Z, - "id": "1", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "scheduledTaskId": "task-123", - "updatedAt": 2019-02-12T21:01:22.479Z, - } - `); - }); - - test('creates a disabled alert', async () => { - const data = getMockData({ enabled: false }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - enabled: false, - alertTypeId: '123', - schedule: { interval: 10000 }, - params: { - bar: true, - }, - createdAt: new Date().toISOString(), - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - const result = await alertsClient.create({ data }); - expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionTypeId": "test", - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "createdAt": 2019-02-12T21:01:22.479Z, - "enabled": false, - "id": "1", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": 10000, - }, - "updatedAt": 2019-02-12T21:01:22.479Z, - } - `); - expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); - expect(taskManager.schedule).toHaveBeenCalledTimes(0); - }); - - test('should trim alert name when creating API key', async () => { - const data = getMockData({ name: ' my alert name ' }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - enabled: false, - name: ' my alert name ', - alertTypeId: '123', - schedule: { interval: 10000 }, - params: { - bar: true, - }, - createdAt: new Date().toISOString(), - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - taskManager.schedule.mockResolvedValueOnce({ - id: 'task-123', - taskType: 'alerting:123', - scheduledAt: new Date(), - attempts: 1, - status: TaskStatus.Idle, - runAt: new Date(), - startedAt: null, - retryAt: null, - state: {}, - params: {}, - ownerId: null, - }); - - await alertsClient.create({ data }); - expect(alertsClientParams.createAPIKey).toHaveBeenCalledWith('Alerting: 123/my alert name'); - }); - - test('should validate params', async () => { - const data = getMockData(); - alertTypeRegistry.get.mockReturnValue({ - id: '123', - name: 'Test', - actionGroups: [ - { - id: 'default', - name: 'Default', - }, - ], - defaultActionGroupId: 'default', - validate: { - params: schema.object({ - param1: schema.string(), - threshold: schema.number({ min: 0, max: 1 }), - }), - }, - async executor() {}, - producer: 'alerts', - }); - await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( - `"params invalid: [param1]: expected value of type [string] but got [undefined]"` - ); - }); - - test('throws error if loading actions fails', async () => { - const data = getMockData(); - const actionsClient = actionsClientMock.create(); - actionsClient.getBulk.mockRejectedValueOnce(new Error('Test Error')); - alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); - await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Test Error"` - ); - expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); - expect(taskManager.schedule).not.toHaveBeenCalled(); - }); - - test('throws error and invalidates API key when create saved object fails', async () => { - const data = getMockData(); - alertsClientParams.createAPIKey.mockResolvedValueOnce({ - apiKeysEnabled: true, - result: { id: '123', name: '123', api_key: 'abc' }, - }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Test failure')); - await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Test failure"` - ); - expect(taskManager.schedule).not.toHaveBeenCalled(); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); - }); - - test('attempts to remove saved object if scheduling failed', async () => { - const data = getMockData(); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - taskManager.schedule.mockRejectedValueOnce(new Error('Test failure')); - unsecuredSavedObjectsClient.delete.mockResolvedValueOnce({}); - await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Test failure"` - ); - expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "alert", - "1", - ] - `); - }); - - test('returns task manager error if cleanup fails, logs to console', async () => { - const data = getMockData(); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - taskManager.schedule.mockRejectedValueOnce(new Error('Task manager error')); - unsecuredSavedObjectsClient.delete.mockRejectedValueOnce( - new Error('Saved object delete error') - ); - await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Task manager error"` - ); - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'Failed to cleanup alert "1" after scheduling task failed. Error: Saved object delete error' - ); - }); - - test('throws an error if alert type not registerd', async () => { - const data = getMockData(); - alertTypeRegistry.get.mockImplementation(() => { - throw new Error('Invalid type'); - }); - await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Invalid type"` - ); - }); - - test('calls the API key function', async () => { - const data = getMockData(); - alertsClientParams.createAPIKey.mockResolvedValueOnce({ - apiKeysEnabled: true, - result: { id: '123', name: '123', api_key: 'abc' }, - }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - taskManager.schedule.mockResolvedValueOnce({ - id: 'task-123', - taskType: 'alerting:123', - scheduledAt: new Date(), - attempts: 1, - status: TaskStatus.Idle, - runAt: new Date(), - startedAt: null, - retryAt: null, - state: {}, - params: {}, - ownerId: null, - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [], - scheduledTaskId: 'task-123', - }, - references: [ - { - id: '1', - name: 'action_0', - type: 'action', - }, - ], - }); - await alertsClient.create({ data }); - - expect(alertsClientParams.createAPIKey).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith( - 'alert', - { - actions: [ - { - actionRef: 'action_0', - group: 'default', - actionTypeId: 'test', - params: { foo: true }, - }, - ], - alertTypeId: '123', - consumer: 'bar', - name: 'abc', - params: { bar: true }, - apiKey: Buffer.from('123:abc').toString('base64'), - apiKeyOwner: 'elastic', - createdBy: 'elastic', - createdAt: '2019-02-12T21:01:22.479Z', - updatedBy: 'elastic', - enabled: true, - meta: { - versionApiKeyLastmodified: 'v7.10.0', - }, - schedule: { interval: '10s' }, - throttle: null, - muteAll: false, - mutedInstanceIds: [], - tags: ['foo'], - executionStatus: { - lastExecutionDate: '2019-02-12T21:01:22.479Z', - status: 'pending', - error: null, - }, - }, - { - references: [ - { - id: '1', - name: 'action_0', - type: 'action', - }, - ], - } - ); - }); - - test(`doesn't create API key for disabled alerts`, async () => { - const data = getMockData({ enabled: false }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - taskManager.schedule.mockResolvedValueOnce({ - id: 'task-123', - taskType: 'alerting:123', - scheduledAt: new Date(), - attempts: 1, - status: TaskStatus.Idle, - runAt: new Date(), - startedAt: null, - retryAt: null, - state: {}, - params: {}, - ownerId: null, - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [], - scheduledTaskId: 'task-123', - }, - references: [ - { - id: '1', - name: 'action_0', - type: 'action', - }, - ], - }); - await alertsClient.create({ data }); - - expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith( - 'alert', - { - actions: [ - { - actionRef: 'action_0', - group: 'default', - actionTypeId: 'test', - params: { foo: true }, - }, - ], - alertTypeId: '123', - consumer: 'bar', - name: 'abc', - params: { bar: true }, - apiKey: null, - apiKeyOwner: null, - createdBy: 'elastic', - createdAt: '2019-02-12T21:01:22.479Z', - updatedBy: 'elastic', - enabled: false, - meta: { - versionApiKeyLastmodified: 'v7.10.0', - }, - schedule: { interval: '10s' }, - throttle: null, - muteAll: false, - mutedInstanceIds: [], - tags: ['foo'], - executionStatus: { - lastExecutionDate: '2019-02-12T21:01:22.479Z', - status: 'pending', - error: null, - }, - }, - { - references: [ - { - id: '1', - name: 'action_0', - type: 'action', - }, - ], - } - ); - }); -}); - -describe('enable()', () => { - let alertsClient: AlertsClient; - const existingAlert = { - id: '1', - type: 'alert', - attributes: { - consumer: 'myApp', - schedule: { interval: '10s' }, - alertTypeId: 'myType', - enabled: false, - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - }, - version: '123', - references: [], - }; - - beforeEach(() => { - alertsClient = new AlertsClient(alertsClientParams); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingAlert); - unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); - alertsClientParams.createAPIKey.mockResolvedValue({ - apiKeysEnabled: false, - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - ...existingAlert, - attributes: { - ...existingAlert.attributes, - enabled: true, - apiKey: null, - apiKeyOwner: null, - updatedBy: 'elastic', - }, - }); - taskManager.schedule.mockResolvedValue({ - id: 'task-123', - scheduledAt: new Date(), - attempts: 0, - status: TaskStatus.Idle, - runAt: new Date(), - state: {}, - params: {}, - taskType: '', - startedAt: null, - retryAt: null, - ownerId: null, - }); - }); - - describe('authorization', () => { - beforeEach(() => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingAlert); - unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); - alertsClientParams.createAPIKey.mockResolvedValue({ - apiKeysEnabled: false, - }); - taskManager.schedule.mockResolvedValue({ - id: 'task-123', - scheduledAt: new Date(), - attempts: 0, - status: TaskStatus.Idle, - runAt: new Date(), - state: {}, - params: {}, - taskType: '', - startedAt: null, - retryAt: null, - ownerId: null, - }); - }); - - test('ensures user is authorised to enable this type of alert under the consumer', async () => { - await alertsClient.enable({ id: '1' }); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'enable'); - expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); - }); - - test('throws when user is not authorised to enable this type of alert', async () => { - authorization.ensureAuthorized.mockRejectedValue( - new Error(`Unauthorized to enable a "myType" alert for "myApp"`) - ); - - await expect(alertsClient.enable({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to enable a "myType" alert for "myApp"]` - ); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'enable'); - }); - }); - - test('enables an alert', async () => { - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - ...existingAlert, - attributes: { - ...existingAlert.attributes, - enabled: true, - apiKey: null, - apiKeyOwner: null, - updatedBy: 'elastic', - }, - }); - - await alertsClient.enable({ id: '1' }); - expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); - expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { - namespace: 'default', - }); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); - expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( - 'alert', - '1', - { - schedule: { interval: '10s' }, - alertTypeId: 'myType', - consumer: 'myApp', - enabled: true, - meta: { - versionApiKeyLastmodified: kibanaVersion, - }, - updatedBy: 'elastic', - apiKey: null, - apiKeyOwner: null, - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - }, - { - version: '123', - } - ); - expect(taskManager.schedule).toHaveBeenCalledWith({ - taskType: `alerting:myType`, - params: { - alertId: '1', - spaceId: 'default', - }, - state: { - alertInstances: {}, - alertTypeState: {}, - previousStartedAt: null, - }, - scope: ['alerting'], - }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { - scheduledTaskId: 'task-123', - }); - }); - - test('invalidates API key if ever one existed prior to updating', async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ - ...existingAlert, - attributes: { - ...existingAlert.attributes, - apiKey: Buffer.from('123:abc').toString('base64'), - }, - }); - - await alertsClient.enable({ id: '1' }); - expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); - expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { - namespace: 'default', - }); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); - }); - - test(`doesn't enable already enabled alerts`, async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ - ...existingAlert, - attributes: { - ...existingAlert.attributes, - enabled: true, - }, - }); - - await alertsClient.enable({ id: '1' }); - expect(alertsClientParams.getUserName).not.toHaveBeenCalled(); - expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); - expect(taskManager.schedule).not.toHaveBeenCalled(); - }); - - test('sets API key when createAPIKey returns one', async () => { - alertsClientParams.createAPIKey.mockResolvedValueOnce({ - apiKeysEnabled: true, - result: { id: '123', name: '123', api_key: 'abc' }, - }); - - await alertsClient.enable({ id: '1' }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( - 'alert', - '1', - { - schedule: { interval: '10s' }, - alertTypeId: 'myType', - consumer: 'myApp', - enabled: true, - meta: { - versionApiKeyLastmodified: kibanaVersion, - }, - apiKey: Buffer.from('123:abc').toString('base64'), - apiKeyOwner: 'elastic', - updatedBy: 'elastic', - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - }, - { - version: '123', - } - ); - }); - - test('falls back when failing to getDecryptedAsInternalUser', async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); - - await alertsClient.enable({ id: '1' }); - expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'enable(): Failed to load API key to invalidate on alert 1: Fail' - ); - }); - - test('throws error when failing to load the saved object using SOC', async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); - unsecuredSavedObjectsClient.get.mockRejectedValueOnce(new Error('Fail to get')); - - await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Fail to get"` - ); - expect(alertsClientParams.getUserName).not.toHaveBeenCalled(); - expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); - expect(taskManager.schedule).not.toHaveBeenCalled(); - }); - - test('throws error when failing to update the first time', async () => { - alertsClientParams.createAPIKey.mockResolvedValueOnce({ - apiKeysEnabled: true, - result: { id: '123', name: '123', api_key: 'abc' }, - }); - unsecuredSavedObjectsClient.update.mockReset(); - unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail to update')); - - await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Fail to update"` - ); - expect(alertsClientParams.getUserName).toHaveBeenCalled(); - expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); - expect(taskManager.schedule).not.toHaveBeenCalled(); - }); - - test('throws error when failing to update the second time', async () => { - unsecuredSavedObjectsClient.update.mockReset(); - unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ - ...existingAlert, - attributes: { - ...existingAlert.attributes, - enabled: true, - }, - }); - unsecuredSavedObjectsClient.update.mockRejectedValueOnce( - new Error('Fail to update second time') - ); - - await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Fail to update second time"` - ); - expect(alertsClientParams.getUserName).toHaveBeenCalled(); - expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(2); - expect(taskManager.schedule).toHaveBeenCalled(); - }); - - test('throws error when failing to schedule task', async () => { - taskManager.schedule.mockRejectedValueOnce(new Error('Fail to schedule')); - - await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Fail to schedule"` - ); - expect(alertsClientParams.getUserName).toHaveBeenCalled(); - expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); - }); -}); - -describe('disable()', () => { - let alertsClient: AlertsClient; - const existingAlert = { - id: '1', - type: 'alert', - attributes: { - consumer: 'myApp', - schedule: { interval: '10s' }, - alertTypeId: 'myType', - enabled: true, - scheduledTaskId: 'task-123', - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - }, - version: '123', - references: [], - }; - const existingDecryptedAlert = { - ...existingAlert, - attributes: { - ...existingAlert.attributes, - apiKey: Buffer.from('123:abc').toString('base64'), - }, - version: '123', - references: [], - }; - - beforeEach(() => { - alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); - }); - - describe('authorization', () => { - test('ensures user is authorised to disable this type of alert under the consumer', async () => { - await alertsClient.disable({ id: '1' }); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'disable'); - }); - - test('throws when user is not authorised to disable this type of alert', async () => { - authorization.ensureAuthorized.mockRejectedValue( - new Error(`Unauthorized to disable a "myType" alert for "myApp"`) - ); - - await expect(alertsClient.disable({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to disable a "myType" alert for "myApp"]` - ); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'disable'); - }); - }); - - test('disables an alert', async () => { - await alertsClient.disable({ id: '1' }); - expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); - expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { - namespace: 'default', - }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( - 'alert', - '1', - { - consumer: 'myApp', - schedule: { interval: '10s' }, - alertTypeId: 'myType', - enabled: false, - meta: { - versionApiKeyLastmodified: kibanaVersion, - }, - scheduledTaskId: null, - apiKey: null, - apiKeyOwner: null, - updatedBy: 'elastic', - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - }, - { - version: '123', - } - ); - expect(taskManager.remove).toHaveBeenCalledWith('task-123'); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); - }); - - test('falls back when getDecryptedAsInternalUser throws an error', async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); - - await alertsClient.disable({ id: '1' }); - expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); - expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { - namespace: 'default', - }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( - 'alert', - '1', - { - consumer: 'myApp', - schedule: { interval: '10s' }, - alertTypeId: 'myType', - enabled: false, - meta: { - versionApiKeyLastmodified: kibanaVersion, - }, - scheduledTaskId: null, - apiKey: null, - apiKeyOwner: null, - updatedBy: 'elastic', - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - }, - { - version: '123', - } - ); - expect(taskManager.remove).toHaveBeenCalledWith('task-123'); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); - }); - - test(`doesn't disable already disabled alerts`, async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ - ...existingDecryptedAlert, - attributes: { - ...existingDecryptedAlert.attributes, - actions: [], - enabled: false, - }, - }); - - await alertsClient.disable({ id: '1' }); - expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); - expect(taskManager.remove).not.toHaveBeenCalled(); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); - }); - - test(`doesn't invalidate when no API key is used`, async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce(existingAlert); - - await alertsClient.disable({ id: '1' }); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); - }); - - test('swallows error when failing to load decrypted saved object', async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); - - await alertsClient.disable({ id: '1' }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); - expect(taskManager.remove).toHaveBeenCalled(); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'disable(): Failed to load API key to invalidate on alert 1: Fail' - ); - }); - - test('throws when unsecuredSavedObjectsClient update fails', async () => { - unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Failed to update')); - - await expect(alertsClient.disable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to update"` - ); - }); - - test('swallows error when invalidate API key throws', async () => { - alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); - - await alertsClient.disable({ id: '1' }); - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'Failed to invalidate API Key: Fail' - ); - }); - - test('throws when failing to remove task from task manager', async () => { - taskManager.remove.mockRejectedValueOnce(new Error('Failed to remove task')); - - await expect(alertsClient.disable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to remove task"` - ); - }); -}); - -describe('muteAll()', () => { - test('mutes an alert', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - muteAll: false, - }, - references: [], - version: '123', - }); - - await alertsClient.muteAll({ id: '1' }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( - 'alert', - '1', - { - muteAll: true, - mutedInstanceIds: [], - updatedBy: 'elastic', - }, - { - version: '123', - } - ); - }); - - describe('authorization', () => { - beforeEach(() => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - consumer: 'myApp', - schedule: { interval: '10s' }, - alertTypeId: 'myType', - apiKey: null, - apiKeyOwner: null, - enabled: false, - scheduledTaskId: null, - updatedBy: 'elastic', - muteAll: false, - }, - references: [], - }); - }); - - test('ensures user is authorised to muteAll this type of alert under the consumer', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - await alertsClient.muteAll({ id: '1' }); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'muteAll'); - expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); - }); - - test('throws when user is not authorised to muteAll this type of alert', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - authorization.ensureAuthorized.mockRejectedValue( - new Error(`Unauthorized to muteAll a "myType" alert for "myApp"`) - ); - - await expect(alertsClient.muteAll({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to muteAll a "myType" alert for "myApp"]` - ); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'muteAll'); - }); - }); -}); - -describe('unmuteAll()', () => { - test('unmutes an alert', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - muteAll: true, - }, - references: [], - version: '123', - }); - - await alertsClient.unmuteAll({ id: '1' }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( - 'alert', - '1', - { - muteAll: false, - mutedInstanceIds: [], - updatedBy: 'elastic', - }, - { - version: '123', - } - ); - }); - - describe('authorization', () => { - beforeEach(() => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - consumer: 'myApp', - schedule: { interval: '10s' }, - alertTypeId: 'myType', - apiKey: null, - apiKeyOwner: null, - enabled: false, - scheduledTaskId: null, - updatedBy: 'elastic', - muteAll: false, - }, - references: [], - }); - }); - - test('ensures user is authorised to unmuteAll this type of alert under the consumer', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - await alertsClient.unmuteAll({ id: '1' }); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'unmuteAll'); - expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); - }); - - test('throws when user is not authorised to unmuteAll this type of alert', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - authorization.ensureAuthorized.mockRejectedValue( - new Error(`Unauthorized to unmuteAll a "myType" alert for "myApp"`) - ); - - await expect(alertsClient.unmuteAll({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to unmuteAll a "myType" alert for "myApp"]` - ); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'unmuteAll'); - }); - }); -}); - -describe('muteInstance()', () => { - test('mutes an alert instance', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [], - schedule: { interval: '10s' }, - alertTypeId: '2', - enabled: true, - scheduledTaskId: 'task-123', - mutedInstanceIds: [], - }, - version: '123', - references: [], - }); - - await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( - 'alert', - '1', - { - mutedInstanceIds: ['2'], - updatedBy: 'elastic', - }, - { - version: '123', - } - ); - }); - - test('skips muting when alert instance already muted', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [], - schedule: { interval: '10s' }, - alertTypeId: '2', - enabled: true, - scheduledTaskId: 'task-123', - mutedInstanceIds: ['2'], - }, - references: [], - }); - - await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); - }); - - test('skips muting when alert is muted', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [], - schedule: { interval: '10s' }, - alertTypeId: '2', - enabled: true, - scheduledTaskId: 'task-123', - mutedInstanceIds: [], - muteAll: true, - }, - references: [], - }); - - await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); - }); - - describe('authorization', () => { - beforeEach(() => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - schedule: { interval: '10s' }, - alertTypeId: 'myType', - consumer: 'myApp', - enabled: true, - scheduledTaskId: 'task-123', - mutedInstanceIds: [], - }, - version: '123', - references: [], - }); - }); - - test('ensures user is authorised to muteInstance this type of alert under the consumer', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); - - expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'muteInstance' - ); - }); - - test('throws when user is not authorised to muteInstance this type of alert', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - authorization.ensureAuthorized.mockRejectedValue( - new Error(`Unauthorized to muteInstance a "myType" alert for "myApp"`) - ); - - await expect( - alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }) - ).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to muteInstance a "myType" alert for "myApp"]` - ); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'muteInstance' - ); - }); - }); -}); - -describe('unmuteInstance()', () => { - test('unmutes an alert instance', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [], - schedule: { interval: '10s' }, - alertTypeId: '2', - enabled: true, - scheduledTaskId: 'task-123', - mutedInstanceIds: ['2'], - }, - version: '123', - references: [], - }); - - await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( - 'alert', - '1', - { - mutedInstanceIds: [], - updatedBy: 'elastic', - }, - { version: '123' } - ); - }); - - test('skips unmuting when alert instance not muted', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [], - schedule: { interval: '10s' }, - alertTypeId: '2', - enabled: true, - scheduledTaskId: 'task-123', - mutedInstanceIds: [], - }, - references: [], - }); - - await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); - }); - - test('skips unmuting when alert is muted', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [], - schedule: { interval: '10s' }, - alertTypeId: '2', - enabled: true, - scheduledTaskId: 'task-123', - mutedInstanceIds: [], - muteAll: true, - }, - references: [], - }); - - await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); - expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); - }); - - describe('authorization', () => { - beforeEach(() => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - alertTypeId: 'myType', - consumer: 'myApp', - schedule: { interval: '10s' }, - enabled: true, - scheduledTaskId: 'task-123', - mutedInstanceIds: ['2'], - }, - version: '123', - references: [], - }); - }); - - test('ensures user is authorised to unmuteInstance this type of alert under the consumer', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); - - expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'unmuteInstance' - ); - }); - - test('throws when user is not authorised to unmuteInstance this type of alert', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - authorization.ensureAuthorized.mockRejectedValue( - new Error(`Unauthorized to unmuteInstance a "myType" alert for "myApp"`) - ); - - await expect( - alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }) - ).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to unmuteInstance a "myType" alert for "myApp"]` - ); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'unmuteInstance' - ); - }); - }); -}); - -describe('get()', () => { - test('calls saved objects client with given params', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - createdAt: new Date().toISOString(), - actions: [ - { - group: 'default', - actionRef: 'action_0', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - const result = await alertsClient.get({ id: '1' }); - expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "123", - "createdAt": 2019-02-12T21:01:22.479Z, - "id": "1", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "updatedAt": 2019-02-12T21:01:22.479Z, - } - `); - expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "alert", - "1", - ] - `); - }); - - test(`throws an error when references aren't found`, async () => { - const alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - params: { - foo: true, - }, - }, - ], - }, - references: [], - }); - await expect(alertsClient.get({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Action reference \\"action_0\\" not found in alert id: 1"` - ); - }); - - describe('authorization', () => { - beforeEach(() => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: 'myType', - consumer: 'myApp', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - }); - - test('ensures user is authorised to get this type of alert under the consumer', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - await alertsClient.get({ id: '1' }); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'get'); - }); - - test('throws when user is not authorised to get this type of alert', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - authorization.ensureAuthorized.mockRejectedValue( - new Error(`Unauthorized to get a "myType" alert for "myApp"`) - ); - - await expect(alertsClient.get({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to get a "myType" alert for "myApp"]` - ); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'get'); - }); - }); -}); - -describe('getAlertState()', () => { - test('calls saved objects client with given params', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - - taskManager.get.mockResolvedValueOnce({ - id: '1', - taskType: 'alerting:123', - scheduledAt: new Date(), - attempts: 1, - status: TaskStatus.Idle, - runAt: new Date(), - startedAt: null, - retryAt: null, - state: {}, - params: {}, - ownerId: null, - }); - - await alertsClient.getAlertState({ id: '1' }); - expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "alert", - "1", - ] - `); - }); - - test('gets the underlying task from TaskManager', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - - const scheduledTaskId = 'task-123'; - - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - params: { - foo: true, - }, - }, - ], - enabled: true, - scheduledTaskId, - mutedInstanceIds: [], - muteAll: true, - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - - taskManager.get.mockResolvedValueOnce({ - id: scheduledTaskId, - taskType: 'alerting:123', - scheduledAt: new Date(), - attempts: 1, - status: TaskStatus.Idle, - runAt: new Date(), - startedAt: null, - retryAt: null, - state: {}, - params: { - alertId: '1', - }, - ownerId: null, - }); - - await alertsClient.getAlertState({ id: '1' }); - expect(taskManager.get).toHaveBeenCalledTimes(1); - expect(taskManager.get).toHaveBeenCalledWith(scheduledTaskId); - }); - - describe('authorization', () => { - beforeEach(() => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: 'myType', - consumer: 'myApp', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - - taskManager.get.mockResolvedValueOnce({ - id: '1', - taskType: 'alerting:123', - scheduledAt: new Date(), - attempts: 1, - status: TaskStatus.Idle, - runAt: new Date(), - startedAt: null, - retryAt: null, - state: {}, - params: {}, - ownerId: null, - }); - }); - - test('ensures user is authorised to get this type of alert under the consumer', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - await alertsClient.getAlertState({ id: '1' }); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'getAlertState' - ); - }); - - test('throws when user is not authorised to getAlertState this type of alert', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - // `get` check - authorization.ensureAuthorized.mockResolvedValueOnce(); - // `getAlertState` check - authorization.ensureAuthorized.mockRejectedValueOnce( - new Error(`Unauthorized to getAlertState a "myType" alert for "myApp"`) - ); - - await expect(alertsClient.getAlertState({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to getAlertState a "myType" alert for "myApp"]` - ); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'getAlertState' - ); - }); - }); -}); - -const AlertInstanceSummaryFindEventsResult: QueryEventsBySavedObjectResult = { - page: 1, - per_page: 10000, - total: 0, - data: [], -}; - -const AlertInstanceSummaryIntervalSeconds = 1; - -const BaseAlertInstanceSummarySavedObject: SavedObject = { - id: '1', - type: 'alert', - attributes: { - enabled: true, - name: 'alert-name', - tags: ['tag-1', 'tag-2'], - alertTypeId: '123', - consumer: 'alert-consumer', - schedule: { interval: `${AlertInstanceSummaryIntervalSeconds}s` }, - actions: [], - params: {}, - createdBy: null, - updatedBy: null, - createdAt: mockedDateString, - apiKey: null, - apiKeyOwner: null, - throttle: null, - muteAll: false, - mutedInstanceIds: [], - executionStatus: { - status: 'unknown', - lastExecutionDate: '2020-08-20T19:23:38Z', - error: null, - }, - }, - references: [], -}; - -function getAlertInstanceSummarySavedObject( - attributes: Partial = {} -): SavedObject { - return { - ...BaseAlertInstanceSummarySavedObject, - attributes: { ...BaseAlertInstanceSummarySavedObject.attributes, ...attributes }, - }; -} - -describe('getAlertInstanceSummary()', () => { - let alertsClient: AlertsClient; - - beforeEach(() => { - alertsClient = new AlertsClient(alertsClientParams); - }); - - test('runs as expected with some event log data', async () => { - const alertSO = getAlertInstanceSummarySavedObject({ - mutedInstanceIds: ['instance-muted-no-activity'], - }); - unsecuredSavedObjectsClient.get.mockResolvedValueOnce(alertSO); - - const eventsFactory = new EventsFactory(mockedDateString); - const events = eventsFactory - .addExecute() - .addNewInstance('instance-currently-active') - .addNewInstance('instance-previously-active') - .addActiveInstance('instance-currently-active') - .addActiveInstance('instance-previously-active') - .advanceTime(10000) - .addExecute() - .addResolvedInstance('instance-previously-active') - .addActiveInstance('instance-currently-active') - .getEvents(); - const eventsResult = { - ...AlertInstanceSummaryFindEventsResult, - total: events.length, - data: events, - }; - eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(eventsResult); - - const dateStart = new Date(Date.now() - 60 * 1000).toISOString(); - - const result = await alertsClient.getAlertInstanceSummary({ id: '1', dateStart }); - expect(result).toMatchInlineSnapshot(` - Object { - "alertTypeId": "123", - "consumer": "alert-consumer", - "enabled": true, - "errorMessages": Array [], - "id": "1", - "instances": Object { - "instance-currently-active": Object { - "activeStartDate": "2019-02-12T21:01:22.479Z", - "muted": false, - "status": "Active", - }, - "instance-muted-no-activity": Object { - "activeStartDate": undefined, - "muted": true, - "status": "OK", - }, - "instance-previously-active": Object { - "activeStartDate": undefined, - "muted": false, - "status": "OK", - }, - }, - "lastRun": "2019-02-12T21:01:32.479Z", - "muteAll": false, - "name": "alert-name", - "status": "Active", - "statusEndDate": "2019-02-12T21:01:22.479Z", - "statusStartDate": "2019-02-12T21:00:22.479Z", - "tags": Array [ - "tag-1", - "tag-2", - ], - "throttle": null, - } - `); - }); - - // Further tests don't check the result of `getAlertInstanceSummary()`, as the result - // is just the result from the `alertInstanceSummaryFromEventLog()`, which itself - // has a complete set of tests. These tests just make sure the data gets - // sent into `getAlertInstanceSummary()` as appropriate. - - test('calls saved objects and event log client with default params', async () => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); - eventLogClient.findEventsBySavedObject.mockResolvedValueOnce( - AlertInstanceSummaryFindEventsResult - ); - - await alertsClient.getAlertInstanceSummary({ id: '1' }); - - expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1); - expect(eventLogClient.findEventsBySavedObject.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "alert", - "1", - Object { - "end": "2019-02-12T21:01:22.479Z", - "page": 1, - "per_page": 10000, - "sort_order": "desc", - "start": "2019-02-12T21:00:22.479Z", - }, - ] - `); - // calculate the expected start/end date for one test - const { start, end } = eventLogClient.findEventsBySavedObject.mock.calls[0][2]!; - expect(end).toBe(mockedDateString); - - const startMillis = Date.parse(start!); - const endMillis = Date.parse(end!); - const expectedDuration = 60 * AlertInstanceSummaryIntervalSeconds * 1000; - expect(endMillis - startMillis).toBeGreaterThan(expectedDuration - 2); - expect(endMillis - startMillis).toBeLessThan(expectedDuration + 2); - }); - - test('calls event log client with start date', async () => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); - eventLogClient.findEventsBySavedObject.mockResolvedValueOnce( - AlertInstanceSummaryFindEventsResult - ); - - const dateStart = new Date( - Date.now() - 60 * AlertInstanceSummaryIntervalSeconds * 1000 - ).toISOString(); - await alertsClient.getAlertInstanceSummary({ id: '1', dateStart }); - - expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1); - const { start, end } = eventLogClient.findEventsBySavedObject.mock.calls[0][2]!; - - expect({ start, end }).toMatchInlineSnapshot(` - Object { - "end": "2019-02-12T21:01:22.479Z", - "start": "2019-02-12T21:00:22.479Z", - } - `); - }); - - test('calls event log client with relative start date', async () => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); - eventLogClient.findEventsBySavedObject.mockResolvedValueOnce( - AlertInstanceSummaryFindEventsResult - ); - - const dateStart = '2m'; - await alertsClient.getAlertInstanceSummary({ id: '1', dateStart }); - - expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1); - const { start, end } = eventLogClient.findEventsBySavedObject.mock.calls[0][2]!; - - expect({ start, end }).toMatchInlineSnapshot(` - Object { - "end": "2019-02-12T21:01:22.479Z", - "start": "2019-02-12T20:59:22.479Z", - } - `); - }); - - test('invalid start date throws an error', async () => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); - eventLogClient.findEventsBySavedObject.mockResolvedValueOnce( - AlertInstanceSummaryFindEventsResult - ); - - const dateStart = 'ain"t no way this will get parsed as a date'; - expect( - alertsClient.getAlertInstanceSummary({ id: '1', dateStart }) - ).rejects.toMatchInlineSnapshot( - `[Error: Invalid date for parameter dateStart: "ain"t no way this will get parsed as a date"]` - ); - }); - - test('saved object get throws an error', async () => { - unsecuredSavedObjectsClient.get.mockRejectedValueOnce(new Error('OMG!')); - eventLogClient.findEventsBySavedObject.mockResolvedValueOnce( - AlertInstanceSummaryFindEventsResult - ); - - expect(alertsClient.getAlertInstanceSummary({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: OMG!]` - ); - }); - - test('findEvents throws an error', async () => { - unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); - eventLogClient.findEventsBySavedObject.mockRejectedValueOnce(new Error('OMG 2!')); - - // error eaten but logged - await alertsClient.getAlertInstanceSummary({ id: '1' }); - }); -}); - -describe('find()', () => { - const listedTypes = new Set([ - { - actionGroups: [], - actionVariables: undefined, - defaultActionGroupId: 'default', - id: 'myType', - name: 'myType', - producer: 'myApp', - }, - ]); - beforeEach(() => { - authorization.getFindAuthorizationFilter.mockResolvedValue({ - ensureAlertTypeIsAuthorized() {}, - logSuccessfulAuthorization() {}, - }); - unsecuredSavedObjectsClient.find.mockResolvedValueOnce({ - total: 1, - per_page: 10, - page: 1, - saved_objects: [ - { - id: '1', - type: 'alert', - attributes: { - alertTypeId: 'myType', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - createdAt: new Date().toISOString(), - actions: [ - { - group: 'default', - actionRef: 'action_0', - params: { - foo: true, - }, - }, - ], - }, - score: 1, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }, - ], - }); - alertTypeRegistry.list.mockReturnValue(listedTypes); - authorization.filterByAlertTypeAuthorization.mockResolvedValue( - new Set([ - { - id: 'myType', - name: 'Test', - actionGroups: [{ id: 'default', name: 'Default' }], - defaultActionGroupId: 'default', - producer: 'alerts', - authorizedConsumers: { - myApp: { read: true, all: true }, - }, - }, - ]) - ); - }); - - test('calls saved objects client with given params', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - const result = await alertsClient.find({ options: {} }); - expect(result).toMatchInlineSnapshot(` - Object { - "data": Array [ - Object { - "actions": Array [ - Object { - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "myType", - "createdAt": 2019-02-12T21:01:22.479Z, - "id": "1", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "updatedAt": 2019-02-12T21:01:22.479Z, - }, - ], - "page": 1, - "perPage": 10, - "total": 1, - } - `); - expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - Object { - "fields": undefined, - "filter": undefined, - "type": "alert", - }, - ] - `); - }); - - describe('authorization', () => { - test('ensures user is query filter types down to those the user is authorized to find', async () => { - const filter = esKuery.fromKueryExpression( - '((alert.attributes.alertTypeId:myType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myOtherApp))' - ); - authorization.getFindAuthorizationFilter.mockResolvedValue({ - filter, - ensureAlertTypeIsAuthorized() {}, - logSuccessfulAuthorization() {}, - }); - - const alertsClient = new AlertsClient(alertsClientParams); - await alertsClient.find({ options: { filter: 'someTerm' } }); - - const [options] = unsecuredSavedObjectsClient.find.mock.calls[0]; - expect(options.filter).toEqual( - nodeTypes.function.buildNode('and', [esKuery.fromKueryExpression('someTerm'), filter]) - ); - expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledTimes(1); - }); - - test('throws if user is not authorized to find any types', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - authorization.getFindAuthorizationFilter.mockRejectedValue(new Error('not authorized')); - await expect(alertsClient.find({ options: {} })).rejects.toThrowErrorMatchingInlineSnapshot( - `"not authorized"` - ); - }); - - test('ensures authorization even when the fields required to authorize are omitted from the find', async () => { - const ensureAlertTypeIsAuthorized = jest.fn(); - const logSuccessfulAuthorization = jest.fn(); - authorization.getFindAuthorizationFilter.mockResolvedValue({ - ensureAlertTypeIsAuthorized, - logSuccessfulAuthorization, - }); - - unsecuredSavedObjectsClient.find.mockReset(); - unsecuredSavedObjectsClient.find.mockResolvedValue({ - total: 1, - per_page: 10, - page: 1, - saved_objects: [ - { - id: '1', - type: 'alert', - attributes: { - actions: [], - alertTypeId: 'myType', - consumer: 'myApp', - tags: ['myTag'], - }, - score: 1, - references: [], - }, - ], - }); - - const alertsClient = new AlertsClient(alertsClientParams); - expect(await alertsClient.find({ options: { fields: ['tags'] } })).toMatchInlineSnapshot(` - Object { - "data": Array [ - Object { - "actions": Array [], - "id": "1", - "schedule": undefined, - "tags": Array [ - "myTag", - ], - }, - ], - "page": 1, - "perPage": 10, - "total": 1, - } - `); - - expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledWith({ - fields: ['tags', 'alertTypeId', 'consumer'], - type: 'alert', - }); - expect(ensureAlertTypeIsAuthorized).toHaveBeenCalledWith('myType', 'myApp'); - expect(logSuccessfulAuthorization).toHaveBeenCalled(); - }); - }); -}); - -describe('delete()', () => { - let alertsClient: AlertsClient; - const existingAlert = { - id: '1', - type: 'alert', - attributes: { - alertTypeId: 'myType', - consumer: 'myApp', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - scheduledTaskId: 'task-123', - actions: [ - { - group: 'default', - actionTypeId: '.no-op', - actionRef: 'action_0', - params: { - foo: true, - }, - }, - ], - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }; - const existingDecryptedAlert = { - ...existingAlert, - attributes: { - ...existingAlert.attributes, - apiKey: Buffer.from('123:abc').toString('base64'), - }, - }; - - beforeEach(() => { - alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); - unsecuredSavedObjectsClient.delete.mockResolvedValue({ - success: true, - }); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); - }); - - test('successfully removes an alert', async () => { - const result = await alertsClient.delete({ id: '1' }); - expect(result).toEqual({ success: true }); - expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); - expect(taskManager.remove).toHaveBeenCalledWith('task-123'); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); - expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { - namespace: 'default', - }); - expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); - }); - - test('falls back to SOC.get when getDecryptedAsInternalUser throws an error', async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); - - const result = await alertsClient.delete({ id: '1' }); - expect(result).toEqual({ success: true }); - expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); - expect(taskManager.remove).toHaveBeenCalledWith('task-123'); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'delete(): Failed to load API key to invalidate on alert 1: Fail' - ); - }); - - test(`doesn't remove a task when scheduledTaskId is null`, async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ - ...existingDecryptedAlert, - attributes: { - ...existingDecryptedAlert.attributes, - scheduledTaskId: null, - }, - }); - - await alertsClient.delete({ id: '1' }); - expect(taskManager.remove).not.toHaveBeenCalled(); - }); - - test(`doesn't invalidate API key when apiKey is null`, async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ - ...existingAlert, - attributes: { - ...existingAlert.attributes, - apiKey: null, - }, - }); - - await alertsClient.delete({ id: '1' }); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); - }); - - test('swallows error when invalidate API key throws', async () => { - alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); - - await alertsClient.delete({ id: '1' }); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'Failed to invalidate API Key: Fail' - ); - }); - - test('swallows error when getDecryptedAsInternalUser throws an error', async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); - - await alertsClient.delete({ id: '1' }); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'delete(): Failed to load API key to invalidate on alert 1: Fail' - ); - }); - - test('throws error when unsecuredSavedObjectsClient.get throws an error', async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); - unsecuredSavedObjectsClient.get.mockRejectedValue(new Error('SOC Fail')); - - await expect(alertsClient.delete({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"SOC Fail"` - ); - }); - - test('throws error when taskManager.remove throws an error', async () => { - taskManager.remove.mockRejectedValue(new Error('TM Fail')); - - await expect(alertsClient.delete({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"TM Fail"` - ); - }); - - describe('authorization', () => { - test('ensures user is authorised to delete this type of alert under the consumer', async () => { - await alertsClient.delete({ id: '1' }); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'delete'); - }); - - test('throws when user is not authorised to delete this type of alert', async () => { - authorization.ensureAuthorized.mockRejectedValue( - new Error(`Unauthorized to delete a "myType" alert for "myApp"`) - ); - - await expect(alertsClient.delete({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to delete a "myType" alert for "myApp"]` - ); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'delete'); - }); - }); -}); - -describe('update()', () => { - let alertsClient: AlertsClient; - const existingAlert = { - id: '1', - type: 'alert', - attributes: { - enabled: true, - tags: ['foo'], - alertTypeId: 'myType', - schedule: { interval: '10s' }, - consumer: 'myApp', - scheduledTaskId: 'task-123', - params: {}, - throttle: null, - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - }, - references: [], - version: '123', - }; - const existingDecryptedAlert = { - ...existingAlert, - attributes: { - ...existingAlert.attributes, - apiKey: Buffer.from('123:abc').toString('base64'), - }, - }; - - beforeEach(() => { - alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); - alertTypeRegistry.get.mockReturnValue({ - id: 'myType', - name: 'Test', - actionGroups: [{ id: 'default', name: 'Default' }], - defaultActionGroupId: 'default', - async executor() {}, - producer: 'alerts', - }); - }); - - test('updates given parameters', async () => { - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - enabled: true, - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - { - group: 'default', - actionRef: 'action_1', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - { - group: 'default', - actionRef: 'action_2', - actionTypeId: 'test2', - params: { - foo: true, - }, - }, - ], - scheduledTaskId: 'task-123', - createdAt: new Date().toISOString(), - }, - updated_at: new Date().toISOString(), - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - { - name: 'action_1', - type: 'action', - id: '1', - }, - { - name: 'action_2', - type: 'action', - id: '2', - }, - ], - }); - const result = await alertsClient.update({ - id: '1', - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: null, - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - { - group: 'default', - id: '2', - params: { - foo: true, - }, - }, - ], - }, - }); - expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionTypeId": "test", - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - Object { - "actionTypeId": "test", - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - Object { - "actionTypeId": "test2", - "group": "default", - "id": "2", - "params": Object { - "foo": true, - }, - }, - ], - "createdAt": 2019-02-12T21:01:22.479Z, - "enabled": true, - "id": "1", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "scheduledTaskId": "task-123", - "updatedAt": 2019-02-12T21:01:22.479Z, - } - `); - expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { - namespace: 'default', - }); - expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); - expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionRef": "action_0", - "actionTypeId": "test", - "group": "default", - "params": Object { - "foo": true, - }, - }, - Object { - "actionRef": "action_1", - "actionTypeId": "test", - "group": "default", - "params": Object { - "foo": true, - }, - }, - Object { - "actionRef": "action_2", - "actionTypeId": "test2", - "group": "default", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "myType", - "apiKey": null, - "apiKeyOwner": null, - "consumer": "myApp", - "enabled": true, - "meta": Object { - "versionApiKeyLastmodified": "v7.10.0", - }, - "name": "abc", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "scheduledTaskId": "task-123", - "tags": Array [ - "foo", - ], - "throttle": null, - "updatedBy": "elastic", - } - `); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "id": "1", - "overwrite": true, - "references": Array [ - Object { - "id": "1", - "name": "action_0", - "type": "action", - }, - Object { - "id": "1", - "name": "action_1", - "type": "action", - }, - Object { - "id": "2", - "name": "action_2", - "type": "action", - }, - ], - "version": "123", - } - `); - }); - - it('calls the createApiKey function', async () => { - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - alertsClientParams.createAPIKey.mockResolvedValueOnce({ - apiKeysEnabled: true, - result: { id: '123', name: '123', api_key: 'abc' }, - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - enabled: true, - schedule: { interval: '10s' }, - params: { - bar: true, - }, - createdAt: new Date().toISOString(), - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - apiKey: Buffer.from('123:abc').toString('base64'), - scheduledTaskId: 'task-123', - }, - updated_at: new Date().toISOString(), - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - const result = await alertsClient.update({ - id: '1', - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: '5m', - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - ], - }, - }); - expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionTypeId": "test", - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "apiKey": "MTIzOmFiYw==", - "createdAt": 2019-02-12T21:01:22.479Z, - "enabled": true, - "id": "1", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "scheduledTaskId": "task-123", - "updatedAt": 2019-02-12T21:01:22.479Z, - } - `); - expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionRef": "action_0", - "actionTypeId": "test", - "group": "default", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "myType", - "apiKey": "MTIzOmFiYw==", - "apiKeyOwner": "elastic", - "consumer": "myApp", - "enabled": true, - "meta": Object { - "versionApiKeyLastmodified": "v7.10.0", - }, - "name": "abc", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "scheduledTaskId": "task-123", - "tags": Array [ - "foo", - ], - "throttle": "5m", - "updatedBy": "elastic", - } - `); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "id": "1", - "overwrite": true, - "references": Array [ - Object { - "id": "1", - "name": "action_0", - "type": "action", - }, - ], - "version": "123", - } - `); - }); - - it(`doesn't call the createAPIKey function when alert is disabled`, async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ - ...existingDecryptedAlert, - attributes: { - ...existingDecryptedAlert.attributes, - enabled: false, - }, - }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - enabled: false, - schedule: { interval: '10s' }, - params: { - bar: true, - }, - createdAt: new Date().toISOString(), - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - scheduledTaskId: 'task-123', - apiKey: null, - }, - updated_at: new Date().toISOString(), - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - const result = await alertsClient.update({ - id: '1', - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: '5m', - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - ], - }, - }); - expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled(); - expect(result).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionTypeId": "test", - "group": "default", - "id": "1", - "params": Object { - "foo": true, - }, - }, - ], - "apiKey": null, - "createdAt": 2019-02-12T21:01:22.479Z, - "enabled": false, - "id": "1", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "scheduledTaskId": "task-123", - "updatedAt": 2019-02-12T21:01:22.479Z, - } - `); - expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); - expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` - Object { - "actions": Array [ - Object { - "actionRef": "action_0", - "actionTypeId": "test", - "group": "default", - "params": Object { - "foo": true, - }, - }, - ], - "alertTypeId": "myType", - "apiKey": null, - "apiKeyOwner": null, - "consumer": "myApp", - "enabled": false, - "meta": Object { - "versionApiKeyLastmodified": "v7.10.0", - }, - "name": "abc", - "params": Object { - "bar": true, - }, - "schedule": Object { - "interval": "10s", - }, - "scheduledTaskId": "task-123", - "tags": Array [ - "foo", - ], - "throttle": "5m", - "updatedBy": "elastic", - } - `); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "id": "1", - "overwrite": true, - "references": Array [ - Object { - "id": "1", - "name": "action_0", - "type": "action", - }, - ], - "version": "123", - } - `); - }); - - it('should validate params', async () => { - alertTypeRegistry.get.mockReturnValueOnce({ - id: '123', - name: 'Test', - actionGroups: [{ id: 'default', name: 'Default' }], - defaultActionGroupId: 'default', - validate: { - params: schema.object({ - param1: schema.string(), - }), - }, - async executor() {}, - producer: 'alerts', - }); - await expect( - alertsClient.update({ - id: '1', - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: null, - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - ], - }, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"params invalid: [param1]: expected value of type [string] but got [undefined]"` - ); - }); - - it('should trim alert name in the API key name', async () => { - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - enabled: false, - name: ' my alert name ', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - createdAt: new Date().toISOString(), - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - scheduledTaskId: 'task-123', - apiKey: null, - }, - updated_at: new Date().toISOString(), - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - await alertsClient.update({ - id: '1', - data: { - ...existingAlert.attributes, - name: ' my alert name ', - }, - }); - - expect(alertsClientParams.createAPIKey).toHaveBeenCalledWith('Alerting: myType/my alert name'); - }); - - it('swallows error when invalidate API key throws', async () => { - alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - enabled: true, - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - scheduledTaskId: 'task-123', - }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], - }); - await alertsClient.update({ - id: '1', - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: null, - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - ], - }, - }); - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'Failed to invalidate API Key: Fail' - ); - }); - - it('swallows error when getDecryptedAsInternalUser throws', async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - { - id: '2', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test2', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - enabled: true, - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - { - group: 'default', - actionRef: 'action_1', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - { - group: 'default', - actionRef: 'action_2', - actionTypeId: 'test2', - params: { - foo: true, - }, - }, - ], - scheduledTaskId: 'task-123', - createdAt: new Date().toISOString(), - }, - updated_at: new Date().toISOString(), - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - { - name: 'action_1', - type: 'action', - id: '1', - }, - { - name: 'action_2', - type: 'action', - id: '2', - }, - ], - }); - await alertsClient.update({ - id: '1', - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: '5m', - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - { - group: 'default', - id: '2', - params: { - foo: true, - }, - }, - ], - }, - }); - expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'update(): Failed to load API key to invalidate on alert 1: Fail' - ); - }); - - test('throws when unsecuredSavedObjectsClient update fails and invalidates newly created API key', async () => { - alertsClientParams.createAPIKey.mockResolvedValueOnce({ - apiKeysEnabled: true, - result: { id: '234', name: '234', api_key: 'abc' }, - }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - unsecuredSavedObjectsClient.create.mockRejectedValue(new Error('Fail')); - await expect( - alertsClient.update({ - id: '1', - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: null, - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - ], - }, - }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalledWith({ id: '123' }); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '234' }); - }); - - describe('updating an alert schedule', () => { - function mockApiCalls( - alertId: string, - taskId: string, - currentSchedule: IntervalSchedule, - updatedSchedule: IntervalSchedule - ) { - // mock return values from deps - alertTypeRegistry.get.mockReturnValueOnce({ - id: '123', - name: 'Test', - actionGroups: [{ id: 'default', name: 'Default' }], - defaultActionGroupId: 'default', - async executor() {}, - producer: 'alerts', - }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: alertId, - type: 'alert', - attributes: { - actions: [], - enabled: true, - alertTypeId: '123', - schedule: currentSchedule, - scheduledTaskId: 'task-123', - }, - references: [], - version: '123', - }); - - taskManager.schedule.mockResolvedValueOnce({ - id: taskId, - taskType: 'alerting:123', - scheduledAt: new Date(), - attempts: 1, - status: TaskStatus.Idle, - runAt: new Date(), - startedAt: null, - retryAt: null, - state: {}, - params: {}, - ownerId: null, - }); - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: alertId, - type: 'alert', - attributes: { - enabled: true, - schedule: updatedSchedule, - actions: [ - { - group: 'default', - actionRef: 'action_0', - actionTypeId: 'test', - params: { - foo: true, - }, - }, - ], - scheduledTaskId: taskId, - }, - references: [ - { - name: 'action_0', - type: 'action', - id: alertId, - }, - ], - }); - - taskManager.runNow.mockReturnValueOnce(Promise.resolve({ id: alertId })); - } - - test('updating the alert schedule should rerun the task immediately', async () => { - const alertId = uuid.v4(); - const taskId = uuid.v4(); - - mockApiCalls(alertId, taskId, { interval: '60m' }, { interval: '10s' }); - - await alertsClient.update({ - id: alertId, - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: null, - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - ], - }, - }); - - expect(taskManager.runNow).toHaveBeenCalledWith(taskId); - }); - - test('updating the alert without changing the schedule should not rerun the task', async () => { - const alertId = uuid.v4(); - const taskId = uuid.v4(); - - mockApiCalls(alertId, taskId, { interval: '10s' }, { interval: '10s' }); - - await alertsClient.update({ - id: alertId, - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: null, - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - ], - }, - }); - - expect(taskManager.runNow).not.toHaveBeenCalled(); - }); - - test('updating the alert should not wait for the rerun the task to complete', async () => { - const alertId = uuid.v4(); - const taskId = uuid.v4(); - - mockApiCalls(alertId, taskId, { interval: '10s' }, { interval: '30s' }); - - const resolveAfterAlertUpdatedCompletes = resolvable<{ id: string }>(); - - taskManager.runNow.mockReset(); - taskManager.runNow.mockReturnValue(resolveAfterAlertUpdatedCompletes); - - await alertsClient.update({ - id: alertId, - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: null, - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - ], - }, - }); - - expect(taskManager.runNow).toHaveBeenCalled(); - resolveAfterAlertUpdatedCompletes.resolve({ id: alertId }); - }); - - test('logs when the rerun of an alerts underlying task fails', async () => { - const alertId = uuid.v4(); - const taskId = uuid.v4(); - - mockApiCalls(alertId, taskId, { interval: '10s' }, { interval: '30s' }); - - taskManager.runNow.mockReset(); - taskManager.runNow.mockRejectedValue(new Error('Failed to run alert')); - - await alertsClient.update({ - id: alertId, - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: null, - actions: [ - { - group: 'default', - id: '1', - params: { - foo: true, - }, - }, - ], - }, - }); - - expect(taskManager.runNow).toHaveBeenCalled(); - - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - `Alert update failed to run its underlying task. TaskManager runNow failed with Error: Failed to run alert` - ); - }); - }); - - describe('authorization', () => { - beforeEach(() => { - unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: 'myType', - consumer: 'myApp', - enabled: true, - schedule: { interval: '10s' }, - params: { - bar: true, - }, - actions: [], - scheduledTaskId: 'task-123', - createdAt: new Date().toISOString(), - }, - updated_at: new Date().toISOString(), - references: [], - }); - }); - - test('ensures user is authorised to update this type of alert under the consumer', async () => { - await alertsClient.update({ - id: '1', - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: null, - actions: [], - }, - }); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'update'); - }); - - test('throws when user is not authorised to update this type of alert', async () => { - authorization.ensureAuthorized.mockRejectedValue( - new Error(`Unauthorized to update a "myType" alert for "myApp"`) - ); - - await expect( - alertsClient.update({ - id: '1', - data: { - schedule: { interval: '10s' }, - name: 'abc', - tags: ['foo'], - params: { - bar: true, - }, - throttle: null, - actions: [], - }, - }) - ).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to update a "myType" alert for "myApp"]` - ); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'update'); - }); - }); -}); - -describe('updateApiKey()', () => { - let alertsClient: AlertsClient; - const existingAlert = { - id: '1', - type: 'alert', - attributes: { - schedule: { interval: '10s' }, - alertTypeId: 'myType', - consumer: 'myApp', - enabled: true, - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - }, - version: '123', - references: [], - }; - const existingEncryptedAlert = { - ...existingAlert, - attributes: { - ...existingAlert.attributes, - apiKey: Buffer.from('123:abc').toString('base64'), - }, - }; - - beforeEach(() => { - alertsClient = new AlertsClient(alertsClientParams); - unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingEncryptedAlert); - alertsClientParams.createAPIKey.mockResolvedValueOnce({ - apiKeysEnabled: true, - result: { id: '234', name: '123', api_key: 'abc' }, - }); - }); - - test('updates the API key for the alert', async () => { - await alertsClient.updateApiKey({ id: '1' }); - expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); - expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { - namespace: 'default', - }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( - 'alert', - '1', - { - schedule: { interval: '10s' }, - alertTypeId: 'myType', - consumer: 'myApp', - enabled: true, - apiKey: Buffer.from('234:abc').toString('base64'), - apiKeyOwner: 'elastic', - updatedBy: 'elastic', - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - meta: { - versionApiKeyLastmodified: kibanaVersion, - }, - }, - { version: '123' } - ); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); - }); - - test('falls back to SOC when getDecryptedAsInternalUser throws an error', async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); - - await alertsClient.updateApiKey({ id: '1' }); - expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); - expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { - namespace: 'default', - }); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( - 'alert', - '1', - { - schedule: { interval: '10s' }, - alertTypeId: 'myType', - consumer: 'myApp', - enabled: true, - apiKey: Buffer.from('234:abc').toString('base64'), - apiKeyOwner: 'elastic', - updatedBy: 'elastic', - actions: [ - { - group: 'default', - id: '1', - actionTypeId: '1', - actionRef: '1', - params: { - foo: true, - }, - }, - ], - meta: { - versionApiKeyLastmodified: kibanaVersion, - }, - }, - { version: '123' } - ); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); - }); - - test('swallows error when invalidate API key throws', async () => { - alertsClientParams.invalidateAPIKey.mockRejectedValue(new Error('Fail')); - - await alertsClient.updateApiKey({ id: '1' }); - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'Failed to invalidate API Key: Fail' - ); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); - }); - - test('swallows error when getting decrypted object throws', async () => { - encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); - - await alertsClient.updateApiKey({ id: '1' }); - expect(alertsClientParams.logger.error).toHaveBeenCalledWith( - 'updateApiKey(): Failed to load API key to invalidate on alert 1: Fail' - ); - expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); - }); - - test('throws when unsecuredSavedObjectsClient update fails and invalidates newly created API key', async () => { - alertsClientParams.createAPIKey.mockResolvedValueOnce({ - apiKeysEnabled: true, - result: { id: '234', name: '234', api_key: 'abc' }, - }); - unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail')); - - await expect(alertsClient.updateApiKey({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Fail"` - ); - expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalledWith({ id: '123' }); - expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '234' }); - }); - - describe('authorization', () => { - test('ensures user is authorised to updateApiKey this type of alert under the consumer', async () => { - await alertsClient.updateApiKey({ id: '1' }); - - expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); - expect(authorization.ensureAuthorized).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'updateApiKey' - ); - }); - - test('throws when user is not authorised to updateApiKey this type of alert', async () => { - authorization.ensureAuthorized.mockRejectedValue( - new Error(`Unauthorized to updateApiKey a "myType" alert for "myApp"`) - ); - - await expect(alertsClient.updateApiKey({ id: '1' })).rejects.toMatchInlineSnapshot( - `[Error: Unauthorized to updateApiKey a "myType" alert for "myApp"]` - ); - - expect(authorization.ensureAuthorized).toHaveBeenCalledWith( - 'myType', - 'myApp', - 'updateApiKey' - ); - }); - }); -}); - -describe('listAlertTypes', () => { - let alertsClient: AlertsClient; - const alertingAlertType = { - actionGroups: [], - actionVariables: undefined, - defaultActionGroupId: 'default', - id: 'alertingAlertType', - name: 'alertingAlertType', - producer: 'alerts', - }; - const myAppAlertType = { - actionGroups: [], - actionVariables: undefined, - defaultActionGroupId: 'default', - id: 'myAppAlertType', - name: 'myAppAlertType', - producer: 'myApp', - }; - const setOfAlertTypes = new Set([myAppAlertType, alertingAlertType]); - - const authorizedConsumers = { - alerts: { read: true, all: true }, - myApp: { read: true, all: true }, - myOtherApp: { read: true, all: true }, - }; - - beforeEach(() => { - alertsClient = new AlertsClient(alertsClientParams); - }); - - test('should return a list of AlertTypes that exist in the registry', async () => { - alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - authorization.filterByAlertTypeAuthorization.mockResolvedValue( - new Set([ - { ...myAppAlertType, authorizedConsumers }, - { ...alertingAlertType, authorizedConsumers }, - ]) - ); - expect(await alertsClient.listAlertTypes()).toEqual( - new Set([ - { ...myAppAlertType, authorizedConsumers }, - { ...alertingAlertType, authorizedConsumers }, - ]) - ); - }); - - describe('authorization', () => { - const listedTypes = new Set([ - { - actionGroups: [], - actionVariables: undefined, - defaultActionGroupId: 'default', - id: 'myType', - name: 'myType', - producer: 'myApp', - }, - { - id: 'myOtherType', - name: 'Test', - actionGroups: [{ id: 'default', name: 'Default' }], - defaultActionGroupId: 'default', - producer: 'alerts', - }, - ]); - beforeEach(() => { - alertTypeRegistry.list.mockReturnValue(listedTypes); - }); - - test('should return a list of AlertTypes that exist in the registry only if the user is authorised to get them', async () => { - const authorizedTypes = new Set([ - { - id: 'myType', - name: 'Test', - actionGroups: [{ id: 'default', name: 'Default' }], - defaultActionGroupId: 'default', - producer: 'alerts', - authorizedConsumers: { - myApp: { read: true, all: true }, - }, - }, - ]); - authorization.filterByAlertTypeAuthorization.mockResolvedValue(authorizedTypes); - - expect(await alertsClient.listAlertTypes()).toEqual(authorizedTypes); - }); - }); -}); diff --git a/x-pack/plugins/alerts/server/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts similarity index 96% rename from x-pack/plugins/alerts/server/alerts_client.ts rename to x-pack/plugins/alerts/server/alerts_client/alerts_client.ts index bd278d39c622..ef3a9e42b983 100644 --- a/x-pack/plugins/alerts/server/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts @@ -14,8 +14,8 @@ import { SavedObject, PluginInitializerContext, } from 'src/core/server'; -import { esKuery } from '../../../../src/plugins/data/server'; -import { ActionsClient, ActionsAuthorization } from '../../actions/server'; +import { esKuery } from '../../../../../src/plugins/data/server'; +import { ActionsClient, ActionsAuthorization } from '../../../actions/server'; import { Alert, PartialAlert, @@ -27,26 +27,26 @@ import { SanitizedAlert, AlertTaskState, AlertInstanceSummary, -} from './types'; -import { validateAlertTypeParams, alertExecutionStatusFromRaw } from './lib'; +} from '../types'; +import { validateAlertTypeParams, alertExecutionStatusFromRaw } from '../lib'; import { InvalidateAPIKeyParams, GrantAPIKeyResult as SecurityPluginGrantAPIKeyResult, InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult, -} from '../../security/server'; -import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server'; -import { TaskManagerStartContract } from '../../task_manager/server'; -import { taskInstanceToAlertTaskInstance } from './task_runner/alert_task_instance'; -import { deleteTaskIfItExists } from './lib/delete_task_if_it_exists'; -import { RegistryAlertType } from './alert_type_registry'; -import { AlertsAuthorization, WriteOperations, ReadOperations, and } from './authorization'; -import { IEventLogClient } from '../../../plugins/event_log/server'; -import { parseIsoOrRelativeDate } from './lib/iso_or_relative_date'; -import { alertInstanceSummaryFromEventLog } from './lib/alert_instance_summary_from_event_log'; -import { IEvent } from '../../event_log/server'; -import { parseDuration } from '../common/parse_duration'; -import { retryIfConflicts } from './lib/retry_if_conflicts'; -import { partiallyUpdateAlert } from './saved_objects'; +} from '../../../security/server'; +import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; +import { TaskManagerStartContract } from '../../../task_manager/server'; +import { taskInstanceToAlertTaskInstance } from '../task_runner/alert_task_instance'; +import { deleteTaskIfItExists } from '../lib/delete_task_if_it_exists'; +import { RegistryAlertType } from '../alert_type_registry'; +import { AlertsAuthorization, WriteOperations, ReadOperations, and } from '../authorization'; +import { IEventLogClient } from '../../../../plugins/event_log/server'; +import { parseIsoOrRelativeDate } from '../lib/iso_or_relative_date'; +import { alertInstanceSummaryFromEventLog } from '../lib/alert_instance_summary_from_event_log'; +import { IEvent } from '../../../event_log/server'; +import { parseDuration } from '../../common/parse_duration'; +import { retryIfConflicts } from '../lib/retry_if_conflicts'; +import { partiallyUpdateAlert } from '../saved_objects'; export interface RegistryAlertTypeWithAuth extends RegistryAlertType { authorizedConsumers: string[]; diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/index.js b/x-pack/plugins/alerts/server/alerts_client/index.ts similarity index 85% rename from x-pack/plugins/monitoring/public/lib/jquery_flot/index.js rename to x-pack/plugins/alerts/server/alerts_client/index.ts index abf060aca8c0..e40076a29fff 100644 --- a/x-pack/plugins/monitoring/public/lib/jquery_flot/index.js +++ b/x-pack/plugins/alerts/server/alerts_client/index.ts @@ -3,5 +3,4 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -export { default } from './jquery_flot'; +export * from './alerts_client'; 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 new file mode 100644 index 000000000000..d91896d17bf1 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts @@ -0,0 +1,1097 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; +import { AlertsClient, ConstructorOptions, CreateOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/mocks'; +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 { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { TaskStatus } from '../../../../task_manager/server'; +import { getBeforeSetup, setGlobalDate } from './lib'; + +const taskManager = taskManagerMock.createStart(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +setGlobalDate(); + +function getMockData(overwrites: Record = {}): CreateOptions['data'] { + return { + enabled: true, + name: 'abc', + tags: ['foo'], + alertTypeId: '123', + consumer: 'bar', + schedule: { interval: '10s' }, + throttle: null, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + ...overwrites, + }; +} + +describe('create()', () => { + let alertsClient: AlertsClient; + + beforeEach(() => { + alertsClient = new AlertsClient(alertsClientParams); + }); + + describe('authorization', () => { + function tryToExecuteOperation(options: CreateOptions): Promise { + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: '2019-02-12T21:01:22.479Z', + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + taskManager.schedule.mockResolvedValueOnce({ + id: 'task-123', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [], + scheduledTaskId: 'task-123', + }, + references: [ + { + id: '1', + name: 'action_0', + type: 'action', + }, + ], + }); + + return alertsClient.create(options); + } + + test('ensures user is authorised to create this type of alert under the consumer', async () => { + const data = getMockData({ + alertTypeId: 'myType', + consumer: 'myApp', + }); + + await tryToExecuteOperation({ data }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'create'); + }); + + test('throws when user is not authorised to create this type of alert', async () => { + const data = getMockData({ + alertTypeId: 'myType', + consumer: 'myApp', + }); + + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to create a "myType" alert for "myApp"`) + ); + + await expect(tryToExecuteOperation({ data })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to create a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'create'); + }); + }); + + test('creates an alert', async () => { + const data = getMockData(); + const createdAttributes = { + ...data, + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: '2019-02-12T21:01:22.479Z', + createdBy: 'elastic', + updatedBy: 'elastic', + muteAll: false, + mutedInstanceIds: [], + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }; + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: createdAttributes, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + taskManager.schedule.mockResolvedValueOnce({ + id: 'task-123', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + ...createdAttributes, + scheduledTaskId: 'task-123', + }, + references: [ + { + id: '1', + name: 'action_0', + type: 'action', + }, + ], + }); + const result = await alertsClient.create({ data }); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('123', 'bar', 'create'); + expect(result).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionTypeId": "test", + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "consumer": "bar", + "createdAt": 2019-02-12T21:01:22.479Z, + "createdBy": "elastic", + "enabled": true, + "id": "1", + "muteAll": false, + "mutedInstanceIds": Array [], + "name": "abc", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "scheduledTaskId": "task-123", + "tags": Array [ + "foo", + ], + "throttle": null, + "updatedAt": 2019-02-12T21:01:22.479Z, + "updatedBy": "elastic", + } + `); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionRef": "action_0", + "actionTypeId": "test", + "group": "default", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "apiKey": null, + "apiKeyOwner": null, + "consumer": "bar", + "createdAt": "2019-02-12T21:01:22.479Z", + "createdBy": "elastic", + "enabled": true, + "executionStatus": Object { + "error": null, + "lastExecutionDate": "2019-02-12T21:01:22.479Z", + "status": "pending", + }, + "meta": Object { + "versionApiKeyLastmodified": "v7.10.0", + }, + "muteAll": false, + "mutedInstanceIds": Array [], + "name": "abc", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "tags": Array [ + "foo", + ], + "throttle": null, + "updatedBy": "elastic", + } + `); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "references": Array [ + Object { + "id": "1", + "name": "action_0", + "type": "action", + }, + ], + } + `); + expect(taskManager.schedule).toHaveBeenCalledTimes(1); + expect(taskManager.schedule.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "params": Object { + "alertId": "1", + "spaceId": "default", + }, + "scope": Array [ + "alerting", + ], + "state": Object { + "alertInstances": Object {}, + "alertTypeState": Object {}, + "previousStartedAt": null, + }, + "taskType": "alerting:123", + }, + ] + `); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(3); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1'); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "scheduledTaskId": "task-123", + } + `); + }); + + test('creates an alert with multiple actions', async () => { + const data = getMockData({ + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + group: 'default', + id: '2', + params: { + foo: true, + }, + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + { + group: 'default', + actionRef: 'action_1', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + { + group: 'default', + actionRef: 'action_2', + actionTypeId: 'test2', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + { + name: 'action_1', + type: 'action', + id: '1', + }, + { + name: 'action_2', + type: 'action', + id: '2', + }, + ], + }); + taskManager.schedule.mockResolvedValueOnce({ + id: 'task-123', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [], + scheduledTaskId: 'task-123', + }, + references: [], + }); + const result = await alertsClient.create({ data }); + expect(result).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionTypeId": "test", + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + Object { + "actionTypeId": "test", + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + Object { + "actionTypeId": "test2", + "group": "default", + "id": "2", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "createdAt": 2019-02-12T21:01:22.479Z, + "id": "1", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "scheduledTaskId": "task-123", + "updatedAt": 2019-02-12T21:01:22.479Z, + } + `); + }); + + test('creates a disabled alert', async () => { + const data = getMockData({ enabled: false }); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + enabled: false, + alertTypeId: '123', + schedule: { interval: 10000 }, + params: { + bar: true, + }, + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + const result = await alertsClient.create({ data }); + expect(result).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionTypeId": "test", + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "createdAt": 2019-02-12T21:01:22.479Z, + "enabled": false, + "id": "1", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": 10000, + }, + "updatedAt": 2019-02-12T21:01:22.479Z, + } + `); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(taskManager.schedule).toHaveBeenCalledTimes(0); + }); + + test('should trim alert name when creating API key', async () => { + const data = getMockData({ name: ' my alert name ' }); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + enabled: false, + name: ' my alert name ', + alertTypeId: '123', + schedule: { interval: 10000 }, + params: { + bar: true, + }, + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + taskManager.schedule.mockResolvedValueOnce({ + id: 'task-123', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + + await alertsClient.create({ data }); + expect(alertsClientParams.createAPIKey).toHaveBeenCalledWith('Alerting: 123/my alert name'); + }); + + test('should validate params', async () => { + const data = getMockData(); + alertTypeRegistry.get.mockReturnValue({ + id: '123', + name: 'Test', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + validate: { + params: schema.object({ + param1: schema.string(), + threshold: schema.number({ min: 0, max: 1 }), + }), + }, + async executor() {}, + producer: 'alerts', + }); + await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( + `"params invalid: [param1]: expected value of type [string] but got [undefined]"` + ); + }); + + test('throws error if loading actions fails', async () => { + const data = getMockData(); + const actionsClient = actionsClientMock.create(); + actionsClient.getBulk.mockRejectedValueOnce(new Error('Test Error')); + alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); + await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Test Error"` + ); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); + expect(taskManager.schedule).not.toHaveBeenCalled(); + }); + + test('throws error and invalidates API key when create saved object fails', async () => { + const data = getMockData(); + alertsClientParams.createAPIKey.mockResolvedValueOnce({ + apiKeysEnabled: true, + result: { id: '123', name: '123', api_key: 'abc' }, + }); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Test failure')); + await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Test failure"` + ); + expect(taskManager.schedule).not.toHaveBeenCalled(); + expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + }); + + test('attempts to remove saved object if scheduling failed', async () => { + const data = getMockData(); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + taskManager.schedule.mockRejectedValueOnce(new Error('Test failure')); + unsecuredSavedObjectsClient.delete.mockResolvedValueOnce({}); + await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Test failure"` + ); + expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "alert", + "1", + ] + `); + }); + + test('returns task manager error if cleanup fails, logs to console', async () => { + const data = getMockData(); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + taskManager.schedule.mockRejectedValueOnce(new Error('Task manager error')); + unsecuredSavedObjectsClient.delete.mockRejectedValueOnce( + new Error('Saved object delete error') + ); + await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Task manager error"` + ); + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + 'Failed to cleanup alert "1" after scheduling task failed. Error: Saved object delete error' + ); + }); + + test('throws an error if alert type not registerd', async () => { + const data = getMockData(); + alertTypeRegistry.get.mockImplementation(() => { + throw new Error('Invalid type'); + }); + await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Invalid type"` + ); + }); + + test('calls the API key function', async () => { + const data = getMockData(); + alertsClientParams.createAPIKey.mockResolvedValueOnce({ + apiKeysEnabled: true, + result: { id: '123', name: '123', api_key: 'abc' }, + }); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + taskManager.schedule.mockResolvedValueOnce({ + id: 'task-123', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [], + scheduledTaskId: 'task-123', + }, + references: [ + { + id: '1', + name: 'action_0', + type: 'action', + }, + ], + }); + await alertsClient.create({ data }); + + expect(alertsClientParams.createAPIKey).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith( + 'alert', + { + actions: [ + { + actionRef: 'action_0', + group: 'default', + actionTypeId: 'test', + params: { foo: true }, + }, + ], + alertTypeId: '123', + consumer: 'bar', + name: 'abc', + params: { bar: true }, + apiKey: Buffer.from('123:abc').toString('base64'), + apiKeyOwner: 'elastic', + createdBy: 'elastic', + createdAt: '2019-02-12T21:01:22.479Z', + updatedBy: 'elastic', + enabled: true, + meta: { + versionApiKeyLastmodified: 'v7.10.0', + }, + schedule: { interval: '10s' }, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + tags: ['foo'], + executionStatus: { + lastExecutionDate: '2019-02-12T21:01:22.479Z', + status: 'pending', + error: null, + }, + }, + { + references: [ + { + id: '1', + name: 'action_0', + type: 'action', + }, + ], + } + ); + }); + + test(`doesn't create API key for disabled alerts`, async () => { + const data = getMockData({ enabled: false }); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + taskManager.schedule.mockResolvedValueOnce({ + id: 'task-123', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [], + scheduledTaskId: 'task-123', + }, + references: [ + { + id: '1', + name: 'action_0', + type: 'action', + }, + ], + }); + await alertsClient.create({ data }); + + expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith( + 'alert', + { + actions: [ + { + actionRef: 'action_0', + group: 'default', + actionTypeId: 'test', + params: { foo: true }, + }, + ], + alertTypeId: '123', + consumer: 'bar', + name: 'abc', + params: { bar: true }, + apiKey: null, + apiKeyOwner: null, + createdBy: 'elastic', + createdAt: '2019-02-12T21:01:22.479Z', + updatedBy: 'elastic', + enabled: false, + meta: { + versionApiKeyLastmodified: 'v7.10.0', + }, + schedule: { interval: '10s' }, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + tags: ['foo'], + executionStatus: { + lastExecutionDate: '2019-02-12T21:01:22.479Z', + status: 'pending', + error: null, + }, + }, + { + references: [ + { + id: '1', + name: 'action_0', + type: 'action', + }, + ], + } + ); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts new file mode 100644 index 000000000000..d9b253c3a56e --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/delete.test.ts @@ -0,0 +1,204 @@ +/* + * 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 { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/mocks'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { getBeforeSetup } from './lib'; + +const taskManager = taskManagerMock.createStart(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +describe('delete()', () => { + let alertsClient: AlertsClient; + const existingAlert = { + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + scheduledTaskId: 'task-123', + actions: [ + { + group: 'default', + actionTypeId: '.no-op', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }; + const existingDecryptedAlert = { + ...existingAlert, + attributes: { + ...existingAlert.attributes, + apiKey: Buffer.from('123:abc').toString('base64'), + }, + }; + + beforeEach(() => { + alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); + unsecuredSavedObjectsClient.delete.mockResolvedValue({ + success: true, + }); + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); + }); + + test('successfully removes an alert', async () => { + const result = await alertsClient.delete({ id: '1' }); + expect(result).toEqual({ success: true }); + expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); + expect(taskManager.remove).toHaveBeenCalledWith('task-123'); + expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', + }); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + }); + + test('falls back to SOC.get when getDecryptedAsInternalUser throws an error', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); + + const result = await alertsClient.delete({ id: '1' }); + expect(result).toEqual({ success: true }); + expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); + expect(taskManager.remove).toHaveBeenCalledWith('task-123'); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + 'delete(): Failed to load API key to invalidate on alert 1: Fail' + ); + }); + + test(`doesn't remove a task when scheduledTaskId is null`, async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ + ...existingDecryptedAlert, + attributes: { + ...existingDecryptedAlert.attributes, + scheduledTaskId: null, + }, + }); + + await alertsClient.delete({ id: '1' }); + expect(taskManager.remove).not.toHaveBeenCalled(); + }); + + test(`doesn't invalidate API key when apiKey is null`, async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ + ...existingAlert, + attributes: { + ...existingAlert.attributes, + apiKey: null, + }, + }); + + await alertsClient.delete({ id: '1' }); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + }); + + test('swallows error when invalidate API key throws', async () => { + alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); + + await alertsClient.delete({ id: '1' }); + expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + 'Failed to invalidate API Key: Fail' + ); + }); + + test('swallows error when getDecryptedAsInternalUser throws an error', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); + + await alertsClient.delete({ id: '1' }); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + 'delete(): Failed to load API key to invalidate on alert 1: Fail' + ); + }); + + test('throws error when unsecuredSavedObjectsClient.get throws an error', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); + unsecuredSavedObjectsClient.get.mockRejectedValue(new Error('SOC Fail')); + + await expect(alertsClient.delete({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"SOC Fail"` + ); + }); + + test('throws error when taskManager.remove throws an error', async () => { + taskManager.remove.mockRejectedValue(new Error('TM Fail')); + + await expect(alertsClient.delete({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"TM Fail"` + ); + }); + + describe('authorization', () => { + test('ensures user is authorised to delete this type of alert under the consumer', async () => { + await alertsClient.delete({ id: '1' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'delete'); + }); + + test('throws when user is not authorised to delete this type of alert', async () => { + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to delete a "myType" alert for "myApp"`) + ); + + await expect(alertsClient.delete({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to delete a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'delete'); + }); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts new file mode 100644 index 000000000000..d0557df62202 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/disable.test.ts @@ -0,0 +1,253 @@ +/* + * 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 { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/mocks'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { getBeforeSetup } from './lib'; + +const taskManager = taskManagerMock.createStart(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +describe('disable()', () => { + let alertsClient: AlertsClient; + const existingAlert = { + id: '1', + type: 'alert', + attributes: { + consumer: 'myApp', + schedule: { interval: '10s' }, + alertTypeId: 'myType', + enabled: true, + scheduledTaskId: 'task-123', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + }, + version: '123', + references: [], + }; + const existingDecryptedAlert = { + ...existingAlert, + attributes: { + ...existingAlert.attributes, + apiKey: Buffer.from('123:abc').toString('base64'), + }, + version: '123', + references: [], + }; + + beforeEach(() => { + alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); + }); + + describe('authorization', () => { + test('ensures user is authorised to disable this type of alert under the consumer', async () => { + await alertsClient.disable({ id: '1' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'disable'); + }); + + test('throws when user is not authorised to disable this type of alert', async () => { + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to disable a "myType" alert for "myApp"`) + ); + + await expect(alertsClient.disable({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to disable a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'disable'); + }); + }); + + test('disables an alert', async () => { + await alertsClient.disable({ id: '1' }); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', + }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + consumer: 'myApp', + schedule: { interval: '10s' }, + alertTypeId: 'myType', + enabled: false, + meta: { + versionApiKeyLastmodified: kibanaVersion, + }, + scheduledTaskId: null, + apiKey: null, + apiKeyOwner: null, + updatedBy: 'elastic', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + }, + { + version: '123', + } + ); + expect(taskManager.remove).toHaveBeenCalledWith('task-123'); + expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + }); + + test('falls back when getDecryptedAsInternalUser throws an error', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); + + await alertsClient.disable({ id: '1' }); + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', + }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + consumer: 'myApp', + schedule: { interval: '10s' }, + alertTypeId: 'myType', + enabled: false, + meta: { + versionApiKeyLastmodified: kibanaVersion, + }, + scheduledTaskId: null, + apiKey: null, + apiKeyOwner: null, + updatedBy: 'elastic', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + }, + { + version: '123', + } + ); + expect(taskManager.remove).toHaveBeenCalledWith('task-123'); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + }); + + test(`doesn't disable already disabled alerts`, async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ + ...existingDecryptedAlert, + attributes: { + ...existingDecryptedAlert.attributes, + actions: [], + enabled: false, + }, + }); + + await alertsClient.disable({ id: '1' }); + expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); + expect(taskManager.remove).not.toHaveBeenCalled(); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + }); + + test(`doesn't invalidate when no API key is used`, async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce(existingAlert); + + await alertsClient.disable({ id: '1' }); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + }); + + test('swallows error when failing to load decrypted saved object', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); + + await alertsClient.disable({ id: '1' }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); + expect(taskManager.remove).toHaveBeenCalled(); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + 'disable(): Failed to load API key to invalidate on alert 1: Fail' + ); + }); + + test('throws when unsecuredSavedObjectsClient update fails', async () => { + unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Failed to update')); + + await expect(alertsClient.disable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Failed to update"` + ); + }); + + test('swallows error when invalidate API key throws', async () => { + alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); + + await alertsClient.disable({ id: '1' }); + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + 'Failed to invalidate API Key: Fail' + ); + }); + + test('throws when failing to remove task from task manager', async () => { + taskManager.remove.mockRejectedValueOnce(new Error('Failed to remove task')); + + await expect(alertsClient.disable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Failed to remove task"` + ); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts new file mode 100644 index 000000000000..f098bbcad8d0 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/enable.test.ts @@ -0,0 +1,361 @@ +/* + * 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 { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/mocks'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { TaskStatus } from '../../../../task_manager/server'; +import { getBeforeSetup } from './lib'; + +const taskManager = taskManagerMock.createStart(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +describe('enable()', () => { + let alertsClient: AlertsClient; + const existingAlert = { + id: '1', + type: 'alert', + attributes: { + consumer: 'myApp', + schedule: { interval: '10s' }, + alertTypeId: 'myType', + enabled: false, + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + }, + version: '123', + references: [], + }; + + beforeEach(() => { + alertsClient = new AlertsClient(alertsClientParams); + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingAlert); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); + alertsClientParams.createAPIKey.mockResolvedValue({ + apiKeysEnabled: false, + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + ...existingAlert, + attributes: { + ...existingAlert.attributes, + enabled: true, + apiKey: null, + apiKeyOwner: null, + updatedBy: 'elastic', + }, + }); + taskManager.schedule.mockResolvedValue({ + id: 'task-123', + scheduledAt: new Date(), + attempts: 0, + status: TaskStatus.Idle, + runAt: new Date(), + state: {}, + params: {}, + taskType: '', + startedAt: null, + retryAt: null, + ownerId: null, + }); + }); + + describe('authorization', () => { + beforeEach(() => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingAlert); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); + alertsClientParams.createAPIKey.mockResolvedValue({ + apiKeysEnabled: false, + }); + taskManager.schedule.mockResolvedValue({ + id: 'task-123', + scheduledAt: new Date(), + attempts: 0, + status: TaskStatus.Idle, + runAt: new Date(), + state: {}, + params: {}, + taskType: '', + startedAt: null, + retryAt: null, + ownerId: null, + }); + }); + + test('ensures user is authorised to enable this type of alert under the consumer', async () => { + await alertsClient.enable({ id: '1' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'enable'); + expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); + }); + + test('throws when user is not authorised to enable this type of alert', async () => { + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to enable a "myType" alert for "myApp"`) + ); + + await expect(alertsClient.enable({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to enable a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'enable'); + }); + }); + + test('enables an alert', async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + ...existingAlert, + attributes: { + ...existingAlert.attributes, + enabled: true, + apiKey: null, + apiKeyOwner: null, + updatedBy: 'elastic', + }, + }); + + await alertsClient.enable({ id: '1' }); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', + }); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + schedule: { interval: '10s' }, + alertTypeId: 'myType', + consumer: 'myApp', + enabled: true, + meta: { + versionApiKeyLastmodified: kibanaVersion, + }, + updatedBy: 'elastic', + apiKey: null, + apiKeyOwner: null, + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + }, + { + version: '123', + } + ); + expect(taskManager.schedule).toHaveBeenCalledWith({ + taskType: `alerting:myType`, + params: { + alertId: '1', + spaceId: 'default', + }, + state: { + alertInstances: {}, + alertTypeState: {}, + previousStartedAt: null, + }, + scope: ['alerting'], + }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith('alert', '1', { + scheduledTaskId: 'task-123', + }); + }); + + test('invalidates API key if ever one existed prior to updating', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ + ...existingAlert, + attributes: { + ...existingAlert.attributes, + apiKey: Buffer.from('123:abc').toString('base64'), + }, + }); + + await alertsClient.enable({ id: '1' }); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', + }); + expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + }); + + test(`doesn't enable already enabled alerts`, async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ + ...existingAlert, + attributes: { + ...existingAlert.attributes, + enabled: true, + }, + }); + + await alertsClient.enable({ id: '1' }); + expect(alertsClientParams.getUserName).not.toHaveBeenCalled(); + expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); + expect(taskManager.schedule).not.toHaveBeenCalled(); + }); + + test('sets API key when createAPIKey returns one', async () => { + alertsClientParams.createAPIKey.mockResolvedValueOnce({ + apiKeysEnabled: true, + result: { id: '123', name: '123', api_key: 'abc' }, + }); + + await alertsClient.enable({ id: '1' }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + schedule: { interval: '10s' }, + alertTypeId: 'myType', + consumer: 'myApp', + enabled: true, + meta: { + versionApiKeyLastmodified: kibanaVersion, + }, + apiKey: Buffer.from('123:abc').toString('base64'), + apiKeyOwner: 'elastic', + updatedBy: 'elastic', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + }, + { + version: '123', + } + ); + }); + + test('falls back when failing to getDecryptedAsInternalUser', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); + + await alertsClient.enable({ id: '1' }); + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + 'enable(): Failed to load API key to invalidate on alert 1: Fail' + ); + }); + + test('throws error when failing to load the saved object using SOC', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); + unsecuredSavedObjectsClient.get.mockRejectedValueOnce(new Error('Fail to get')); + + await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Fail to get"` + ); + expect(alertsClientParams.getUserName).not.toHaveBeenCalled(); + expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled(); + expect(taskManager.schedule).not.toHaveBeenCalled(); + }); + + test('throws error when failing to update the first time', async () => { + alertsClientParams.createAPIKey.mockResolvedValueOnce({ + apiKeysEnabled: true, + result: { id: '123', name: '123', api_key: 'abc' }, + }); + unsecuredSavedObjectsClient.update.mockReset(); + unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail to update')); + + await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Fail to update"` + ); + expect(alertsClientParams.getUserName).toHaveBeenCalled(); + expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); + expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1); + expect(taskManager.schedule).not.toHaveBeenCalled(); + }); + + test('throws error when failing to update the second time', async () => { + unsecuredSavedObjectsClient.update.mockReset(); + unsecuredSavedObjectsClient.update.mockResolvedValueOnce({ + ...existingAlert, + attributes: { + ...existingAlert.attributes, + enabled: true, + }, + }); + unsecuredSavedObjectsClient.update.mockRejectedValueOnce( + new Error('Fail to update second time') + ); + + await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Fail to update second time"` + ); + expect(alertsClientParams.getUserName).toHaveBeenCalled(); + expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(2); + expect(taskManager.schedule).toHaveBeenCalled(); + }); + + test('throws error when failing to schedule task', async () => { + taskManager.schedule.mockRejectedValueOnce(new Error('Fail to schedule')); + + await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Fail to schedule"` + ); + expect(alertsClientParams.getUserName).toHaveBeenCalled(); + expect(alertsClientParams.createAPIKey).toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts new file mode 100644 index 000000000000..c1adaddc80d9 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/find.test.ts @@ -0,0 +1,251 @@ +/* + * 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 { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/mocks'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { nodeTypes } from '../../../../../../src/plugins/data/common'; +import { esKuery } from '../../../../../../src/plugins/data/server'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { getBeforeSetup, setGlobalDate } from './lib'; + +const taskManager = taskManagerMock.createStart(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +setGlobalDate(); + +describe('find()', () => { + const listedTypes = new Set([ + { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'myType', + name: 'myType', + producer: 'myApp', + }, + ]); + beforeEach(() => { + authorization.getFindAuthorizationFilter.mockResolvedValue({ + ensureAlertTypeIsAuthorized() {}, + logSuccessfulAuthorization() {}, + }); + unsecuredSavedObjectsClient.find.mockResolvedValueOnce({ + total: 1, + per_page: 10, + page: 1, + saved_objects: [ + { + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + score: 1, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }, + ], + }); + alertTypeRegistry.list.mockReturnValue(listedTypes); + authorization.filterByAlertTypeAuthorization.mockResolvedValue( + new Set([ + { + id: 'myType', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + producer: 'alerts', + authorizedConsumers: { + myApp: { read: true, all: true }, + }, + }, + ]) + ); + }); + + test('calls saved objects client with given params', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + const result = await alertsClient.find({ options: {} }); + expect(result).toMatchInlineSnapshot(` + Object { + "data": Array [ + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "myType", + "createdAt": 2019-02-12T21:01:22.479Z, + "id": "1", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "updatedAt": 2019-02-12T21:01:22.479Z, + }, + ], + "page": 1, + "perPage": 10, + "total": 1, + } + `); + expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "fields": undefined, + "filter": undefined, + "type": "alert", + }, + ] + `); + }); + + describe('authorization', () => { + test('ensures user is query filter types down to those the user is authorized to find', async () => { + const filter = esKuery.fromKueryExpression( + '((alert.attributes.alertTypeId:myType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myApp) or (alert.attributes.alertTypeId:myOtherType and alert.attributes.consumer:myOtherApp))' + ); + authorization.getFindAuthorizationFilter.mockResolvedValue({ + filter, + ensureAlertTypeIsAuthorized() {}, + logSuccessfulAuthorization() {}, + }); + + const alertsClient = new AlertsClient(alertsClientParams); + await alertsClient.find({ options: { filter: 'someTerm' } }); + + const [options] = unsecuredSavedObjectsClient.find.mock.calls[0]; + expect(options.filter).toEqual( + nodeTypes.function.buildNode('and', [esKuery.fromKueryExpression('someTerm'), filter]) + ); + expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledTimes(1); + }); + + test('throws if user is not authorized to find any types', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + authorization.getFindAuthorizationFilter.mockRejectedValue(new Error('not authorized')); + await expect(alertsClient.find({ options: {} })).rejects.toThrowErrorMatchingInlineSnapshot( + `"not authorized"` + ); + }); + + test('ensures authorization even when the fields required to authorize are omitted from the find', async () => { + const ensureAlertTypeIsAuthorized = jest.fn(); + const logSuccessfulAuthorization = jest.fn(); + authorization.getFindAuthorizationFilter.mockResolvedValue({ + ensureAlertTypeIsAuthorized, + logSuccessfulAuthorization, + }); + + unsecuredSavedObjectsClient.find.mockReset(); + unsecuredSavedObjectsClient.find.mockResolvedValue({ + total: 1, + per_page: 10, + page: 1, + saved_objects: [ + { + id: '1', + type: 'alert', + attributes: { + actions: [], + alertTypeId: 'myType', + consumer: 'myApp', + tags: ['myTag'], + }, + score: 1, + references: [], + }, + ], + }); + + const alertsClient = new AlertsClient(alertsClientParams); + expect(await alertsClient.find({ options: { fields: ['tags'] } })).toMatchInlineSnapshot(` + Object { + "data": Array [ + Object { + "actions": Array [], + "id": "1", + "schedule": undefined, + "tags": Array [ + "myTag", + ], + }, + ], + "page": 1, + "perPage": 10, + "total": 1, + } + `); + + expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledWith({ + fields: ['tags', 'alertTypeId', 'consumer'], + type: 'alert', + }); + expect(ensureAlertTypeIsAuthorized).toHaveBeenCalledWith('myType', 'myApp'); + expect(logSuccessfulAuthorization).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts new file mode 100644 index 000000000000..004230403de2 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get.test.ts @@ -0,0 +1,194 @@ +/* + * 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 { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/mocks'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { getBeforeSetup, setGlobalDate } from './lib'; + +const taskManager = taskManagerMock.createStart(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +setGlobalDate(); + +describe('get()', () => { + test('calls saved objects client with given params', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + const result = await alertsClient.get({ id: '1' }); + expect(result).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "createdAt": 2019-02-12T21:01:22.479Z, + "id": "1", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "updatedAt": 2019-02-12T21:01:22.479Z, + } + `); + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "alert", + "1", + ] + `); + }); + + test(`throws an error when references aren't found`, async () => { + const alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + references: [], + }); + await expect(alertsClient.get({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Action reference \\"action_0\\" not found in alert id: 1"` + ); + }); + + describe('authorization', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + }); + + test('ensures user is authorised to get this type of alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + await alertsClient.get({ id: '1' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'get'); + }); + + test('throws when user is not authorised to get this type of alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to get a "myType" alert for "myApp"`) + ); + + await expect(alertsClient.get({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to get a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'get'); + }); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts new file mode 100644 index 000000000000..a53e49337f38 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts @@ -0,0 +1,292 @@ +/* + * 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 { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/mocks'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { eventLogClientMock } from '../../../../event_log/server/mocks'; +import { QueryEventsBySavedObjectResult } from '../../../../event_log/server'; +import { SavedObject } from 'kibana/server'; +import { EventsFactory } from '../../lib/alert_instance_summary_from_event_log.test'; +import { RawAlert } from '../../types'; +import { getBeforeSetup, mockedDateString, setGlobalDate } from './lib'; + +const taskManager = taskManagerMock.createStart(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); +const eventLogClient = eventLogClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry, eventLogClient); +}); + +setGlobalDate(); + +const AlertInstanceSummaryFindEventsResult: QueryEventsBySavedObjectResult = { + page: 1, + per_page: 10000, + total: 0, + data: [], +}; + +const AlertInstanceSummaryIntervalSeconds = 1; + +const BaseAlertInstanceSummarySavedObject: SavedObject = { + id: '1', + type: 'alert', + attributes: { + enabled: true, + name: 'alert-name', + tags: ['tag-1', 'tag-2'], + alertTypeId: '123', + consumer: 'alert-consumer', + schedule: { interval: `${AlertInstanceSummaryIntervalSeconds}s` }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + createdAt: mockedDateString, + apiKey: null, + apiKeyOwner: null, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'unknown', + lastExecutionDate: '2020-08-20T19:23:38Z', + error: null, + }, + }, + references: [], +}; + +function getAlertInstanceSummarySavedObject( + attributes: Partial = {} +): SavedObject { + return { + ...BaseAlertInstanceSummarySavedObject, + attributes: { ...BaseAlertInstanceSummarySavedObject.attributes, ...attributes }, + }; +} + +describe('getAlertInstanceSummary()', () => { + let alertsClient: AlertsClient; + + beforeEach(() => { + alertsClient = new AlertsClient(alertsClientParams); + }); + + test('runs as expected with some event log data', async () => { + const alertSO = getAlertInstanceSummarySavedObject({ + mutedInstanceIds: ['instance-muted-no-activity'], + }); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(alertSO); + + const eventsFactory = new EventsFactory(mockedDateString); + const events = eventsFactory + .addExecute() + .addNewInstance('instance-currently-active') + .addNewInstance('instance-previously-active') + .addActiveInstance('instance-currently-active') + .addActiveInstance('instance-previously-active') + .advanceTime(10000) + .addExecute() + .addResolvedInstance('instance-previously-active') + .addActiveInstance('instance-currently-active') + .getEvents(); + const eventsResult = { + ...AlertInstanceSummaryFindEventsResult, + total: events.length, + data: events, + }; + eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(eventsResult); + + const dateStart = new Date(Date.now() - 60 * 1000).toISOString(); + + const result = await alertsClient.getAlertInstanceSummary({ id: '1', dateStart }); + expect(result).toMatchInlineSnapshot(` + Object { + "alertTypeId": "123", + "consumer": "alert-consumer", + "enabled": true, + "errorMessages": Array [], + "id": "1", + "instances": Object { + "instance-currently-active": Object { + "activeStartDate": "2019-02-12T21:01:22.479Z", + "muted": false, + "status": "Active", + }, + "instance-muted-no-activity": Object { + "activeStartDate": undefined, + "muted": true, + "status": "OK", + }, + "instance-previously-active": Object { + "activeStartDate": undefined, + "muted": false, + "status": "OK", + }, + }, + "lastRun": "2019-02-12T21:01:32.479Z", + "muteAll": false, + "name": "alert-name", + "status": "Active", + "statusEndDate": "2019-02-12T21:01:22.479Z", + "statusStartDate": "2019-02-12T21:00:22.479Z", + "tags": Array [ + "tag-1", + "tag-2", + ], + "throttle": null, + } + `); + }); + + // Further tests don't check the result of `getAlertInstanceSummary()`, as the result + // is just the result from the `alertInstanceSummaryFromEventLog()`, which itself + // has a complete set of tests. These tests just make sure the data gets + // sent into `getAlertInstanceSummary()` as appropriate. + + test('calls saved objects and event log client with default params', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); + eventLogClient.findEventsBySavedObject.mockResolvedValueOnce( + AlertInstanceSummaryFindEventsResult + ); + + await alertsClient.getAlertInstanceSummary({ id: '1' }); + + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObject.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "alert", + "1", + Object { + "end": "2019-02-12T21:01:22.479Z", + "page": 1, + "per_page": 10000, + "sort_order": "desc", + "start": "2019-02-12T21:00:22.479Z", + }, + ] + `); + // calculate the expected start/end date for one test + const { start, end } = eventLogClient.findEventsBySavedObject.mock.calls[0][2]!; + expect(end).toBe(mockedDateString); + + const startMillis = Date.parse(start!); + const endMillis = Date.parse(end!); + const expectedDuration = 60 * AlertInstanceSummaryIntervalSeconds * 1000; + expect(endMillis - startMillis).toBeGreaterThan(expectedDuration - 2); + expect(endMillis - startMillis).toBeLessThan(expectedDuration + 2); + }); + + test('calls event log client with start date', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); + eventLogClient.findEventsBySavedObject.mockResolvedValueOnce( + AlertInstanceSummaryFindEventsResult + ); + + const dateStart = new Date( + Date.now() - 60 * AlertInstanceSummaryIntervalSeconds * 1000 + ).toISOString(); + await alertsClient.getAlertInstanceSummary({ id: '1', dateStart }); + + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1); + const { start, end } = eventLogClient.findEventsBySavedObject.mock.calls[0][2]!; + + expect({ start, end }).toMatchInlineSnapshot(` + Object { + "end": "2019-02-12T21:01:22.479Z", + "start": "2019-02-12T21:00:22.479Z", + } + `); + }); + + test('calls event log client with relative start date', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); + eventLogClient.findEventsBySavedObject.mockResolvedValueOnce( + AlertInstanceSummaryFindEventsResult + ); + + const dateStart = '2m'; + await alertsClient.getAlertInstanceSummary({ id: '1', dateStart }); + + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1); + const { start, end } = eventLogClient.findEventsBySavedObject.mock.calls[0][2]!; + + expect({ start, end }).toMatchInlineSnapshot(` + Object { + "end": "2019-02-12T21:01:22.479Z", + "start": "2019-02-12T20:59:22.479Z", + } + `); + }); + + test('invalid start date throws an error', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); + eventLogClient.findEventsBySavedObject.mockResolvedValueOnce( + AlertInstanceSummaryFindEventsResult + ); + + const dateStart = 'ain"t no way this will get parsed as a date'; + expect( + alertsClient.getAlertInstanceSummary({ id: '1', dateStart }) + ).rejects.toMatchInlineSnapshot( + `[Error: Invalid date for parameter dateStart: "ain"t no way this will get parsed as a date"]` + ); + }); + + test('saved object get throws an error', async () => { + unsecuredSavedObjectsClient.get.mockRejectedValueOnce(new Error('OMG!')); + eventLogClient.findEventsBySavedObject.mockResolvedValueOnce( + AlertInstanceSummaryFindEventsResult + ); + + expect(alertsClient.getAlertInstanceSummary({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: OMG!]` + ); + }); + + test('findEvents throws an error', async () => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); + eventLogClient.findEventsBySavedObject.mockRejectedValueOnce(new Error('OMG 2!')); + + // error eaten but logged + await alertsClient.getAlertInstanceSummary({ id: '1' }); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_state.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_state.test.ts new file mode 100644 index 000000000000..8b32f05f6d5a --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_state.test.ts @@ -0,0 +1,239 @@ +/* + * 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 { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/mocks'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { TaskStatus } from '../../../../task_manager/server'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { getBeforeSetup } from './lib'; + +const taskManager = taskManagerMock.createStart(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +describe('getAlertState()', () => { + test('calls saved objects client with given params', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + + taskManager.get.mockResolvedValueOnce({ + id: '1', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + + await alertsClient.getAlertState({ id: '1' }); + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "alert", + "1", + ] + `); + }); + + test('gets the underlying task from TaskManager', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + + const scheduledTaskId = 'task-123'; + + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + enabled: true, + scheduledTaskId, + mutedInstanceIds: [], + muteAll: true, + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + + taskManager.get.mockResolvedValueOnce({ + id: scheduledTaskId, + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: { + alertId: '1', + }, + ownerId: null, + }); + + await alertsClient.getAlertState({ id: '1' }); + expect(taskManager.get).toHaveBeenCalledTimes(1); + expect(taskManager.get).toHaveBeenCalledWith(scheduledTaskId); + }); + + describe('authorization', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + + taskManager.get.mockResolvedValueOnce({ + id: '1', + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + }); + + test('ensures user is authorised to get this type of alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + await alertsClient.getAlertState({ id: '1' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'getAlertState' + ); + }); + + test('throws when user is not authorised to getAlertState this type of alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + // `get` check + authorization.ensureAuthorized.mockResolvedValueOnce(); + // `getAlertState` check + authorization.ensureAuthorized.mockRejectedValueOnce( + new Error(`Unauthorized to getAlertState a "myType" alert for "myApp"`) + ); + + await expect(alertsClient.getAlertState({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to getAlertState a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'getAlertState' + ); + }); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts b/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts new file mode 100644 index 000000000000..96e49e21b904 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/lib.ts @@ -0,0 +1,103 @@ +/* + * 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-next-line @kbn/eslint/no-restricted-paths +import { TaskManager } from '../../../../task_manager/server/task_manager'; +import { IEventLogClient } from '../../../../event_log/server'; +import { actionsClientMock } from '../../../../actions/server/mocks'; +import { ConstructorOptions } from '../alerts_client'; +import { eventLogClientMock } from '../../../../event_log/server/mocks'; +import { AlertTypeRegistry } from '../../alert_type_registry'; + +export const mockedDateString = '2019-02-12T21:01:22.479Z'; + +export function setGlobalDate() { + const mockedDate = new Date(mockedDateString); + const DateOriginal = Date; + // A version of date that responds to `new Date(null|undefined)` and `Date.now()` + // by returning a fixed date, otherwise should be same as Date. + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + (global as any).Date = class Date { + constructor(...args: unknown[]) { + // sometimes the ctor has no args, sometimes has a single `null` arg + if (args[0] == null) { + // @ts-ignore + return mockedDate; + } else { + // @ts-ignore + return new DateOriginal(...args); + } + } + static now() { + return mockedDate.getTime(); + } + static parse(string: string) { + return DateOriginal.parse(string); + } + }; +} + +export function getBeforeSetup( + alertsClientParams: jest.Mocked, + taskManager: jest.Mocked< + Pick + >, + alertTypeRegistry: jest.Mocked>, + eventLogClient?: jest.Mocked +) { + jest.resetAllMocks(); + alertsClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false }); + alertsClientParams.invalidateAPIKey.mockResolvedValue({ + apiKeysEnabled: true, + result: { + invalidated_api_keys: [], + previously_invalidated_api_keys: [], + error_count: 0, + }, + }); + alertsClientParams.getUserName.mockResolvedValue('elastic'); + taskManager.runNow.mockResolvedValue({ id: '' }); + const actionsClient = actionsClientMock.create(); + + actionsClient.getBulk.mockResolvedValueOnce([ + { + id: '1', + isPreconfigured: false, + actionTypeId: 'test', + name: 'test', + config: { + foo: 'bar', + }, + }, + { + id: '2', + isPreconfigured: false, + actionTypeId: 'test2', + name: 'test2', + config: { + foo: 'bar', + }, + }, + { + id: 'testPreconfigured', + actionTypeId: '.slack', + isPreconfigured: true, + name: 'test', + }, + ]); + alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); + + alertTypeRegistry.get.mockImplementation(() => ({ + id: '123', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + async executor() {}, + producer: 'alerts', + })); + alertsClientParams.getEventLogClient.mockResolvedValue( + eventLogClient ?? eventLogClientMock.create() + ); +} diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts new file mode 100644 index 000000000000..b2f5c5498f84 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/list_alert_types.test.ts @@ -0,0 +1,134 @@ +/* + * 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 { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/mocks'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { getBeforeSetup } from './lib'; + +const taskManager = taskManagerMock.createStart(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +describe('listAlertTypes', () => { + let alertsClient: AlertsClient; + const alertingAlertType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'alertingAlertType', + name: 'alertingAlertType', + producer: 'alerts', + }; + const myAppAlertType = { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'myAppAlertType', + name: 'myAppAlertType', + producer: 'myApp', + }; + const setOfAlertTypes = new Set([myAppAlertType, alertingAlertType]); + + const authorizedConsumers = { + alerts: { read: true, all: true }, + myApp: { read: true, all: true }, + myOtherApp: { read: true, all: true }, + }; + + beforeEach(() => { + alertsClient = new AlertsClient(alertsClientParams); + }); + + test('should return a list of AlertTypes that exist in the registry', async () => { + alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); + authorization.filterByAlertTypeAuthorization.mockResolvedValue( + new Set([ + { ...myAppAlertType, authorizedConsumers }, + { ...alertingAlertType, authorizedConsumers }, + ]) + ); + expect(await alertsClient.listAlertTypes()).toEqual( + new Set([ + { ...myAppAlertType, authorizedConsumers }, + { ...alertingAlertType, authorizedConsumers }, + ]) + ); + }); + + describe('authorization', () => { + const listedTypes = new Set([ + { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'myType', + name: 'myType', + producer: 'myApp', + }, + { + id: 'myOtherType', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + producer: 'alerts', + }, + ]); + beforeEach(() => { + alertTypeRegistry.list.mockReturnValue(listedTypes); + }); + + test('should return a list of AlertTypes that exist in the registry only if the user is authorised to get them', async () => { + const authorizedTypes = new Set([ + { + id: 'myType', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + producer: 'alerts', + authorizedConsumers: { + myApp: { read: true, all: true }, + }, + }, + ]); + authorization.filterByAlertTypeAuthorization.mockResolvedValue(authorizedTypes); + + expect(await alertsClient.listAlertTypes()).toEqual(authorizedTypes); + }); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts new file mode 100644 index 000000000000..88199dfd1f7b --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/mute_all.test.ts @@ -0,0 +1,138 @@ +/* + * 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 { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/mocks'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { getBeforeSetup } from './lib'; + +const taskManager = taskManagerMock.createStart(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +describe('muteAll()', () => { + test('mutes an alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + muteAll: false, + }, + references: [], + version: '123', + }); + + await alertsClient.muteAll({ id: '1' }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + muteAll: true, + mutedInstanceIds: [], + updatedBy: 'elastic', + }, + { + version: '123', + } + ); + }); + + describe('authorization', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + consumer: 'myApp', + schedule: { interval: '10s' }, + alertTypeId: 'myType', + apiKey: null, + apiKeyOwner: null, + enabled: false, + scheduledTaskId: null, + updatedBy: 'elastic', + muteAll: false, + }, + references: [], + }); + }); + + test('ensures user is authorised to muteAll this type of alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + await alertsClient.muteAll({ id: '1' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'muteAll'); + expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); + }); + + test('throws when user is not authorised to muteAll this type of alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to muteAll a "myType" alert for "myApp"`) + ); + + await expect(alertsClient.muteAll({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to muteAll a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'muteAll'); + }); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts new file mode 100644 index 000000000000..cd7112b3551b --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/mute_instance.test.ts @@ -0,0 +1,181 @@ +/* + * 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 { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/mocks'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { getBeforeSetup } from './lib'; + +const taskManager = taskManagerMock.createStart(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +describe('muteInstance()', () => { + test('mutes an alert instance', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [], + schedule: { interval: '10s' }, + alertTypeId: '2', + enabled: true, + scheduledTaskId: 'task-123', + mutedInstanceIds: [], + }, + version: '123', + references: [], + }); + + await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + mutedInstanceIds: ['2'], + updatedBy: 'elastic', + }, + { + version: '123', + } + ); + }); + + test('skips muting when alert instance already muted', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [], + schedule: { interval: '10s' }, + alertTypeId: '2', + enabled: true, + scheduledTaskId: 'task-123', + mutedInstanceIds: ['2'], + }, + references: [], + }); + + await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); + }); + + test('skips muting when alert is muted', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [], + schedule: { interval: '10s' }, + alertTypeId: '2', + enabled: true, + scheduledTaskId: 'task-123', + mutedInstanceIds: [], + muteAll: true, + }, + references: [], + }); + + await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); + }); + + describe('authorization', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + schedule: { interval: '10s' }, + alertTypeId: 'myType', + consumer: 'myApp', + enabled: true, + scheduledTaskId: 'task-123', + mutedInstanceIds: [], + }, + version: '123', + references: [], + }); + }); + + test('ensures user is authorised to muteInstance this type of alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + await alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }); + + expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'muteInstance' + ); + }); + + test('throws when user is not authorised to muteInstance this type of alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to muteInstance a "myType" alert for "myApp"`) + ); + + await expect( + alertsClient.muteInstance({ alertId: '1', alertInstanceId: '2' }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to muteInstance a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'muteInstance' + ); + }); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts new file mode 100644 index 000000000000..07666c1cc626 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_all.test.ts @@ -0,0 +1,139 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/mocks'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { getBeforeSetup } from './lib'; + +const taskManager = taskManagerMock.createStart(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +describe('unmuteAll()', () => { + test('unmutes an alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + muteAll: true, + }, + references: [], + version: '123', + }); + + await alertsClient.unmuteAll({ id: '1' }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + muteAll: false, + mutedInstanceIds: [], + updatedBy: 'elastic', + }, + { + version: '123', + } + ); + }); + + describe('authorization', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + consumer: 'myApp', + schedule: { interval: '10s' }, + alertTypeId: 'myType', + apiKey: null, + apiKeyOwner: null, + enabled: false, + scheduledTaskId: null, + updatedBy: 'elastic', + muteAll: false, + }, + references: [], + }); + }); + + test('ensures user is authorised to unmuteAll this type of alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + await alertsClient.unmuteAll({ id: '1' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'unmuteAll'); + expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); + }); + + test('throws when user is not authorised to unmuteAll this type of alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to unmuteAll a "myType" alert for "myApp"`) + ); + + await expect(alertsClient.unmuteAll({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to unmuteAll a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'unmuteAll'); + }); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts new file mode 100644 index 000000000000..97711b8c1457 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/unmute_instance.test.ts @@ -0,0 +1,179 @@ +/* + * 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 { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/mocks'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { getBeforeSetup } from './lib'; + +const taskManager = taskManagerMock.createStart(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +describe('unmuteInstance()', () => { + test('unmutes an alert instance', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [], + schedule: { interval: '10s' }, + alertTypeId: '2', + enabled: true, + scheduledTaskId: 'task-123', + mutedInstanceIds: ['2'], + }, + version: '123', + references: [], + }); + + await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + mutedInstanceIds: [], + updatedBy: 'elastic', + }, + { version: '123' } + ); + }); + + test('skips unmuting when alert instance not muted', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [], + schedule: { interval: '10s' }, + alertTypeId: '2', + enabled: true, + scheduledTaskId: 'task-123', + mutedInstanceIds: [], + }, + references: [], + }); + + await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); + }); + + test('skips unmuting when alert is muted', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [], + schedule: { interval: '10s' }, + alertTypeId: '2', + enabled: true, + scheduledTaskId: 'task-123', + mutedInstanceIds: [], + muteAll: true, + }, + references: [], + }); + + await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); + }); + + describe('authorization', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.get.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + alertTypeId: 'myType', + consumer: 'myApp', + schedule: { interval: '10s' }, + enabled: true, + scheduledTaskId: 'task-123', + mutedInstanceIds: ['2'], + }, + version: '123', + references: [], + }); + }); + + test('ensures user is authorised to unmuteInstance this type of alert under the consumer', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + await alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }); + + expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'unmuteInstance' + ); + }); + + test('throws when user is not authorised to unmuteInstance this type of alert', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to unmuteInstance a "myType" alert for "myApp"`) + ); + + await expect( + alertsClient.unmuteInstance({ alertId: '1', alertInstanceId: '2' }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to unmuteInstance a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'unmuteInstance' + ); + }); + }); +}); 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 new file mode 100644 index 000000000000..146f8ac400ad --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/update.test.ts @@ -0,0 +1,1257 @@ +/* + * 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 uuid from 'uuid'; +import { schema } from '@kbn/config-schema'; +import { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/mocks'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { IntervalSchedule } from '../../types'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { resolvable } from '../../test_utils'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { TaskStatus } from '../../../../task_manager/server'; +import { getBeforeSetup, setGlobalDate } from './lib'; + +const taskManager = taskManagerMock.createStart(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +setGlobalDate(); + +describe('update()', () => { + let alertsClient: AlertsClient; + const existingAlert = { + id: '1', + type: 'alert', + attributes: { + enabled: true, + tags: ['foo'], + alertTypeId: 'myType', + schedule: { interval: '10s' }, + consumer: 'myApp', + scheduledTaskId: 'task-123', + params: {}, + throttle: null, + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + }, + references: [], + version: '123', + }; + const existingDecryptedAlert = { + ...existingAlert, + attributes: { + ...existingAlert.attributes, + apiKey: Buffer.from('123:abc').toString('base64'), + }, + }; + + beforeEach(() => { + alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); + alertTypeRegistry.get.mockReturnValue({ + id: 'myType', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + async executor() {}, + producer: 'alerts', + }); + }); + + test('updates given parameters', async () => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + enabled: true, + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + { + group: 'default', + actionRef: 'action_1', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + { + group: 'default', + actionRef: 'action_2', + actionTypeId: 'test2', + params: { + foo: true, + }, + }, + ], + scheduledTaskId: 'task-123', + createdAt: new Date().toISOString(), + }, + updated_at: new Date().toISOString(), + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + { + name: 'action_1', + type: 'action', + id: '1', + }, + { + name: 'action_2', + type: 'action', + id: '2', + }, + ], + }); + const result = await alertsClient.update({ + id: '1', + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + group: 'default', + id: '2', + params: { + foo: true, + }, + }, + ], + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionTypeId": "test", + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + Object { + "actionTypeId": "test", + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + Object { + "actionTypeId": "test2", + "group": "default", + "id": "2", + "params": Object { + "foo": true, + }, + }, + ], + "createdAt": 2019-02-12T21:01:22.479Z, + "enabled": true, + "id": "1", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "scheduledTaskId": "task-123", + "updatedAt": 2019-02-12T21:01:22.479Z, + } + `); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', + }); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionRef": "action_0", + "actionTypeId": "test", + "group": "default", + "params": Object { + "foo": true, + }, + }, + Object { + "actionRef": "action_1", + "actionTypeId": "test", + "group": "default", + "params": Object { + "foo": true, + }, + }, + Object { + "actionRef": "action_2", + "actionTypeId": "test2", + "group": "default", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "myType", + "apiKey": null, + "apiKeyOwner": null, + "consumer": "myApp", + "enabled": true, + "meta": Object { + "versionApiKeyLastmodified": "v7.10.0", + }, + "name": "abc", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "scheduledTaskId": "task-123", + "tags": Array [ + "foo", + ], + "throttle": null, + "updatedBy": "elastic", + } + `); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "id": "1", + "overwrite": true, + "references": Array [ + Object { + "id": "1", + "name": "action_0", + "type": "action", + }, + Object { + "id": "1", + "name": "action_1", + "type": "action", + }, + Object { + "id": "2", + "name": "action_2", + "type": "action", + }, + ], + "version": "123", + } + `); + }); + + it('calls the createApiKey function', async () => { + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + alertsClientParams.createAPIKey.mockResolvedValueOnce({ + apiKeysEnabled: true, + result: { id: '123', name: '123', api_key: 'abc' }, + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + enabled: true, + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + apiKey: Buffer.from('123:abc').toString('base64'), + scheduledTaskId: 'task-123', + }, + updated_at: new Date().toISOString(), + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + const result = await alertsClient.update({ + id: '1', + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: '5m', + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionTypeId": "test", + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "apiKey": "MTIzOmFiYw==", + "createdAt": 2019-02-12T21:01:22.479Z, + "enabled": true, + "id": "1", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "scheduledTaskId": "task-123", + "updatedAt": 2019-02-12T21:01:22.479Z, + } + `); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionRef": "action_0", + "actionTypeId": "test", + "group": "default", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "myType", + "apiKey": "MTIzOmFiYw==", + "apiKeyOwner": "elastic", + "consumer": "myApp", + "enabled": true, + "meta": Object { + "versionApiKeyLastmodified": "v7.10.0", + }, + "name": "abc", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "scheduledTaskId": "task-123", + "tags": Array [ + "foo", + ], + "throttle": "5m", + "updatedBy": "elastic", + } + `); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "id": "1", + "overwrite": true, + "references": Array [ + Object { + "id": "1", + "name": "action_0", + "type": "action", + }, + ], + "version": "123", + } + `); + }); + + it(`doesn't call the createAPIKey function when alert is disabled`, async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ + ...existingDecryptedAlert, + attributes: { + ...existingDecryptedAlert.attributes, + enabled: false, + }, + }); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + enabled: false, + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + scheduledTaskId: 'task-123', + apiKey: null, + }, + updated_at: new Date().toISOString(), + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + const result = await alertsClient.update({ + id: '1', + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: '5m', + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + }, + }); + expect(alertsClientParams.createAPIKey).not.toHaveBeenCalled(); + expect(result).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionTypeId": "test", + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "apiKey": null, + "createdAt": 2019-02-12T21:01:22.479Z, + "enabled": false, + "id": "1", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "scheduledTaskId": "task-123", + "updatedAt": 2019-02-12T21:01:22.479Z, + } + `); + expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert'); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "actionRef": "action_0", + "actionTypeId": "test", + "group": "default", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "myType", + "apiKey": null, + "apiKeyOwner": null, + "consumer": "myApp", + "enabled": false, + "meta": Object { + "versionApiKeyLastmodified": "v7.10.0", + }, + "name": "abc", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "scheduledTaskId": "task-123", + "tags": Array [ + "foo", + ], + "throttle": "5m", + "updatedBy": "elastic", + } + `); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "id": "1", + "overwrite": true, + "references": Array [ + Object { + "id": "1", + "name": "action_0", + "type": "action", + }, + ], + "version": "123", + } + `); + }); + + it('should validate params', async () => { + alertTypeRegistry.get.mockReturnValueOnce({ + id: '123', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + validate: { + params: schema.object({ + param1: schema.string(), + }), + }, + async executor() {}, + producer: 'alerts', + }); + await expect( + alertsClient.update({ + id: '1', + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + }, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"params invalid: [param1]: expected value of type [string] but got [undefined]"` + ); + }); + + it('should trim alert name in the API key name', async () => { + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + enabled: false, + name: ' my alert name ', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + scheduledTaskId: 'task-123', + apiKey: null, + }, + updated_at: new Date().toISOString(), + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + await alertsClient.update({ + id: '1', + data: { + ...existingAlert.attributes, + name: ' my alert name ', + }, + }); + + expect(alertsClientParams.createAPIKey).toHaveBeenCalledWith('Alerting: myType/my alert name'); + }); + + it('swallows error when invalidate API key throws', async () => { + alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + enabled: true, + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + scheduledTaskId: 'task-123', + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }); + await alertsClient.update({ + id: '1', + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + }, + }); + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + 'Failed to invalidate API Key: Fail' + ); + }); + + it('swallows error when getDecryptedAsInternalUser throws', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + { + id: '2', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test2', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + enabled: true, + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + { + group: 'default', + actionRef: 'action_1', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + { + group: 'default', + actionRef: 'action_2', + actionTypeId: 'test2', + params: { + foo: true, + }, + }, + ], + scheduledTaskId: 'task-123', + createdAt: new Date().toISOString(), + }, + updated_at: new Date().toISOString(), + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + { + name: 'action_1', + type: 'action', + id: '1', + }, + { + name: 'action_2', + type: 'action', + id: '2', + }, + ], + }); + await alertsClient.update({ + id: '1', + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: '5m', + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + group: 'default', + id: '2', + params: { + foo: true, + }, + }, + ], + }, + }); + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + 'update(): Failed to load API key to invalidate on alert 1: Fail' + ); + }); + + test('throws when unsecuredSavedObjectsClient update fails and invalidates newly created API key', async () => { + alertsClientParams.createAPIKey.mockResolvedValueOnce({ + apiKeysEnabled: true, + result: { id: '234', name: '234', api_key: 'abc' }, + }); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + unsecuredSavedObjectsClient.create.mockRejectedValue(new Error('Fail')); + await expect( + alertsClient.update({ + id: '1', + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + }, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalledWith({ id: '123' }); + expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '234' }); + }); + + describe('updating an alert schedule', () => { + function mockApiCalls( + alertId: string, + taskId: string, + currentSchedule: IntervalSchedule, + updatedSchedule: IntervalSchedule + ) { + // mock return values from deps + alertTypeRegistry.get.mockReturnValueOnce({ + id: '123', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + async executor() {}, + producer: 'alerts', + }); + unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: '1', + type: 'action', + attributes: { + actions: [], + actionTypeId: 'test', + }, + references: [], + }, + ], + }); + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ + id: alertId, + type: 'alert', + attributes: { + actions: [], + enabled: true, + alertTypeId: '123', + schedule: currentSchedule, + scheduledTaskId: 'task-123', + }, + references: [], + version: '123', + }); + + taskManager.schedule.mockResolvedValueOnce({ + id: taskId, + taskType: 'alerting:123', + scheduledAt: new Date(), + attempts: 1, + status: TaskStatus.Idle, + runAt: new Date(), + startedAt: null, + retryAt: null, + state: {}, + params: {}, + ownerId: null, + }); + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: alertId, + type: 'alert', + attributes: { + enabled: true, + schedule: updatedSchedule, + actions: [ + { + group: 'default', + actionRef: 'action_0', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + scheduledTaskId: taskId, + }, + references: [ + { + name: 'action_0', + type: 'action', + id: alertId, + }, + ], + }); + + taskManager.runNow.mockReturnValueOnce(Promise.resolve({ id: alertId })); + } + + test('updating the alert schedule should rerun the task immediately', async () => { + const alertId = uuid.v4(); + const taskId = uuid.v4(); + + mockApiCalls(alertId, taskId, { interval: '60m' }, { interval: '10s' }); + + await alertsClient.update({ + id: alertId, + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + }, + }); + + expect(taskManager.runNow).toHaveBeenCalledWith(taskId); + }); + + test('updating the alert without changing the schedule should not rerun the task', async () => { + const alertId = uuid.v4(); + const taskId = uuid.v4(); + + mockApiCalls(alertId, taskId, { interval: '10s' }, { interval: '10s' }); + + await alertsClient.update({ + id: alertId, + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + }, + }); + + expect(taskManager.runNow).not.toHaveBeenCalled(); + }); + + test('updating the alert should not wait for the rerun the task to complete', async () => { + const alertId = uuid.v4(); + const taskId = uuid.v4(); + + mockApiCalls(alertId, taskId, { interval: '10s' }, { interval: '30s' }); + + const resolveAfterAlertUpdatedCompletes = resolvable<{ id: string }>(); + + taskManager.runNow.mockReset(); + taskManager.runNow.mockReturnValue(resolveAfterAlertUpdatedCompletes); + + await alertsClient.update({ + id: alertId, + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + }, + }); + + expect(taskManager.runNow).toHaveBeenCalled(); + resolveAfterAlertUpdatedCompletes.resolve({ id: alertId }); + }); + + test('logs when the rerun of an alerts underlying task fails', async () => { + const alertId = uuid.v4(); + const taskId = uuid.v4(); + + mockApiCalls(alertId, taskId, { interval: '10s' }, { interval: '30s' }); + + taskManager.runNow.mockReset(); + taskManager.runNow.mockRejectedValue(new Error('Failed to run alert')); + + await alertsClient.update({ + id: alertId, + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + ], + }, + }); + + expect(taskManager.runNow).toHaveBeenCalled(); + + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + `Alert update failed to run its underlying task. TaskManager runNow failed with Error: Failed to run alert` + ); + }); + }); + + describe('authorization', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + enabled: true, + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [], + scheduledTaskId: 'task-123', + createdAt: new Date().toISOString(), + }, + updated_at: new Date().toISOString(), + references: [], + }); + }); + + test('ensures user is authorised to update this type of alert under the consumer', async () => { + await alertsClient.update({ + id: '1', + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + actions: [], + }, + }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'update'); + }); + + test('throws when user is not authorised to update this type of alert', async () => { + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to update a "myType" alert for "myApp"`) + ); + + await expect( + alertsClient.update({ + id: '1', + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + actions: [], + }, + }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to update a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'update'); + }); + }); +}); diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts new file mode 100644 index 000000000000..1f3b567b2c03 --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/update_api_key.test.ts @@ -0,0 +1,229 @@ +/* + * 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 { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/mocks'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { getBeforeSetup } from './lib'; + +const taskManager = taskManagerMock.createStart(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +describe('updateApiKey()', () => { + let alertsClient: AlertsClient; + const existingAlert = { + id: '1', + type: 'alert', + attributes: { + schedule: { interval: '10s' }, + alertTypeId: 'myType', + consumer: 'myApp', + enabled: true, + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + }, + version: '123', + references: [], + }; + const existingEncryptedAlert = { + ...existingAlert, + attributes: { + ...existingAlert.attributes, + apiKey: Buffer.from('123:abc').toString('base64'), + }, + }; + + beforeEach(() => { + alertsClient = new AlertsClient(alertsClientParams); + unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingEncryptedAlert); + alertsClientParams.createAPIKey.mockResolvedValueOnce({ + apiKeysEnabled: true, + result: { id: '234', name: '123', api_key: 'abc' }, + }); + }); + + test('updates the API key for the alert', async () => { + await alertsClient.updateApiKey({ id: '1' }); + expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled(); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', + }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + schedule: { interval: '10s' }, + alertTypeId: 'myType', + consumer: 'myApp', + enabled: true, + apiKey: Buffer.from('234:abc').toString('base64'), + apiKeyOwner: 'elastic', + updatedBy: 'elastic', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + meta: { + versionApiKeyLastmodified: kibanaVersion, + }, + }, + { version: '123' } + ); + expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); + }); + + test('falls back to SOC when getDecryptedAsInternalUser throws an error', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); + + await alertsClient.updateApiKey({ id: '1' }); + expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1'); + expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', { + namespace: 'default', + }); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith( + 'alert', + '1', + { + schedule: { interval: '10s' }, + alertTypeId: 'myType', + consumer: 'myApp', + enabled: true, + apiKey: Buffer.from('234:abc').toString('base64'), + apiKeyOwner: 'elastic', + updatedBy: 'elastic', + actions: [ + { + group: 'default', + id: '1', + actionTypeId: '1', + actionRef: '1', + params: { + foo: true, + }, + }, + ], + meta: { + versionApiKeyLastmodified: kibanaVersion, + }, + }, + { version: '123' } + ); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + }); + + test('swallows error when invalidate API key throws', async () => { + alertsClientParams.invalidateAPIKey.mockRejectedValue(new Error('Fail')); + + await alertsClient.updateApiKey({ id: '1' }); + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + 'Failed to invalidate API Key: Fail' + ); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); + }); + + test('swallows error when getting decrypted object throws', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail')); + + await alertsClient.updateApiKey({ id: '1' }); + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + 'updateApiKey(): Failed to load API key to invalidate on alert 1: Fail' + ); + expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled(); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + }); + + test('throws when unsecuredSavedObjectsClient update fails and invalidates newly created API key', async () => { + alertsClientParams.createAPIKey.mockResolvedValueOnce({ + apiKeysEnabled: true, + result: { id: '234', name: '234', api_key: 'abc' }, + }); + unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail')); + + await expect(alertsClient.updateApiKey({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Fail"` + ); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalledWith({ id: '123' }); + expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '234' }); + }); + + describe('authorization', () => { + test('ensures user is authorised to updateApiKey this type of alert under the consumer', async () => { + await alertsClient.updateApiKey({ id: '1' }); + + expect(actionsAuthorization.ensureAuthorized).toHaveBeenCalledWith('execute'); + expect(authorization.ensureAuthorized).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'updateApiKey' + ); + }); + + test('throws when user is not authorised to updateApiKey this type of alert', async () => { + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to updateApiKey a "myType" alert for "myApp"`) + ); + + await expect(alertsClient.updateApiKey({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to updateApiKey a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'updateApiKey' + ); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.stories.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.stories.tsx index 5ad6fd547169..ff95d6fd1254 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.stories.tsx @@ -4,804 +4,2538 @@ * you may not use this file except in compliance with the Elastic License. */ -import { storiesOf } from '@storybook/react'; -import React from 'react'; +import React, { ComponentType } from 'react'; import { EuiThemeProvider } from '../../../../../../observability/public'; import { Exception } from '../../../../../typings/es_schemas/raw/error_raw'; import { ExceptionStacktrace } from './ExceptionStacktrace'; -storiesOf('app/ErrorGroupDetails/DetailView/ExceptionStacktrace', module) - .addDecorator((storyFn) => { - return {storyFn()}; - }) - .add('JavaScript with some context', () => { - const exceptions: Exception[] = [ - { - code: '503', - stacktrace: [ - { - library_frame: true, - exclude_from_grouping: false, - filename: 'node_modules/elastic-apm-http-client/index.js', - abs_path: '/app/node_modules/elastic-apm-http-client/index.js', - line: { - number: 711, - context: - " const err = new Error('Unexpected APM Server response when polling config')", - }, - function: 'processConfigErrorResponse', - context: { - pre: ['', 'function processConfigErrorResponse (res, buf) {'], - post: ['', ' err.code = res.statusCode'], - }, - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'node_modules/elastic-apm-http-client/index.js', - abs_path: '/app/node_modules/elastic-apm-http-client/index.js', - line: { - number: 196, - context: - ' res.destroy(processConfigErrorResponse(res, buf))', - }, - function: '', - context: { - pre: [' }', ' } else {'], - post: [' }', ' })'], - }, - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'node_modules/fast-stream-to-buffer/index.js', - abs_path: '/app/node_modules/fast-stream-to-buffer/index.js', - line: { - number: 20, - context: ' cb(err, buffers[0], stream)', - }, - function: 'IncomingMessage.', - context: { - pre: [' break', ' case 1:'], - post: [' break', ' default:'], - }, - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'node_modules/once/once.js', - abs_path: '/app/node_modules/once/once.js', - line: { - number: 25, - context: ' return f.value = fn.apply(this, arguments)', - }, - function: 'f', - context: { - pre: [' if (f.called) return f.value', ' f.called = true'], - post: [' }', ' f.called = false'], - }, - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'node_modules/end-of-stream/index.js', - abs_path: '/app/node_modules/end-of-stream/index.js', - line: { - number: 36, - context: '\t\tif (!writable) callback.call(stream);', - }, - function: 'onend', - context: { - pre: ['\tvar onend = function() {', '\t\treadable = false;'], - post: ['\t};', ''], - }, - }, - { - library_frame: true, - exclude_from_grouping: false, - abs_path: 'events.js', - filename: 'events.js', - line: { - number: 327, - }, - function: 'emit', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: '_stream_readable.js', - abs_path: '_stream_readable.js', - line: { - number: 1220, - }, - function: 'endReadableNT', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'internal/process/task_queues.js', - abs_path: 'internal/process/task_queues.js', - line: { - number: 84, - }, - function: 'processTicksAndRejections', - }, - ], - module: 'elastic-apm-http-client', - handled: false, - attributes: { - response: - '\r\n503 Service Temporarily Unavailable\r\n\r\n

503 Service Temporarily Unavailable

\r\n
nginx/1.17.7
\r\n\r\n\r\n', - }, - type: 'Error', - message: 'Unexpected APM Server response when polling config', - }, - ]; +export default { + title: 'app/ErrorGroupDetails/DetailView/ExceptionStacktrace', + component: ExceptionStacktrace, + decorators: [ + (Story: ComponentType) => { + return ( + + + + ); + }, + ], +}; +export function JavaWithLongLines() { + const exceptions: Exception[] = [ + { + stacktrace: [ + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractJackson2HttpMessageConverter.java', + classname: + 'org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter', + line: { + number: 296, + }, + module: 'org.springframework.http.converter.json', + function: 'writeInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractGenericHttpMessageConverter.java', + classname: + 'org.springframework.http.converter.AbstractGenericHttpMessageConverter', + line: { + number: 102, + }, + module: 'org.springframework.http.converter', + function: 'write', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractMessageConverterMethodProcessor.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor', + line: { + number: 272, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'writeWithMessageConverters', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'RequestResponseBodyMethodProcessor.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor', + line: { + number: 180, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'handleReturnValue', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HandlerMethodReturnValueHandlerComposite.java', + classname: + 'org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite', + line: { + number: 82, + }, + module: 'org.springframework.web.method.support', + function: 'handleReturnValue', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ServletInvocableHandlerMethod.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod', + line: { + number: 119, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'invokeAndHandle', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'RequestMappingHandlerAdapter.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter', + line: { + number: 877, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'invokeHandlerMethod', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'RequestMappingHandlerAdapter.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter', + line: { + number: 783, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'handleInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractHandlerMethodAdapter.java', + classname: + 'org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter', + line: { + number: 87, + }, + function: 'handle', + module: 'org.springframework.web.servlet.mvc.method', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'DispatcherServlet.java', + classname: 'org.springframework.web.servlet.DispatcherServlet', + line: { + number: 991, + }, + module: 'org.springframework.web.servlet', + function: 'doDispatch', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'DispatcherServlet.java', + classname: 'org.springframework.web.servlet.DispatcherServlet', + line: { + number: 925, + }, + module: 'org.springframework.web.servlet', + function: 'doService', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'FrameworkServlet.java', + classname: 'org.springframework.web.servlet.FrameworkServlet', + line: { + number: 974, + }, + module: 'org.springframework.web.servlet', + function: 'processRequest', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'FrameworkServlet.java', + classname: 'org.springframework.web.servlet.FrameworkServlet', + line: { + number: 866, + }, + module: 'org.springframework.web.servlet', + function: 'doGet', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HttpServlet.java', + classname: 'javax.servlet.http.HttpServlet', + line: { + number: 635, + }, + function: 'service', + module: 'javax.servlet.http', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'FrameworkServlet.java', + classname: 'org.springframework.web.servlet.FrameworkServlet', + line: { + number: 851, + }, + module: 'org.springframework.web.servlet', + function: 'service', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HttpServlet.java', + classname: 'javax.servlet.http.HttpServlet', + line: { + number: 742, + }, + module: 'javax.servlet.http', + function: 'service', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 231, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'WsFilter.java', + classname: 'org.apache.tomcat.websocket.server.WsFilter', + line: { + number: 52, + }, + module: 'org.apache.tomcat.websocket.server', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'RequestContextFilter.java', + classname: 'org.springframework.web.filter.RequestContextFilter', + line: { + number: 99, + }, + module: 'org.springframework.web.filter', + function: 'doFilterInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'OncePerRequestFilter.java', + classname: 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HttpPutFormContentFilter.java', + classname: 'org.springframework.web.filter.HttpPutFormContentFilter', + line: { + number: 109, + }, + module: 'org.springframework.web.filter', + function: 'doFilterInternal', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'OncePerRequestFilter.java', + classname: 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HiddenHttpMethodFilter.java', + classname: 'org.springframework.web.filter.HiddenHttpMethodFilter', + line: { + number: 81, + }, + module: 'org.springframework.web.filter', + function: 'doFilterInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'OncePerRequestFilter.java', + classname: 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'CharacterEncodingFilter.java', + classname: 'org.springframework.web.filter.CharacterEncodingFilter', + line: { + number: 200, + }, + module: 'org.springframework.web.filter', + function: 'doFilterInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'OncePerRequestFilter.java', + classname: 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'StandardWrapperValve.java', + classname: 'org.apache.catalina.core.StandardWrapperValve', + line: { + number: 198, + }, + module: 'org.apache.catalina.core', + function: 'invoke', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'StandardContextValve.java', + classname: 'org.apache.catalina.core.StandardContextValve', + line: { + number: 96, + }, + module: 'org.apache.catalina.core', + function: 'invoke', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AuthenticatorBase.java', + classname: 'org.apache.catalina.authenticator.AuthenticatorBase', + line: { + number: 496, + }, + module: 'org.apache.catalina.authenticator', + function: 'invoke', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'StandardHostValve.java', + classname: 'org.apache.catalina.core.StandardHostValve', + line: { + number: 140, + }, + module: 'org.apache.catalina.core', + function: 'invoke', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ErrorReportValve.java', + classname: 'org.apache.catalina.valves.ErrorReportValve', + line: { + number: 81, + }, + module: 'org.apache.catalina.valves', + function: 'invoke', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'StandardEngineValve.java', + classname: 'org.apache.catalina.core.StandardEngineValve', + line: { + number: 87, + }, + function: 'invoke', + module: 'org.apache.catalina.core', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'CoyoteAdapter.java', + classname: 'org.apache.catalina.connector.CoyoteAdapter', + line: { + number: 342, + }, + module: 'org.apache.catalina.connector', + function: 'service', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'Http11Processor.java', + classname: 'org.apache.coyote.http11.Http11Processor', + line: { + number: 803, + }, + module: 'org.apache.coyote.http11', + function: 'service', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractProcessorLight.java', + classname: 'org.apache.coyote.AbstractProcessorLight', + line: { + number: 66, + }, + module: 'org.apache.coyote', + function: 'process', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractProtocol.java', + classname: 'org.apache.coyote.AbstractProtocol$ConnectionHandler', + line: { + number: 790, + }, + module: 'org.apache.coyote', + function: 'process', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'NioEndpoint.java', + classname: 'org.apache.tomcat.util.net.NioEndpoint$SocketProcessor', + line: { + number: 1468, + }, + function: 'doRun', + module: 'org.apache.tomcat.util.net', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'SocketProcessorBase.java', + classname: 'org.apache.tomcat.util.net.SocketProcessorBase', + line: { + number: 49, + }, + module: 'org.apache.tomcat.util.net', + function: 'run', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'TaskThread.java', + classname: + 'org.apache.tomcat.util.threads.TaskThread$WrappingRunnable', + line: { + number: 61, + }, + function: 'run', + module: 'org.apache.tomcat.util.threads', + }, + ], + type: + 'org.springframework.http.converter.HttpMessageNotWritableException', + message: + 'Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue() (through reference chain: co.elastic.apm.opbeans.repositories.Stats["numbers"]->com.sun.proxy.$Proxy128["revenue"])', + }, + { + stacktrace: [ + { + exclude_from_grouping: false, + library_frame: true, + filename: 'JsonMappingException.java', + classname: 'com.fasterxml.jackson.databind.JsonMappingException', + line: { + number: 391, + }, + module: 'com.fasterxml.jackson.databind', + function: 'wrapWithPath', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'JsonMappingException.java', + classname: 'com.fasterxml.jackson.databind.JsonMappingException', + line: { + number: 351, + }, + module: 'com.fasterxml.jackson.databind', + function: 'wrapWithPath', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'StdSerializer.java', + classname: 'com.fasterxml.jackson.databind.ser.std.StdSerializer', + line: { + number: 316, + }, + function: 'wrapAndThrow', + module: 'com.fasterxml.jackson.databind.ser.std', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'BeanSerializerBase.java', + classname: + 'com.fasterxml.jackson.databind.ser.std.BeanSerializerBase', + line: { + number: 727, + }, + module: 'com.fasterxml.jackson.databind.ser.std', + function: 'serializeFields', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'BeanSerializer.java', + classname: 'com.fasterxml.jackson.databind.ser.BeanSerializer', + line: { + number: 155, + }, + module: 'com.fasterxml.jackson.databind.ser', + function: 'serialize', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'BeanPropertyWriter.java', + classname: 'com.fasterxml.jackson.databind.ser.BeanPropertyWriter', + line: { + number: 727, + }, + module: 'com.fasterxml.jackson.databind.ser', + function: 'serializeAsField', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'BeanSerializerBase.java', + classname: + 'com.fasterxml.jackson.databind.ser.std.BeanSerializerBase', + line: { + number: 719, + }, + module: 'com.fasterxml.jackson.databind.ser.std', + function: 'serializeFields', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'BeanSerializer.java', + classname: 'com.fasterxml.jackson.databind.ser.BeanSerializer', + line: { + number: 155, + }, + module: 'com.fasterxml.jackson.databind.ser', + function: 'serialize', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'DefaultSerializerProvider.java', + classname: + 'com.fasterxml.jackson.databind.ser.DefaultSerializerProvider', + line: { + number: 480, + }, + module: 'com.fasterxml.jackson.databind.ser', + function: '_serialize', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'DefaultSerializerProvider.java', + classname: + 'com.fasterxml.jackson.databind.ser.DefaultSerializerProvider', + line: { + number: 319, + }, + module: 'com.fasterxml.jackson.databind.ser', + function: 'serializeValue', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ObjectWriter.java', + classname: 'com.fasterxml.jackson.databind.ObjectWriter$Prefetch', + line: { + number: 1396, + }, + module: 'com.fasterxml.jackson.databind', + function: 'serialize', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ObjectWriter.java', + classname: 'com.fasterxml.jackson.databind.ObjectWriter', + line: { + number: 913, + }, + module: 'com.fasterxml.jackson.databind', + function: 'writeValue', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractJackson2HttpMessageConverter.java', + classname: + 'org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter', + line: { + number: 286, + }, + module: 'org.springframework.http.converter.json', + function: 'writeInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractGenericHttpMessageConverter.java', + classname: + 'org.springframework.http.converter.AbstractGenericHttpMessageConverter', + line: { + number: 102, + }, + module: 'org.springframework.http.converter', + function: 'write', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractMessageConverterMethodProcessor.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor', + line: { + number: 272, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'writeWithMessageConverters', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'RequestResponseBodyMethodProcessor.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor', + line: { + number: 180, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'handleReturnValue', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HandlerMethodReturnValueHandlerComposite.java', + classname: + 'org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite', + line: { + number: 82, + }, + module: 'org.springframework.web.method.support', + function: 'handleReturnValue', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ServletInvocableHandlerMethod.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod', + line: { + number: 119, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'invokeAndHandle', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'RequestMappingHandlerAdapter.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter', + line: { + number: 877, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'invokeHandlerMethod', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'RequestMappingHandlerAdapter.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter', + line: { + number: 783, + }, + function: 'handleInternal', + module: 'org.springframework.web.servlet.mvc.method.annotation', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractHandlerMethodAdapter.java', + classname: + 'org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter', + line: { + number: 87, + }, + module: 'org.springframework.web.servlet.mvc.method', + function: 'handle', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'DispatcherServlet.java', + classname: 'org.springframework.web.servlet.DispatcherServlet', + line: { + number: 991, + }, + module: 'org.springframework.web.servlet', + function: 'doDispatch', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'DispatcherServlet.java', + classname: 'org.springframework.web.servlet.DispatcherServlet', + line: { + number: 925, + }, + module: 'org.springframework.web.servlet', + function: 'doService', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'FrameworkServlet.java', + classname: 'org.springframework.web.servlet.FrameworkServlet', + line: { + number: 974, + }, + module: 'org.springframework.web.servlet', + function: 'processRequest', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'FrameworkServlet.java', + classname: 'org.springframework.web.servlet.FrameworkServlet', + line: { + number: 866, + }, + module: 'org.springframework.web.servlet', + function: 'doGet', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HttpServlet.java', + classname: 'javax.servlet.http.HttpServlet', + line: { + number: 635, + }, + module: 'javax.servlet.http', + function: 'service', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'FrameworkServlet.java', + classname: 'org.springframework.web.servlet.FrameworkServlet', + line: { + number: 851, + }, + module: 'org.springframework.web.servlet', + function: 'service', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HttpServlet.java', + classname: 'javax.servlet.http.HttpServlet', + line: { + number: 742, + }, + module: 'javax.servlet.http', + function: 'service', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 231, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'WsFilter.java', + classname: 'org.apache.tomcat.websocket.server.WsFilter', + line: { + number: 52, + }, + module: 'org.apache.tomcat.websocket.server', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'RequestContextFilter.java', + classname: 'org.springframework.web.filter.RequestContextFilter', + line: { + number: 99, + }, + module: 'org.springframework.web.filter', + function: 'doFilterInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'OncePerRequestFilter.java', + classname: 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HttpPutFormContentFilter.java', + classname: 'org.springframework.web.filter.HttpPutFormContentFilter', + line: { + number: 109, + }, + module: 'org.springframework.web.filter', + function: 'doFilterInternal', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'OncePerRequestFilter.java', + classname: 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'HiddenHttpMethodFilter.java', + classname: 'org.springframework.web.filter.HiddenHttpMethodFilter', + line: { + number: 81, + }, + module: 'org.springframework.web.filter', + function: 'doFilterInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'OncePerRequestFilter.java', + classname: 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'CharacterEncodingFilter.java', + classname: 'org.springframework.web.filter.CharacterEncodingFilter', + line: { + number: 200, + }, + module: 'org.springframework.web.filter', + function: 'doFilterInternal', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'OncePerRequestFilter.java', + classname: 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + function: 'doFilter', + module: 'org.apache.catalina.core', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'StandardWrapperValve.java', + classname: 'org.apache.catalina.core.StandardWrapperValve', + line: { + number: 198, + }, + module: 'org.apache.catalina.core', + function: 'invoke', + }, + ], + message: + 'Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue() (through reference chain: co.elastic.apm.opbeans.repositories.Stats["numbers"]->com.sun.proxy.$Proxy128["revenue"])', + type: 'com.fasterxml.jackson.databind.JsonMappingException', + }, + { + stacktrace: [ + { + exclude_from_grouping: false, + library_frame: true, + filename: 'JdkDynamicAopProxy.java', + classname: 'org.springframework.aop.framework.JdkDynamicAopProxy', + line: { + number: 226, + }, + module: 'org.springframework.aop.framework', + function: 'invoke', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'BeanPropertyWriter.java', + classname: 'com.fasterxml.jackson.databind.ser.BeanPropertyWriter', + line: { + number: 688, + }, + module: 'com.fasterxml.jackson.databind.ser', + function: 'serializeAsField', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'BeanSerializerBase.java', + classname: + 'com.fasterxml.jackson.databind.ser.std.BeanSerializerBase', + line: { + number: 719, + }, + module: 'com.fasterxml.jackson.databind.ser.std', + function: 'serializeFields', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'BeanSerializer.java', + classname: 'com.fasterxml.jackson.databind.ser.BeanSerializer', + line: { + number: 155, + }, + module: 'com.fasterxml.jackson.databind.ser', + function: 'serialize', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'BeanPropertyWriter.java', + classname: 'com.fasterxml.jackson.databind.ser.BeanPropertyWriter', + line: { + number: 727, + }, + module: 'com.fasterxml.jackson.databind.ser', + function: 'serializeAsField', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'BeanSerializerBase.java', + classname: + 'com.fasterxml.jackson.databind.ser.std.BeanSerializerBase', + line: { + number: 719, + }, + module: 'com.fasterxml.jackson.databind.ser.std', + function: 'serializeFields', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'BeanSerializer.java', + classname: 'com.fasterxml.jackson.databind.ser.BeanSerializer', + line: { + number: 155, + }, + module: 'com.fasterxml.jackson.databind.ser', + function: 'serialize', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'DefaultSerializerProvider.java', + classname: + 'com.fasterxml.jackson.databind.ser.DefaultSerializerProvider', + line: { + number: 480, + }, + module: 'com.fasterxml.jackson.databind.ser', + function: '_serialize', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'DefaultSerializerProvider.java', + classname: + 'com.fasterxml.jackson.databind.ser.DefaultSerializerProvider', + line: { + number: 319, + }, + module: 'com.fasterxml.jackson.databind.ser', + function: 'serializeValue', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ObjectWriter.java', + classname: 'com.fasterxml.jackson.databind.ObjectWriter$Prefetch', + line: { + number: 1396, + }, + module: 'com.fasterxml.jackson.databind', + function: 'serialize', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'ObjectWriter.java', + classname: 'com.fasterxml.jackson.databind.ObjectWriter', + line: { + number: 913, + }, + module: 'com.fasterxml.jackson.databind', + function: 'writeValue', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractJackson2HttpMessageConverter.java', + classname: + 'org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter', + line: { + number: 286, + }, + module: 'org.springframework.http.converter.json', + function: 'writeInternal', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'AbstractGenericHttpMessageConverter.java', + classname: + 'org.springframework.http.converter.AbstractGenericHttpMessageConverter', + line: { + number: 102, + }, + module: 'org.springframework.http.converter', + function: 'write', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractMessageConverterMethodProcessor.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor', + line: { + number: 272, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'writeWithMessageConverters', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'RequestResponseBodyMethodProcessor.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor', + line: { + number: 180, + }, + function: 'handleReturnValue', + module: 'org.springframework.web.servlet.mvc.method.annotation', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'HandlerMethodReturnValueHandlerComposite.java', + classname: + 'org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite', + line: { + number: 82, + }, + module: 'org.springframework.web.method.support', + function: 'handleReturnValue', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ServletInvocableHandlerMethod.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod', + line: { + number: 119, + }, + function: 'invokeAndHandle', + module: 'org.springframework.web.servlet.mvc.method.annotation', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'RequestMappingHandlerAdapter.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter', + line: { + number: 877, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'invokeHandlerMethod', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'RequestMappingHandlerAdapter.java', + classname: + 'org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter', + line: { + number: 783, + }, + module: 'org.springframework.web.servlet.mvc.method.annotation', + function: 'handleInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'AbstractHandlerMethodAdapter.java', + classname: + 'org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter', + line: { + number: 87, + }, + module: 'org.springframework.web.servlet.mvc.method', + function: 'handle', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'DispatcherServlet.java', + classname: 'org.springframework.web.servlet.DispatcherServlet', + line: { + number: 991, + }, + module: 'org.springframework.web.servlet', + function: 'doDispatch', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'DispatcherServlet.java', + classname: 'org.springframework.web.servlet.DispatcherServlet', + line: { + number: 925, + }, + module: 'org.springframework.web.servlet', + function: 'doService', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'FrameworkServlet.java', + classname: 'org.springframework.web.servlet.FrameworkServlet', + line: { + number: 974, + }, + module: 'org.springframework.web.servlet', + function: 'processRequest', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'FrameworkServlet.java', + classname: 'org.springframework.web.servlet.FrameworkServlet', + line: { + number: 866, + }, + module: 'org.springframework.web.servlet', + function: 'doGet', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HttpServlet.java', + classname: 'javax.servlet.http.HttpServlet', + line: { + number: 635, + }, + module: 'javax.servlet.http', + function: 'service', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'FrameworkServlet.java', + classname: 'org.springframework.web.servlet.FrameworkServlet', + line: { + number: 851, + }, + module: 'org.springframework.web.servlet', + function: 'service', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HttpServlet.java', + classname: 'javax.servlet.http.HttpServlet', + line: { + number: 742, + }, + module: 'javax.servlet.http', + function: 'service', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 231, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'WsFilter.java', + classname: 'org.apache.tomcat.websocket.server.WsFilter', + line: { + number: 52, + }, + module: 'org.apache.tomcat.websocket.server', + function: 'doFilter', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'RequestContextFilter.java', + classname: 'org.springframework.web.filter.RequestContextFilter', + line: { + number: 99, + }, + module: 'org.springframework.web.filter', + function: 'doFilterInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'OncePerRequestFilter.java', + classname: 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HttpPutFormContentFilter.java', + classname: 'org.springframework.web.filter.HttpPutFormContentFilter', + line: { + number: 109, + }, + module: 'org.springframework.web.filter', + function: 'doFilterInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'OncePerRequestFilter.java', + classname: 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'HiddenHttpMethodFilter.java', + classname: 'org.springframework.web.filter.HiddenHttpMethodFilter', + line: { + number: 81, + }, + module: 'org.springframework.web.filter', + function: 'doFilterInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'OncePerRequestFilter.java', + classname: 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + function: 'doFilter', + module: 'org.springframework.web.filter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + function: 'doFilter', + module: 'org.apache.catalina.core', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'CharacterEncodingFilter.java', + classname: 'org.springframework.web.filter.CharacterEncodingFilter', + line: { + number: 200, + }, + module: 'org.springframework.web.filter', + function: 'doFilterInternal', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'OncePerRequestFilter.java', + classname: 'org.springframework.web.filter.OncePerRequestFilter', + line: { + number: 107, + }, + module: 'org.springframework.web.filter', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 193, + }, + module: 'org.apache.catalina.core', + function: 'internalDoFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'ApplicationFilterChain.java', + classname: 'org.apache.catalina.core.ApplicationFilterChain', + line: { + number: 166, + }, + module: 'org.apache.catalina.core', + function: 'doFilter', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'StandardWrapperValve.java', + classname: 'org.apache.catalina.core.StandardWrapperValve', + line: { + number: 198, + }, + module: 'org.apache.catalina.core', + function: 'invoke', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'StandardContextValve.java', + classname: 'org.apache.catalina.core.StandardContextValve', + line: { + number: 96, + }, + function: 'invoke', + module: 'org.apache.catalina.core', + }, + ], + message: + 'Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getRevenue()', + type: 'org.springframework.aop.AopInvocationException', + }, + ]; + + return ; +} +JavaWithLongLines.decorators = [ + (Story: ComponentType) => { return ( - +
+ +
); - }) - .add('Ruby with context and library frames', () => { - const exceptions: Exception[] = [ - { - stacktrace: [ - { - library_frame: true, - exclude_from_grouping: false, - filename: 'active_record/core.rb', - abs_path: - '/usr/local/bundle/gems/activerecord-5.2.4.1/lib/active_record/core.rb', - line: { - number: 177, - }, - function: 'find', - }, - { - library_frame: false, - exclude_from_grouping: false, - filename: 'api/orders_controller.rb', - abs_path: '/app/app/controllers/api/orders_controller.rb', - line: { - number: 23, - context: ' render json: Order.find(params[:id])\n', - }, - function: 'show', - context: { - pre: ['\n', ' def show\n'], - post: [' end\n', ' end\n'], - }, - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_controller/metal/basic_implicit_render.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/basic_implicit_render.rb', - line: { - number: 6, - }, - function: 'send_action', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'abstract_controller/base.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/abstract_controller/base.rb', - line: { - number: 194, - }, - function: 'process_action', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_controller/metal/rendering.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/rendering.rb', - line: { - number: 30, - }, - function: 'process_action', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'abstract_controller/callbacks.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/abstract_controller/callbacks.rb', - line: { - number: 42, - }, - function: 'block in process_action', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'active_support/callbacks.rb', - abs_path: - '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/callbacks.rb', - line: { - number: 132, - }, - function: 'run_callbacks', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'abstract_controller/callbacks.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/abstract_controller/callbacks.rb', - line: { - number: 41, - }, - function: 'process_action', - }, - { - library_frame: true, - exclude_from_grouping: false, - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/rescue.rb', - filename: 'action_controller/metal/rescue.rb', - line: { - number: 22, - }, - function: 'process_action', - }, - { - library_frame: true, - exclude_from_grouping: false, - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/instrumentation.rb', - filename: 'action_controller/metal/instrumentation.rb', - line: { - number: 34, - }, - function: 'block in process_action', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'active_support/notifications.rb', - abs_path: - '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/notifications.rb', - line: { - number: 168, - }, - function: 'block in instrument', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'active_support/notifications/instrumenter.rb', - abs_path: - '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/notifications/instrumenter.rb', - line: { - number: 23, - }, - function: 'instrument', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'active_support/notifications.rb', - abs_path: - '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/notifications.rb', - line: { - number: 168, - }, - function: 'instrument', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_controller/metal/instrumentation.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/instrumentation.rb', - line: { - number: 32, - }, - function: 'process_action', - }, - { - library_frame: true, - exclude_from_grouping: false, - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/params_wrapper.rb', - filename: 'action_controller/metal/params_wrapper.rb', - line: { - number: 256, - }, - function: 'process_action', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'active_record/railties/controller_runtime.rb', - abs_path: - '/usr/local/bundle/gems/activerecord-5.2.4.1/lib/active_record/railties/controller_runtime.rb', - line: { - number: 24, - }, - function: 'process_action', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'abstract_controller/base.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/abstract_controller/base.rb', - line: { - number: 134, - }, - function: 'process', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_view/rendering.rb', - abs_path: - '/usr/local/bundle/gems/actionview-5.2.4.1/lib/action_view/rendering.rb', - line: { - number: 32, - }, - function: 'process', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_controller/metal.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal.rb', - line: { - number: 191, - }, - function: 'dispatch', - }, - { - library_frame: true, - exclude_from_grouping: false, - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal.rb', - filename: 'action_controller/metal.rb', - line: { - number: 252, - }, - function: 'dispatch', - }, - { - exclude_from_grouping: false, - library_frame: true, - filename: 'action_dispatch/routing/route_set.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/routing/route_set.rb', - line: { - number: 52, - }, - function: 'dispatch', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_dispatch/routing/route_set.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/routing/route_set.rb', - line: { - number: 34, - }, - function: 'serve', - }, - { - library_frame: true, - exclude_from_grouping: false, - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/journey/router.rb', - filename: 'action_dispatch/journey/router.rb', - line: { - number: 52, - }, - function: 'block in serve', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_dispatch/journey/router.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/journey/router.rb', - line: { - number: 35, - }, - function: 'each', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_dispatch/journey/router.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/journey/router.rb', - line: { - number: 35, - }, - function: 'serve', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_dispatch/routing/route_set.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/routing/route_set.rb', - line: { - number: 840, - }, - function: 'call', - }, - { - exclude_from_grouping: false, - library_frame: true, - filename: 'rack/static.rb', - abs_path: '/usr/local/bundle/gems/rack-2.2.3/lib/rack/static.rb', - line: { - number: 161, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'rack/tempfile_reaper.rb', - abs_path: - '/usr/local/bundle/gems/rack-2.2.3/lib/rack/tempfile_reaper.rb', - line: { - number: 15, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'rack/etag.rb', - abs_path: '/usr/local/bundle/gems/rack-2.2.3/lib/rack/etag.rb', - line: { - number: 27, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'rack/conditional_get.rb', - abs_path: - '/usr/local/bundle/gems/rack-2.2.3/lib/rack/conditional_get.rb', - line: { - number: 27, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'rack/head.rb', - abs_path: '/usr/local/bundle/gems/rack-2.2.3/lib/rack/head.rb', - line: { - number: 12, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_dispatch/http/content_security_policy.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/http/content_security_policy.rb', - line: { - number: 18, - }, - function: 'call', - }, - { - exclude_from_grouping: false, - library_frame: true, - filename: 'rack/session/abstract/id.rb', - abs_path: - '/usr/local/bundle/gems/rack-2.2.3/lib/rack/session/abstract/id.rb', - line: { - number: 266, - }, - function: 'context', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'rack/session/abstract/id.rb', - abs_path: - '/usr/local/bundle/gems/rack-2.2.3/lib/rack/session/abstract/id.rb', - line: { - number: 260, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_dispatch/middleware/cookies.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/cookies.rb', - line: { - number: 670, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_dispatch/middleware/callbacks.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/callbacks.rb', - line: { - number: 28, - }, - function: 'block in call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'active_support/callbacks.rb', - abs_path: - '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/callbacks.rb', - line: { - number: 98, - }, - function: 'run_callbacks', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_dispatch/middleware/callbacks.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/callbacks.rb', - line: { - number: 26, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_dispatch/middleware/debug_exceptions.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/debug_exceptions.rb', - line: { - number: 61, - }, - function: 'call', - }, - { - exclude_from_grouping: false, - library_frame: true, - filename: 'action_dispatch/middleware/show_exceptions.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/show_exceptions.rb', - line: { - number: 33, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'lograge/rails_ext/rack/logger.rb', - abs_path: - '/usr/local/bundle/gems/lograge-0.11.2/lib/lograge/rails_ext/rack/logger.rb', - line: { - number: 15, - }, - function: 'call_app', - }, - { - exclude_from_grouping: false, - library_frame: true, - filename: 'rails/rack/logger.rb', - abs_path: - '/usr/local/bundle/gems/railties-5.2.4.1/lib/rails/rack/logger.rb', - line: { - number: 28, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/remote_ip.rb', - filename: 'action_dispatch/middleware/remote_ip.rb', - line: { - number: 81, - }, - function: 'call', - }, - { - exclude_from_grouping: false, - library_frame: true, - filename: 'request_store/middleware.rb', - abs_path: - '/usr/local/bundle/gems/request_store-1.5.0/lib/request_store/middleware.rb', - line: { - number: 19, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_dispatch/middleware/request_id.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/request_id.rb', - line: { - number: 27, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'rack/method_override.rb', - abs_path: - '/usr/local/bundle/gems/rack-2.2.3/lib/rack/method_override.rb', - line: { - number: 24, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'rack/runtime.rb', - abs_path: '/usr/local/bundle/gems/rack-2.2.3/lib/rack/runtime.rb', - line: { - number: 22, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'active_support/cache/strategy/local_cache_middleware.rb', - abs_path: - '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/cache/strategy/local_cache_middleware.rb', - line: { - number: 29, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_dispatch/middleware/executor.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/executor.rb', - line: { - number: 14, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'action_dispatch/middleware/static.rb', - abs_path: - '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/static.rb', - line: { - number: 127, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'rack/sendfile.rb', - abs_path: '/usr/local/bundle/gems/rack-2.2.3/lib/rack/sendfile.rb', - line: { - number: 110, - }, - function: 'call', - }, - { - library_frame: false, - exclude_from_grouping: false, - filename: 'opbeans_shuffle.rb', - abs_path: '/app/lib/opbeans_shuffle.rb', - line: { - number: 32, - context: ' @app.call(env)\n', - }, - function: 'call', - context: { - pre: [' end\n', ' else\n'], - post: [' end\n', ' rescue Timeout::Error\n'], - }, - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'elastic_apm/middleware.rb', - abs_path: - '/usr/local/bundle/gems/elastic-apm-3.8.0/lib/elastic_apm/middleware.rb', - line: { - number: 36, - }, - function: 'call', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'rails/engine.rb', - abs_path: - '/usr/local/bundle/gems/railties-5.2.4.1/lib/rails/engine.rb', - line: { - number: 524, - }, - function: 'call', - }, - { - exclude_from_grouping: false, - library_frame: true, - filename: 'puma/configuration.rb', - abs_path: - '/usr/local/bundle/gems/puma-4.3.5/lib/puma/configuration.rb', - line: { - number: 228, - }, - function: 'call', - }, - { - exclude_from_grouping: false, - library_frame: true, - filename: 'puma/server.rb', - abs_path: '/usr/local/bundle/gems/puma-4.3.5/lib/puma/server.rb', - line: { - number: 713, - }, - function: 'handle_request', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'puma/server.rb', - abs_path: '/usr/local/bundle/gems/puma-4.3.5/lib/puma/server.rb', - line: { - number: 472, - }, - function: 'process_client', - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'puma/server.rb', - abs_path: '/usr/local/bundle/gems/puma-4.3.5/lib/puma/server.rb', - line: { - number: 328, - }, - function: 'block in run', - }, - { - exclude_from_grouping: false, - library_frame: true, - filename: 'puma/thread_pool.rb', - abs_path: - '/usr/local/bundle/gems/puma-4.3.5/lib/puma/thread_pool.rb', - line: { - number: 134, - }, - function: 'block in spawn_thread', - }, - ], - handled: false, - module: 'ActiveRecord', - message: "Couldn't find Order with 'id'=956", - type: 'ActiveRecord::RecordNotFound', + }, +]; + +export function JavaScriptWithSomeContext() { + const exceptions: Exception[] = [ + { + code: '503', + stacktrace: [ + { + library_frame: true, + exclude_from_grouping: false, + filename: 'node_modules/elastic-apm-http-client/index.js', + abs_path: '/app/node_modules/elastic-apm-http-client/index.js', + line: { + number: 711, + context: + " const err = new Error('Unexpected APM Server response when polling config')", + }, + function: 'processConfigErrorResponse', + context: { + pre: ['', 'function processConfigErrorResponse (res, buf) {'], + post: ['', ' err.code = res.statusCode'], + }, + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'node_modules/elastic-apm-http-client/index.js', + abs_path: '/app/node_modules/elastic-apm-http-client/index.js', + line: { + number: 196, + context: + ' res.destroy(processConfigErrorResponse(res, buf))', + }, + function: '', + context: { + pre: [' }', ' } else {'], + post: [' }', ' })'], + }, + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'node_modules/fast-stream-to-buffer/index.js', + abs_path: '/app/node_modules/fast-stream-to-buffer/index.js', + line: { + number: 20, + context: ' cb(err, buffers[0], stream)', + }, + function: 'IncomingMessage.', + context: { + pre: [' break', ' case 1:'], + post: [' break', ' default:'], + }, + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'node_modules/once/once.js', + abs_path: '/app/node_modules/once/once.js', + line: { + number: 25, + context: ' return f.value = fn.apply(this, arguments)', + }, + function: 'f', + context: { + pre: [' if (f.called) return f.value', ' f.called = true'], + post: [' }', ' f.called = false'], + }, + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'node_modules/end-of-stream/index.js', + abs_path: '/app/node_modules/end-of-stream/index.js', + line: { + number: 36, + context: '\t\tif (!writable) callback.call(stream);', + }, + function: 'onend', + context: { + pre: ['\tvar onend = function() {', '\t\treadable = false;'], + post: ['\t};', ''], + }, + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: 'events.js', + filename: 'events.js', + line: { + number: 327, + }, + function: 'emit', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: '_stream_readable.js', + abs_path: '_stream_readable.js', + line: { + number: 1220, + }, + function: 'endReadableNT', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'internal/process/task_queues.js', + abs_path: 'internal/process/task_queues.js', + line: { + number: 84, + }, + function: 'processTicksAndRejections', + }, + ], + module: 'elastic-apm-http-client', + handled: false, + attributes: { + response: + '\r\n503 Service Temporarily Unavailable\r\n\r\n

503 Service Temporarily Unavailable

\r\n
nginx/1.17.7
\r\n\r\n\r\n', }, - ]; + type: 'Error', + message: 'Unexpected APM Server response when polling config', + }, + ]; + + return ( + + ); +} +JavaScriptWithSomeContext.storyName = 'JavaScript With Some Context'; + +export function RubyWithContextAndLibraryFrames() { + const exceptions: Exception[] = [ + { + stacktrace: [ + { + library_frame: true, + exclude_from_grouping: false, + filename: 'active_record/core.rb', + abs_path: + '/usr/local/bundle/gems/activerecord-5.2.4.1/lib/active_record/core.rb', + line: { + number: 177, + }, + function: 'find', + }, + { + library_frame: false, + exclude_from_grouping: false, + filename: 'api/orders_controller.rb', + abs_path: '/app/app/controllers/api/orders_controller.rb', + line: { + number: 23, + context: ' render json: Order.find(params[:id])\n', + }, + function: 'show', + context: { + pre: ['\n', ' def show\n'], + post: [' end\n', ' end\n'], + }, + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_controller/metal/basic_implicit_render.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/basic_implicit_render.rb', + line: { + number: 6, + }, + function: 'send_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'abstract_controller/base.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/abstract_controller/base.rb', + line: { + number: 194, + }, + function: 'process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_controller/metal/rendering.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/rendering.rb', + line: { + number: 30, + }, + function: 'process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'abstract_controller/callbacks.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/abstract_controller/callbacks.rb', + line: { + number: 42, + }, + function: 'block in process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'active_support/callbacks.rb', + abs_path: + '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/callbacks.rb', + line: { + number: 132, + }, + function: 'run_callbacks', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'abstract_controller/callbacks.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/abstract_controller/callbacks.rb', + line: { + number: 41, + }, + function: 'process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/rescue.rb', + filename: 'action_controller/metal/rescue.rb', + line: { + number: 22, + }, + function: 'process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/instrumentation.rb', + filename: 'action_controller/metal/instrumentation.rb', + line: { + number: 34, + }, + function: 'block in process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'active_support/notifications.rb', + abs_path: + '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/notifications.rb', + line: { + number: 168, + }, + function: 'block in instrument', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'active_support/notifications/instrumenter.rb', + abs_path: + '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/notifications/instrumenter.rb', + line: { + number: 23, + }, + function: 'instrument', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'active_support/notifications.rb', + abs_path: + '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/notifications.rb', + line: { + number: 168, + }, + function: 'instrument', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_controller/metal/instrumentation.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/instrumentation.rb', + line: { + number: 32, + }, + function: 'process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal/params_wrapper.rb', + filename: 'action_controller/metal/params_wrapper.rb', + line: { + number: 256, + }, + function: 'process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'active_record/railties/controller_runtime.rb', + abs_path: + '/usr/local/bundle/gems/activerecord-5.2.4.1/lib/active_record/railties/controller_runtime.rb', + line: { + number: 24, + }, + function: 'process_action', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'abstract_controller/base.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/abstract_controller/base.rb', + line: { + number: 134, + }, + function: 'process', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_view/rendering.rb', + abs_path: + '/usr/local/bundle/gems/actionview-5.2.4.1/lib/action_view/rendering.rb', + line: { + number: 32, + }, + function: 'process', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_controller/metal.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal.rb', + line: { + number: 191, + }, + function: 'dispatch', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_controller/metal.rb', + filename: 'action_controller/metal.rb', + line: { + number: 252, + }, + function: 'dispatch', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'action_dispatch/routing/route_set.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/routing/route_set.rb', + line: { + number: 52, + }, + function: 'dispatch', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/routing/route_set.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/routing/route_set.rb', + line: { + number: 34, + }, + function: 'serve', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/journey/router.rb', + filename: 'action_dispatch/journey/router.rb', + line: { + number: 52, + }, + function: 'block in serve', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/journey/router.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/journey/router.rb', + line: { + number: 35, + }, + function: 'each', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/journey/router.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/journey/router.rb', + line: { + number: 35, + }, + function: 'serve', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/routing/route_set.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/routing/route_set.rb', + line: { + number: 840, + }, + function: 'call', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'rack/static.rb', + abs_path: '/usr/local/bundle/gems/rack-2.2.3/lib/rack/static.rb', + line: { + number: 161, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rack/tempfile_reaper.rb', + abs_path: + '/usr/local/bundle/gems/rack-2.2.3/lib/rack/tempfile_reaper.rb', + line: { + number: 15, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rack/etag.rb', + abs_path: '/usr/local/bundle/gems/rack-2.2.3/lib/rack/etag.rb', + line: { + number: 27, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rack/conditional_get.rb', + abs_path: + '/usr/local/bundle/gems/rack-2.2.3/lib/rack/conditional_get.rb', + line: { + number: 27, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rack/head.rb', + abs_path: '/usr/local/bundle/gems/rack-2.2.3/lib/rack/head.rb', + line: { + number: 12, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/http/content_security_policy.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/http/content_security_policy.rb', + line: { + number: 18, + }, + function: 'call', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'rack/session/abstract/id.rb', + abs_path: + '/usr/local/bundle/gems/rack-2.2.3/lib/rack/session/abstract/id.rb', + line: { + number: 266, + }, + function: 'context', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rack/session/abstract/id.rb', + abs_path: + '/usr/local/bundle/gems/rack-2.2.3/lib/rack/session/abstract/id.rb', + line: { + number: 260, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/middleware/cookies.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/cookies.rb', + line: { + number: 670, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/middleware/callbacks.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/callbacks.rb', + line: { + number: 28, + }, + function: 'block in call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'active_support/callbacks.rb', + abs_path: + '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/callbacks.rb', + line: { + number: 98, + }, + function: 'run_callbacks', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/middleware/callbacks.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/callbacks.rb', + line: { + number: 26, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/middleware/debug_exceptions.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/debug_exceptions.rb', + line: { + number: 61, + }, + function: 'call', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'action_dispatch/middleware/show_exceptions.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/show_exceptions.rb', + line: { + number: 33, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'lograge/rails_ext/rack/logger.rb', + abs_path: + '/usr/local/bundle/gems/lograge-0.11.2/lib/lograge/rails_ext/rack/logger.rb', + line: { + number: 15, + }, + function: 'call_app', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'rails/rack/logger.rb', + abs_path: + '/usr/local/bundle/gems/railties-5.2.4.1/lib/rails/rack/logger.rb', + line: { + number: 28, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/remote_ip.rb', + filename: 'action_dispatch/middleware/remote_ip.rb', + line: { + number: 81, + }, + function: 'call', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'request_store/middleware.rb', + abs_path: + '/usr/local/bundle/gems/request_store-1.5.0/lib/request_store/middleware.rb', + line: { + number: 19, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/middleware/request_id.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/request_id.rb', + line: { + number: 27, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rack/method_override.rb', + abs_path: + '/usr/local/bundle/gems/rack-2.2.3/lib/rack/method_override.rb', + line: { + number: 24, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rack/runtime.rb', + abs_path: '/usr/local/bundle/gems/rack-2.2.3/lib/rack/runtime.rb', + line: { + number: 22, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'active_support/cache/strategy/local_cache_middleware.rb', + abs_path: + '/usr/local/bundle/gems/activesupport-5.2.4.1/lib/active_support/cache/strategy/local_cache_middleware.rb', + line: { + number: 29, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/middleware/executor.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/executor.rb', + line: { + number: 14, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'action_dispatch/middleware/static.rb', + abs_path: + '/usr/local/bundle/gems/actionpack-5.2.4.1/lib/action_dispatch/middleware/static.rb', + line: { + number: 127, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rack/sendfile.rb', + abs_path: '/usr/local/bundle/gems/rack-2.2.3/lib/rack/sendfile.rb', + line: { + number: 110, + }, + function: 'call', + }, + { + library_frame: false, + exclude_from_grouping: false, + filename: 'opbeans_shuffle.rb', + abs_path: '/app/lib/opbeans_shuffle.rb', + line: { + number: 32, + context: ' @app.call(env)\n', + }, + function: 'call', + context: { + pre: [' end\n', ' else\n'], + post: [' end\n', ' rescue Timeout::Error\n'], + }, + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'elastic_apm/middleware.rb', + abs_path: + '/usr/local/bundle/gems/elastic-apm-3.8.0/lib/elastic_apm/middleware.rb', + line: { + number: 36, + }, + function: 'call', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'rails/engine.rb', + abs_path: + '/usr/local/bundle/gems/railties-5.2.4.1/lib/rails/engine.rb', + line: { + number: 524, + }, + function: 'call', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'puma/configuration.rb', + abs_path: + '/usr/local/bundle/gems/puma-4.3.5/lib/puma/configuration.rb', + line: { + number: 228, + }, + function: 'call', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'puma/server.rb', + abs_path: '/usr/local/bundle/gems/puma-4.3.5/lib/puma/server.rb', + line: { + number: 713, + }, + function: 'handle_request', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'puma/server.rb', + abs_path: '/usr/local/bundle/gems/puma-4.3.5/lib/puma/server.rb', + line: { + number: 472, + }, + function: 'process_client', + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'puma/server.rb', + abs_path: '/usr/local/bundle/gems/puma-4.3.5/lib/puma/server.rb', + line: { + number: 328, + }, + function: 'block in run', + }, + { + exclude_from_grouping: false, + library_frame: true, + filename: 'puma/thread_pool.rb', + abs_path: '/usr/local/bundle/gems/puma-4.3.5/lib/puma/thread_pool.rb', + line: { + number: 134, + }, + function: 'block in spawn_thread', + }, + ], + handled: false, + module: 'ActiveRecord', + message: "Couldn't find Order with 'id'=956", + type: 'ActiveRecord::RecordNotFound', + }, + ]; - return ; - }); + return ; +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx index d65ce1879ce0..7b944ed1b6ce 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx @@ -84,6 +84,11 @@ function CytoscapeComponent({ cy.elements().forEach((element) => { if (!elementIds.includes(element.data('id'))) { cy.remove(element); + } else { + // Doing an "add" with an element with the same id will keep the original + // element. Set the data with the new element data. + const newElement = elements.find((el) => el.data.id === element.id()); + element.data(newElement?.data ?? element.data()); } }); cy.trigger('custom:data', [fit]); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx index 7771a232a5c9..d0902c427aac 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx @@ -10,7 +10,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import cytoscape from 'cytoscape'; -import React from 'react'; +import React, { Fragment } from 'react'; import styled from 'styled-components'; import { SPAN_SUBTYPE, @@ -71,7 +71,7 @@ export function Info(data: InfoProps) { resource.label || resource['span.destination.service.resource']; const desc = `${resource['span.type']} (${resource['span.subtype']})`; return ( - <> + {desc} - + ); })} @@ -97,8 +97,8 @@ export function Info(data: InfoProps) { {listItems.map( ({ title, description }) => description && ( -
- +
+ {title} 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 7f24ad8b0d30..2e213c44bccf 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 7c306c16cca1..000000000000 --- 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 e6a9823f3ee2..000000000000 --- 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 aa0222582b89..49319f167703 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 @@ -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, 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 000000000000..daddd0a60fe1 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/service_list.test.tsx @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { 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(() => + // Types of property 'avgResponseTime' are incompatible. + // Type 'null' is not assignable to type '{ value: number | null; timeseries: { x: number; y: number | null; }[]; } | undefined'.ts(2322) + 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 7c887da6dc5e..8c7d088d36eb 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 becae4d7eb5d..41e84d4acfba 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/public/components/shared/Stacktrace/FrameHeading.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx index dfeb537b0486..6632b22b5996 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx @@ -27,10 +27,12 @@ const FileDetails = styled.div` const LibraryFrameFileDetail = styled.span` color: ${({ theme }) => theme.eui.euiColorDarkShade}; + word-break: break-word; `; const AppFrameFileDetail = styled.span` color: ${({ theme }) => theme.eui.euiColorFullShade}; + word-break: break-word; `; interface Props { diff --git a/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts b/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts index b0083da69cf8..cf17c9dbbf2e 100644 --- a/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts +++ b/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts @@ -122,11 +122,69 @@ async function init() { }); await createRole({ roleName: KIBANA_READ_ROLE, - kibanaPrivileges: { base: ['read'] }, + kibanaPrivileges: { + feature: { + // core + discover: ['read'], + dashboard: ['read'], + canvas: ['read'], + ml: ['read'], + maps: ['read'], + graph: ['read'], + visualize: ['read'], + + // observability + logs: ['read'], + infrastructure: ['read'], + apm: ['read'], + uptime: ['read'], + + // security + siem: ['read'], + + // management + dev_tools: ['read'], + advancedSettings: ['read'], + indexPatterns: ['read'], + savedObjectsManagement: ['read'], + stackAlerts: ['read'], + ingestManager: ['read'], + actions: ['read'], + }, + }, }); await createRole({ roleName: KIBANA_WRITE_ROLE, - kibanaPrivileges: { base: ['all'] }, + kibanaPrivileges: { + feature: { + // core + discover: ['all'], + dashboard: ['all'], + canvas: ['all'], + ml: ['all'], + maps: ['all'], + graph: ['all'], + visualize: ['all'], + + // observability + logs: ['all'], + infrastructure: ['all'], + apm: ['all'], + uptime: ['all'], + + // security + siem: ['all'], + + // management + dev_tools: ['all'], + advancedSettings: ['all'], + indexPatterns: ['all'], + savedObjectsManagement: ['all'], + stackAlerts: ['all'], + ingestManager: ['all'], + actions: ['all'], + }, + }, }); // read access only to APM + apm index access 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 cd3927b4b9df..000000000000 --- 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 ff3de33b017a..000000000000 --- 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 5111695e3d12..000000000000 --- 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 2583d5c20c32..000000000000 --- 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 625a03571d27..000000000000 --- 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 39f3e4cf3efe..000000000000 --- 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 d3c20fa4e12f..000000000000 --- 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 e75a7dfc0743..000000000000 --- 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 79f634971b6f..000000000000 --- 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 34c1d121259a..000000000000 --- 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 29e823e0a373..e8bffc873307 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 9d70ca418f49..62af4fe7c736 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/server/collectors/collector.ts b/x-pack/plugins/canvas/server/collectors/collector.ts index 39a8262a5dee..a084e8fe3349 100644 --- a/x-pack/plugins/canvas/server/collectors/collector.ts +++ b/x-pack/plugins/canvas/server/collectors/collector.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { CollectorFetchContext, UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { TelemetryCollector } from '../../types'; import { workpadCollector, workpadSchema, WorkpadTelemetry } from './workpad_collector'; @@ -37,7 +37,7 @@ export function registerCanvasUsageCollector( const canvasCollector = usageCollection.makeUsageCollector({ type: 'canvas', isReady: () => true, - fetch: async (callCluster) => { + fetch: async ({ callCluster }: CollectorFetchContext) => { const collectorResults = await Promise.all( collectors.map((collector) => collector(kibanaIndex, callCluster)) ); diff --git a/x-pack/plugins/case/common/api/cases/case.ts b/x-pack/plugins/case/common/api/cases/case.ts index ffeecf27743f..52e4a15a3f44 100644 --- a/x-pack/plugins/case/common/api/cases/case.ts +++ b/x-pack/plugins/case/common/api/cases/case.ts @@ -10,7 +10,7 @@ import { NumberFromString } from '../saved_object'; import { UserRT } from '../user'; import { CommentResponseRt } from './comment'; import { CasesStatusResponseRt } from './status'; -import { CaseConnectorRt, ESCaseConnector } from '../connectors'; +import { CaseConnectorRt, ESCaseConnector, ConnectorPartialFieldsRt } from '../connectors'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths export { ActionTypeExecutorResult } from '../../../../actions/server/types'; @@ -133,7 +133,7 @@ export const ServiceConnectorCommentParamsRt = rt.type({ updatedBy: rt.union([ServiceConnectorUserParams, rt.null]), }); -export const ServiceConnectorCaseParamsRt = rt.type({ +export const ServiceConnectorBasicCaseParamsRt = rt.type({ comments: rt.union([rt.array(ServiceConnectorCommentParamsRt), rt.null]), createdAt: rt.string, createdBy: ServiceConnectorUserParams, @@ -145,6 +145,11 @@ export const ServiceConnectorCaseParamsRt = rt.type({ updatedBy: rt.union([ServiceConnectorUserParams, rt.null]), }); +export const ServiceConnectorCaseParamsRt = rt.intersection([ + ServiceConnectorBasicCaseParamsRt, + ConnectorPartialFieldsRt, +]); + export const ServiceConnectorCaseResponseRt = rt.intersection([ rt.type({ title: rt.string, diff --git a/x-pack/plugins/case/common/api/connectors/index.ts b/x-pack/plugins/case/common/api/connectors/index.ts index 88d81eed2d87..0019afe7c6b7 100644 --- a/x-pack/plugins/case/common/api/connectors/index.ts +++ b/x-pack/plugins/case/common/api/connectors/index.ts @@ -20,6 +20,12 @@ export const ConnectorFieldsRt = rt.union([ rt.null, ]); +export const ConnectorPartialFieldsRt = rt.partial({ + ...JiraFieldsRT.props, + ...ResilientFieldsRT.props, + ...ServiceNowFieldsRT.props, +}); + export enum ConnectorTypes { jira = '.jira', resilient = '.resilient', diff --git a/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.test.ts b/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.test.ts index 7ec3888e4e1e..6634ca630daf 100644 --- a/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.test.ts +++ b/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.test.ts @@ -5,6 +5,7 @@ */ import { createCloudUsageCollector } from './cloud_usage_collector'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; const mockUsageCollection = () => ({ makeUsageCollector: jest.fn().mockImplementation((args: any) => ({ ...args })), @@ -25,9 +26,9 @@ describe('createCloudUsageCollector', () => { const mockConfigs = getMockConfigs(true); const usageCollection = mockUsageCollection() as any; const collector = createCloudUsageCollector(usageCollection, mockConfigs); - const callCluster = {} as any; // Sending any as the callCluster client because it's not needed in this collector but TS requires it when calling it. + const collectorFetchContext = createCollectorFetchContextMock(); - expect((await collector.fetch(callCluster)).isCloudEnabled).toBe(true); // Adding the await because the fetch can be a Promise or a synchronous method and TS complains in the test if not awaited + expect((await collector.fetch(collectorFetchContext)).isCloudEnabled).toBe(true); // Adding the await because the fetch can be a Promise or a synchronous method and TS complains in the test if not awaited }); }); }); diff --git a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.test.ts index 7f6663a39eeb..5b634fe4cf26 100644 --- a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.test.ts @@ -82,7 +82,7 @@ describe('EQL search strategy', () => { describe('async functionality', () => { it('performs an eql client search with params when no ID is provided', async () => { const eqlSearch = await eqlSearchStrategyProvider(mockLogger); - await eqlSearch.search(mockContext, { options, params }); + await eqlSearch.search({ options, params }, {}, mockContext).toPromise(); const [[request, requestOptions]] = mockEqlSearch.mock.calls; expect(request.index).toEqual('logstash-*'); @@ -92,7 +92,7 @@ describe('EQL search strategy', () => { it('retrieves the current request if an id is provided', async () => { const eqlSearch = await eqlSearchStrategyProvider(mockLogger); - await eqlSearch.search(mockContext, { id: 'my-search-id' }); + await eqlSearch.search({ id: 'my-search-id' }, {}, mockContext).toPromise(); const [[requestParams]] = mockEqlGet.mock.calls; expect(mockEqlSearch).not.toHaveBeenCalled(); @@ -103,7 +103,7 @@ describe('EQL search strategy', () => { describe('arguments', () => { it('sends along async search options', async () => { const eqlSearch = await eqlSearchStrategyProvider(mockLogger); - await eqlSearch.search(mockContext, { options, params }); + await eqlSearch.search({ options, params }, {}, mockContext).toPromise(); const [[request]] = mockEqlSearch.mock.calls; expect(request).toEqual( @@ -116,7 +116,7 @@ describe('EQL search strategy', () => { it('sends along default search parameters', async () => { const eqlSearch = await eqlSearchStrategyProvider(mockLogger); - await eqlSearch.search(mockContext, { options, params }); + await eqlSearch.search({ options, params }, {}, mockContext).toPromise(); const [[request]] = mockEqlSearch.mock.calls; expect(request).toEqual( @@ -129,14 +129,20 @@ describe('EQL search strategy', () => { it('allows search parameters to be overridden', async () => { const eqlSearch = await eqlSearchStrategyProvider(mockLogger); - await eqlSearch.search(mockContext, { - options, - params: { - ...params, - wait_for_completion_timeout: '5ms', - keep_on_completion: false, - }, - }); + await eqlSearch + .search( + { + options, + params: { + ...params, + wait_for_completion_timeout: '5ms', + keep_on_completion: false, + }, + }, + {}, + mockContext + ) + .toPromise(); const [[request]] = mockEqlSearch.mock.calls; expect(request).toEqual( @@ -150,10 +156,16 @@ describe('EQL search strategy', () => { it('allows search options to be overridden', async () => { const eqlSearch = await eqlSearchStrategyProvider(mockLogger); - await eqlSearch.search(mockContext, { - options: { ...options, maxRetries: 2, ignore: [300] }, - params, - }); + await eqlSearch + .search( + { + options: { ...options, maxRetries: 2, ignore: [300] }, + params, + }, + {}, + mockContext + ) + .toPromise(); const [[, requestOptions]] = mockEqlSearch.mock.calls; expect(requestOptions).toEqual( @@ -166,7 +178,9 @@ describe('EQL search strategy', () => { it('passes transport options for an existing request', async () => { const eqlSearch = await eqlSearchStrategyProvider(mockLogger); - await eqlSearch.search(mockContext, { id: 'my-search-id', options: { ignore: [400] } }); + await eqlSearch + .search({ id: 'my-search-id', options: { ignore: [400] } }, {}, mockContext) + .toPromise(); const [[, requestOptions]] = mockEqlGet.mock.calls; expect(mockEqlSearch).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts index 2516693a7f29..a7ca999699e2 100644 --- a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { from } from 'rxjs'; import { Logger } from 'kibana/server'; import { ApiResponse, TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; @@ -26,48 +27,51 @@ export const eqlSearchStrategyProvider = ( id, }); }, - search: async (context, request, options) => { - logger.debug(`_eql/search ${JSON.stringify(request.params) || request.id}`); - let promise: TransportRequestPromise; - const eqlClient = context.core.elasticsearch.client.asCurrentUser.eql; - const uiSettingsClient = await context.core.uiSettings.client; - const asyncOptions = getAsyncOptions(); - const searchOptions = toSnakeCase({ ...request.options }); + search: (request, options, context) => + from( + new Promise(async (resolve) => { + logger.debug(`_eql/search ${JSON.stringify(request.params) || request.id}`); + let promise: TransportRequestPromise; + const eqlClient = context.core.elasticsearch.client.asCurrentUser.eql; + const uiSettingsClient = await context.core.uiSettings.client; + const asyncOptions = getAsyncOptions(); + const searchOptions = toSnakeCase({ ...request.options }); - if (request.id) { - promise = eqlClient.get( - { - id: request.id, - ...toSnakeCase(asyncOptions), - }, - searchOptions - ); - } else { - const { ignoreThrottled, ignoreUnavailable } = await getDefaultSearchParams( - uiSettingsClient - ); - const searchParams = toSnakeCase({ - ignoreThrottled, - ignoreUnavailable, - ...asyncOptions, - ...request.params, - }); + if (request.id) { + promise = eqlClient.get( + { + id: request.id, + ...toSnakeCase(asyncOptions), + }, + searchOptions + ); + } else { + const { ignoreThrottled, ignoreUnavailable } = await getDefaultSearchParams( + uiSettingsClient + ); + const searchParams = toSnakeCase({ + ignoreThrottled, + ignoreUnavailable, + ...asyncOptions, + ...request.params, + }); - promise = eqlClient.search( - searchParams as EqlSearchStrategyRequest['params'], - searchOptions - ); - } + promise = eqlClient.search( + searchParams as EqlSearchStrategyRequest['params'], + searchOptions + ); + } - const rawResponse = await shimAbortSignal(promise, options?.abortSignal); - const { id, is_partial: isPartial, is_running: isRunning } = rawResponse.body; + const rawResponse = await shimAbortSignal(promise, options?.abortSignal); + const { id, is_partial: isPartial, is_running: isRunning } = rawResponse.body; - return { - id, - isPartial, - isRunning, - rawResponse, - }; - }, + resolve({ + id, + isPartial, + isRunning, + rawResponse, + }); + }) + ), }; }; diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts index f4f3d894a457..bab304b6afc9 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts @@ -86,7 +86,9 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', body: { query: {} } }; const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); - await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params }); + await esSearch + .search({ params }, {}, (mockContext as unknown) as RequestHandlerContext) + .toPromise(); expect(mockSubmitCaller).toBeCalled(); const request = mockSubmitCaller.mock.calls[0][0]; @@ -100,7 +102,9 @@ describe('ES search strategy', () => { const params = { index: 'logstash-*', body: { query: {} } }; const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); - await esSearch.search((mockContext as unknown) as RequestHandlerContext, { id: 'foo', params }); + await esSearch + .search({ id: 'foo', params }, {}, (mockContext as unknown) as RequestHandlerContext) + .toPromise(); expect(mockGetCaller).toBeCalled(); const request = mockGetCaller.mock.calls[0][0]; @@ -115,10 +119,16 @@ describe('ES search strategy', () => { const params = { index: 'foo-程', body: {} }; const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); - await esSearch.search((mockContext as unknown) as RequestHandlerContext, { - indexType: 'rollup', - params, - }); + await esSearch + .search( + { + indexType: 'rollup', + params, + }, + {}, + (mockContext as unknown) as RequestHandlerContext + ) + .toPromise(); expect(mockApiCaller).toBeCalled(); const { method, path } = mockApiCaller.mock.calls[0][0]; @@ -132,7 +142,9 @@ describe('ES search strategy', () => { const params = { index: 'foo-*', body: {} }; const esSearch = await enhancedEsSearchStrategyProvider(mockConfig$, mockLogger); - await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params }); + await esSearch + .search({ params }, {}, (mockContext as unknown) as RequestHandlerContext) + .toPromise(); expect(mockSubmitCaller).toBeCalled(); const request = mockSubmitCaller.mock.calls[0][0]; diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 747522872438..9b89fb9fab3c 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { from } from 'rxjs'; import { first } from 'rxjs/operators'; import { SearchResponse } from 'elasticsearch'; import { Observable } from 'rxjs'; @@ -36,35 +37,38 @@ export const enhancedEsSearchStrategyProvider = ( logger: Logger, usage?: SearchUsage ): ISearchStrategy => { - const search = async ( - context: RequestHandlerContext, + const search = ( request: IEnhancedEsSearchRequest, - options?: ISearchOptions - ) => { - logger.debug(`search ${JSON.stringify(request.params) || request.id}`); - - const isAsync = request.indexType !== 'rollup'; - - try { - const response = isAsync - ? await asyncSearch(context, request, options) - : await rollupSearch(context, request, options); - - if ( - usage && - isAsync && - isEnhancedEsSearchResponse(response) && - isCompleteResponse(response) - ) { - usage.trackSuccess(response.rawResponse.took); - } - - return response; - } catch (e) { - if (usage) usage.trackError(); - throw e; - } - }; + options: ISearchOptions, + context: RequestHandlerContext + ) => + from( + new Promise(async (resolve, reject) => { + logger.debug(`search ${JSON.stringify(request.params) || request.id}`); + + const isAsync = request.indexType !== 'rollup'; + + try { + const response = isAsync + ? await asyncSearch(request, options, context) + : await rollupSearch(request, options, context); + + if ( + usage && + isAsync && + isEnhancedEsSearchResponse(response) && + isCompleteResponse(response) + ) { + usage.trackSuccess(response.rawResponse.took); + } + + resolve(response); + } catch (e) { + if (usage) usage.trackError(); + reject(e); + } + }) + ); const cancel = async (context: RequestHandlerContext, id: string) => { logger.debug(`cancel ${id}`); @@ -74,9 +78,9 @@ export const enhancedEsSearchStrategyProvider = ( }; async function asyncSearch( - context: RequestHandlerContext, request: IEnhancedEsSearchRequest, - options?: ISearchOptions + options: ISearchOptions, + context: RequestHandlerContext ): Promise { let promise: TransportRequestPromise; const esClient = context.core.elasticsearch.client.asCurrentUser; @@ -112,9 +116,9 @@ export const enhancedEsSearchStrategyProvider = ( } const rollupSearch = async function ( - context: RequestHandlerContext, request: IEnhancedEsSearchRequest, - options?: ISearchOptions + options: ISearchOptions, + context: RequestHandlerContext ): Promise { const esClient = context.core.elasticsearch.client.asCurrentUser; const uiSettingsClient = await context.core.uiSettings.client; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx index dfbe19ba21a9..953e6244b077 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx @@ -21,8 +21,10 @@ import { fatalErrorsServiceMock, } from '../../../../../src/core/public/mocks'; import { usageCollectionPluginMock } from '../../../../../src/plugins/usage_collection/public/mocks'; +import { CloudSetup } from '../../../cloud/public'; import { EditPolicy } from '../../public/application/sections/edit_policy/edit_policy'; +import { KibanaContextProvider } from '../../public/shared_imports'; import { init as initHttp } from '../../public/application/services/http'; import { init as initUiMetric } from '../../public/application/services/ui_metric'; import { init as initNotification } from '../../public/application/services/notification'; @@ -148,7 +150,14 @@ const save = (rendered: ReactWrapper) => { describe('edit policy', () => { beforeEach(() => { component = ( - + + + ); ({ http } = editPolicyHelpers.setup()); @@ -447,6 +456,7 @@ describe('edit policy', () => { http.setupNodeListResponse({ nodesByAttributes: {}, nodesByRoles: { data: ['node1'] }, + isUsingDeprecatedDataRoleConfig: false, }); const rendered = mountWithIntl(component); noRollover(rendered); @@ -495,6 +505,7 @@ describe('edit policy', () => { http.setupNodeListResponse({ nodesByAttributes: {}, nodesByRoles: {}, + isUsingDeprecatedDataRoleConfig: false, }); const rendered = mountWithIntl(component); noRollover(rendered); @@ -507,6 +518,7 @@ describe('edit policy', () => { http.setupNodeListResponse({ nodesByAttributes: {}, nodesByRoles: { data_hot: ['test'], data_cold: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, }); const rendered = mountWithIntl(component); noRollover(rendered); @@ -519,6 +531,7 @@ describe('edit policy', () => { http.setupNodeListResponse({ nodesByAttributes: {}, nodesByRoles: { data: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, }); const rendered = mountWithIntl(component); noRollover(rendered); @@ -568,6 +581,7 @@ describe('edit policy', () => { http.setupNodeListResponse({ nodesByAttributes: {}, nodesByRoles: { data: ['node1'] }, + isUsingDeprecatedDataRoleConfig: false, }); const rendered = mountWithIntl(component); noRollover(rendered); @@ -626,6 +640,7 @@ describe('edit policy', () => { http.setupNodeListResponse({ nodesByAttributes: {}, nodesByRoles: {}, + isUsingDeprecatedDataRoleConfig: false, }); const rendered = mountWithIntl(component); noRollover(rendered); @@ -638,6 +653,7 @@ describe('edit policy', () => { http.setupNodeListResponse({ nodesByAttributes: {}, nodesByRoles: { data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, }); const rendered = mountWithIntl(component); noRollover(rendered); @@ -650,6 +666,7 @@ describe('edit policy', () => { http.setupNodeListResponse({ nodesByAttributes: {}, nodesByRoles: { data: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, }); const rendered = mountWithIntl(component); noRollover(rendered); @@ -679,4 +696,104 @@ describe('edit policy', () => { expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); }); }); + describe('not on cloud', () => { + beforeEach(() => { + server.respondImmediately = true; + }); + test('should show all allocation options, even if using legacy config', async () => { + http.setupNodeListResponse({ + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'warm'); + expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + // Assert that only the custom and off options exist + findTestSubject(rendered, 'dataTierSelect').simulate('click'); + expect(findTestSubject(rendered, 'defaultDataAllocationOption').exists()).toBeTruthy(); + expect(findTestSubject(rendered, 'customDataAllocationOption').exists()).toBeTruthy(); + expect(findTestSubject(rendered, 'noneDataAllocationOption').exists()).toBeTruthy(); + }); + }); + describe('on cloud', () => { + beforeEach(() => { + component = ( + + + + ); + ({ http } = editPolicyHelpers.setup()); + ({ server, httpRequestsMockHelpers } = http); + server.respondImmediately = true; + + httpRequestsMockHelpers.setPoliciesResponse(policies); + }); + + describe('with legacy data role config', () => { + test('should hide data tier option on cloud using legacy node role configuration', async () => { + http.setupNodeListResponse({ + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'warm'); + expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + // Assert that only the custom and off options exist + findTestSubject(rendered, 'dataTierSelect').simulate('click'); + expect(findTestSubject(rendered, 'defaultDataAllocationOption').exists()).toBeFalsy(); + expect(findTestSubject(rendered, 'customDataAllocationOption').exists()).toBeTruthy(); + expect(findTestSubject(rendered, 'noneDataAllocationOption').exists()).toBeTruthy(); + }); + }); + + describe('with node role config', () => { + test('should show off, custom and data role options on cloud with data roles', async () => { + http.setupNodeListResponse({ + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'warm'); + expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + findTestSubject(rendered, 'dataTierSelect').simulate('click'); + expect(findTestSubject(rendered, 'defaultDataAllocationOption').exists()).toBeTruthy(); + expect(findTestSubject(rendered, 'customDataAllocationOption').exists()).toBeTruthy(); + expect(findTestSubject(rendered, 'noneDataAllocationOption').exists()).toBeTruthy(); + }); + + test('should show cloud notice when cold tier nodes do not exist', async () => { + http.setupNodeListResponse({ + nodesByAttributes: {}, + nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + const rendered = mountWithIntl(component); + noRollover(rendered); + setPolicyName(rendered, 'mypolicy'); + await activatePhase(rendered, 'cold'); + expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(findTestSubject(rendered, 'cloudDataTierCallout').exists()).toBeTruthy(); + // Assert that other notices are not showing + expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeFalsy(); + expect(findTestSubject(rendered, 'noNodeAttributesWarning').exists()).toBeFalsy(); + }); + }); + }); }); diff --git a/x-pack/plugins/index_lifecycle_management/common/types/api.ts b/x-pack/plugins/index_lifecycle_management/common/types/api.ts index fcdbdf2c9cc9..ccdd7fcb1177 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/api.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/api.ts @@ -9,4 +9,12 @@ import { NodeDataRoleWithCatchAll } from '.'; export interface ListNodesRouteResponse { nodesByAttributes: { [attributePair: string]: string[] }; nodesByRoles: { [role in NodeDataRoleWithCatchAll]?: string[] }; + + /** + * A flag to indicate whether a node is using `settings.node.data` which is the now deprecated way cloud configured + * nodes to have data (and other) roles. + * + * If this is true, it means the cluster is using legacy cloud configuration for data allocation, not node roles. + */ + isUsingDeprecatedDataRoleConfig: boolean; } diff --git a/x-pack/plugins/index_lifecycle_management/kibana.json b/x-pack/plugins/index_lifecycle_management/kibana.json index 479d651fc669..1b0a73c6a013 100644 --- a/x-pack/plugins/index_lifecycle_management/kibana.json +++ b/x-pack/plugins/index_lifecycle_management/kibana.json @@ -9,6 +9,7 @@ "features" ], "optionalPlugins": [ + "cloud", "usageCollection", "indexManagement", "home" diff --git a/x-pack/plugins/index_lifecycle_management/public/application/index.tsx b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx index d7812f186a03..7a7fd20e96c6 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/index.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx @@ -8,6 +8,9 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nStart, ScopedHistory, ApplicationStart } from 'kibana/public'; import { UnmountCallback } from 'src/core/public'; +import { CloudSetup } from '../../../cloud/public'; + +import { KibanaContextProvider } from '../shared_imports'; import { App } from './app'; @@ -16,11 +19,14 @@ export const renderApp = ( I18nContext: I18nStart['Context'], history: ScopedHistory, navigateToApp: ApplicationStart['navigateToApp'], - getUrlForApp: ApplicationStart['getUrlForApp'] + getUrlForApp: ApplicationStart['getUrlForApp'], + cloud?: CloudSetup ): UnmountCallback => { render( - + + + , element ); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/cloud_data_tier_callout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/cloud_data_tier_callout.tsx new file mode 100644 index 000000000000..2dff55ac10de --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/cloud_data_tier_callout.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { EuiCallOut } from '@elastic/eui'; + +const i18nTexts = { + title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.title', { + defaultMessage: 'Create a cold tier', + }), + body: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.body', { + defaultMessage: 'Edit your Elastic Cloud deployment to set up a cold tier.', + }), +}; + +export const CloudDataTierCallout: FunctionComponent = () => { + return ( + + {i18nTexts.body} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.tsx index 4ec488f95c94..f58f36fc45a0 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/data_tier_allocation.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FunctionComponent } from 'react'; +import React, { FunctionComponent, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiText, EuiFormRow, EuiSpacer, EuiSuperSelect, EuiSuperSelectOption } from '@elastic/eui'; @@ -90,7 +90,25 @@ const i18nTexts = { }; export const DataTierAllocation: FunctionComponent = (props) => { - const { phaseData, setPhaseData, phase, hasNodeAttributes } = props; + const { phaseData, setPhaseData, phase, hasNodeAttributes, disableDataTierOption } = props; + + useEffect(() => { + if (disableDataTierOption && phaseData.dataTierAllocationType === 'default') { + /** + * @TODO + * This is a slight hack because we only know we should disable the "default" option further + * down the component tree (i.e., after the policy has been deserialized). + * + * We reset the value to "custom" if we deserialized to "default". + * + * It would be better if we had all the information we needed before deserializing and + * were able to handle this at the deserialization step instead of patching further down + * the component tree - this should be a future refactor. + */ + setPhaseData('dataTierAllocationType', 'custom'); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return (
@@ -102,21 +120,40 @@ export const DataTierAllocation: FunctionComponent = (props) => { onChange={(value) => setPhaseData('dataTierAllocationType', value)} options={ [ + disableDataTierOption + ? undefined + : { + 'data-test-subj': 'defaultDataAllocationOption', + value: 'default', + inputDisplay: i18nTexts.allocationOptions[phase].default.input, + dropdownDisplay: ( + <> + {i18nTexts.allocationOptions[phase].default.input} + +

+ {i18nTexts.allocationOptions[phase].default.helpText} +

+
+ + ), + }, { - value: 'default', - inputDisplay: i18nTexts.allocationOptions[phase].default.input, + 'data-test-subj': 'customDataAllocationOption', + value: 'custom', + inputDisplay: i18nTexts.allocationOptions[phase].custom.inputDisplay, dropdownDisplay: ( <> - {i18nTexts.allocationOptions[phase].default.input} + {i18nTexts.allocationOptions[phase].custom.inputDisplay}

- {i18nTexts.allocationOptions[phase].default.helpText} + {i18nTexts.allocationOptions[phase].custom.helpText}

), }, { + 'data-test-subj': 'noneDataAllocationOption', value: 'none', inputDisplay: i18nTexts.allocationOptions[phase].none.inputDisplay, dropdownDisplay: ( @@ -130,22 +167,7 @@ export const DataTierAllocation: FunctionComponent = (props) => { ), }, - { - 'data-test-subj': 'customDataAllocationOption', - value: 'custom', - inputDisplay: i18nTexts.allocationOptions[phase].custom.inputDisplay, - dropdownDisplay: ( - <> - {i18nTexts.allocationOptions[phase].custom.inputDisplay} - -

- {i18nTexts.allocationOptions[phase].custom.helpText} -

-
- - ), - }, - ] as SelectOptions[] + ].filter(Boolean) as SelectOptions[] } /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/default_allocation_notice.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/default_allocation_notice.tsx index 8faa9bb2972c..42f9e8494a0b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/default_allocation_notice.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/default_allocation_notice.tsx @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import React, { FunctionComponent } from 'react'; -import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { EuiCallOut } from '@elastic/eui'; import { PhaseWithAllocation, NodeDataRole } from '../../../../../../common/types'; @@ -102,10 +102,5 @@ export const DefaultAllocationNotice: FunctionComponent = ({ phase, targe ); - return ( - <> - - {content} - - ); + return content; }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/index.ts index dcbdf960fd38..937e3dd28da9 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/index.ts @@ -10,3 +10,4 @@ export { NodeAttrsDetails } from './node_attrs_details'; export { DataTierAllocation } from './data_tier_allocation'; export { DefaultAllocationNotice } from './default_allocation_notice'; export { NoNodeAttributesWarning } from './no_node_attributes_warning'; +export { CloudDataTierCallout } from './cloud_data_tier_callout'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/no_node_attributes_warning.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/no_node_attributes_warning.tsx index ceccc51f95c1..69185277f64c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/no_node_attributes_warning.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/no_node_attributes_warning.tsx @@ -5,7 +5,7 @@ */ import React, { FunctionComponent } from 'react'; -import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { PhaseWithAllocation } from '../../../../../../common/types'; @@ -38,16 +38,13 @@ export const NoNodeAttributesWarning: FunctionComponent<{ phase: PhaseWithAlloca phase, }) => { return ( - <> - - - {i18nTexts[phase].body} - - + + {i18nTexts[phase].body} + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/types.ts index d4cb31a3be9e..d3dd536d97df 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/data_tier_allocation/types.ts @@ -19,4 +19,10 @@ export interface SharedProps { isShowingErrors: boolean; nodes: ListNodesRouteResponse['nodesByAttributes']; hasNodeAttributes: boolean; + /** + * When on Cloud we want to disable the data tier allocation option when we detect that we are not + * using node roles in our Node config yet. See {@link ListNodesRouteResponse} for information about how this is + * detected. + */ + disableDataTierOption: boolean; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/data_tier_allocation_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/data_tier_allocation_field.tsx index 623d443a1db0..b3772a6e3ebd 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/data_tier_allocation_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/shared/data_tier_allocation_field.tsx @@ -6,8 +6,9 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; +import { EuiDescribedFormGroup, EuiFormRow, EuiSpacer } from '@elastic/eui'; +import { useKibana } from '../../../../../shared_imports'; import { PhaseWithAllocationAction, PhaseWithAllocation } from '../../../../../../common/types'; import { PhaseValidationErrors } from '../../../../services/policies/policy_validation'; import { getAvailableNodeRoleForPhase } from '../../../../lib/data_tiers'; @@ -18,6 +19,7 @@ import { DefaultAllocationNotice, NoNodeAttributesWarning, NodesDataProvider, + CloudDataTierCallout, } from '../../components/data_tier_allocation'; const i18nTexts = { @@ -46,35 +48,61 @@ export const DataTierAllocationField: FunctionComponent = ({ isShowingErrors, errors, }) => { + const { + services: { cloud }, + } = useKibana(); + return ( - {(nodesData) => { - const hasNodeAttrs = Boolean(Object.keys(nodesData.nodesByAttributes ?? {}).length); - - const renderDefaultAllocationNotice = () => { - if (phaseData.dataTierAllocationType !== 'default') { - return null; - } + {({ nodesByRoles, nodesByAttributes, isUsingDeprecatedDataRoleConfig }) => { + const hasNodeAttrs = Boolean(Object.keys(nodesByAttributes ?? {}).length); - const allocationNodeRole = getAvailableNodeRoleForPhase(phase, nodesData.nodesByRoles); - if ( - allocationNodeRole !== 'none' && - isNodeRoleFirstPreference(phase, allocationNodeRole) - ) { - return null; - } + const renderNotice = () => { + switch (phaseData.dataTierAllocationType) { + case 'default': + const isCloudEnabled = cloud?.isCloudEnabled ?? false; + const isUsingNodeRoles = !isUsingDeprecatedDataRoleConfig; + if ( + isCloudEnabled && + isUsingNodeRoles && + phase === 'cold' && + !nodesByRoles.data_cold?.length + ) { + // Tell cloud users they can deploy cold tier nodes. + return ( + <> + + + + ); + } - return ; - }; - - const renderNodeAttributesWarning = () => { - if (phaseData.dataTierAllocationType !== 'custom') { - return null; - } - if (hasNodeAttrs) { - return null; + const allocationNodeRole = getAvailableNodeRoleForPhase(phase, nodesByRoles); + if ( + allocationNodeRole === 'none' || + !isNodeRoleFirstPreference(phase, allocationNodeRole) + ) { + return ( + <> + + + + ); + } + break; + case 'custom': + if (!hasNodeAttrs) { + return ( + <> + + + + ); + } + break; + default: + return null; } - return ; }; return ( @@ -92,12 +120,14 @@ export const DataTierAllocationField: FunctionComponent = ({ setPhaseData={setPhaseData} phaseData={phaseData} isShowingErrors={isShowingErrors} - nodes={nodesData.nodesByAttributes} + nodes={nodesByAttributes} + disableDataTierOption={ + !!(isUsingDeprecatedDataRoleConfig && cloud?.isCloudEnabled) + } /> - {/* Data tier related warnings */} - {renderDefaultAllocationNotice()} - {renderNodeAttributesWarning()} + {/* Data tier related warnings and call-to-action notices */} + {renderNotice()} diff --git a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx index 645a78bfc99b..24ce036c0e05 100644 --- a/x-pack/plugins/index_lifecycle_management/public/plugin.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/plugin.tsx @@ -31,7 +31,7 @@ export class IndexLifecycleManagementPlugin { getStartServices, } = coreSetup; - const { usageCollection, management, indexManagement, home } = plugins; + const { usageCollection, management, indexManagement, home, cloud } = plugins; // Initialize services even if the app isn't mounted, because they're used by index management extensions. initHttp(http); @@ -65,7 +65,8 @@ export class IndexLifecycleManagementPlugin { I18nContext, history, navigateToApp, - getUrlForApp + getUrlForApp, + cloud ); return () => { diff --git a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts new file mode 100644 index 000000000000..d479b821ceef --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AppServicesContext } from './types'; +import { useKibana as _useKibana } from '../../../../src/plugins/kibana_react/public'; + +export { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public'; +export const useKibana = () => _useKibana(); diff --git a/x-pack/plugins/index_lifecycle_management/public/types.ts b/x-pack/plugins/index_lifecycle_management/public/types.ts index 65db00f1e68c..c9b9b063cd45 100644 --- a/x-pack/plugins/index_lifecycle_management/public/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/types.ts @@ -8,10 +8,12 @@ import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; import { ManagementSetup } from '../../../../src/plugins/management/public'; import { IndexManagementPluginSetup } from '../../index_management/public'; +import { CloudSetup } from '../../cloud/public'; export interface PluginsDependencies { usageCollection?: UsageCollectionSetup; management: ManagementSetup; + cloud?: CloudSetup; indexManagement?: IndexManagementPluginSetup; home?: HomePublicPluginSetup; } @@ -21,3 +23,7 @@ export interface ClientConfigType { enabled: boolean; }; } + +export interface AppServicesContext { + cloud?: CloudSetup; +} diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/__jest__/fixtures.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/__jest__/fixtures.ts new file mode 100644 index 000000000000..e547c3f66243 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/__jest__/fixtures.ts @@ -0,0 +1,2295 @@ +/* + * 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 */ + +/** + * The fixtures below are from the "_nodes/settings" endpoint on a 7.9.2 Cloud-created cluster. + */ + +export const cloudNodeSettingsWithLegacy = { + _nodes: { + successful: 5, + failed: 0, + total: 5, + }, + cluster_name: '6ee9547c30214d278d2a63c4de98dea5', + nodes: { + t49k7mdeRIiELuOt_MOZ1g: { + transport_address: '10.47.32.43:19833', + name: 'instance-0000000002', + roles: ['data', 'ingest', 'remote_cluster_client', 'transform'], + settings: { + node: { + attr: { + xpack: { + installed: 'true', + }, + server_name: 'instance-0000000002.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-c', + region: 'unknown-region', + transform: { + node: 'true', + }, + instance_configuration: 'gcp.data.highstorage.1', + logical_availability_zone: 'zone-0', + data: 'warm', + }, + ml: 'false', + ingest: 'true', + name: 'instance-0000000002', + master: 'false', + data: 'true', + pidfile: '/app/es.pid', + max_local_storage_nodes: '1', + }, + reindex: { + remote: { + whitelist: ['*.io:*', '*.com:*'], + }, + }, + http: { + compression: 'true', + type: 'security4', + max_warning_header_count: '64', + publish_port: '18120', + 'type.default': 'netty4', + max_warning_header_size: '7168b', + port: '18120', + }, + network: { + publish_host: '10.47.32.43', + bind_host: '_site:ipv4_', + }, + xpack: { + monitoring: { + collection: { + enabled: 'false', + }, + history: { + duration: '3d', + }, + }, + license: { + self_generated: { + type: 'trial', + }, + }, + ml: { + enabled: 'true', + }, + ccr: { + enabled: 'false', + }, + notification: { + email: { + account: { + work: { + email_defaults: { + from: 'Watcher Alert ', + }, + smtp: { + host: 'dockerhost', + port: '10025', + }, + }, + }, + }, + }, + security: { + authc: { + token: { + enabled: 'true', + }, + reserved_realm: { + enabled: 'false', + }, + realms: { + native: { + native: { + order: '1', + }, + }, + file: { + found: { + order: '0', + }, + }, + saml: { + 'cloud-saml-kibana-916c269173df465f9826f4471799de42': { + attributes: { + name: 'http://saml.elastic-cloud.com/attributes/name', + groups: 'http://saml.elastic-cloud.com/attributes/roles', + principal: 'http://saml.elastic-cloud.com/attributes/principal', + }, + sp: { + acs: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/api/security/saml/callback', + entity_id: 'ec:2628060457:916c269173df465f9826f4471799de42', + logout: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/logout', + }, + order: '3', + idp: { + entity_id: 'urn:idp-cloud-elastic-co', + metadata: { + path: + '/app/config/cloud-saml-metadata-916c269173df465f9826f4471799de42.xml', + }, + }, + }, + }, + }, + anonymous: { + username: 'anonymous', + authz_exception: 'false', + roles: 'anonymous', + }, + }, + enabled: 'true', + http: { + ssl: { + enabled: 'true', + }, + }, + transport: { + ssl: { + enabled: 'true', + }, + }, + }, + }, + script: { + allowed_types: 'stored,inline', + }, + cluster: { + routing: { + allocation: { + disk: { + threshold_enabled: 'true', + watermark: { + enable_for_single_data_node: 'true', + }, + }, + awareness: { + attributes: 'region,logical_availability_zone', + }, + }, + }, + name: '6ee9547c30214d278d2a63c4de98dea5', + initial_master_nodes: 'instance-0000000000,instance-0000000001,tiebreaker-0000000004', + election: { + strategy: 'supports_voting_only', + }, + indices: { + close: { + enable: 'true', + }, + }, + metadata: { + managed_policies: ['cloud-snapshot-policy'], + managed_repository: 'found-snapshots', + managed_index_templates: '.cloud-', + }, + }, + client: { + type: 'node', + }, + action: { + auto_create_index: 'true', + destructive_requires_name: 'false', + }, + path: { + home: '/usr/share/elasticsearch', + data: ['/app/data'], + logs: '/app/logs', + }, + transport: { + 'type.default': 'netty4', + type: 'security4', + tcp: { + port: '19833', + }, + profiles: { + client: { + port: '20296', + }, + }, + features: { + 'x-pack': 'true', + }, + }, + discovery: { + seed_hosts: [], + seed_providers: 'file', + }, + }, + ip: '10.47.32.43', + host: '10.47.32.43', + version: '7.9.2', + build_flavor: 'default', + build_hash: 'd34da0ea4a966c4e49417f2da2f244e3e97b4e6e', + attributes: { + server_name: 'instance-0000000002.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-c', + 'transform.node': 'true', + region: 'unknown-region', + instance_configuration: 'gcp.data.highstorage.1', + 'xpack.installed': 'true', + logical_availability_zone: 'zone-0', + data: 'warm', + }, + build_type: 'docker', + }, + 'SgaCpsXAQu-oTsP4iLGZWw': { + transport_address: '10.47.32.33:19227', + name: 'tiebreaker-0000000004', + roles: ['master', 'remote_cluster_client', 'voting_only'], + settings: { + node: { + attr: { + xpack: { + installed: 'true', + }, + server_name: 'tiebreaker-0000000004.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-b', + region: 'unknown-region', + transform: { + node: 'false', + }, + instance_configuration: 'gcp.master.1', + logical_availability_zone: 'tiebreaker', + data: 'hot', + }, + ml: 'false', + ingest: 'false', + name: 'tiebreaker-0000000004', + master: 'true', + voting_only: 'true', + data: 'false', + pidfile: '/app/es.pid', + max_local_storage_nodes: '1', + }, + reindex: { + remote: { + whitelist: ['*.io:*', '*.com:*'], + }, + }, + http: { + compression: 'true', + type: 'security4', + max_warning_header_count: '64', + publish_port: '18013', + 'type.default': 'netty4', + max_warning_header_size: '7168b', + port: '18013', + }, + network: { + publish_host: '10.47.32.33', + bind_host: '_site:ipv4_', + }, + xpack: { + monitoring: { + collection: { + enabled: 'false', + }, + history: { + duration: '3d', + }, + }, + license: { + self_generated: { + type: 'trial', + }, + }, + ml: { + enabled: 'true', + }, + ccr: { + enabled: 'false', + }, + notification: { + email: { + account: { + work: { + email_defaults: { + from: 'Watcher Alert ', + }, + smtp: { + host: 'dockerhost', + port: '10025', + }, + }, + }, + }, + }, + security: { + authc: { + token: { + enabled: 'true', + }, + reserved_realm: { + enabled: 'false', + }, + realms: { + native: { + native: { + order: '1', + }, + }, + file: { + found: { + order: '0', + }, + }, + saml: { + 'cloud-saml-kibana-916c269173df465f9826f4471799de42': { + attributes: { + name: 'http://saml.elastic-cloud.com/attributes/name', + groups: 'http://saml.elastic-cloud.com/attributes/roles', + principal: 'http://saml.elastic-cloud.com/attributes/principal', + }, + sp: { + acs: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/api/security/saml/callback', + entity_id: 'ec:2628060457:916c269173df465f9826f4471799de42', + logout: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/logout', + }, + order: '3', + idp: { + entity_id: 'urn:idp-cloud-elastic-co', + metadata: { + path: + '/app/config/cloud-saml-metadata-916c269173df465f9826f4471799de42.xml', + }, + }, + }, + }, + }, + anonymous: { + username: 'anonymous', + authz_exception: 'false', + roles: 'anonymous', + }, + }, + enabled: 'true', + http: { + ssl: { + enabled: 'true', + }, + }, + transport: { + ssl: { + enabled: 'true', + }, + }, + }, + }, + script: { + allowed_types: 'stored,inline', + }, + cluster: { + routing: { + allocation: { + disk: { + threshold_enabled: 'true', + watermark: { + enable_for_single_data_node: 'true', + }, + }, + awareness: { + attributes: 'region,logical_availability_zone', + }, + }, + }, + name: '6ee9547c30214d278d2a63c4de98dea5', + initial_master_nodes: 'instance-0000000000,instance-0000000001,tiebreaker-0000000004', + election: { + strategy: 'supports_voting_only', + }, + indices: { + close: { + enable: 'true', + }, + }, + metadata: { + managed_policies: ['cloud-snapshot-policy'], + managed_repository: 'found-snapshots', + managed_index_templates: '.cloud-', + }, + }, + client: { + type: 'node', + }, + action: { + auto_create_index: 'true', + destructive_requires_name: 'false', + }, + path: { + home: '/usr/share/elasticsearch', + data: ['/app/data'], + logs: '/app/logs', + }, + transport: { + 'type.default': 'netty4', + type: 'security4', + tcp: { + port: '19227', + }, + profiles: { + client: { + port: '20281', + }, + }, + features: { + 'x-pack': 'true', + }, + }, + discovery: { + seed_hosts: [], + seed_providers: 'file', + }, + }, + ip: '10.47.32.33', + host: '10.47.32.33', + version: '7.9.2', + build_flavor: 'default', + build_hash: 'd34da0ea4a966c4e49417f2da2f244e3e97b4e6e', + attributes: { + server_name: 'tiebreaker-0000000004.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-b', + 'transform.node': 'false', + region: 'unknown-region', + instance_configuration: 'gcp.master.1', + 'xpack.installed': 'true', + logical_availability_zone: 'tiebreaker', + data: 'hot', + }, + build_type: 'docker', + }, + 'ZVndRfrfSl-kmEyZgJu0JQ': { + transport_address: '10.47.47.205:19570', + name: 'instance-0000000001', + roles: ['data', 'ingest', 'master', 'remote_cluster_client', 'transform'], + settings: { + node: { + attr: { + xpack: { + installed: 'true', + }, + server_name: 'instance-0000000001.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-a', + region: 'unknown-region', + transform: { + node: 'true', + }, + instance_configuration: 'gcp.data.highio.1', + logical_availability_zone: 'zone-1', + data: 'hot', + }, + ml: 'false', + ingest: 'true', + name: 'instance-0000000001', + master: 'true', + data: 'true', + pidfile: '/app/es.pid', + max_local_storage_nodes: '1', + }, + reindex: { + remote: { + whitelist: ['*.io:*', '*.com:*'], + }, + }, + http: { + compression: 'true', + type: 'security4', + max_warning_header_count: '64', + publish_port: '18760', + 'type.default': 'netty4', + max_warning_header_size: '7168b', + port: '18760', + }, + network: { + publish_host: '10.47.47.205', + bind_host: '_site:ipv4_', + }, + xpack: { + monitoring: { + collection: { + enabled: 'false', + }, + history: { + duration: '3d', + }, + }, + license: { + self_generated: { + type: 'trial', + }, + }, + ml: { + enabled: 'true', + }, + ccr: { + enabled: 'false', + }, + notification: { + email: { + account: { + work: { + email_defaults: { + from: 'Watcher Alert ', + }, + smtp: { + host: 'dockerhost', + port: '10025', + }, + }, + }, + }, + }, + security: { + authc: { + token: { + enabled: 'true', + }, + reserved_realm: { + enabled: 'false', + }, + realms: { + native: { + native: { + order: '1', + }, + }, + file: { + found: { + order: '0', + }, + }, + saml: { + 'cloud-saml-kibana-916c269173df465f9826f4471799de42': { + attributes: { + name: 'http://saml.elastic-cloud.com/attributes/name', + groups: 'http://saml.elastic-cloud.com/attributes/roles', + principal: 'http://saml.elastic-cloud.com/attributes/principal', + }, + sp: { + acs: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/api/security/saml/callback', + entity_id: 'ec:2628060457:916c269173df465f9826f4471799de42', + logout: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/logout', + }, + order: '3', + idp: { + entity_id: 'urn:idp-cloud-elastic-co', + metadata: { + path: + '/app/config/cloud-saml-metadata-916c269173df465f9826f4471799de42.xml', + }, + }, + }, + }, + }, + anonymous: { + username: 'anonymous', + authz_exception: 'false', + roles: 'anonymous', + }, + }, + enabled: 'true', + http: { + ssl: { + enabled: 'true', + }, + }, + transport: { + ssl: { + enabled: 'true', + }, + }, + }, + }, + script: { + allowed_types: 'stored,inline', + }, + cluster: { + routing: { + allocation: { + disk: { + threshold_enabled: 'true', + watermark: { + enable_for_single_data_node: 'true', + }, + }, + awareness: { + attributes: 'region,logical_availability_zone', + }, + }, + }, + name: '6ee9547c30214d278d2a63c4de98dea5', + initial_master_nodes: 'instance-0000000000,instance-0000000001,tiebreaker-0000000004', + election: { + strategy: 'supports_voting_only', + }, + indices: { + close: { + enable: 'true', + }, + }, + metadata: { + managed_policies: ['cloud-snapshot-policy'], + managed_repository: 'found-snapshots', + managed_index_templates: '.cloud-', + }, + }, + client: { + type: 'node', + }, + action: { + auto_create_index: 'true', + destructive_requires_name: 'false', + }, + path: { + home: '/usr/share/elasticsearch', + data: ['/app/data'], + logs: '/app/logs', + }, + transport: { + 'type.default': 'netty4', + type: 'security4', + tcp: { + port: '19570', + }, + profiles: { + client: { + port: '20803', + }, + }, + features: { + 'x-pack': 'true', + }, + }, + discovery: { + seed_hosts: [], + seed_providers: 'file', + }, + }, + ip: '10.47.47.205', + host: '10.47.47.205', + version: '7.9.2', + build_flavor: 'default', + build_hash: 'd34da0ea4a966c4e49417f2da2f244e3e97b4e6e', + attributes: { + server_name: 'instance-0000000001.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-a', + 'transform.node': 'true', + region: 'unknown-region', + instance_configuration: 'gcp.data.highio.1', + 'xpack.installed': 'true', + logical_availability_zone: 'zone-1', + data: 'hot', + }, + build_type: 'docker', + }, + Tx8Xig60SIuitXhY0srD6Q: { + transport_address: '10.47.32.41:19901', + name: 'instance-0000000003', + roles: ['data', 'ingest', 'remote_cluster_client', 'transform'], + settings: { + node: { + attr: { + xpack: { + installed: 'true', + }, + server_name: 'instance-0000000003.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-a', + region: 'unknown-region', + transform: { + node: 'true', + }, + instance_configuration: 'gcp.data.highstorage.1', + logical_availability_zone: 'zone-1', + data: 'warm', + }, + ml: 'false', + ingest: 'true', + name: 'instance-0000000003', + master: 'false', + data: 'true', + pidfile: '/app/es.pid', + max_local_storage_nodes: '1', + }, + reindex: { + remote: { + whitelist: ['*.io:*', '*.com:*'], + }, + }, + http: { + compression: 'true', + type: 'security4', + max_warning_header_count: '64', + publish_port: '18977', + 'type.default': 'netty4', + max_warning_header_size: '7168b', + port: '18977', + }, + network: { + publish_host: '10.47.32.41', + bind_host: '_site:ipv4_', + }, + xpack: { + monitoring: { + collection: { + enabled: 'false', + }, + history: { + duration: '3d', + }, + }, + license: { + self_generated: { + type: 'trial', + }, + }, + ml: { + enabled: 'true', + }, + ccr: { + enabled: 'false', + }, + notification: { + email: { + account: { + work: { + email_defaults: { + from: 'Watcher Alert ', + }, + smtp: { + host: 'dockerhost', + port: '10025', + }, + }, + }, + }, + }, + security: { + authc: { + token: { + enabled: 'true', + }, + reserved_realm: { + enabled: 'false', + }, + realms: { + native: { + native: { + order: '1', + }, + }, + file: { + found: { + order: '0', + }, + }, + saml: { + 'cloud-saml-kibana-916c269173df465f9826f4471799de42': { + attributes: { + name: 'http://saml.elastic-cloud.com/attributes/name', + groups: 'http://saml.elastic-cloud.com/attributes/roles', + principal: 'http://saml.elastic-cloud.com/attributes/principal', + }, + sp: { + acs: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/api/security/saml/callback', + entity_id: 'ec:2628060457:916c269173df465f9826f4471799de42', + logout: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/logout', + }, + order: '3', + idp: { + entity_id: 'urn:idp-cloud-elastic-co', + metadata: { + path: + '/app/config/cloud-saml-metadata-916c269173df465f9826f4471799de42.xml', + }, + }, + }, + }, + }, + anonymous: { + username: 'anonymous', + authz_exception: 'false', + roles: 'anonymous', + }, + }, + enabled: 'true', + http: { + ssl: { + enabled: 'true', + }, + }, + transport: { + ssl: { + enabled: 'true', + }, + }, + }, + }, + script: { + allowed_types: 'stored,inline', + }, + cluster: { + routing: { + allocation: { + disk: { + threshold_enabled: 'true', + watermark: { + enable_for_single_data_node: 'true', + }, + }, + awareness: { + attributes: 'region,logical_availability_zone', + }, + }, + }, + name: '6ee9547c30214d278d2a63c4de98dea5', + initial_master_nodes: 'instance-0000000000,instance-0000000001,tiebreaker-0000000004', + election: { + strategy: 'supports_voting_only', + }, + indices: { + close: { + enable: 'true', + }, + }, + metadata: { + managed_policies: ['cloud-snapshot-policy'], + managed_repository: 'found-snapshots', + managed_index_templates: '.cloud-', + }, + }, + client: { + type: 'node', + }, + action: { + auto_create_index: 'true', + destructive_requires_name: 'false', + }, + path: { + home: '/usr/share/elasticsearch', + data: ['/app/data'], + logs: '/app/logs', + }, + transport: { + 'type.default': 'netty4', + type: 'security4', + tcp: { + port: '19901', + }, + profiles: { + client: { + port: '20466', + }, + }, + features: { + 'x-pack': 'true', + }, + }, + discovery: { + seed_hosts: [], + seed_providers: 'file', + }, + }, + ip: '10.47.32.41', + host: '10.47.32.41', + version: '7.9.2', + build_flavor: 'default', + build_hash: 'd34da0ea4a966c4e49417f2da2f244e3e97b4e6e', + attributes: { + server_name: 'instance-0000000003.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-a', + 'transform.node': 'true', + region: 'unknown-region', + instance_configuration: 'gcp.data.highstorage.1', + 'xpack.installed': 'true', + logical_availability_zone: 'zone-1', + data: 'warm', + }, + build_type: 'docker', + }, + Qtpmy7aBSIaOZisv9Q92TA: { + transport_address: '10.47.47.203:19498', + name: 'instance-0000000000', + roles: ['data', 'ingest', 'master', 'remote_cluster_client', 'transform'], + settings: { + node: { + attr: { + xpack: { + installed: 'true', + }, + server_name: 'instance-0000000000.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-c', + region: 'unknown-region', + transform: { + node: 'true', + }, + instance_configuration: 'gcp.data.highio.1', + logical_availability_zone: 'zone-0', + data: 'hot', + }, + ml: 'false', + ingest: 'true', + name: 'instance-0000000000', + master: 'true', + data: 'true', + pidfile: '/app/es.pid', + max_local_storage_nodes: '1', + }, + reindex: { + remote: { + whitelist: ['*.io:*', '*.com:*'], + }, + }, + http: { + compression: 'true', + type: 'security4', + max_warning_header_count: '64', + publish_port: '18221', + 'type.default': 'netty4', + max_warning_header_size: '7168b', + port: '18221', + }, + network: { + publish_host: '10.47.47.203', + bind_host: '_site:ipv4_', + }, + xpack: { + monitoring: { + collection: { + enabled: 'false', + }, + history: { + duration: '3d', + }, + }, + license: { + self_generated: { + type: 'trial', + }, + }, + ml: { + enabled: 'true', + }, + ccr: { + enabled: 'false', + }, + notification: { + email: { + account: { + work: { + email_defaults: { + from: 'Watcher Alert ', + }, + smtp: { + host: 'dockerhost', + port: '10025', + }, + }, + }, + }, + }, + security: { + authc: { + token: { + enabled: 'true', + }, + reserved_realm: { + enabled: 'false', + }, + realms: { + native: { + native: { + order: '1', + }, + }, + file: { + found: { + order: '0', + }, + }, + saml: { + 'cloud-saml-kibana-916c269173df465f9826f4471799de42': { + attributes: { + name: 'http://saml.elastic-cloud.com/attributes/name', + groups: 'http://saml.elastic-cloud.com/attributes/roles', + principal: 'http://saml.elastic-cloud.com/attributes/principal', + }, + sp: { + acs: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/api/security/saml/callback', + entity_id: 'ec:2628060457:916c269173df465f9826f4471799de42', + logout: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/logout', + }, + order: '3', + idp: { + entity_id: 'urn:idp-cloud-elastic-co', + metadata: { + path: + '/app/config/cloud-saml-metadata-916c269173df465f9826f4471799de42.xml', + }, + }, + }, + }, + }, + anonymous: { + username: 'anonymous', + authz_exception: 'false', + roles: 'anonymous', + }, + }, + enabled: 'true', + http: { + ssl: { + enabled: 'true', + }, + }, + transport: { + ssl: { + enabled: 'true', + }, + }, + }, + }, + script: { + allowed_types: 'stored,inline', + }, + cluster: { + routing: { + allocation: { + disk: { + threshold_enabled: 'true', + watermark: { + enable_for_single_data_node: 'true', + }, + }, + awareness: { + attributes: 'region,logical_availability_zone', + }, + }, + }, + name: '6ee9547c30214d278d2a63c4de98dea5', + initial_master_nodes: 'instance-0000000000,instance-0000000001,tiebreaker-0000000004', + election: { + strategy: 'supports_voting_only', + }, + indices: { + close: { + enable: 'true', + }, + }, + metadata: { + managed_policies: ['cloud-snapshot-policy'], + managed_repository: 'found-snapshots', + managed_index_templates: '.cloud-', + }, + }, + client: { + type: 'node', + }, + action: { + auto_create_index: 'true', + destructive_requires_name: 'false', + }, + path: { + home: '/usr/share/elasticsearch', + data: ['/app/data'], + logs: '/app/logs', + }, + transport: { + 'type.default': 'netty4', + type: 'security4', + tcp: { + port: '19498', + }, + profiles: { + client: { + port: '20535', + }, + }, + features: { + 'x-pack': 'true', + }, + }, + discovery: { + seed_hosts: [], + seed_providers: 'file', + }, + }, + ip: '10.47.47.203', + host: '10.47.47.203', + version: '7.9.2', + build_flavor: 'default', + build_hash: 'd34da0ea4a966c4e49417f2da2f244e3e97b4e6e', + attributes: { + server_name: 'instance-0000000000.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-c', + 'transform.node': 'true', + region: 'unknown-region', + instance_configuration: 'gcp.data.highio.1', + 'xpack.installed': 'true', + logical_availability_zone: 'zone-0', + data: 'hot', + }, + build_type: 'docker', + }, + }, +}; + +export const cloudNodeSettingsWithoutLegacy = { + _nodes: { + successful: 5, + failed: 0, + total: 5, + }, + cluster_name: '6ee9547c30214d278d2a63c4de98dea5', + nodes: { + t49k7mdeRIiELuOt_MOZ1g: { + transport_address: '10.47.32.43:19833', + name: 'instance-0000000002', + roles: ['data', 'ingest', 'remote_cluster_client', 'transform'], + settings: { + node: { + attr: { + xpack: { + installed: 'true', + }, + server_name: 'instance-0000000002.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-c', + region: 'unknown-region', + transform: { + node: 'true', + }, + instance_configuration: 'gcp.data.highstorage.1', + logical_availability_zone: 'zone-0', + data: 'warm', + }, + ml: 'false', + ingest: 'true', + name: 'instance-0000000002', + master: 'false', + data: undefined, + pidfile: '/app/es.pid', + max_local_storage_nodes: '1', + }, + reindex: { + remote: { + whitelist: ['*.io:*', '*.com:*'], + }, + }, + http: { + compression: 'true', + type: 'security4', + max_warning_header_count: '64', + publish_port: '18120', + 'type.default': 'netty4', + max_warning_header_size: '7168b', + port: '18120', + }, + network: { + publish_host: '10.47.32.43', + bind_host: '_site:ipv4_', + }, + xpack: { + monitoring: { + collection: { + enabled: 'false', + }, + history: { + duration: '3d', + }, + }, + license: { + self_generated: { + type: 'trial', + }, + }, + ml: { + enabled: 'true', + }, + ccr: { + enabled: 'false', + }, + notification: { + email: { + account: { + work: { + email_defaults: { + from: 'Watcher Alert ', + }, + smtp: { + host: 'dockerhost', + port: '10025', + }, + }, + }, + }, + }, + security: { + authc: { + token: { + enabled: 'true', + }, + reserved_realm: { + enabled: 'false', + }, + realms: { + native: { + native: { + order: '1', + }, + }, + file: { + found: { + order: '0', + }, + }, + saml: { + 'cloud-saml-kibana-916c269173df465f9826f4471799de42': { + attributes: { + name: 'http://saml.elastic-cloud.com/attributes/name', + groups: 'http://saml.elastic-cloud.com/attributes/roles', + principal: 'http://saml.elastic-cloud.com/attributes/principal', + }, + sp: { + acs: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/api/security/saml/callback', + entity_id: 'ec:2628060457:916c269173df465f9826f4471799de42', + logout: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/logout', + }, + order: '3', + idp: { + entity_id: 'urn:idp-cloud-elastic-co', + metadata: { + path: + '/app/config/cloud-saml-metadata-916c269173df465f9826f4471799de42.xml', + }, + }, + }, + }, + }, + anonymous: { + username: 'anonymous', + authz_exception: 'false', + roles: 'anonymous', + }, + }, + enabled: 'true', + http: { + ssl: { + enabled: 'true', + }, + }, + transport: { + ssl: { + enabled: 'true', + }, + }, + }, + }, + script: { + allowed_types: 'stored,inline', + }, + cluster: { + routing: { + allocation: { + disk: { + threshold_enabled: 'true', + watermark: { + enable_for_single_data_node: 'true', + }, + }, + awareness: { + attributes: 'region,logical_availability_zone', + }, + }, + }, + name: '6ee9547c30214d278d2a63c4de98dea5', + initial_master_nodes: 'instance-0000000000,instance-0000000001,tiebreaker-0000000004', + election: { + strategy: 'supports_voting_only', + }, + indices: { + close: { + enable: 'true', + }, + }, + metadata: { + managed_policies: ['cloud-snapshot-policy'], + managed_repository: 'found-snapshots', + managed_index_templates: '.cloud-', + }, + }, + client: { + type: 'node', + }, + action: { + auto_create_index: 'true', + destructive_requires_name: 'false', + }, + path: { + home: '/usr/share/elasticsearch', + data: ['/app/data'], + logs: '/app/logs', + }, + transport: { + 'type.default': 'netty4', + type: 'security4', + tcp: { + port: '19833', + }, + profiles: { + client: { + port: '20296', + }, + }, + features: { + 'x-pack': 'true', + }, + }, + discovery: { + seed_hosts: [], + seed_providers: 'file', + }, + }, + ip: '10.47.32.43', + host: '10.47.32.43', + version: '7.9.2', + build_flavor: 'default', + build_hash: 'd34da0ea4a966c4e49417f2da2f244e3e97b4e6e', + attributes: { + server_name: 'instance-0000000002.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-c', + 'transform.node': 'true', + region: 'unknown-region', + instance_configuration: 'gcp.data.highstorage.1', + 'xpack.installed': 'true', + logical_availability_zone: 'zone-0', + data: 'warm', + }, + build_type: 'docker', + }, + 'SgaCpsXAQu-oTsP4iLGZWw': { + transport_address: '10.47.32.33:19227', + name: 'tiebreaker-0000000004', + roles: ['master', 'remote_cluster_client', 'voting_only'], + settings: { + node: { + attr: { + xpack: { + installed: 'true', + }, + server_name: 'tiebreaker-0000000004.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-b', + region: 'unknown-region', + transform: { + node: 'false', + }, + instance_configuration: 'gcp.master.1', + logical_availability_zone: 'tiebreaker', + data: 'hot', + }, + ml: 'false', + ingest: 'false', + name: 'tiebreaker-0000000004', + master: 'true', + voting_only: 'true', + data: 'false', + pidfile: '/app/es.pid', + max_local_storage_nodes: '1', + }, + reindex: { + remote: { + whitelist: ['*.io:*', '*.com:*'], + }, + }, + http: { + compression: 'true', + type: 'security4', + max_warning_header_count: '64', + publish_port: '18013', + 'type.default': 'netty4', + max_warning_header_size: '7168b', + port: '18013', + }, + network: { + publish_host: '10.47.32.33', + bind_host: '_site:ipv4_', + }, + xpack: { + monitoring: { + collection: { + enabled: 'false', + }, + history: { + duration: '3d', + }, + }, + license: { + self_generated: { + type: 'trial', + }, + }, + ml: { + enabled: 'true', + }, + ccr: { + enabled: 'false', + }, + notification: { + email: { + account: { + work: { + email_defaults: { + from: 'Watcher Alert ', + }, + smtp: { + host: 'dockerhost', + port: '10025', + }, + }, + }, + }, + }, + security: { + authc: { + token: { + enabled: 'true', + }, + reserved_realm: { + enabled: 'false', + }, + realms: { + native: { + native: { + order: '1', + }, + }, + file: { + found: { + order: '0', + }, + }, + saml: { + 'cloud-saml-kibana-916c269173df465f9826f4471799de42': { + attributes: { + name: 'http://saml.elastic-cloud.com/attributes/name', + groups: 'http://saml.elastic-cloud.com/attributes/roles', + principal: 'http://saml.elastic-cloud.com/attributes/principal', + }, + sp: { + acs: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/api/security/saml/callback', + entity_id: 'ec:2628060457:916c269173df465f9826f4471799de42', + logout: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/logout', + }, + order: '3', + idp: { + entity_id: 'urn:idp-cloud-elastic-co', + metadata: { + path: + '/app/config/cloud-saml-metadata-916c269173df465f9826f4471799de42.xml', + }, + }, + }, + }, + }, + anonymous: { + username: 'anonymous', + authz_exception: 'false', + roles: 'anonymous', + }, + }, + enabled: 'true', + http: { + ssl: { + enabled: 'true', + }, + }, + transport: { + ssl: { + enabled: 'true', + }, + }, + }, + }, + script: { + allowed_types: 'stored,inline', + }, + cluster: { + routing: { + allocation: { + disk: { + threshold_enabled: 'true', + watermark: { + enable_for_single_data_node: 'true', + }, + }, + awareness: { + attributes: 'region,logical_availability_zone', + }, + }, + }, + name: '6ee9547c30214d278d2a63c4de98dea5', + initial_master_nodes: 'instance-0000000000,instance-0000000001,tiebreaker-0000000004', + election: { + strategy: 'supports_voting_only', + }, + indices: { + close: { + enable: 'true', + }, + }, + metadata: { + managed_policies: ['cloud-snapshot-policy'], + managed_repository: 'found-snapshots', + managed_index_templates: '.cloud-', + }, + }, + client: { + type: 'node', + }, + action: { + auto_create_index: 'true', + destructive_requires_name: 'false', + }, + path: { + home: '/usr/share/elasticsearch', + data: ['/app/data'], + logs: '/app/logs', + }, + transport: { + 'type.default': 'netty4', + type: 'security4', + tcp: { + port: '19227', + }, + profiles: { + client: { + port: '20281', + }, + }, + features: { + 'x-pack': 'true', + }, + }, + discovery: { + seed_hosts: [], + seed_providers: 'file', + }, + }, + ip: '10.47.32.33', + host: '10.47.32.33', + version: '7.9.2', + build_flavor: 'default', + build_hash: 'd34da0ea4a966c4e49417f2da2f244e3e97b4e6e', + attributes: { + server_name: 'tiebreaker-0000000004.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-b', + 'transform.node': 'false', + region: 'unknown-region', + instance_configuration: 'gcp.master.1', + 'xpack.installed': 'true', + logical_availability_zone: 'tiebreaker', + data: 'hot', + }, + build_type: 'docker', + }, + 'ZVndRfrfSl-kmEyZgJu0JQ': { + transport_address: '10.47.47.205:19570', + name: 'instance-0000000001', + roles: ['data', 'ingest', 'master', 'remote_cluster_client', 'transform'], + settings: { + node: { + attr: { + xpack: { + installed: 'true', + }, + server_name: 'instance-0000000001.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-a', + region: 'unknown-region', + transform: { + node: 'true', + }, + instance_configuration: 'gcp.data.highio.1', + logical_availability_zone: 'zone-1', + data: 'hot', + }, + ml: 'false', + ingest: 'true', + name: 'instance-0000000001', + master: 'true', + data: undefined, + pidfile: '/app/es.pid', + max_local_storage_nodes: '1', + }, + reindex: { + remote: { + whitelist: ['*.io:*', '*.com:*'], + }, + }, + http: { + compression: 'true', + type: 'security4', + max_warning_header_count: '64', + publish_port: '18760', + 'type.default': 'netty4', + max_warning_header_size: '7168b', + port: '18760', + }, + network: { + publish_host: '10.47.47.205', + bind_host: '_site:ipv4_', + }, + xpack: { + monitoring: { + collection: { + enabled: 'false', + }, + history: { + duration: '3d', + }, + }, + license: { + self_generated: { + type: 'trial', + }, + }, + ml: { + enabled: 'true', + }, + ccr: { + enabled: 'false', + }, + notification: { + email: { + account: { + work: { + email_defaults: { + from: 'Watcher Alert ', + }, + smtp: { + host: 'dockerhost', + port: '10025', + }, + }, + }, + }, + }, + security: { + authc: { + token: { + enabled: 'true', + }, + reserved_realm: { + enabled: 'false', + }, + realms: { + native: { + native: { + order: '1', + }, + }, + file: { + found: { + order: '0', + }, + }, + saml: { + 'cloud-saml-kibana-916c269173df465f9826f4471799de42': { + attributes: { + name: 'http://saml.elastic-cloud.com/attributes/name', + groups: 'http://saml.elastic-cloud.com/attributes/roles', + principal: 'http://saml.elastic-cloud.com/attributes/principal', + }, + sp: { + acs: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/api/security/saml/callback', + entity_id: 'ec:2628060457:916c269173df465f9826f4471799de42', + logout: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/logout', + }, + order: '3', + idp: { + entity_id: 'urn:idp-cloud-elastic-co', + metadata: { + path: + '/app/config/cloud-saml-metadata-916c269173df465f9826f4471799de42.xml', + }, + }, + }, + }, + }, + anonymous: { + username: 'anonymous', + authz_exception: 'false', + roles: 'anonymous', + }, + }, + enabled: 'true', + http: { + ssl: { + enabled: 'true', + }, + }, + transport: { + ssl: { + enabled: 'true', + }, + }, + }, + }, + script: { + allowed_types: 'stored,inline', + }, + cluster: { + routing: { + allocation: { + disk: { + threshold_enabled: 'true', + watermark: { + enable_for_single_data_node: 'true', + }, + }, + awareness: { + attributes: 'region,logical_availability_zone', + }, + }, + }, + name: '6ee9547c30214d278d2a63c4de98dea5', + initial_master_nodes: 'instance-0000000000,instance-0000000001,tiebreaker-0000000004', + election: { + strategy: 'supports_voting_only', + }, + indices: { + close: { + enable: 'true', + }, + }, + metadata: { + managed_policies: ['cloud-snapshot-policy'], + managed_repository: 'found-snapshots', + managed_index_templates: '.cloud-', + }, + }, + client: { + type: 'node', + }, + action: { + auto_create_index: 'true', + destructive_requires_name: 'false', + }, + path: { + home: '/usr/share/elasticsearch', + data: ['/app/data'], + logs: '/app/logs', + }, + transport: { + 'type.default': 'netty4', + type: 'security4', + tcp: { + port: '19570', + }, + profiles: { + client: { + port: '20803', + }, + }, + features: { + 'x-pack': 'true', + }, + }, + discovery: { + seed_hosts: [], + seed_providers: 'file', + }, + }, + ip: '10.47.47.205', + host: '10.47.47.205', + version: '7.9.2', + build_flavor: 'default', + build_hash: 'd34da0ea4a966c4e49417f2da2f244e3e97b4e6e', + attributes: { + server_name: 'instance-0000000001.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-a', + 'transform.node': 'true', + region: 'unknown-region', + instance_configuration: 'gcp.data.highio.1', + 'xpack.installed': 'true', + logical_availability_zone: 'zone-1', + data: 'hot', + }, + build_type: 'docker', + }, + Tx8Xig60SIuitXhY0srD6Q: { + transport_address: '10.47.32.41:19901', + name: 'instance-0000000003', + roles: ['data', 'ingest', 'remote_cluster_client', 'transform'], + settings: { + node: { + attr: { + xpack: { + installed: 'true', + }, + server_name: 'instance-0000000003.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-a', + region: 'unknown-region', + transform: { + node: 'true', + }, + instance_configuration: 'gcp.data.highstorage.1', + logical_availability_zone: 'zone-1', + data: 'warm', + }, + ml: 'false', + ingest: 'true', + name: 'instance-0000000003', + master: 'false', + data: undefined, + pidfile: '/app/es.pid', + max_local_storage_nodes: '1', + }, + reindex: { + remote: { + whitelist: ['*.io:*', '*.com:*'], + }, + }, + http: { + compression: 'true', + type: 'security4', + max_warning_header_count: '64', + publish_port: '18977', + 'type.default': 'netty4', + max_warning_header_size: '7168b', + port: '18977', + }, + network: { + publish_host: '10.47.32.41', + bind_host: '_site:ipv4_', + }, + xpack: { + monitoring: { + collection: { + enabled: 'false', + }, + history: { + duration: '3d', + }, + }, + license: { + self_generated: { + type: 'trial', + }, + }, + ml: { + enabled: 'true', + }, + ccr: { + enabled: 'false', + }, + notification: { + email: { + account: { + work: { + email_defaults: { + from: 'Watcher Alert ', + }, + smtp: { + host: 'dockerhost', + port: '10025', + }, + }, + }, + }, + }, + security: { + authc: { + token: { + enabled: 'true', + }, + reserved_realm: { + enabled: 'false', + }, + realms: { + native: { + native: { + order: '1', + }, + }, + file: { + found: { + order: '0', + }, + }, + saml: { + 'cloud-saml-kibana-916c269173df465f9826f4471799de42': { + attributes: { + name: 'http://saml.elastic-cloud.com/attributes/name', + groups: 'http://saml.elastic-cloud.com/attributes/roles', + principal: 'http://saml.elastic-cloud.com/attributes/principal', + }, + sp: { + acs: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/api/security/saml/callback', + entity_id: 'ec:2628060457:916c269173df465f9826f4471799de42', + logout: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/logout', + }, + order: '3', + idp: { + entity_id: 'urn:idp-cloud-elastic-co', + metadata: { + path: + '/app/config/cloud-saml-metadata-916c269173df465f9826f4471799de42.xml', + }, + }, + }, + }, + }, + anonymous: { + username: 'anonymous', + authz_exception: 'false', + roles: 'anonymous', + }, + }, + enabled: 'true', + http: { + ssl: { + enabled: 'true', + }, + }, + transport: { + ssl: { + enabled: 'true', + }, + }, + }, + }, + script: { + allowed_types: 'stored,inline', + }, + cluster: { + routing: { + allocation: { + disk: { + threshold_enabled: 'true', + watermark: { + enable_for_single_data_node: 'true', + }, + }, + awareness: { + attributes: 'region,logical_availability_zone', + }, + }, + }, + name: '6ee9547c30214d278d2a63c4de98dea5', + initial_master_nodes: 'instance-0000000000,instance-0000000001,tiebreaker-0000000004', + election: { + strategy: 'supports_voting_only', + }, + indices: { + close: { + enable: 'true', + }, + }, + metadata: { + managed_policies: ['cloud-snapshot-policy'], + managed_repository: 'found-snapshots', + managed_index_templates: '.cloud-', + }, + }, + client: { + type: 'node', + }, + action: { + auto_create_index: 'true', + destructive_requires_name: 'false', + }, + path: { + home: '/usr/share/elasticsearch', + data: ['/app/data'], + logs: '/app/logs', + }, + transport: { + 'type.default': 'netty4', + type: 'security4', + tcp: { + port: '19901', + }, + profiles: { + client: { + port: '20466', + }, + }, + features: { + 'x-pack': 'true', + }, + }, + discovery: { + seed_hosts: [], + seed_providers: 'file', + }, + }, + ip: '10.47.32.41', + host: '10.47.32.41', + version: '7.9.2', + build_flavor: 'default', + build_hash: 'd34da0ea4a966c4e49417f2da2f244e3e97b4e6e', + attributes: { + server_name: 'instance-0000000003.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-a', + 'transform.node': 'true', + region: 'unknown-region', + instance_configuration: 'gcp.data.highstorage.1', + 'xpack.installed': 'true', + logical_availability_zone: 'zone-1', + data: 'warm', + }, + build_type: 'docker', + }, + Qtpmy7aBSIaOZisv9Q92TA: { + transport_address: '10.47.47.203:19498', + name: 'instance-0000000000', + roles: ['data', 'ingest', 'master', 'remote_cluster_client', 'transform'], + settings: { + node: { + attr: { + xpack: { + installed: 'true', + }, + server_name: 'instance-0000000000.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-c', + region: 'unknown-region', + transform: { + node: 'true', + }, + instance_configuration: 'gcp.data.highio.1', + logical_availability_zone: 'zone-0', + data: 'hot', + }, + ml: 'false', + ingest: 'true', + name: 'instance-0000000000', + master: 'true', + data: undefined, + pidfile: '/app/es.pid', + max_local_storage_nodes: '1', + }, + reindex: { + remote: { + whitelist: ['*.io:*', '*.com:*'], + }, + }, + http: { + compression: 'true', + type: 'security4', + max_warning_header_count: '64', + publish_port: '18221', + 'type.default': 'netty4', + max_warning_header_size: '7168b', + port: '18221', + }, + network: { + publish_host: '10.47.47.203', + bind_host: '_site:ipv4_', + }, + xpack: { + monitoring: { + collection: { + enabled: 'false', + }, + history: { + duration: '3d', + }, + }, + license: { + self_generated: { + type: 'trial', + }, + }, + ml: { + enabled: 'true', + }, + ccr: { + enabled: 'false', + }, + notification: { + email: { + account: { + work: { + email_defaults: { + from: 'Watcher Alert ', + }, + smtp: { + host: 'dockerhost', + port: '10025', + }, + }, + }, + }, + }, + security: { + authc: { + token: { + enabled: 'true', + }, + reserved_realm: { + enabled: 'false', + }, + realms: { + native: { + native: { + order: '1', + }, + }, + file: { + found: { + order: '0', + }, + }, + saml: { + 'cloud-saml-kibana-916c269173df465f9826f4471799de42': { + attributes: { + name: 'http://saml.elastic-cloud.com/attributes/name', + groups: 'http://saml.elastic-cloud.com/attributes/roles', + principal: 'http://saml.elastic-cloud.com/attributes/principal', + }, + sp: { + acs: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/api/security/saml/callback', + entity_id: 'ec:2628060457:916c269173df465f9826f4471799de42', + logout: + 'https://916c269173df465f9826f4471799de42.europe-west4.gcp.elastic-cloud.com:9243/logout', + }, + order: '3', + idp: { + entity_id: 'urn:idp-cloud-elastic-co', + metadata: { + path: + '/app/config/cloud-saml-metadata-916c269173df465f9826f4471799de42.xml', + }, + }, + }, + }, + }, + anonymous: { + username: 'anonymous', + authz_exception: 'false', + roles: 'anonymous', + }, + }, + enabled: 'true', + http: { + ssl: { + enabled: 'true', + }, + }, + transport: { + ssl: { + enabled: 'true', + }, + }, + }, + }, + script: { + allowed_types: 'stored,inline', + }, + cluster: { + routing: { + allocation: { + disk: { + threshold_enabled: 'true', + watermark: { + enable_for_single_data_node: 'true', + }, + }, + awareness: { + attributes: 'region,logical_availability_zone', + }, + }, + }, + name: '6ee9547c30214d278d2a63c4de98dea5', + initial_master_nodes: 'instance-0000000000,instance-0000000001,tiebreaker-0000000004', + election: { + strategy: 'supports_voting_only', + }, + indices: { + close: { + enable: 'true', + }, + }, + metadata: { + managed_policies: ['cloud-snapshot-policy'], + managed_repository: 'found-snapshots', + managed_index_templates: '.cloud-', + }, + }, + client: { + type: 'node', + }, + action: { + auto_create_index: 'true', + destructive_requires_name: 'false', + }, + path: { + home: '/usr/share/elasticsearch', + data: ['/app/data'], + logs: '/app/logs', + }, + transport: { + 'type.default': 'netty4', + type: 'security4', + tcp: { + port: '19498', + }, + profiles: { + client: { + port: '20535', + }, + }, + features: { + 'x-pack': 'true', + }, + }, + discovery: { + seed_hosts: [], + seed_providers: 'file', + }, + }, + ip: '10.47.47.203', + host: '10.47.47.203', + version: '7.9.2', + build_flavor: 'default', + build_hash: 'd34da0ea4a966c4e49417f2da2f244e3e97b4e6e', + attributes: { + server_name: 'instance-0000000000.6ee9547c30214d278d2a63c4de98dea5', + availability_zone: 'europe-west4-c', + 'transform.node': 'true', + region: 'unknown-region', + instance_configuration: 'gcp.data.highio.1', + 'xpack.installed': 'true', + logical_availability_zone: 'zone-0', + data: 'hot', + }, + build_type: 'docker', + }, + }, +}; diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/__jest__/register_list_route.test.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/__jest__/register_list_route.test.ts new file mode 100644 index 000000000000..5adb7763074f --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/__jest__/register_list_route.test.ts @@ -0,0 +1,113 @@ +/* + * 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 { convertSettingsIntoLists } from '../register_list_route'; +import { cloudNodeSettingsWithLegacy, cloudNodeSettingsWithoutLegacy } from './fixtures'; + +describe('convertSettingsIntoLists', () => { + it('detects node role config', () => { + const result = convertSettingsIntoLists(cloudNodeSettingsWithoutLegacy, []); + expect(result.isUsingDeprecatedDataRoleConfig).toBe(false); + }); + + it('converts cloud settings into the expected response and detects deprecated config', () => { + const result = convertSettingsIntoLists(cloudNodeSettingsWithLegacy, []); + + expect(result.isUsingDeprecatedDataRoleConfig).toBe(true); + expect(result.nodesByRoles).toEqual({ + data: [ + 't49k7mdeRIiELuOt_MOZ1g', + 'ZVndRfrfSl-kmEyZgJu0JQ', + 'Tx8Xig60SIuitXhY0srD6Q', + 'Qtpmy7aBSIaOZisv9Q92TA', + ], + }); + expect(result.nodesByAttributes).toMatchInlineSnapshot(` + Object { + "availability_zone:europe-west4-a": Array [ + "ZVndRfrfSl-kmEyZgJu0JQ", + "Tx8Xig60SIuitXhY0srD6Q", + ], + "availability_zone:europe-west4-b": Array [ + "SgaCpsXAQu-oTsP4iLGZWw", + ], + "availability_zone:europe-west4-c": Array [ + "t49k7mdeRIiELuOt_MOZ1g", + "Qtpmy7aBSIaOZisv9Q92TA", + ], + "data:hot": Array [ + "SgaCpsXAQu-oTsP4iLGZWw", + "ZVndRfrfSl-kmEyZgJu0JQ", + "Qtpmy7aBSIaOZisv9Q92TA", + ], + "data:warm": Array [ + "t49k7mdeRIiELuOt_MOZ1g", + "Tx8Xig60SIuitXhY0srD6Q", + ], + "instance_configuration:gcp.data.highio.1": Array [ + "ZVndRfrfSl-kmEyZgJu0JQ", + "Qtpmy7aBSIaOZisv9Q92TA", + ], + "instance_configuration:gcp.data.highstorage.1": Array [ + "t49k7mdeRIiELuOt_MOZ1g", + "Tx8Xig60SIuitXhY0srD6Q", + ], + "instance_configuration:gcp.master.1": Array [ + "SgaCpsXAQu-oTsP4iLGZWw", + ], + "logical_availability_zone:tiebreaker": Array [ + "SgaCpsXAQu-oTsP4iLGZWw", + ], + "logical_availability_zone:zone-0": Array [ + "t49k7mdeRIiELuOt_MOZ1g", + "Qtpmy7aBSIaOZisv9Q92TA", + ], + "logical_availability_zone:zone-1": Array [ + "ZVndRfrfSl-kmEyZgJu0JQ", + "Tx8Xig60SIuitXhY0srD6Q", + ], + "region:unknown-region": Array [ + "t49k7mdeRIiELuOt_MOZ1g", + "SgaCpsXAQu-oTsP4iLGZWw", + "ZVndRfrfSl-kmEyZgJu0JQ", + "Tx8Xig60SIuitXhY0srD6Q", + "Qtpmy7aBSIaOZisv9Q92TA", + ], + "server_name:instance-0000000000.6ee9547c30214d278d2a63c4de98dea5": Array [ + "Qtpmy7aBSIaOZisv9Q92TA", + ], + "server_name:instance-0000000001.6ee9547c30214d278d2a63c4de98dea5": Array [ + "ZVndRfrfSl-kmEyZgJu0JQ", + ], + "server_name:instance-0000000002.6ee9547c30214d278d2a63c4de98dea5": Array [ + "t49k7mdeRIiELuOt_MOZ1g", + ], + "server_name:instance-0000000003.6ee9547c30214d278d2a63c4de98dea5": Array [ + "Tx8Xig60SIuitXhY0srD6Q", + ], + "server_name:tiebreaker-0000000004.6ee9547c30214d278d2a63c4de98dea5": Array [ + "SgaCpsXAQu-oTsP4iLGZWw", + ], + "transform.node:false": Array [ + "SgaCpsXAQu-oTsP4iLGZWw", + ], + "transform.node:true": Array [ + "t49k7mdeRIiELuOt_MOZ1g", + "ZVndRfrfSl-kmEyZgJu0JQ", + "Tx8Xig60SIuitXhY0srD6Q", + "Qtpmy7aBSIaOZisv9Q92TA", + ], + "xpack.installed:true": Array [ + "t49k7mdeRIiELuOt_MOZ1g", + "SgaCpsXAQu-oTsP4iLGZWw", + "ZVndRfrfSl-kmEyZgJu0JQ", + "Tx8Xig60SIuitXhY0srD6Q", + "Qtpmy7aBSIaOZisv9Q92TA", + ], + } + `); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts index f7f048e809d7..bb1679e695e1 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts @@ -9,22 +9,27 @@ import { ListNodesRouteResponse, NodeDataRole } from '../../../../common/types'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../../../services'; -interface Stats { +interface Settings { nodes: { [nodeId: string]: { attributes: Record; roles: string[]; + settings: { + node: { + data?: string; + }; + }; }; }; } -function convertStatsIntoList( - stats: Stats, +export function convertSettingsIntoLists( + settings: Settings, disallowedNodeAttributes: string[] ): ListNodesRouteResponse { - return Object.entries(stats.nodes).reduce( - (accum, [nodeId, nodeStats]) => { - const attributes = nodeStats.attributes || {}; + return Object.entries(settings.nodes).reduce( + (accum, [nodeId, nodeSettings]) => { + const attributes = nodeSettings.attributes || {}; for (const [key, value] of Object.entries(attributes)) { const isNodeAttributeAllowed = !disallowedNodeAttributes.includes(key); if (isNodeAttributeAllowed) { @@ -34,14 +39,26 @@ function convertStatsIntoList( } } - const dataRoles = nodeStats.roles.filter((r) => r.startsWith('data')) as NodeDataRole[]; + const dataRoles = nodeSettings.roles.filter((r) => r.startsWith('data')) as NodeDataRole[]; for (const role of dataRoles) { accum.nodesByRoles[role as NodeDataRole] = accum.nodesByRoles[role] ?? []; accum.nodesByRoles[role as NodeDataRole]!.push(nodeId); } + + // If we detect a single node using legacy "data:true" setting we know we are not using data roles for + // data allocation. + if (nodeSettings.settings?.node?.data === 'true') { + accum.isUsingDeprecatedDataRoleConfig = true; + } + return accum; }, - { nodesByAttributes: {}, nodesByRoles: {} } as ListNodesRouteResponse + { + nodesByAttributes: {}, + nodesByRoles: {}, + // Start with assumption that we are not using deprecated config + isUsingDeprecatedDataRoleConfig: false, + } as ListNodesRouteResponse ); } @@ -64,11 +81,17 @@ export function registerListRoute({ router, config, license }: RouteDependencies { path: addBasePath('/nodes/list'), validate: false }, license.guardApiRoute(async (context, request, response) => { try { - const statsResponse = await context.core.elasticsearch.client.asCurrentUser.nodes.stats< - Stats - >(); - const body: ListNodesRouteResponse = convertStatsIntoList( - statsResponse.body, + const settingsResponse = await context.core.elasticsearch.client.asCurrentUser.transport.request( + { + method: 'GET', + path: '/_nodes/settings', + querystring: { + format: 'json', + }, + } + ); + const body: ListNodesRouteResponse = convertSettingsIntoLists( + settingsResponse.body as Settings, disallowedNodeAttributes ); return response.ok({ body }); diff --git a/x-pack/plugins/infra/public/components/loading_page.tsx b/x-pack/plugins/infra/public/components/loading_page.tsx index ae8e18a2f98e..87cd1e9aebf6 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 698034f8154d..1515175b5115 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 ac2c87248ae7..022c62b6bb06 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 9cb84c7fff43..7c6e58125b48 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 712578be7dff..b9caef704d07 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 a705a0be3a39..aa6157dc48d5 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 a3b02b858385..d66fd44feba5 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 449c0a89b464..6922398e57d7 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 89b1b9b2211d..6621b110a6df 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 76756637eb69..3dbe881cd5dc 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/ingest_manager/common/openapi/README.md b/x-pack/plugins/ingest_manager/common/openapi/README.md new file mode 100644 index 000000000000..72204d483b12 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/README.md @@ -0,0 +1,14 @@ +## The `openapi` folder + +* `entrypoint.yaml` is the overview file which links to the various files on disk. +* `bundled.{yaml,json}` is the resolved output of that entry & other files in a single file. It's currently generated with: + + ``` + npx swagger-cli bundle -o bundled.json -t json entrypoint.yaml + npx swagger-cli bundle -o bundled.yaml -t yaml entrypoint.yaml + ``` +* [Paths](paths/README.md): this defines each endpoint. A path can have one operation per http method. +* [Components](components/README.md): Reusable components like [`schemas`](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject), + [`responses`](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#responseObject) + [`parameters`](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject), etc + \ No newline at end of file diff --git a/x-pack/plugins/ingest_manager/common/openapi/bundled.json b/x-pack/plugins/ingest_manager/common/openapi/bundled.json new file mode 100644 index 000000000000..1d00855de893 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/bundled.json @@ -0,0 +1,2088 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Ingest Manager", + "version": "0.2", + "contact": { + "name": "Ingest Team" + }, + "license": { + "name": "Elastic" + } + }, + "servers": [ + { + "url": "http://localhost:5601/api/fleet", + "description": "local" + } + ], + "paths": { + "/agent_policies": { + "get": { + "summary": "Agent policy - List", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/agent_policy" + } + }, + "total": { + "type": "number" + }, + "page": { + "type": "number" + }, + "perPage": { + "type": "number" + } + }, + "required": [ + "items", + "total", + "page", + "perPage" + ] + } + } + } + } + }, + "operationId": "agent-policy-list", + "parameters": [ + { + "$ref": "#/components/parameters/page_size" + }, + { + "$ref": "#/components/parameters/page_index" + }, + { + "$ref": "#/components/parameters/kuery" + } + ], + "description": "" + }, + "post": { + "summary": "Agent policy - Create", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "item": { + "$ref": "#/components/schemas/agent_policy" + } + } + } + } + } + } + }, + "operationId": "post-agent-policy", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/new_agent_policy" + } + } + } + }, + "security": [], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + } + }, + "/agent_policies/{agentPolicyId}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "agentPolicyId", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Agent policy - Info", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "item": { + "$ref": "#/components/schemas/agent_policy" + } + }, + "required": [ + "item" + ] + } + } + } + } + }, + "operationId": "agent-policy-info", + "description": "Get one agent policy", + "parameters": [] + }, + "put": { + "summary": "Agent policy - Update", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "item": { + "$ref": "#/components/schemas/agent_policy" + } + }, + "required": [ + "item" + ] + } + } + } + } + }, + "operationId": "put-agent-policy-agentPolicyId", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/new_agent_policy" + } + } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + } + }, + "/agent_policies/{agentPolicyId}/copy": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "agentPolicyId", + "in": "path", + "required": true + } + ], + "post": { + "summary": "Agent policy - copy one policy", + "operationId": "agent-policy-copy", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "item": { + "$ref": "#/components/schemas/agent_policy" + } + }, + "required": [ + "item" + ] + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + } + }, + "description": "" + }, + "description": "Copies one agent policy" + } + }, + "/agent_policies/delete": { + "post": { + "summary": "Agent policy - Delete", + "operationId": "post-agent-policy-delete", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "id", + "success" + ] + } + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agentPolicyIds": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + }, + "parameters": [] + }, + "/agent-status": { + "get": { + "summary": "Fleet - Agent - Status for policy", + "tags": [], + "responses": {}, + "operationId": "get-fleet-agent-status", + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "policyId", + "in": "query", + "required": false + } + ] + } + }, + "/agents": { + "get": { + "summary": "Fleet - Agent - List", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "type": "object" + } + }, + "total": { + "type": "number" + }, + "page": { + "type": "number" + }, + "perPage": { + "type": "number" + } + }, + "required": [ + "list", + "total", + "page", + "perPage" + ] + } + } + } + } + }, + "operationId": "get-fleet-agents", + "security": [ + { + "basicAuth": [] + } + ] + } + }, + "/agents/{agentId}/acks": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "agentId", + "in": "path", + "required": true + } + ], + "post": { + "summary": "Fleet - Agent - Acks", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "acks" + ] + } + }, + "required": [ + "action" + ] + } + } + } + } + }, + "operationId": "post-fleet-agents-agentId-acks", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + } + } + } + }, + "/agents/{agentId}/checkin": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "agentId", + "in": "path", + "required": true + } + ], + "post": { + "summary": "Fleet - Agent - Check In", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "checkin" + ] + }, + "actions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "agent_id": { + "type": "string" + }, + "data": { + "type": "object" + }, + "id": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "type": { + "type": "string" + } + }, + "required": [ + "agent_id", + "data", + "id", + "created_at", + "type" + ] + } + } + } + } + } + } + } + }, + "operationId": "post-fleet-agents-agentId-checkin", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ], + "security": [ + { + "Access API Key": [] + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "local_metadata": { + "$ref": "#/components/schemas/agent_metadata" + }, + "events": { + "type": "array", + "items": { + "$ref": "#/components/schemas/new_agent_event" + } + } + } + } + } + } + } + } + }, + "/agents/{agentId}/events": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "agentId", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Fleet - Agent - Events", + "tags": [], + "responses": {}, + "operationId": "get-fleet-agents-agentId-events" + } + }, + "/agents/{agentId}/unenroll": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "agentId", + "in": "path", + "required": true + } + ], + "post": { + "summary": "Fleet - Agent - Unenroll", + "tags": [], + "responses": {}, + "operationId": "post-fleet-agents-unenroll", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "force": { + "type": "boolean" + } + } + } + } + } + } + } + }, + "/agents/{agentId}/upgrade": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "agentId", + "in": "path", + "required": true + } + ], + "post": { + "summary": "Fleet - Agent - Upgrade", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/upgrade_agent" + } + } + } + }, + "400": { + "description": "BAD REQUEST", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/upgrade_agent" + } + } + } + } + }, + "operationId": "post-fleet-agents-upgrade", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/upgrade_agent" + } + } + } + } + } + }, + "/agents/bulk_upgrade": { + "post": { + "summary": "Fleet - Agent - Bulk Upgrade", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/bulk_upgrade_agents" + } + } + } + }, + "400": { + "description": "BAD REQUEST", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/upgrade_agent" + } + } + } + } + }, + "operationId": "post-fleet-agents-bulk-upgrade", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/bulk_upgrade_agents" + } + } + } + } + } + }, + "/agents/enroll": { + "post": { + "summary": "Fleet - Agent - Enroll", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "item": { + "$ref": "#/components/schemas/agent" + } + } + } + } + } + } + }, + "operationId": "post-fleet-agents-enroll", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "PERMANENT", + "EPHEMERAL", + "TEMPORARY" + ] + }, + "shared_id": { + "type": "string" + }, + "metadata": { + "type": "object", + "required": [ + "local", + "user_provided" + ], + "properties": { + "local": { + "$ref": "#/components/schemas/agent_metadata" + }, + "user_provided": { + "$ref": "#/components/schemas/agent_metadata" + } + } + } + }, + "required": [ + "type", + "metadata" + ] + } + } + } + }, + "security": [ + { + "Enrollment API Key": [] + } + ] + } + }, + "/agents/setup": { + "get": { + "summary": "Agents setup - Info", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "isInitialized": { + "type": "boolean" + } + }, + "required": [ + "isInitialized" + ] + } + } + } + } + }, + "operationId": "get-agents-setup", + "security": [ + { + "basicAuth": [] + } + ] + }, + "post": { + "summary": "Agents setup - Create", + "operationId": "post-agents-setup", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "isInitialized": { + "type": "boolean" + } + }, + "required": [ + "isInitialized" + ] + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "admin_username": { + "type": "string" + }, + "admin_password": { + "type": "string" + } + }, + "required": [ + "admin_username", + "admin_password" + ] + } + } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + } + }, + "/enrollment-api-keys": { + "get": { + "summary": "Enrollment - List", + "tags": [], + "responses": {}, + "operationId": "get-fleet-enrollment-api-keys", + "parameters": [] + }, + "post": { + "summary": "Enrollment - Create", + "tags": [], + "responses": {}, + "operationId": "post-fleet-enrollment-api-keys", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + } + }, + "/enrollment-api-keys/{keyId}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "keyId", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Enrollment - Info", + "tags": [], + "responses": {}, + "operationId": "get-fleet-enrollment-api-keys-keyId" + }, + "delete": { + "summary": "Enrollment - Delete", + "tags": [], + "responses": {}, + "operationId": "delete-fleet-enrollment-api-keys-keyId", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + } + }, + "/epm/categories": { + "get": { + "summary": "EPM - Categories", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "count": { + "type": "number" + } + }, + "required": [ + "id", + "title", + "count" + ] + } + } + } + } + } + }, + "operationId": "get-epm-categories" + } + }, + "/epm/packages": { + "get": { + "summary": "EPM - Packages - List", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/search_result" + } + } + } + } + } + }, + "operationId": "get-epm-list" + }, + "parameters": [] + }, + "/epm/packages/{pkgkey}": { + "get": { + "summary": "EPM - Packages - Info", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "allOf": [ + { + "properties": { + "response": { + "$ref": "#/components/schemas/package_info" + } + } + }, + { + "properties": { + "status": { + "type": "string", + "enum": [ + "installed", + "not_installed" + ] + }, + "savedObject": { + "type": "string" + } + }, + "required": [ + "status", + "savedObject" + ] + } + ] + } + } + } + } + }, + "operationId": "get-epm-package-pkgkey", + "security": [ + { + "basicAuth": [] + } + ] + }, + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "pkgkey", + "in": "path", + "required": true + } + ], + "post": { + "summary": "EPM - Packages - Install", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "response": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "id", + "type" + ] + } + } + }, + "required": [ + "response" + ] + } + } + } + } + }, + "operationId": "post-epm-install-pkgkey", + "description": "", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + }, + "delete": { + "summary": "EPM - Packages - Delete", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "response": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "id", + "type" + ] + } + } + }, + "required": [ + "response" + ] + } + } + } + } + }, + "operationId": "post-epm-delete-pkgkey", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + } + }, + "/agents/{agentId}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "agentId", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Fleet - Agent - Info", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "item": { + "type": "object" + } + }, + "required": [ + "item" + ] + } + } + } + } + }, + "operationId": "get-fleet-agents-agentId" + }, + "put": { + "summary": "Fleet - Agent - Update", + "tags": [], + "responses": {}, + "operationId": "put-fleet-agents-agentId", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + }, + "delete": { + "summary": "Fleet - Agent - Delete", + "tags": [], + "responses": {}, + "operationId": "delete-fleet-agents-agentId", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + } + }, + "/install/{osType}": { + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "osType", + "in": "path", + "required": true + } + ], + "get": { + "summary": "Fleet - Get OS install script", + "tags": [], + "responses": {}, + "operationId": "get-fleet-install-osType" + } + }, + "/package_policies": { + "get": { + "summary": "PackagePolicies - List", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/package_policy" + } + }, + "total": { + "type": "number" + }, + "page": { + "type": "number" + }, + "perPage": { + "type": "number" + } + }, + "required": [ + "items" + ] + } + } + } + } + }, + "operationId": "get-packagePolicies", + "security": [], + "parameters": [] + }, + "parameters": [], + "post": { + "summary": "PackagePolicies - Create", + "operationId": "post-packagePolicies", + "responses": { + "200": { + "description": "OK" + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/new_package_policy" + } + } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + } + }, + "/package_policies/{packagePolicyId}": { + "get": { + "summary": "PackagePolicies - Info", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "item": { + "$ref": "#/components/schemas/package_policy" + } + }, + "required": [ + "item" + ] + } + } + } + } + }, + "operationId": "get-packagePolicies-packagePolicyId" + }, + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "packagePolicyId", + "in": "path", + "required": true + } + ], + "put": { + "summary": "PackagePolicies - Update", + "operationId": "put-packagePolicies-packagePolicyId", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "item": { + "$ref": "#/components/schemas/package_policy" + }, + "sucess": { + "type": "boolean" + } + }, + "required": [ + "item", + "sucess" + ] + } + } + } + } + }, + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + } + }, + "/setup": { + "post": { + "summary": "Ingest Manager - Setup", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "isInitialized": { + "type": "boolean" + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + }, + "operationId": "post-setup", + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + } + } + }, + "components": { + "securitySchemes": { + "basicAuth": { + "type": "http", + "scheme": "basic" + }, + "Enrollment API Key": { + "name": "Authorization", + "type": "apiKey", + "in": "header", + "description": "e.g. Authorization: ApiKey base64EnrollmentApiKey" + }, + "Access API Key": { + "name": "Authorization", + "type": "apiKey", + "in": "header", + "description": "e.g. Authorization: ApiKey base64AccessApiKey" + } + }, + "parameters": { + "page_size": { + "name": "perPage", + "in": "query", + "description": "The number of items to return", + "required": false, + "schema": { + "type": "integer", + "default": 50 + } + }, + "page_index": { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 1 + } + }, + "kuery": { + "name": "kuery", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + "kbn_xsrf": { + "schema": { + "type": "string" + }, + "in": "header", + "name": "kbn-xsrf", + "required": true + } + }, + "schemas": { + "new_agent_policy": { + "title": "NewAgentPolicy", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "description": { + "type": "string" + } + } + }, + "new_package_policy": { + "title": "NewPackagePolicy", + "type": "object", + "description": "", + "properties": { + "enabled": { + "type": "boolean" + }, + "package": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "required": [ + "name", + "version", + "title" + ] + }, + "namespace": { + "type": "string" + }, + "output_id": { + "type": "string" + }, + "inputs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "processors": { + "type": "array", + "items": { + "type": "string" + } + }, + "streams": { + "type": "array", + "items": {} + }, + "config": { + "type": "object" + }, + "vars": { + "type": "object" + } + }, + "required": [ + "type", + "enabled", + "streams" + ] + } + }, + "policy_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "output_id", + "inputs", + "policy_id", + "name" + ] + }, + "package_policy": { + "title": "PackagePolicy", + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "revision": { + "type": "number" + }, + "inputs": { + "type": "array", + "items": {} + } + }, + "required": [ + "id", + "revision" + ] + }, + { + "$ref": "#/components/schemas/new_package_policy" + } + ] + }, + "agent_policy": { + "allOf": [ + { + "$ref": "#/components/schemas/new_agent_policy" + }, + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "packagePolicies": { + "oneOf": [ + { + "items": { + "type": "string" + } + }, + { + "items": { + "$ref": "#/components/schemas/package_policy" + } + } + ], + "type": "array" + }, + "updated_on": { + "type": "string", + "format": "date-time" + }, + "updated_by": { + "type": "string" + }, + "revision": { + "type": "number" + }, + "agents": { + "type": "number" + } + }, + "required": [ + "id", + "status" + ] + } + ] + }, + "agent_metadata": { + "title": "AgentMetadata", + "type": "object" + }, + "new_agent_event": { + "title": "NewAgentEvent", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "STATE", + "ERROR", + "ACTION_RESULT", + "ACTION" + ] + }, + "subtype": { + "type": "string", + "enum": [ + "RUNNING", + "STARTING", + "IN_PROGRESS", + "CONFIG", + "FAILED", + "STOPPING", + "STOPPED", + "DEGRADED", + "DATA_DUMP", + "ACKNOWLEDGED", + "UNKNOWN" + ] + }, + "timestamp": { + "type": "string" + }, + "message": { + "type": "string" + }, + "payload": { + "type": "string" + }, + "agent_id": { + "type": "string" + }, + "policy_id": { + "type": "string" + }, + "stream_id": { + "type": "string" + }, + "action_id": { + "type": "string" + } + }, + "required": [ + "type", + "subtype", + "timestamp", + "message", + "agent_id" + ] + }, + "upgrade_agent": { + "title": "UpgradeAgent", + "oneOf": [ + { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": [ + "version" + ] + }, + { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "source_uri": { + "type": "string" + } + }, + "required": [ + "version" + ] + } + ] + }, + "bulk_upgrade_agents": { + "title": "BulkUpgradeAgents", + "oneOf": [ + { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "agents": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "version", + "agents" + ] + }, + { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "source_uri": { + "type": "string" + }, + "agents": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "version", + "agents" + ] + }, + { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "source_uri": { + "type": "string" + }, + "agents": { + "type": "string" + } + }, + "required": [ + "version", + "agents" + ] + } + ] + }, + "agent_type": { + "type": "string", + "title": "AgentType", + "enum": [ + "PERMANENT", + "EPHEMERAL", + "TEMPORARY" + ] + }, + "agent_event": { + "title": "AgentEvent", + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ] + }, + { + "$ref": "#/components/schemas/new_agent_event" + } + ] + }, + "agent_status": { + "type": "string", + "title": "AgentStatus", + "enum": [ + "offline", + "error", + "online", + "inactive", + "warning" + ] + }, + "agent": { + "title": "Agent", + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/agent_type" + }, + "active": { + "type": "boolean" + }, + "enrolled_at": { + "type": "string" + }, + "unenrolled_at": { + "type": "string" + }, + "unenrollment_started_at": { + "type": "string" + }, + "shared_id": { + "type": "string" + }, + "access_api_key_id": { + "type": "string" + }, + "default_api_key_id": { + "type": "string" + }, + "policy_id": { + "type": "string" + }, + "policy_revision": { + "type": "number" + }, + "last_checkin": { + "type": "string" + }, + "user_provided_metadata": { + "$ref": "#/components/schemas/agent_metadata" + }, + "local_metadata": { + "$ref": "#/components/schemas/agent_metadata" + }, + "id": { + "type": "string" + }, + "current_error_events": { + "type": "array", + "items": { + "$ref": "#/components/schemas/agent_event" + } + }, + "access_api_key": { + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/agent_status" + }, + "default_api_key": { + "type": "string" + } + }, + "required": [ + "type", + "active", + "enrolled_at", + "id", + "current_error_events", + "status" + ] + }, + "search_result": { + "title": "SearchResult", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "download": { + "type": "string" + }, + "icons": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + }, + "version": { + "type": "string" + }, + "status": { + "type": "string" + }, + "savedObject": { + "type": "object" + } + }, + "required": [ + "description", + "download", + "icons", + "name", + "path", + "title", + "type", + "version", + "status" + ] + }, + "package_info": { + "title": "PackageInfo", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "title": { + "type": "string" + }, + "version": { + "type": "string" + }, + "readme": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string" + }, + "categories": { + "type": "array", + "items": { + "type": "string" + } + }, + "requirement": { + "oneOf": [ + { + "properties": { + "kibana": { + "type": "object", + "properties": { + "versions": { + "type": "string" + } + } + } + } + }, + { + "properties": { + "elasticsearch": { + "type": "object", + "properties": { + "versions": { + "type": "string" + } + } + } + } + } + ], + "type": "object" + }, + "screenshots": { + "type": "array", + "items": { + "type": "object", + "properties": { + "src": { + "type": "string" + }, + "path": { + "type": "string" + }, + "title": { + "type": "string" + }, + "size": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "src", + "path" + ] + } + }, + "icons": { + "type": "array", + "items": { + "type": "string" + } + }, + "assets": { + "type": "array", + "items": { + "type": "string" + } + }, + "internal": { + "type": "boolean" + }, + "format_version": { + "type": "string" + }, + "data_streams": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "name": { + "type": "string" + }, + "release": { + "type": "string" + }, + "ingeset_pipeline": { + "type": "string" + }, + "vars": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "default": { + "type": "string" + } + }, + "required": [ + "name", + "default" + ] + } + }, + "type": { + "type": "string" + }, + "package": { + "type": "string" + } + }, + "required": [ + "title", + "name", + "release", + "ingeset_pipeline", + "type", + "package" + ] + } + }, + "download": { + "type": "string" + }, + "path": { + "type": "string" + }, + "removable": { + "type": "boolean" + } + }, + "required": [ + "name", + "title", + "version", + "description", + "type", + "categories", + "requirement", + "assets", + "format_version", + "download", + "path" + ] + } + } + }, + "security": [ + { + "basicAuth": [] + } + ] +} \ No newline at end of file diff --git a/x-pack/plugins/ingest_manager/common/openapi/bundled.yaml b/x-pack/plugins/ingest_manager/common/openapi/bundled.yaml new file mode 100644 index 000000000000..9ab85ab2b823 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/bundled.yaml @@ -0,0 +1,1327 @@ +openapi: 3.0.0 +info: + title: Ingest Manager + version: '0.2' + contact: + name: Ingest Team + license: + name: Elastic +servers: + - url: 'http://localhost:5601/api/fleet' + description: local +paths: + /agent_policies: + get: + summary: Agent policy - List + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + $ref: '#/components/schemas/agent_policy' + total: + type: number + page: + type: number + perPage: + type: number + required: + - items + - total + - page + - perPage + operationId: agent-policy-list + parameters: + - $ref: '#/components/parameters/page_size' + - $ref: '#/components/parameters/page_index' + - $ref: '#/components/parameters/kuery' + description: '' + post: + summary: Agent policy - Create + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: '#/components/schemas/agent_policy' + operationId: post-agent-policy + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/new_agent_policy' + security: [] + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + '/agent_policies/{agentPolicyId}': + parameters: + - schema: + type: string + name: agentPolicyId + in: path + required: true + get: + summary: Agent policy - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: '#/components/schemas/agent_policy' + required: + - item + operationId: agent-policy-info + description: Get one agent policy + parameters: [] + put: + summary: Agent policy - Update + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: '#/components/schemas/agent_policy' + required: + - item + operationId: put-agent-policy-agentPolicyId + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/new_agent_policy' + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + '/agent_policies/{agentPolicyId}/copy': + parameters: + - schema: + type: string + name: agentPolicyId + in: path + required: true + post: + summary: Agent policy - copy one policy + operationId: agent-policy-copy + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: '#/components/schemas/agent_policy' + required: + - item + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: + type: string + required: + - name + description: '' + description: Copies one agent policy + /agent_policies/delete: + post: + summary: Agent policy - Delete + operationId: post-agent-policy-delete + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: string + success: + type: boolean + required: + - id + - success + requestBody: + content: + application/json: + schema: + type: object + properties: + agentPolicyIds: + type: array + items: + type: string + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + parameters: [] + /agent-status: + get: + summary: Fleet - Agent - Status for policy + tags: [] + responses: {} + operationId: get-fleet-agent-status + parameters: + - schema: + type: string + name: policyId + in: query + required: false + /agents: + get: + summary: Fleet - Agent - List + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + list: + type: array + items: + type: object + total: + type: number + page: + type: number + perPage: + type: number + required: + - list + - total + - page + - perPage + operationId: get-fleet-agents + security: + - basicAuth: [] + '/agents/{agentId}/acks': + parameters: + - schema: + type: string + name: agentId + in: path + required: true + post: + summary: Fleet - Agent - Acks + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + action: + type: string + enum: + - acks + required: + - action + operationId: post-fleet-agents-agentId-acks + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + requestBody: + content: + application/json: + schema: + type: object + properties: {} + '/agents/{agentId}/checkin': + parameters: + - schema: + type: string + name: agentId + in: path + required: true + post: + summary: Fleet - Agent - Check In + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + action: + type: string + enum: + - checkin + actions: + type: array + items: + type: object + properties: + agent_id: + type: string + data: + type: object + id: + type: string + created_at: + type: string + format: date-time + type: + type: string + required: + - agent_id + - data + - id + - created_at + - type + operationId: post-fleet-agents-agentId-checkin + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + security: + - Access API Key: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + local_metadata: + $ref: '#/components/schemas/agent_metadata' + events: + type: array + items: + $ref: '#/components/schemas/new_agent_event' + '/agents/{agentId}/events': + parameters: + - schema: + type: string + name: agentId + in: path + required: true + get: + summary: Fleet - Agent - Events + tags: [] + responses: {} + operationId: get-fleet-agents-agentId-events + '/agents/{agentId}/unenroll': + parameters: + - schema: + type: string + name: agentId + in: path + required: true + post: + summary: Fleet - Agent - Unenroll + tags: [] + responses: {} + operationId: post-fleet-agents-unenroll + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + requestBody: + content: + application/json: + schema: + type: object + properties: + force: + type: boolean + '/agents/{agentId}/upgrade': + parameters: + - schema: + type: string + name: agentId + in: path + required: true + post: + summary: Fleet - Agent - Upgrade + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/upgrade_agent' + '400': + description: BAD REQUEST + content: + application/json: + schema: + $ref: '#/components/schemas/upgrade_agent' + operationId: post-fleet-agents-upgrade + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/upgrade_agent' + /agents/bulk_upgrade: + post: + summary: Fleet - Agent - Bulk Upgrade + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/bulk_upgrade_agents' + '400': + description: BAD REQUEST + content: + application/json: + schema: + $ref: '#/components/schemas/upgrade_agent' + operationId: post-fleet-agents-bulk-upgrade + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/bulk_upgrade_agents' + /agents/enroll: + post: + summary: Fleet - Agent - Enroll + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + action: + type: string + item: + $ref: '#/components/schemas/agent' + operationId: post-fleet-agents-enroll + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + requestBody: + content: + application/json: + schema: + type: object + properties: + type: + type: string + enum: + - PERMANENT + - EPHEMERAL + - TEMPORARY + shared_id: + type: string + metadata: + type: object + required: + - local + - user_provided + properties: + local: + $ref: '#/components/schemas/agent_metadata' + user_provided: + $ref: '#/components/schemas/agent_metadata' + required: + - type + - metadata + security: + - Enrollment API Key: [] + /agents/setup: + get: + summary: Agents setup - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + isInitialized: + type: boolean + required: + - isInitialized + operationId: get-agents-setup + security: + - basicAuth: [] + post: + summary: Agents setup - Create + operationId: post-agents-setup + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + isInitialized: + type: boolean + required: + - isInitialized + requestBody: + content: + application/json: + schema: + type: object + properties: + admin_username: + type: string + admin_password: + type: string + required: + - admin_username + - admin_password + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + /enrollment-api-keys: + get: + summary: Enrollment - List + tags: [] + responses: {} + operationId: get-fleet-enrollment-api-keys + parameters: [] + post: + summary: Enrollment - Create + tags: [] + responses: {} + operationId: post-fleet-enrollment-api-keys + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + '/enrollment-api-keys/{keyId}': + parameters: + - schema: + type: string + name: keyId + in: path + required: true + get: + summary: Enrollment - Info + tags: [] + responses: {} + operationId: get-fleet-enrollment-api-keys-keyId + delete: + summary: Enrollment - Delete + tags: [] + responses: {} + operationId: delete-fleet-enrollment-api-keys-keyId + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + /epm/categories: + get: + summary: EPM - Categories + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: string + title: + type: string + count: + type: number + required: + - id + - title + - count + operationId: get-epm-categories + /epm/packages: + get: + summary: EPM - Packages - List + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/search_result' + operationId: get-epm-list + parameters: [] + '/epm/packages/{pkgkey}': + get: + summary: EPM - Packages - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + allOf: + - properties: + response: + $ref: '#/components/schemas/package_info' + - properties: + status: + type: string + enum: + - installed + - not_installed + savedObject: + type: string + required: + - status + - savedObject + operationId: get-epm-package-pkgkey + security: + - basicAuth: [] + parameters: + - schema: + type: string + name: pkgkey + in: path + required: true + post: + summary: EPM - Packages - Install + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + response: + type: array + items: + type: object + properties: + id: + type: string + type: + type: string + required: + - id + - type + required: + - response + operationId: post-epm-install-pkgkey + description: '' + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + delete: + summary: EPM - Packages - Delete + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + response: + type: array + items: + type: object + properties: + id: + type: string + type: + type: string + required: + - id + - type + required: + - response + operationId: post-epm-delete-pkgkey + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + '/agents/{agentId}': + parameters: + - schema: + type: string + name: agentId + in: path + required: true + get: + summary: Fleet - Agent - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + type: object + required: + - item + operationId: get-fleet-agents-agentId + put: + summary: Fleet - Agent - Update + tags: [] + responses: {} + operationId: put-fleet-agents-agentId + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + delete: + summary: Fleet - Agent - Delete + tags: [] + responses: {} + operationId: delete-fleet-agents-agentId + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + '/install/{osType}': + parameters: + - schema: + type: string + name: osType + in: path + required: true + get: + summary: Fleet - Get OS install script + tags: [] + responses: {} + operationId: get-fleet-install-osType + /package_policies: + get: + summary: PackagePolicies - List + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + $ref: '#/components/schemas/package_policy' + total: + type: number + page: + type: number + perPage: + type: number + required: + - items + operationId: get-packagePolicies + security: [] + parameters: [] + parameters: [] + post: + summary: PackagePolicies - Create + operationId: post-packagePolicies + responses: + '200': + description: OK + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/new_package_policy' + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + '/package_policies/{packagePolicyId}': + get: + summary: PackagePolicies - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: '#/components/schemas/package_policy' + required: + - item + operationId: get-packagePolicies-packagePolicyId + parameters: + - schema: + type: string + name: packagePolicyId + in: path + required: true + put: + summary: PackagePolicies - Update + operationId: put-packagePolicies-packagePolicyId + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: '#/components/schemas/package_policy' + sucess: + type: boolean + required: + - item + - sucess + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + /setup: + post: + summary: Ingest Manager - Setup + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + isInitialized: + type: boolean + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + message: + type: string + operationId: post-setup + parameters: + - $ref: '#/components/parameters/kbn_xsrf' +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + Enrollment API Key: + name: Authorization + type: apiKey + in: header + description: 'e.g. Authorization: ApiKey base64EnrollmentApiKey' + Access API Key: + name: Authorization + type: apiKey + in: header + description: 'e.g. Authorization: ApiKey base64AccessApiKey' + parameters: + page_size: + name: perPage + in: query + description: The number of items to return + required: false + schema: + type: integer + default: 50 + page_index: + name: page + in: query + required: false + schema: + type: integer + default: 1 + kuery: + name: kuery + in: query + required: false + schema: + type: string + kbn_xsrf: + schema: + type: string + in: header + name: kbn-xsrf + required: true + schemas: + new_agent_policy: + title: NewAgentPolicy + type: object + properties: + name: + type: string + namespace: + type: string + description: + type: string + new_package_policy: + title: NewPackagePolicy + type: object + description: '' + properties: + enabled: + type: boolean + package: + type: object + properties: + name: + type: string + version: + type: string + title: + type: string + required: + - name + - version + - title + namespace: + type: string + output_id: + type: string + inputs: + type: array + items: + type: object + properties: + type: + type: string + enabled: + type: boolean + processors: + type: array + items: + type: string + streams: + type: array + items: {} + config: + type: object + vars: + type: object + required: + - type + - enabled + - streams + policy_id: + type: string + name: + type: string + description: + type: string + required: + - output_id + - inputs + - policy_id + - name + package_policy: + title: PackagePolicy + allOf: + - type: object + properties: + id: + type: string + revision: + type: number + inputs: + type: array + items: {} + required: + - id + - revision + - $ref: '#/components/schemas/new_package_policy' + agent_policy: + allOf: + - $ref: '#/components/schemas/new_agent_policy' + - type: object + properties: + id: + type: string + status: + type: string + enum: + - active + - inactive + packagePolicies: + oneOf: + - items: + type: string + - items: + $ref: '#/components/schemas/package_policy' + type: array + updated_on: + type: string + format: date-time + updated_by: + type: string + revision: + type: number + agents: + type: number + required: + - id + - status + agent_metadata: + title: AgentMetadata + type: object + new_agent_event: + title: NewAgentEvent + type: object + properties: + type: + type: string + enum: + - STATE + - ERROR + - ACTION_RESULT + - ACTION + subtype: + type: string + enum: + - RUNNING + - STARTING + - IN_PROGRESS + - CONFIG + - FAILED + - STOPPING + - STOPPED + - DEGRADED + - DATA_DUMP + - ACKNOWLEDGED + - UNKNOWN + timestamp: + type: string + message: + type: string + payload: + type: string + agent_id: + type: string + policy_id: + type: string + stream_id: + type: string + action_id: + type: string + required: + - type + - subtype + - timestamp + - message + - agent_id + upgrade_agent: + title: UpgradeAgent + oneOf: + - type: object + properties: + version: + type: string + required: + - version + - type: object + properties: + version: + type: string + source_uri: + type: string + required: + - version + bulk_upgrade_agents: + title: BulkUpgradeAgents + oneOf: + - type: object + properties: + version: + type: string + agents: + type: array + items: + type: string + required: + - version + - agents + - type: object + properties: + version: + type: string + source_uri: + type: string + agents: + type: array + items: + type: string + required: + - version + - agents + - type: object + properties: + version: + type: string + source_uri: + type: string + agents: + type: string + required: + - version + - agents + agent_type: + type: string + title: AgentType + enum: + - PERMANENT + - EPHEMERAL + - TEMPORARY + agent_event: + title: AgentEvent + allOf: + - type: object + properties: + id: + type: string + required: + - id + - $ref: '#/components/schemas/new_agent_event' + agent_status: + type: string + title: AgentStatus + enum: + - offline + - error + - online + - inactive + - warning + agent: + title: Agent + type: object + properties: + type: + $ref: '#/components/schemas/agent_type' + active: + type: boolean + enrolled_at: + type: string + unenrolled_at: + type: string + unenrollment_started_at: + type: string + shared_id: + type: string + access_api_key_id: + type: string + default_api_key_id: + type: string + policy_id: + type: string + policy_revision: + type: number + last_checkin: + type: string + user_provided_metadata: + $ref: '#/components/schemas/agent_metadata' + local_metadata: + $ref: '#/components/schemas/agent_metadata' + id: + type: string + current_error_events: + type: array + items: + $ref: '#/components/schemas/agent_event' + access_api_key: + type: string + status: + $ref: '#/components/schemas/agent_status' + default_api_key: + type: string + required: + - type + - active + - enrolled_at + - id + - current_error_events + - status + search_result: + title: SearchResult + type: object + properties: + description: + type: string + download: + type: string + icons: + type: string + name: + type: string + path: + type: string + title: + type: string + type: + type: string + version: + type: string + status: + type: string + savedObject: + type: object + required: + - description + - download + - icons + - name + - path + - title + - type + - version + - status + package_info: + title: PackageInfo + type: object + properties: + name: + type: string + title: + type: string + version: + type: string + readme: + type: string + description: + type: string + type: + type: string + categories: + type: array + items: + type: string + requirement: + oneOf: + - properties: + kibana: + type: object + properties: + versions: + type: string + - properties: + elasticsearch: + type: object + properties: + versions: + type: string + type: object + screenshots: + type: array + items: + type: object + properties: + src: + type: string + path: + type: string + title: + type: string + size: + type: string + type: + type: string + required: + - src + - path + icons: + type: array + items: + type: string + assets: + type: array + items: + type: string + internal: + type: boolean + format_version: + type: string + data_streams: + type: array + items: + type: object + properties: + title: + type: string + name: + type: string + release: + type: string + ingeset_pipeline: + type: string + vars: + type: array + items: + type: object + properties: + name: + type: string + default: + type: string + required: + - name + - default + type: + type: string + package: + type: string + required: + - title + - name + - release + - ingeset_pipeline + - type + - package + download: + type: string + path: + type: string + removable: + type: boolean + required: + - name + - title + - version + - description + - type + - categories + - requirement + - assets + - format_version + - download + - path +security: + - basicAuth: [] diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/README.md b/x-pack/plugins/ingest_manager/common/openapi/components/README.md new file mode 100644 index 000000000000..1579c2d2b6eb --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/README.md @@ -0,0 +1,13 @@ +Reusable components +=========== + +* Created the following folders for the various OpenAPI component types: + - `schemas` - reusable [Schema Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject) + - `responses` - reusable [Response Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#responseObject) + - `parameters` - reusable [Parameter Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject) + - `examples` - reusable [Example Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#exampleObject) + - `headers` - reusable [Header Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#headerObject) + - `request_bodies` - reusable [Request Body Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#requestBodyObject) + - `links` - reusable [Link Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#linkObject) + - `callbacks` - reusable [Callback Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#callbackObject) + - `security_schemes` - reusable [Security Scheme Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#securitySchemeObject) diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/headers/kbn_xsrf.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/headers/kbn_xsrf.yaml new file mode 100644 index 000000000000..3d8dfae634e6 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/headers/kbn_xsrf.yaml @@ -0,0 +1,5 @@ +schema: + type: string +in: header +name: kbn-xsrf +required: true diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/parameters/kuery.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/parameters/kuery.yaml new file mode 100644 index 000000000000..b96ffd54d37c --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/parameters/kuery.yaml @@ -0,0 +1,5 @@ +name: kuery +in: query +required: false +schema: + type: string diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/parameters/page_index.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/parameters/page_index.yaml new file mode 100644 index 000000000000..908c19583045 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/parameters/page_index.yaml @@ -0,0 +1,6 @@ +name: page +in: query +required: false +schema: + type: integer + default: 1 diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/parameters/page_size.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/parameters/page_size.yaml new file mode 100644 index 000000000000..698491def3b3 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/parameters/page_size.yaml @@ -0,0 +1,7 @@ +name: perPage +in: query +description: The number of items to return +required: false +schema: + type: integer + default: 50 diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/access_api_key.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/access_api_key.yaml new file mode 100644 index 000000000000..31e2072ddefb --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/access_api_key.yaml @@ -0,0 +1,3 @@ +type: string +title: AccessApiKey +format: byte diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent.yaml new file mode 100644 index 000000000000..df106093a8d8 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent.yaml @@ -0,0 +1,48 @@ +title: Agent +type: object +properties: + type: + $ref: ./agent_type.yaml + active: + type: boolean + enrolled_at: + type: string + unenrolled_at: + type: string + unenrollment_started_at: + type: string + shared_id: + type: string + access_api_key_id: + type: string + default_api_key_id: + type: string + policy_id: + type: string + policy_revision: + type: number + last_checkin: + type: string + user_provided_metadata: + $ref: ./agent_metadata.yaml + local_metadata: + $ref: ./agent_metadata.yaml + id: + type: string + current_error_events: + type: array + items: + $ref: ./agent_event.yaml + access_api_key: + type: string + status: + $ref: ./agent_status.yaml + default_api_key: + type: string +required: + - type + - active + - enrolled_at + - id + - current_error_events + - status diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_event.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_event.yaml new file mode 100644 index 000000000000..ada709378a9b --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_event.yaml @@ -0,0 +1,9 @@ +title: AgentEvent +allOf: + - type: object + properties: + id: + type: string + required: + - id + - $ref: ./new_agent_event.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_metadata.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_metadata.yaml new file mode 100644 index 000000000000..d37321f59a58 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_metadata.yaml @@ -0,0 +1,2 @@ +title: AgentMetadata +type: object diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_policy.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_policy.yaml new file mode 100644 index 000000000000..7395e45365ea --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_policy.yaml @@ -0,0 +1,30 @@ +allOf: + - $ref: ./new_agent_policy.yaml + - type: object + properties: + id: + type: string + status: + type: string + enum: + - active + - inactive + packagePolicies: + oneOf: + - items: + type: string + - items: + $ref: ./package_policy.yaml + type: array + updated_on: + type: string + format: date-time + updated_by: + type: string + revision: + type: number + agents: + type: number + required: + - id + - status diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_status.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_status.yaml new file mode 100644 index 000000000000..076a7cc5036b --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_status.yaml @@ -0,0 +1,8 @@ +type: string +title: AgentStatus +enum: + - offline + - error + - online + - inactive + - warning diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_type.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_type.yaml new file mode 100644 index 000000000000..da42f95c9e1d --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/agent_type.yaml @@ -0,0 +1,6 @@ +type: string +title: AgentType +enum: + - PERMANENT + - EPHEMERAL + - TEMPORARY diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/bulk_upgrade_agents.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/bulk_upgrade_agents.yaml new file mode 100644 index 000000000000..da06aa6fa825 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/bulk_upgrade_agents.yaml @@ -0,0 +1,37 @@ +title: BulkUpgradeAgents +oneOf: + - type: object + properties: + version: + type: string + agents: + type: array + items: + type: string + required: + - version + - agents + - type: object + properties: + version: + type: string + source_uri: + type: string + agents: + type: array + items: + type: string + required: + - version + - agents + - type: object + properties: + version: + type: string + source_uri: + type: string + agents: + type: string + required: + - version + - agents diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/enrollment_api_key.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/enrollment_api_key.yaml new file mode 100644 index 000000000000..3efe77b3bd60 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/enrollment_api_key.yaml @@ -0,0 +1,3 @@ +type: string +title: EnrollmentApiKey +format: byte diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/new_agent_event.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/new_agent_event.yaml new file mode 100644 index 000000000000..ee4ddfb5f004 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/new_agent_event.yaml @@ -0,0 +1,44 @@ +title: NewAgentEvent +type: object +properties: + type: + type: string + enum: + - STATE + - ERROR + - ACTION_RESULT + - ACTION + subtype: + type: string + enum: + - RUNNING + - STARTING + - IN_PROGRESS + - CONFIG + - FAILED + - STOPPING + - STOPPED + - DEGRADED + - DATA_DUMP + - ACKNOWLEDGED + - UNKNOWN + timestamp: + type: string + message: + type: string + payload: + type: string + agent_id: + type: string + policy_id: + type: string + stream_id: + type: string + action_id: + type: string +required: + - type + - subtype + - timestamp + - message + - agent_id diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/new_agent_policy.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/new_agent_policy.yaml new file mode 100644 index 000000000000..7070876cbea5 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/new_agent_policy.yaml @@ -0,0 +1,9 @@ +title: NewAgentPolicy +type: object +properties: + name: + type: string + namespace: + type: string + description: + type: string diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/new_package_policy.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/new_package_policy.yaml new file mode 100644 index 000000000000..61b1fa678d40 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/new_package_policy.yaml @@ -0,0 +1,58 @@ +title: NewPackagePolicy +type: object +description: '' +properties: + enabled: + type: boolean + package: + type: object + properties: + name: + type: string + version: + type: string + title: + type: string + required: + - name + - version + - title + namespace: + type: string + output_id: + type: string + inputs: + type: array + items: + type: object + properties: + type: + type: string + enabled: + type: boolean + processors: + type: array + items: + type: string + streams: + type: array + items: {} + config: + type: object + vars: + type: object + required: + - type + - enabled + - streams + policy_id: + type: string + name: + type: string + description: + type: string +required: + - output_id + - inputs + - policy_id + - name diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/package_info.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/package_info.yaml new file mode 100644 index 000000000000..3e0742c1879c --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/package_info.yaml @@ -0,0 +1,118 @@ +title: PackageInfo +type: object +properties: + name: + type: string + title: + type: string + version: + type: string + readme: + type: string + description: + type: string + type: + type: string + categories: + type: array + items: + type: string + requirement: + oneOf: + - properties: + kibana: + type: object + properties: + versions: + type: string + - properties: + elasticsearch: + type: object + properties: + versions: + type: string + type: object + screenshots: + type: array + items: + type: object + properties: + src: + type: string + path: + type: string + title: + type: string + size: + type: string + type: + type: string + required: + - src + - path + icons: + type: array + items: + type: string + assets: + type: array + items: + type: string + internal: + type: boolean + format_version: + type: string + data_streams: + type: array + items: + type: object + properties: + title: + type: string + name: + type: string + release: + type: string + ingeset_pipeline: + type: string + vars: + type: array + items: + type: object + properties: + name: + type: string + default: + type: string + required: + - name + - default + type: + type: string + package: + type: string + required: + - title + - name + - release + - ingeset_pipeline + - type + - package + download: + type: string + path: + type: string + removable: + type: boolean +required: + - name + - title + - version + - description + - type + - categories + - requirement + - assets + - format_version + - download + - path diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/package_policy.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/package_policy.yaml new file mode 100644 index 000000000000..99bc64f79337 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/package_policy.yaml @@ -0,0 +1,15 @@ +title: PackagePolicy +allOf: + - type: object + properties: + id: + type: string + revision: + type: number + inputs: + type: array + items: {} + required: + - id + - revision + - $ref: ./new_package_policy.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/search_result.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/search_result.yaml new file mode 100644 index 000000000000..b67ff61c5ab6 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/search_result.yaml @@ -0,0 +1,33 @@ +title: SearchResult +type: object +properties: + description: + type: string + download: + type: string + icons: + type: string + name: + type: string + path: + type: string + title: + type: string + type: + type: string + version: + type: string + status: + type: string + savedObject: + type: object +required: + - description + - download + - icons + - name + - path + - title + - type + - version + - status diff --git a/x-pack/plugins/ingest_manager/common/openapi/components/schemas/upgrade_agent.yaml b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/upgrade_agent.yaml new file mode 100644 index 000000000000..11a2b5846ba1 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/components/schemas/upgrade_agent.yaml @@ -0,0 +1,16 @@ +title: UpgradeAgent +oneOf: + - type: object + properties: + version: + type: string + required: + - version + - type: object + properties: + version: + type: string + source_uri: + type: string + required: + - version diff --git a/x-pack/plugins/ingest_manager/common/openapi/entrypoint.yaml b/x-pack/plugins/ingest_manager/common/openapi/entrypoint.yaml new file mode 100644 index 000000000000..791d3da56783 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/entrypoint.yaml @@ -0,0 +1,77 @@ +openapi: 3.0.0 +info: + title: Ingest Manager + version: '0.2' + contact: + name: Ingest Team + license: + name: Elastic +servers: + - url: 'http://localhost:5601/api/fleet' + description: local +paths: + /agent_policies: + $ref: paths/agent_policies.yaml + '/agent_policies/{agentPolicyId}': + $ref: 'paths/agent_policies@{agent_policy_id}.yaml' + '/agent_policies/{agentPolicyId}/copy': + $ref: 'paths/agent_policies@{agent_policy_id}@copy.yaml' + /agent_policies/delete: + $ref: paths/agent_policies@delete.yaml + /agent-status: + $ref: paths/agent_status.yaml + /agents: + $ref: paths/agents.yaml + '/agents/{agentId}/acks': + $ref: 'paths/agents@{agent_id}@acks.yaml' + '/agents/{agentId}/checkin': + $ref: 'paths/agents@{agent_id}@checkin.yaml' + '/agents/{agentId}/events': + $ref: 'paths/agents@{agent_id}@events.yaml' + '/agents/{agentId}/unenroll': + $ref: 'paths/agents@{agent_id}@unenroll.yaml' + '/agents/{agentId}/upgrade': + $ref: 'paths/agents@{agent_id}@upgrade.yaml' + /agents/bulk_upgrade: + $ref: paths/agents@bulk_upgrade.yaml + /agents/enroll: + $ref: paths/agents@enroll.yaml + /agents/setup: + $ref: paths/agents@setup.yaml + /enrollment-api-keys: + $ref: paths/enrollment_api_keys.yaml + '/enrollment-api-keys/{keyId}': + $ref: 'paths/enrollment_api_keys@{key_id}.yaml' + /epm/categories: + $ref: paths/epm@categories.yaml + /epm/packages: + $ref: paths/epm@packages.yaml + '/epm/packages/{pkgkey}': + $ref: 'paths/epm@packages@{pkgkey}.yaml' + '/agents/{agentId}': + $ref: 'paths/agents@{agent_id}.yaml' + '/install/{osType}': + $ref: 'paths/install@{os_type}.yaml' + /package_policies: + $ref: paths/package_policies.yaml + '/package_policies/{packagePolicyId}': + $ref: 'paths/package_policies@{package_policy_id}.yaml' + /setup: + $ref: paths/setup.yaml +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + Enrollment API Key: + name: Authorization + type: apiKey + in: header + description: 'e.g. Authorization: ApiKey base64EnrollmentApiKey' + Access API Key: + name: Authorization + type: apiKey + in: header + description: 'e.g. Authorization: ApiKey base64AccessApiKey' +security: + - basicAuth: [] diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/README.md b/x-pack/plugins/ingest_manager/common/openapi/paths/README.md new file mode 100644 index 000000000000..f5003e3e3473 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/README.md @@ -0,0 +1,130 @@ +Paths +===== + +Organize our path definitions within this folder. We will reference our paths from our main `openapi.json` entrypoint file. + +It may help us to adopt some conventions: + +* path separator token (e.g. `@`) or subfolders +* path parameter (e.g. `{example}`) +* file-per-path or file-per-operation + +There are different benefits and drawbacks to each decision. + +We can adopt any organization we wish. We have some tips for organizing paths based on common practices. + +## Each path in a separate file + +Use a predefined "path separator" and keep all of our path files in the top level of the `paths` folder. + +``` +paths/ +├── README.md +├── agent_policies.yaml +├── agent_policies@delete.yaml +├── agent_policies@{agent_policy_id}.yaml +├── agent_policies@{agent_policy_id}@copy.yaml +├── agent_status.yaml +├── agents.yaml +├── agents@bulk_upgrade.yaml +├── agents@enroll.yaml +├── agents@setup.yaml +├── agents@{agent_id}.yaml +├── agents@{agent_id}@acks.yaml +├── agents@{agent_id}@checkin.yaml +├── agents@{agent_id}@events.yaml +├── agents@{agent_id}@unenroll.yaml +├── agents@{agent_id}@upgrade.yaml +├── enrollment_api_keys.yaml +├── enrollment_api_keys@{key_id}.yaml +├── epm@categories.yaml +├── epm@packages.yaml +├── epm@packages@{pkgkey}.yaml +├── install@{os_type}.yaml +├── package_policies.yaml +├── package_policies@{package_policy_id}.yaml +└── setup.yaml +``` + +Redocly recommends using the `@` character for this case. + +In addition, Redocly recommends placing path parameters within `{}` curly braces if we adopt this style. + +#### Motivations + +* Quickly see a list of all paths. Many people think in terms of the "number" of "endpoints" (paths), and not the "number" of "operations" (paths * http methods). + +* Only the "file-per-path" option is semantically correct with the OpenAPI Specification 3.0.2. However, Redocly's openapi-cli will build valid bundles for any of the other options too. + + +#### Drawbacks + +* This may require multiple definitions per http method within a single file. +* It requires settling on a path separator (that is allowed to be used in filenames) and sticking to that convention. + +## Each operation in a separate file + +We may also place each operation in a separate file. + +In this case, if we want all paths at the top-level, we can concatenate the http method to the path name. Similar to the above option, we can + +### Files at top-level of `paths` + +We may name our files with some concatenation for the http method. For example, following a convention such as: `-.json`. + +#### Motivations + +* Quickly see all operations without needing to navigate subfolders. + +#### Drawbacks + +* Adopting an unusual path separator convention, instead of using subfolders. + +### Use subfolders to mirror API path structure + +Example: +``` +GET /customers + +/paths/customers/get.json +``` + +In this case, the path id defined within subfolders which mirror the API URL structure. + +Example with path parameter: +``` +GET /customers/{id} + +/paths/customers/{id}/get.json +``` + +#### Motivations + +It matches the URL structure. + +It is pretty easy to reference: + +```json +paths: + '/customers/{id}': + get: + $ref: ./paths/customers/{id}/get.json + put: + $ref: ./paths/customers/{id}/put.json +``` + +#### Drawbacks + +If we have a lot of nested folders, it may be confusing to reference our schemas. + +Example +``` +file: /paths/customers/{id}/timeline/{messageId}/get.json + +# excerpt of file + headers: + Rate-Limit-Remaining: + $ref: ../../../../../components/headers/Rate-Limit-Remaining.json + +``` +Notice the `../../../../../` in the ref which requires some attention to formulate correctly. While openapi-cli has a linter which suggests possible refs when there is a mistake, this is still a net drawback for APIs with deep paths. diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agent_policies.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agent_policies.yaml new file mode 100644 index 000000000000..2ba14fba7232 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agent_policies.yaml @@ -0,0 +1,54 @@ +get: + summary: Agent policy - List + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + $ref: ../components/schemas/agent_policy.yaml + total: + type: number + page: + type: number + perPage: + type: number + required: + - items + - total + - page + - perPage + operationId: agent-policy-list + parameters: + - $ref: ../components/parameters/page_size.yaml + - $ref: ../components/parameters/page_index.yaml + - $ref: ../components/parameters/kuery.yaml + description: '' +post: + summary: Agent policy - Create + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: ../components/schemas/agent_policy.yaml + operationId: post-agent-policy + requestBody: + content: + application/json: + schema: + $ref: ../components/schemas/new_agent_policy.yaml + security: [] + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agent_policies@delete.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agent_policies@delete.yaml new file mode 100644 index 000000000000..ae975274d80e --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agent_policies@delete.yaml @@ -0,0 +1,33 @@ +post: + summary: Agent policy - Delete + operationId: post-agent-policy-delete + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: string + success: + type: boolean + required: + - id + - success + requestBody: + content: + application/json: + schema: + type: object + properties: + agentPolicyIds: + type: array + items: + type: string + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml +parameters: [] diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agent_policies@{agent_policy_id}.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agent_policies@{agent_policy_id}.yaml new file mode 100644 index 000000000000..15910b0116b7 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agent_policies@{agent_policy_id}.yaml @@ -0,0 +1,47 @@ +parameters: + - schema: + type: string + name: agentPolicyId + in: path + required: true +get: + summary: Agent policy - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: ../components/schemas/agent_policy.yaml + required: + - item + operationId: agent-policy-info + description: Get one agent policy + parameters: [] +put: + summary: Agent policy - Update + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: ../components/schemas/agent_policy.yaml + required: + - item + operationId: put-agent-policy-agentPolicyId + requestBody: + content: + application/json: + schema: + $ref: ../components/schemas/new_agent_policy.yaml + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agent_policies@{agent_policy_id}@copy.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agent_policies@{agent_policy_id}@copy.yaml new file mode 100644 index 000000000000..4b42f8cab067 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agent_policies@{agent_policy_id}@copy.yaml @@ -0,0 +1,35 @@ +parameters: + - schema: + type: string + name: agentPolicyId + in: path + required: true +post: + summary: Agent policy - copy one policy + operationId: agent-policy-copy + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: ../components/schemas/agent_policy.yaml + required: + - item + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: + type: string + required: + - name + description: '' + description: Copies one agent policy diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agent_status.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agent_status.yaml new file mode 100644 index 000000000000..77ec9e85069a --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agent_status.yaml @@ -0,0 +1,11 @@ +get: + summary: Fleet - Agent - Status for policy + tags: [] + responses: {} + operationId: get-fleet-agent-status + parameters: + - schema: + type: string + name: policyId + in: query + required: false diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agents.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agents.yaml new file mode 100644 index 000000000000..e5039bc2cacc --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agents.yaml @@ -0,0 +1,29 @@ +get: + summary: Fleet - Agent - List + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + list: + type: array + items: + type: object + total: + type: number + page: + type: number + perPage: + type: number + required: + - list + - total + - page + - perPage + operationId: get-fleet-agents + security: + - basicAuth: [] diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agents@bulk_upgrade.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@bulk_upgrade.yaml new file mode 100644 index 000000000000..2092fbf000ab --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@bulk_upgrade.yaml @@ -0,0 +1,25 @@ +post: + summary: Fleet - Agent - Bulk Upgrade + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: ../components/schemas/bulk_upgrade_agents.yaml + '400': + description: BAD REQUEST + content: + application/json: + schema: + $ref: ../components/schemas/upgrade_agent.yaml + operationId: post-fleet-agents-bulk-upgrade + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + requestBody: + required: true + content: + application/json: + schema: + $ref: ../components/schemas/bulk_upgrade_agents.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agents@enroll.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@enroll.yaml new file mode 100644 index 000000000000..a0c1c8c28e72 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@enroll.yaml @@ -0,0 +1,47 @@ +post: + summary: Fleet - Agent - Enroll + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + action: + type: string + item: + $ref: ../components/schemas/agent.yaml + operationId: post-fleet-agents-enroll + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + requestBody: + content: + application/json: + schema: + type: object + properties: + type: + type: string + enum: + - PERMANENT + - EPHEMERAL + - TEMPORARY + shared_id: + type: string + metadata: + type: object + required: + - local + - user_provided + properties: + local: + $ref: ../components/schemas/agent_metadata.yaml + user_provided: + $ref: ../components/schemas/agent_metadata.yaml + required: + - type + - metadata + security: + - Enrollment API Key: [] diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agents@setup.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@setup.yaml new file mode 100644 index 000000000000..87556dca0afb --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@setup.yaml @@ -0,0 +1,48 @@ +get: + summary: Agents setup - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + isInitialized: + type: boolean + required: + - isInitialized + operationId: get-agents-setup + security: + - basicAuth: [] +post: + summary: Agents setup - Create + operationId: post-agents-setup + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + isInitialized: + type: boolean + required: + - isInitialized + requestBody: + content: + application/json: + schema: + type: object + properties: + admin_username: + type: string + admin_password: + type: string + required: + - admin_username + - admin_password + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}.yaml new file mode 100644 index 000000000000..e65c80d8fae8 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}.yaml @@ -0,0 +1,36 @@ +parameters: + - schema: + type: string + name: agentId + in: path + required: true +get: + summary: Fleet - Agent - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + type: object + required: + - item + operationId: get-fleet-agents-agentId +put: + summary: Fleet - Agent - Update + tags: [] + responses: {} + operationId: put-fleet-agents-agentId + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml +delete: + summary: Fleet - Agent - Delete + tags: [] + responses: {} + operationId: delete-fleet-agents-agentId + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@acks.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@acks.yaml new file mode 100644 index 000000000000..6728554bf542 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@acks.yaml @@ -0,0 +1,32 @@ +parameters: + - schema: + type: string + name: agentId + in: path + required: true +post: + summary: Fleet - Agent - Acks + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + action: + type: string + enum: + - acks + required: + - action + operationId: post-fleet-agents-agentId-acks + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + requestBody: + content: + application/json: + schema: + type: object + properties: {} diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@checkin.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@checkin.yaml new file mode 100644 index 000000000000..cc797c735660 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@checkin.yaml @@ -0,0 +1,60 @@ +parameters: + - schema: + type: string + name: agentId + in: path + required: true +post: + summary: Fleet - Agent - Check In + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + action: + type: string + enum: + - checkin + actions: + type: array + items: + type: object + properties: + agent_id: + type: string + data: + type: object + id: + type: string + created_at: + type: string + format: date-time + type: + type: string + required: + - agent_id + - data + - id + - created_at + - type + operationId: post-fleet-agents-agentId-checkin + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + security: + - Access API Key: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + local_metadata: + $ref: ../components/schemas/agent_metadata.yaml + events: + type: array + items: + $ref: ../components/schemas/new_agent_event.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@events.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@events.yaml new file mode 100644 index 000000000000..db8d28f72b5a --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@events.yaml @@ -0,0 +1,11 @@ +parameters: + - schema: + type: string + name: agentId + in: path + required: true +get: + summary: Fleet - Agent - Events + tags: [] + responses: {} + operationId: get-fleet-agents-agentId-events diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@unenroll.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@unenroll.yaml new file mode 100644 index 000000000000..00c9cdfbcf4a --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@unenroll.yaml @@ -0,0 +1,21 @@ +parameters: + - schema: + type: string + name: agentId + in: path + required: true +post: + summary: Fleet - Agent - Unenroll + tags: [] + responses: {} + operationId: post-fleet-agents-unenroll + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + requestBody: + content: + application/json: + schema: + type: object + properties: + force: + type: boolean diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@upgrade.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@upgrade.yaml new file mode 100644 index 000000000000..ce871cac0d06 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/agents@{agent_id}@upgrade.yaml @@ -0,0 +1,32 @@ +parameters: + - schema: + type: string + name: agentId + in: path + required: true +post: + summary: Fleet - Agent - Upgrade + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: ../components/schemas/upgrade_agent.yaml + '400': + description: BAD REQUEST + content: + application/json: + schema: + $ref: ../components/schemas/upgrade_agent.yaml + operationId: post-fleet-agents-upgrade + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + requestBody: + required: true + content: + application/json: + schema: + $ref: ../components/schemas/upgrade_agent.yaml + diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/enrollment_api_keys.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/enrollment_api_keys.yaml new file mode 100644 index 000000000000..22d27c0596d6 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/enrollment_api_keys.yaml @@ -0,0 +1,13 @@ +get: + summary: Enrollment - List + tags: [] + responses: {} + operationId: get-fleet-enrollment-api-keys + parameters: [] +post: + summary: Enrollment - Create + tags: [] + responses: {} + operationId: post-fleet-enrollment-api-keys + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/enrollment_api_keys@{key_id}.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/enrollment_api_keys@{key_id}.yaml new file mode 100644 index 000000000000..3b43950427e8 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/enrollment_api_keys@{key_id}.yaml @@ -0,0 +1,18 @@ +parameters: + - schema: + type: string + name: keyId + in: path + required: true +get: + summary: Enrollment - Info + tags: [] + responses: {} + operationId: get-fleet-enrollment-api-keys-keyId +delete: + summary: Enrollment - Delete + tags: [] + responses: {} + operationId: delete-fleet-enrollment-api-keys-keyId + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/epm@categories.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/epm@categories.yaml new file mode 100644 index 000000000000..0fc26a4e5c82 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/epm@categories.yaml @@ -0,0 +1,24 @@ +get: + summary: EPM - Categories + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: string + title: + type: string + count: + type: number + required: + - id + - title + - count + operationId: get-epm-categories diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/epm@packages.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/epm@packages.yaml new file mode 100644 index 000000000000..afbe8ee2dc32 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/epm@packages.yaml @@ -0,0 +1,14 @@ +get: + summary: EPM - Packages - List + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: ../components/schemas/search_result.yaml + operationId: get-epm-list +parameters: [] diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/epm@packages@{pkgkey}.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/epm@packages@{pkgkey}.yaml new file mode 100644 index 000000000000..43937aa153f5 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/epm@packages@{pkgkey}.yaml @@ -0,0 +1,91 @@ +get: + summary: EPM - Packages - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + allOf: + - properties: + response: + $ref: ../components/schemas/package_info.yaml + - properties: + status: + type: string + enum: + - installed + - not_installed + savedObject: + type: string + required: + - status + - savedObject + operationId: get-epm-package-pkgkey + security: + - basicAuth: [] +parameters: + - schema: + type: string + name: pkgkey + in: path + required: true +post: + summary: EPM - Packages - Install + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + response: + type: array + items: + type: object + properties: + id: + type: string + type: + type: string + required: + - id + - type + required: + - response + operationId: post-epm-install-pkgkey + description: '' + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml +delete: + summary: EPM - Packages - Delete + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + response: + type: array + items: + type: object + properties: + id: + type: string + type: + type: string + required: + - id + - type + required: + - response + operationId: post-epm-delete-pkgkey + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/install@{os_type}.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/install@{os_type}.yaml new file mode 100644 index 000000000000..80351aa7ae11 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/install@{os_type}.yaml @@ -0,0 +1,11 @@ +parameters: + - schema: + type: string + name: osType + in: path + required: true +get: + summary: Fleet - Get OS install script + tags: [] + responses: {} + operationId: get-fleet-install-osType diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/package_policies.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/package_policies.yaml new file mode 100644 index 000000000000..47eca50f0524 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/package_policies.yaml @@ -0,0 +1,40 @@ +get: + summary: PackagePolicies - List + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + $ref: ../components/schemas/package_policy.yaml + total: + type: number + page: + type: number + perPage: + type: number + required: + - items + operationId: get-packagePolicies + security: [] + parameters: [] +parameters: [] +post: + summary: PackagePolicies - Create + operationId: post-packagePolicies + responses: + '200': + description: OK + requestBody: + content: + application/json: + schema: + $ref: ../components/schemas/new_package_policy.yaml + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/package_policies@{package_policy_id}.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/package_policies@{package_policy_id}.yaml new file mode 100644 index 000000000000..3b177be3d032 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/package_policies@{package_policy_id}.yaml @@ -0,0 +1,42 @@ +get: + summary: PackagePolicies - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: ../components/schemas/package_policy.yaml + required: + - item + operationId: get-packagePolicies-packagePolicyId +parameters: + - schema: + type: string + name: packagePolicyId + in: path + required: true +put: + summary: PackagePolicies - Update + operationId: put-packagePolicies-packagePolicyId + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: ../components/schemas/package_policy.yaml + sucess: + type: boolean + required: + - item + - sucess + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/paths/setup.yaml b/x-pack/plugins/ingest_manager/common/openapi/paths/setup.yaml new file mode 100644 index 000000000000..62ad2cb66dac --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/openapi/paths/setup.yaml @@ -0,0 +1,25 @@ +post: + summary: Ingest Manager - Setup + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + isInitialized: + type: boolean + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + message: + type: string + operationId: post-setup + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json deleted file mode 100644 index 69974a87434a..000000000000 --- a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json +++ /dev/null @@ -1,4538 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "Ingest Manager", - "version": "0.2", - "contact": { - "name": "Ingest Team" - }, - "license": { - "name": "Elastic" - } - }, - "servers": [ - { - "url": "http://localhost:5601/api/fleet", - "description": "local" - } - ], - "paths": { - "/agent_policies": { - "get": { - "summary": "Agent policy - List", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AgentPolicy" - } - }, - "total": { - "type": "number" - }, - "page": { - "type": "number" - }, - "perPage": { - "type": "number" - } - }, - "required": ["items", "total", "page", "perPage"] - }, - "examples": { - "success": { - "value": { - "items": [ - { - "id": "82da1fc0-8fbf-11ea-b2ce-01c4a6127154", - "name": "Default policy", - "namespace": "default", - "description": "Default agent policy created by Kibana", - "status": "active", - "packagePolicies": ["8a5679b0-8fbf-11ea-b2ce-01c4a6127154"], - "is_default": true, - "monitoring_enabled": ["logs", "metrics"], - "revision": 2, - "updated_on": "2020-05-06T17:32:21.905Z", - "updated_by": "system", - "agents": 0 - } - ], - "total": 1, - "page": 1, - "perPage": 50 - } - } - } - } - } - } - }, - "operationId": "agent-policy-list", - "parameters": [ - { - "$ref": "#/components/parameters/pageSizeParam" - }, - { - "$ref": "#/components/parameters/pageIndexParam" - }, - { - "$ref": "#/components/parameters/kueryParam" - } - ], - "description": "" - }, - "post": { - "summary": "Agent policy - Create", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "item": { - "$ref": "#/components/schemas/AgentPolicy" - } - } - } - } - } - } - }, - "operationId": "post-agent-policy", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewAgentPolicy" - } - } - } - }, - "security": [], - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - } - }, - "/agent_policies/{agentPolicyId}": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "agentPolicyId", - "in": "path", - "required": true - } - ], - "get": { - "summary": "Agent policy - Info", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "item": { - "$ref": "#/components/schemas/AgentPolicy" - } - }, - "required": ["item"] - }, - "examples": { - "success": { - "value": { - "item": { - "id": "82da1fc0-8fbf-11ea-b2ce-01c4a6127154", - "name": "Default policy", - "namespace": "default", - "description": "Default agent policy created by Kibana", - "status": "active", - "packagePolicies": [ - { - "id": "8a5679b0-8fbf-11ea-b2ce-01c4a6127154", - "name": "system-1", - "namespace": "default", - "package": { - "name": "system", - "title": "System", - "version": "0.0.3" - }, - "enabled": true, - "policy_id": "82da1fc0-8fbf-11ea-b2ce-01c4a6127154", - "output_id": "08adc51c-69f3-4294-80e2-24527c6ff73d", - "inputs": [ - { - "type": "logs", - "enabled": true, - "streams": [ - { - "id": "logs-system.auth", - "enabled": true, - "dataset": "system.auth", - "vars": { - "paths": { - "value": ["/var/log/auth.log*", "/var/log/secure*"], - "type": "text" - } - }, - "agent_stream": { - "paths": ["/var/log/auth.log*", "/var/log/secure*"], - "exclude_files": [".gz$"], - "multiline": { - "pattern": "^\\s", - "match": "after" - }, - "processors": [ - { - "add_locale": null - }, - { - "add_fields": { - "target": "", - "fields": { - "ecs.version": "1.5.0" - } - } - } - ] - } - }, - { - "id": "logs-system.syslog", - "enabled": true, - "dataset": "system.syslog", - "vars": { - "paths": { - "value": ["/var/log/messages*", "/var/log/syslog*"], - "type": "text" - } - }, - "agent_stream": { - "paths": ["/var/log/messages*", "/var/log/syslog*"], - "exclude_files": [".gz$"], - "multiline": { - "pattern": "^\\s", - "match": "after" - }, - "processors": [ - { - "add_locale": null - }, - { - "add_fields": { - "target": "", - "fields": { - "ecs.version": "1.5.0" - } - } - } - ] - } - } - ] - }, - { - "type": "system/metrics", - "enabled": true, - "streams": [ - { - "id": "system/metrics-system.core", - "enabled": true, - "dataset": "system.core", - "vars": { - "core.metrics": { - "value": ["percentages"], - "type": "text" - } - }, - "agent_stream": { - "metricsets": ["core"], - "core.metrics": "percentages" - } - }, - { - "id": "system/metrics-system.cpu", - "enabled": true, - "dataset": "system.cpu", - "vars": { - "core.metrics": { - "value": ["percentages"], - "type": "text" - }, - "cpu.metrics": { - "value": ["percentages", "normalized_percentages"], - "type": "text" - }, - "period": { - "value": "10s", - "type": "text" - }, - "process.include_top_n.by_cpu": { - "value": 5, - "type": "integer" - }, - "process.include_top_n.by_memory": { - "value": 5, - "type": "integer" - }, - "processes": { - "value": [".*"], - "type": "text" - } - }, - "agent_stream": { - "metricsets": ["cpu"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - } - }, - { - "id": "system/metrics-system.diskio", - "enabled": true, - "dataset": "system.diskio", - "agent_stream": { - "metricsets": ["diskio"] - } - }, - { - "id": "system/metrics-system.entropy", - "enabled": true, - "dataset": "system.entropy", - "agent_stream": { - "metricsets": ["entropy"] - } - }, - { - "id": "system/metrics-system.filesystem", - "enabled": true, - "dataset": "system.filesystem", - "vars": { - "period": { - "value": "1m", - "type": "text" - }, - "processors": { - "value": "- drop_event.when.regexp:\n system.filesystem.mount_point: ^/(sys|cgroup|proc|dev|etc|host|lib|snap)($|/)\n", - "type": "yaml" - } - }, - "agent_stream": { - "metricsets": ["filesystem"], - "period": "1m", - "processors": [ - { - "drop_event.when.regexp": { - "system.filesystem.mount_point": "^/(sys|cgroup|proc|dev|etc|host|lib|snap)($|/)" - } - } - ] - } - }, - { - "id": "system/metrics-system.fsstat", - "enabled": true, - "dataset": "system.fsstat", - "vars": { - "period": { - "value": "1m", - "type": "text" - }, - "processors": { - "value": "- drop_event.when.regexp:\n system.filesystem.mount_point: ^/(sys|cgroup|proc|dev|etc|host|lib|snap)($|/)\n", - "type": "yaml" - } - }, - "agent_stream": { - "metricsets": ["fsstat"], - "period": "1m", - "processors": [ - { - "drop_event.when.regexp": { - "system.filesystem.mount_point": "^/(sys|cgroup|proc|dev|etc|host|lib|snap)($|/)" - } - } - ] - } - }, - { - "id": "system/metrics-system.load", - "enabled": true, - "dataset": "system.load", - "vars": { - "core.metrics": { - "value": ["percentages"], - "type": "text" - }, - "cpu.metrics": { - "value": ["percentages", "normalized_percentages"], - "type": "text" - }, - "period": { - "value": "10s", - "type": "text" - }, - "process.include_top_n.by_cpu": { - "value": 5, - "type": "integer" - }, - "process.include_top_n.by_memory": { - "value": 5, - "type": "integer" - }, - "processes": { - "value": [".*"], - "type": "text" - } - }, - "agent_stream": { - "metricsets": ["load"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - } - }, - { - "id": "system/metrics-system.memory", - "enabled": true, - "dataset": "system.memory", - "vars": { - "core.metrics": { - "value": ["percentages"], - "type": "text" - }, - "cpu.metrics": { - "value": ["percentages", "normalized_percentages"], - "type": "text" - }, - "period": { - "value": "10s", - "type": "text" - }, - "process.include_top_n.by_cpu": { - "value": 5, - "type": "integer" - }, - "process.include_top_n.by_memory": { - "value": 5, - "type": "integer" - }, - "processes": { - "value": [".*"], - "type": "text" - } - }, - "agent_stream": { - "metricsets": ["memory"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - } - }, - { - "id": "system/metrics-system.network", - "enabled": true, - "dataset": "system.network", - "vars": { - "core.metrics": { - "value": ["percentages"], - "type": "text" - }, - "cpu.metrics": { - "value": ["percentages", "normalized_percentages"], - "type": "text" - }, - "period": { - "value": "10s", - "type": "text" - }, - "process.include_top_n.by_cpu": { - "value": 5, - "type": "integer" - }, - "process.include_top_n.by_memory": { - "value": 5, - "type": "integer" - }, - "processes": { - "value": [".*"], - "type": "text" - } - }, - "agent_stream": { - "metricsets": ["network"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - } - }, - { - "id": "system/metrics-system.network_summary", - "enabled": true, - "dataset": "system.network_summary", - "agent_stream": { - "metricsets": ["network_summary"] - } - }, - { - "id": "system/metrics-system.process", - "enabled": true, - "dataset": "system.process", - "vars": { - "core.metrics": { - "value": ["percentages"], - "type": "text" - }, - "cpu.metrics": { - "value": ["percentages", "normalized_percentages"], - "type": "text" - }, - "period": { - "value": "10s", - "type": "text" - }, - "process.include_top_n.by_cpu": { - "value": 5, - "type": "integer" - }, - "process.include_top_n.by_memory": { - "value": 5, - "type": "integer" - }, - "processes": { - "value": [".*"], - "type": "text" - } - }, - "agent_stream": { - "metricsets": ["process"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - } - }, - { - "id": "system/metrics-system.process_summary", - "enabled": true, - "dataset": "system.process_summary", - "vars": { - "core.metrics": { - "value": ["percentages"], - "type": "text" - }, - "cpu.metrics": { - "value": ["percentages", "normalized_percentages"], - "type": "text" - }, - "period": { - "value": "10s", - "type": "text" - }, - "process.include_top_n.by_cpu": { - "value": 5, - "type": "integer" - }, - "process.include_top_n.by_memory": { - "value": 5, - "type": "integer" - }, - "processes": { - "value": [".*"], - "type": "text" - } - }, - "agent_stream": { - "metricsets": ["process_summary"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - } - }, - { - "id": "system/metrics-system.raid", - "enabled": true, - "dataset": "system.raid", - "agent_stream": { - "metricsets": ["raid"] - } - }, - { - "id": "system/metrics-system.service", - "enabled": true, - "dataset": "system.service", - "agent_stream": { - "metricsets": ["service"] - } - }, - { - "id": "system/metrics-system.socket", - "enabled": true, - "dataset": "system.socket", - "agent_stream": { - "metricsets": ["socket"] - } - }, - { - "id": "system/metrics-system.socket_summary", - "enabled": true, - "dataset": "system.socket_summary", - "vars": { - "core.metrics": { - "value": ["percentages"], - "type": "text" - }, - "cpu.metrics": { - "value": ["percentages", "normalized_percentages"], - "type": "text" - }, - "period": { - "value": "10s", - "type": "text" - }, - "process.include_top_n.by_cpu": { - "value": 5, - "type": "integer" - }, - "process.include_top_n.by_memory": { - "value": 5, - "type": "integer" - }, - "processes": { - "value": [".*"], - "type": "text" - } - }, - "agent_stream": { - "metricsets": ["socket_summary"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - } - }, - { - "id": "system/metrics-system.uptime", - "enabled": true, - "dataset": "system.uptime", - "vars": { - "core.metrics": { - "value": ["percentages"], - "type": "text" - }, - "cpu.metrics": { - "value": ["percentages", "normalized_percentages"], - "type": "text" - }, - "period": { - "value": "10s", - "type": "text" - }, - "processes": { - "value": [".*"], - "type": "text" - } - }, - "agent_stream": { - "metricsets": ["uptime"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "processes": ".*" - } - }, - { - "id": "system/metrics-system.users", - "enabled": true, - "dataset": "system.users", - "agent_stream": { - "metricsets": ["users"] - } - } - ] - } - ], - "revision": 1 - } - ], - "is_default": true, - "monitoring_enabled": ["logs", "metrics"], - "revision": 2, - "updated_on": "2020-05-06T17:32:21.905Z", - "updated_by": "system" - } - } - } - } - } - } - } - }, - "operationId": "agent-policy-info", - "description": "Get one agent policy", - "parameters": [] - }, - "put": { - "summary": "Agent policy - Update", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "item": { - "$ref": "#/components/schemas/AgentPolicy" - } - }, - "required": ["item"] - }, - "examples": { - "example-1": { - "value": { - "item": { - "id": "0b7130d0-5a37-11ea-ac2c-25e9ab4ecb2a", - "name": "UPDATED name", - "description": "UPDATED description", - "namespace": "UPDATED namespace", - "updated_on": "Fri Feb 28 2020 16:22:31 GMT-0500 (Eastern Standard Time)", - "updated_by": "elastic", - "packagePolicies": [] - } - } - } - } - } - } - } - }, - "operationId": "put-agent-policy-agentPolicyId", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewAgentPolicy" - }, - "examples": { - "example-1": { - "value": { - "name": "UPDATED name", - "description": "UPDATED description", - "namespace": "UPDATED namespace" - } - } - } - } - } - }, - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - } - }, - "/agent_policies/{agentPolicyId}/copy": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "agentPolicyId", - "in": "path", - "required": true - } - ], - "post": { - "summary": "Agent policy - copy one policy", - "operationId": "agent-policy-copy", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "item": { - "$ref": "#/components/schemas/AgentPolicy" - } - }, - "required": ["item"] - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - } - }, - "required": ["name"] - }, - "examples": {} - } - }, - "description": "" - }, - "description": "Copies one agent policy" - } - }, - "/agent_policies/delete": { - "post": { - "summary": "Agent policy - Delete", - "operationId": "post-agent-policy-delete", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "success": { - "type": "boolean" - } - }, - "required": ["id", "success"] - } - }, - "examples": { - "success": { - "value": [ - { - "id": "df7d2540-5a47-11ea-80da-89b5a66da347", - "success": true - } - ] - }, - "fail": { - "value": [ - { - "id": "df7d2540-5a47-11ea-80da-89b5a66da347", - "success": false - } - ] - } - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "agentPolicyIds": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "examples": { - "example-1": { - "value": { - "agentPolicyIds": ["df7d2540-5a47-11ea-80da-89b5a66da347"] - } - } - } - } - } - }, - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - }, - "parameters": [] - }, - "/package_policies": { - "get": { - "summary": "PackagePolicies - List", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PackagePolicy" - } - }, - "total": { - "type": "number" - }, - "page": { - "type": "number" - }, - "perPage": { - "type": "number" - } - }, - "required": ["items"] - }, - "examples": { - "example-1": { - "value": { - "items": [ - { - "id": "5d273cf0-5a44-11ea-80da-89b5a66da347", - "use_output": "default", - "inputs": [ - { - "type": "docker/metrics", - "streams": [ - { - "metricset": "status", - "dataset": "docker.status" - } - ] - }, - { - "type": "logs", - "streams": [ - { - "paths": ["/var/log/hello1.log", "/var/log/hello2.log"] - } - ] - } - ] - }, - { - "id": "66490980-5a44-11ea-80da-89b5a66da347", - "namespace": "testing", - "use_output": "default", - "inputs": [ - { - "type": "apache/metrics", - "streams": [ - { - "enabled": true, - "metricset": "info" - } - ] - } - ] - }, - { - "id": "df1ccae0-5a49-11ea-94a6-81affd263f47", - "enabled": true, - "title": "This is a nice title for human", - "package": { - "name": "epm/nginx", - "version": "1.7.0" - }, - "namespace": "prod", - "use_output": "long_term_storage", - "inputs": [ - { - "type": "logs", - "streams": [ - { - "enabled": true, - "dataset": "nginx.acccess", - "paths": ["/var/log/nginx/access.log"] - }, - { - "enabled": true, - "dataset": "nginx.error", - "paths": ["/var/log/nginx/error.log"] - } - ] - }, - { - "type": "nginx/metrics", - "streams": [ - { - "id": "id string", - "enabled": true, - "dataset": "nginx.stub_status", - "metricset": "stub_status" - } - ] - } - ] - }, - { - "id": "f96a09d0-5a49-11ea-94a6-81affd263f47", - "enabled": true, - "title": "This is a nice title for human", - "package": { - "name": "epm/nginx", - "version": "1.7.0" - }, - "namespace": "prod", - "use_output": "long_term_storage", - "inputs": [ - { - "type": "logs", - "streams": [ - { - "enabled": true, - "dataset": "nginx.acccess", - "paths": ["/var/log/nginx/access.log"] - }, - { - "enabled": true, - "dataset": "nginx.error", - "paths": ["/var/log/nginx/error.log"] - } - ] - }, - { - "type": "nginx/metrics", - "streams": [ - { - "id": "id string", - "enabled": true, - "dataset": "nginx.stub_status", - "metricset": "stub_status" - } - ] - } - ] - }, - { - "id": "9ca403a0-5a66-11ea-9468-c911a41ab4f5", - "enabled": true, - "title": "This is a nice title for human", - "package": { - "name": "epm/nginx", - "version": "1.7.0" - }, - "namespace": "prod", - "use_output": "long_term_storage", - "inputs": [ - { - "type": "logs", - "streams": [ - { - "enabled": true, - "dataset": "nginx.acccess", - "paths": ["/var/log/nginx/access.log"] - }, - { - "enabled": true, - "dataset": "nginx.error", - "paths": ["/var/log/nginx/error.log"] - } - ] - }, - { - "type": "nginx/metrics", - "streams": [ - { - "id": "id string", - "enabled": true, - "dataset": "nginx.stub_status", - "metricset": "stub_status" - } - ] - } - ] - }, - { - "id": "27925980-5a44-11ea-80da-89b5a66da347", - "enabled": true, - "title": "UPDATED title for human", - "package": { - "name": "epm/nginx", - "version": "1.7.0" - }, - "namespace": "prod", - "use_output": "long_term_storage", - "inputs": [ - { - "streams": [ - { - "paths": ["/var/log/nginx/access.log"], - "dataset": "nginx.acccess", - "enabled": true - }, - { - "paths": ["/var/log/nginx/error.log"], - "dataset": "nginx.error", - "enabled": true - } - ], - "type": "logs" - }, - { - "streams": [ - { - "metricset": "stub_status", - "id": "id string", - "dataset": "nginx.stub_status", - "enabled": true - } - ], - "type": "nginx/metrics" - } - ] - } - ], - "total": 6, - "page": 1, - "perPage": 20 - } - } - } - } - } - } - }, - "operationId": "get-packagePolicies", - "security": [], - "parameters": [] - }, - "parameters": [], - "post": { - "summary": "PackagePolicies - Create", - "operationId": "post-packagePolicies", - "responses": { - "200": { - "description": "OK" - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewPackagePolicy" - }, - "examples": { - "example-1": { - "value": { - "enabled": true, - "title": "This is a nice title for human", - "package": { - "name": "epm/nginx", - "version": "1.7.0" - }, - "namespace": "prod", - "use_output": "long_term_storage", - "inputs": [ - { - "type": "logs", - "streams": [ - { - "enabled": true, - "dataset": "nginx.acccess", - "paths": ["/var/log/nginx/access.log"] - }, - { - "enabled": true, - "dataset": "nginx.error", - "paths": ["/var/log/nginx/error.log"] - } - ] - }, - { - "type": "nginx/metrics", - "streams": [ - { - "id": "id string", - "enabled": true, - "dataset": "nginx.stub_status", - "metricset": "stub_status" - } - ] - } - ] - } - } - } - } - } - }, - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - } - }, - "/package_policies/{packagePolicyId}": { - "get": { - "summary": "PackagePolicies - Info", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "item": { - "$ref": "#/components/schemas/PackagePolicy" - } - }, - "required": ["item"] - } - } - } - } - }, - "operationId": "get-packagePolicies-packagePolicyId" - }, - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "packagePolicyId", - "in": "path", - "required": true - } - ], - "put": { - "summary": "PackagePolicies - Update", - "operationId": "put-packagePolicies-packagePolicyId", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "item": { - "$ref": "#/components/schemas/PackagePolicy" - }, - "sucess": { - "type": "boolean" - } - }, - "required": ["item", "sucess"] - } - } - } - } - }, - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - } - }, - "/agents/setup": { - "get": { - "summary": "Agents setup - Info", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "isInitialized": { - "type": "boolean" - } - }, - "required": ["isInitialized"] - }, - "examples": { - "success": { - "value": { - "isInitialized": true - } - }, - "failure": { - "value": { - "isInitialized": false - } - } - } - } - } - } - }, - "operationId": "get-agents-setup", - "security": [ - { - "basicAuth": [] - } - ] - }, - "post": { - "summary": "Agents setup - Create", - "operationId": "post-agents-setup", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "isInitialized": { - "type": "boolean" - } - }, - "required": ["isInitialized"] - }, - "examples": { - "success": { - "value": { - "isInitialized": true - } - } - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "admin_username": { - "type": "string" - }, - "admin_password": { - "type": "string" - } - }, - "required": ["admin_username", "admin_password"] - } - } - } - }, - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - } - }, - "/epm/packages/{pkgkey}": { - "get": { - "summary": "EPM - Packages - Info", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "allOf": [ - { - "properties": { - "response": { - "$ref": "#/components/schemas/PackageInfo" - } - } - }, - { - "properties": { - "status": { - "type": "string", - "enum": ["installed", "not_installed"] - }, - "savedObject": { - "type": "string" - } - }, - "required": ["status", "savedObject"] - } - ] - }, - "examples": { - "example-1": { - "value": { - "response": { - "name": "coredns", - "title": "CoreDNS", - "version": "1.0.1", - "readme": "/package/coredns-1.0.1/docs/README.md", - "description": "CoreDNS logs and metrics integration.\nThe CoreDNS integrations allows to gather logs and metrics from the CoreDNS DNS server to get better insights.\n", - "type": "integration", - "categories": ["logs", "metrics"], - "requirement": { - "kibana": { - "versions": ">6.7.0" - } - }, - "icons": [ - { - "path": "/package/coredns-1.0.1/img/icon.png", - "src": "/img/icon.png", - "size": "1800x1800" - }, - { - "path": "/package/coredns-1.0.1/img/icon.svg", - "src": "/img/icon.svg", - "size": "255x144", - "type": "image/svg+xml" - } - ], - "assets": { - "kibana": { - "dashboard": [ - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "dashboard", - "file": "53aa1f70-443e-11e9-8548-ab7fbe04f038.json", - "path": "coredns-1.0.1/kibana/dashboard/53aa1f70-443e-11e9-8548-ab7fbe04f038.json" - }, - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "dashboard", - "file": "Metricbeat-CoreDNS-Dashboard-ecs.json", - "path": "coredns-1.0.1/kibana/dashboard/Metricbeat-CoreDNS-Dashboard-ecs.json" - } - ], - "visualization": [ - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "visualization", - "file": "277fc650-67a9-11e9-a534-715561d0bf42.json", - "path": "coredns-1.0.1/kibana/visualization/277fc650-67a9-11e9-a534-715561d0bf42.json" - }, - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "visualization", - "file": "27da53f0-53d5-11e9-b466-9be470bbd327-ecs.json", - "path": "coredns-1.0.1/kibana/visualization/27da53f0-53d5-11e9-b466-9be470bbd327-ecs.json" - }, - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "visualization", - "file": "36e08510-53c4-11e9-b466-9be470bbd327-ecs.json", - "path": "coredns-1.0.1/kibana/visualization/36e08510-53c4-11e9-b466-9be470bbd327-ecs.json" - }, - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "visualization", - "file": "3ad75810-4429-11e9-8548-ab7fbe04f038.json", - "path": "coredns-1.0.1/kibana/visualization/3ad75810-4429-11e9-8548-ab7fbe04f038.json" - }, - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "visualization", - "file": "4804eaa0-7315-11e9-b0d0-414c3011ddbb.json", - "path": "coredns-1.0.1/kibana/visualization/4804eaa0-7315-11e9-b0d0-414c3011ddbb.json" - }, - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "visualization", - "file": "57c74300-7308-11e9-b0d0-414c3011ddbb.json", - "path": "coredns-1.0.1/kibana/visualization/57c74300-7308-11e9-b0d0-414c3011ddbb.json" - }, - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "visualization", - "file": "75743f70-443c-11e9-8548-ab7fbe04f038.json", - "path": "coredns-1.0.1/kibana/visualization/75743f70-443c-11e9-8548-ab7fbe04f038.json" - }, - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "visualization", - "file": "86177430-728d-11e9-b0d0-414c3011ddbb.json", - "path": "coredns-1.0.1/kibana/visualization/86177430-728d-11e9-b0d0-414c3011ddbb.json" - }, - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "visualization", - "file": "9dc640e0-4432-11e9-8548-ab7fbe04f038.json", - "path": "coredns-1.0.1/kibana/visualization/9dc640e0-4432-11e9-8548-ab7fbe04f038.json" - }, - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "visualization", - "file": "a19df590-53c4-11e9-b466-9be470bbd327-ecs.json", - "path": "coredns-1.0.1/kibana/visualization/a19df590-53c4-11e9-b466-9be470bbd327-ecs.json" - }, - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "visualization", - "file": "a58345f0-7298-11e9-b0d0-414c3011ddbb.json", - "path": "coredns-1.0.1/kibana/visualization/a58345f0-7298-11e9-b0d0-414c3011ddbb.json" - }, - { - "pkgkey": "coredns-1.0.1", - "service": "kibana", - "type": "visualization", - "file": "cfde7fb0-443d-11e9-8548-ab7fbe04f038.json", - "path": "coredns-1.0.1/kibana/visualization/cfde7fb0-443d-11e9-8548-ab7fbe04f038.json" - } - ] - } - }, - "format_version": "1.0.0", - "data_streams": [ - { - "title": "CoreDNS logs", - "name": "log", - "release": "ga", - "type": "logs", - "ingest_pipeline": "pipeline-entry", - "vars": [ - { - "default": ["/var/log/coredns.log"], - "name": "paths", - "type": "textarea" - }, - { - "default": ["coredns"], - "name": "tags", - "type": "text" - } - ], - "package": "coredns" - }, - { - "title": "CoreDNS stats metrics", - "name": "stats", - "release": "ga", - "type": "metrics", - "vars": [ - { - "default": ["http://localhost:9153"], - "description": "CoreDNS hosts", - "name": "hosts", - "required": true - }, - { - "default": "10s", - "description": "Collection period. Valid values: 10s, 5m, 2h", - "name": "period" - }, - { - "name": "username", - "type": "text" - }, - { - "name": "password", - "type": "password" - } - ], - "package": "coredns" - } - ], - "download": "/epr/coredns/coredns-1.0.1.tar.gz", - "path": "/package/coredns-1.0.1", - "status": "installed", - "savedObject": { - "id": "coredns-1.0.1", - "type": "epm-package", - "updated_at": "2020-02-27T16:25:43.652Z", - "version": "WzU2LDFd", - "attributes": { - "installed": [ - { - "id": "53aa1f70-443e-11e9-8548-ab7fbe04f038", - "type": "dashboard" - }, - { - "id": "Metricbeat-CoreDNS-Dashboard-ecs", - "type": "dashboard" - }, - { - "id": "75743f70-443c-11e9-8548-ab7fbe04f038", - "type": "visualization" - }, - { - "id": "36e08510-53c4-11e9-b466-9be470bbd327-ecs", - "type": "visualization" - }, - { - "id": "277fc650-67a9-11e9-a534-715561d0bf42", - "type": "visualization" - }, - { - "id": "cfde7fb0-443d-11e9-8548-ab7fbe04f038", - "type": "visualization" - }, - { - "id": "a19df590-53c4-11e9-b466-9be470bbd327-ecs", - "type": "visualization" - }, - { - "id": "a58345f0-7298-11e9-b0d0-414c3011ddbb", - "type": "visualization" - }, - { - "id": "9dc640e0-4432-11e9-8548-ab7fbe04f038", - "type": "visualization" - }, - { - "id": "3ad75810-4429-11e9-8548-ab7fbe04f038", - "type": "visualization" - }, - { - "id": "57c74300-7308-11e9-b0d0-414c3011ddbb", - "type": "visualization" - }, - { - "id": "27da53f0-53d5-11e9-b466-9be470bbd327-ecs", - "type": "visualization" - }, - { - "id": "86177430-728d-11e9-b0d0-414c3011ddbb", - "type": "visualization" - }, - { - "id": "4804eaa0-7315-11e9-b0d0-414c3011ddbb", - "type": "visualization" - }, - { - "id": "logs-log-1.0.1-pipeline-plaintext", - "type": "ingest-pipeline" - }, - { - "id": "logs-log-1.0.1-pipeline-json", - "type": "ingest-pipeline" - }, - { - "id": "logs-log-1.0.1", - "type": "ingest-pipeline" - }, - { - "id": "logs-log", - "type": "index-template" - }, - { - "id": "metrics-stats", - "type": "index-template" - } - ] - }, - "references": [] - } - } - } - }, - "required-package": { - "value": { - "response": { - "format_version": "1.0.0", - "name": "endpoint", - "title": "Elastic Endpoint", - "version": "0.3.0", - "readme": "/package/endpoint/0.3.0/docs/README.md", - "license": "basic", - "description": "This is the Elastic Endpoint package.", - "type": "solution", - "categories": ["security"], - "release": "beta", - "requirement": { - "kibana": { - "versions": ">7.4.0" - } - }, - "icons": [ - { - "path": "/package/endpoint/0.3.0/img/logo-endpoint-64-color.svg", - "src": "/img/logo-endpoint-64-color.svg", - "size": "16x16", - "type": "image/svg+xml" - } - ], - "assets": { - "kibana": { - "dashboard": [ - { - "pkgkey": "endpoint-0.3.0", - "service": "kibana", - "type": "dashboard", - "file": "826759f0-7074-11ea-9bc8-6b38f4d29a16.json", - "path": "endpoint-0.3.0/kibana/dashboard/826759f0-7074-11ea-9bc8-6b38f4d29a16.json" - } - ], - "map": [ - { - "pkgkey": "endpoint-0.3.0", - "service": "kibana", - "type": "map", - "file": "a3a3bd10-706b-11ea-9bc8-6b38f4d29a16.json", - "path": "endpoint-0.3.0/kibana/map/a3a3bd10-706b-11ea-9bc8-6b38f4d29a16.json" - } - ], - "visualization": [ - { - "pkgkey": "endpoint-0.3.0", - "service": "kibana", - "type": "visualization", - "file": "1cfceda0-728b-11ea-9bc8-6b38f4d29a16.json", - "path": "endpoint-0.3.0/kibana/visualization/1cfceda0-728b-11ea-9bc8-6b38f4d29a16.json" - }, - { - "pkgkey": "endpoint-0.3.0", - "service": "kibana", - "type": "visualization", - "file": "1e525190-7074-11ea-9bc8-6b38f4d29a16.json", - "path": "endpoint-0.3.0/kibana/visualization/1e525190-7074-11ea-9bc8-6b38f4d29a16.json" - }, - { - "pkgkey": "endpoint-0.3.0", - "service": "kibana", - "type": "visualization", - "file": "55387750-729c-11ea-9bc8-6b38f4d29a16.json", - "path": "endpoint-0.3.0/kibana/visualization/55387750-729c-11ea-9bc8-6b38f4d29a16.json" - }, - { - "pkgkey": "endpoint-0.3.0", - "service": "kibana", - "type": "visualization", - "file": "92b1edc0-706a-11ea-9bc8-6b38f4d29a16.json", - "path": "endpoint-0.3.0/kibana/visualization/92b1edc0-706a-11ea-9bc8-6b38f4d29a16.json" - } - ] - } - }, - "data_streams": [ - { - "id": "endpoint", - "title": "Endpoint Events", - "release": "experimental", - "type": "events", - "package": "endpoint", - "path": "events" - }, - { - "id": "endpoint.metadata", - "title": "Endpoint Metadata", - "release": "experimental", - "type": "metrics", - "package": "endpoint", - "path": "metadata" - }, - { - "id": "endpoint.policy", - "title": "Endpoint Policy Response", - "release": "experimental", - "type": "metrics", - "package": "endpoint", - "path": "policy" - }, - { - "id": "endpoint.telemetry", - "title": "Endpoint Telemetry", - "release": "experimental", - "type": "metrics", - "package": "endpoint", - "path": "telemetry" - } - ], - "packagePolicies": [ - { - "name": "endpoint", - "title": "Endpoint package policy", - "description": "Interact with the endpoint.", - "inputs": null, - "multiple": false - } - ], - "download": "/epr/endpoint/endpoint-0.3.0.tar.gz", - "path": "/package/endpoint/0.3.0", - "latestVersion": "0.3.0", - "removable": false, - "status": "installed", - "savedObject": { - "id": "endpoint", - "type": "epm-packages", - "updated_at": "2020-06-23T21:44:59.319Z", - "version": "Wzk4LDFd", - "attributes": { - "installed": [ - { - "id": "826759f0-7074-11ea-9bc8-6b38f4d29a16", - "type": "dashboard" - }, - { - "id": "1cfceda0-728b-11ea-9bc8-6b38f4d29a16", - "type": "visualization" - }, - { - "id": "1e525190-7074-11ea-9bc8-6b38f4d29a16", - "type": "visualization" - }, - { - "id": "55387750-729c-11ea-9bc8-6b38f4d29a16", - "type": "visualization" - }, - { - "id": "92b1edc0-706a-11ea-9bc8-6b38f4d29a16", - "type": "visualization" - }, - { - "id": "a3a3bd10-706b-11ea-9bc8-6b38f4d29a16", - "type": "map" - }, - { - "id": "events-endpoint", - "type": "index-template" - }, - { - "id": "metrics-endpoint.metadata", - "type": "index-template" - }, - { - "id": "metrics-endpoint.policy", - "type": "index-template" - }, - { - "id": "metrics-endpoint.telemetry", - "type": "index-template" - } - ], - "es_index_patterns": { - "events": "events-endpoint-*", - "metadata": "metrics-endpoint.metadata-*", - "policy": "metrics-endpoint.policy-*", - "telemetry": "metrics-endpoint.telemetry-*" - }, - "name": "endpoint", - "version": "0.3.0", - "internal": false, - "removable": false - }, - "references": [] - } - } - } - } - } - } - } - } - }, - "operationId": "get-epm-package-pkgkey", - "security": [ - { - "basicAuth": [] - } - ] - }, - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "pkgkey", - "in": "path", - "required": true - } - ], - "post": { - "summary": "EPM - Packages - Install", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "response": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": ["id", "type"] - } - } - }, - "required": ["response"] - } - } - } - } - }, - "operationId": "post-epm-install-pkgkey", - "description": "", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - }, - "delete": { - "summary": "EPM - Packages - Delete", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "response": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": ["id", "type"] - } - } - }, - "required": ["response"] - } - } - } - } - }, - "operationId": "post-epm-delete-pkgkey", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - } - }, - "/epm/packages": { - "get": { - "summary": "EPM - Packages - List", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SearchResult" - } - }, - "examples": { - "success": { - "value": { - "response": [ - { - "description": "aws Integration", - "download": "/epr/aws/aws-0.0.3.tar.gz", - "icons": [ - { - "path": "/package/aws/0.0.3/img/logo_aws.svg", - "src": "/img/logo_aws.svg", - "title": "logo aws", - "size": "32x32", - "type": "image/svg+xml" - } - ], - "name": "aws", - "path": "/package/aws/0.0.3", - "title": "aws", - "type": "integration", - "version": "0.0.3", - "status": "not_installed" - }, - { - "description": "This is the Elastic Endpoint package.", - "download": "/epr/endpoint/endpoint-0.1.0.tar.gz", - "icons": [ - { - "path": "/package/endpoint/0.1.0/img/logo-endpoint-64-color.svg", - "src": "/img/logo-endpoint-64-color.svg", - "size": "16x16", - "type": "image/svg+xml" - } - ], - "name": "endpoint", - "path": "/package/endpoint/0.1.0", - "title": "Elastic Endpoint", - "type": "solution", - "version": "0.1.0", - "status": "installed", - "savedObject": { - "type": "epm-packages", - "id": "endpoint", - "attributes": { - "installed": [ - { - "id": "826759f0-7074-11ea-9bc8-6b38f4d29a16", - "type": "dashboard" - }, - { - "id": "55387750-729c-11ea-9bc8-6b38f4d29a16", - "type": "visualization" - }, - { - "id": "92b1edc0-706a-11ea-9bc8-6b38f4d29a16", - "type": "visualization" - }, - { - "id": "1cfceda0-728b-11ea-9bc8-6b38f4d29a16", - "type": "visualization" - }, - { - "id": "1e525190-7074-11ea-9bc8-6b38f4d29a16", - "type": "visualization" - }, - { - "id": "a3a3bd10-706b-11ea-9bc8-6b38f4d29a16", - "type": "map" - }, - { - "id": "events-endpoint", - "type": "index-template" - }, - { - "id": "metrics-endpoint", - "type": "index-template" - } - ], - "es_index_patterns": { - "events": "events-endpoint-*", - "metadata": "metrics-endpoint-*" - }, - "name": "endpoint", - "version": "0.1.0", - "internal": false, - "removable": false - }, - "references": [], - "updated_at": "2020-05-15T20:08:11.739Z", - "version": "WzEwOCwxXQ==" - } - }, - { - "description": "The log package should be used to create package policies for all type of logs for which an package doesn't exist yet.\n", - "download": "/epr/log/log-0.9.0.tar.gz", - "icons": [ - { - "path": "/package/log/0.9.0/img/icon.svg", - "src": "/img/icon.svg", - "type": "image/svg+xml" - } - ], - "name": "log", - "path": "/package/log/0.9.0", - "title": "Log Package", - "type": "integration", - "version": "0.9.0", - "status": "not_installed" - }, - { - "description": "This integration contains pretty long documentation.\nIt is used to show the different visualisations inside a documentation to test how we handle it.\nThe integration does not contain any assets except the documentation page.\n", - "download": "/epr/longdocs/longdocs-1.0.4.tar.gz", - "icons": [ - { - "path": "/package/longdocs/1.0.4/img/icon.svg", - "src": "/img/icon.svg", - "type": "image/svg+xml" - } - ], - "name": "longdocs", - "path": "/package/longdocs/1.0.4", - "title": "Long Docs", - "type": "integration", - "version": "1.0.4", - "status": "not_installed" - }, - { - "description": "This is an integration with only the metrics category.\n", - "download": "/epr/metricsonly/metricsonly-2.0.1.tar.gz", - "icons": [ - { - "path": "/package/metricsonly/2.0.1/img/icon.svg", - "src": "/img/icon.svg", - "type": "image/svg+xml" - } - ], - "name": "metricsonly", - "path": "/package/metricsonly/2.0.1", - "title": "Metrics Only", - "type": "integration", - "version": "2.0.1", - "status": "not_installed" - }, - { - "description": "Multiple versions of this integration exist.\n", - "download": "/epr/multiversion/multiversion-1.1.0.tar.gz", - "icons": [ - { - "path": "/package/multiversion/1.1.0/img/icon.svg", - "src": "/img/icon.svg", - "type": "image/svg+xml" - } - ], - "name": "multiversion", - "path": "/package/multiversion/1.1.0", - "title": "Multi Version", - "type": "integration", - "version": "1.1.0", - "status": "not_installed" - }, - { - "description": "MySQL Integration", - "download": "/epr/mysql/mysql-0.1.0.tar.gz", - "icons": [ - { - "path": "/package/mysql/0.1.0/img/logo_mysql.svg", - "src": "/img/logo_mysql.svg", - "title": "logo mysql", - "size": "32x32", - "type": "image/svg+xml" - } - ], - "name": "mysql", - "path": "/package/mysql/0.1.0", - "title": "MySQL", - "type": "integration", - "version": "0.1.0", - "status": "not_installed" - }, - { - "description": "Nginx Integration", - "download": "/epr/nginx/nginx-0.1.0.tar.gz", - "icons": [ - { - "path": "/package/nginx/0.1.0/img/logo_nginx.svg", - "src": "/img/logo_nginx.svg", - "title": "logo nginx", - "size": "32x32", - "type": "image/svg+xml" - } - ], - "name": "nginx", - "path": "/package/nginx/0.1.0", - "title": "Nginx", - "type": "integration", - "version": "0.1.0", - "status": "not_installed" - }, - { - "description": "Redis Integration", - "download": "/epr/redis/redis-0.1.0.tar.gz", - "icons": [ - { - "path": "/package/redis/0.1.0/img/logo_redis.svg", - "src": "/img/logo_redis.svg", - "title": "logo redis", - "size": "32x32", - "type": "image/svg+xml" - } - ], - "name": "redis", - "path": "/package/redis/0.1.0", - "title": "Redis", - "type": "integration", - "version": "0.1.0", - "status": "not_installed" - }, - { - "description": "This package is used for defining all the properties of a package, the possible assets etc. It serves as a reference on all the config options which are possible.\n", - "download": "/epr/reference/reference-1.0.0.tar.gz", - "icons": [ - { - "path": "/package/reference/1.0.0/img/icon.svg", - "src": "/img/icon.svg", - "size": "32x32", - "type": "image/svg+xml" - } - ], - "name": "reference", - "path": "/package/reference/1.0.0", - "title": "Reference package", - "type": "integration", - "version": "1.0.0", - "status": "not_installed" - }, - { - "description": "System Integration", - "download": "/epr/system/system-0.1.0.tar.gz", - "icons": [ - { - "path": "/package/system/0.1.0/img/system.svg", - "src": "/img/system.svg", - "title": "system", - "size": "1000x1000", - "type": "image/svg+xml" - } - ], - "name": "system", - "path": "/package/system/0.1.0", - "title": "System", - "type": "integration", - "version": "0.1.0", - "status": "installed", - "savedObject": { - "type": "epm-packages", - "id": "system", - "attributes": { - "installed": [ - { - "id": "c431f410-f9ac-11e9-90e8-1fb18e796788", - "type": "dashboard" - }, - { - "id": "Metricbeat-system-overview-ecs", - "type": "dashboard" - }, - { - "id": "277876d0-fa2c-11e6-bbd3-29c986c96e5a-ecs", - "type": "dashboard" - }, - { - "id": "0d3f2380-fa78-11e6-ae9b-81e5311e8cab-ecs", - "type": "dashboard" - }, - { - "id": "CPU-slash-Memory-per-container-ecs", - "type": "dashboard" - }, - { - "id": "79ffd6e0-faa0-11e6-947f-177f697178b8-ecs", - "type": "dashboard" - }, - { - "id": "Filebeat-syslog-dashboard-ecs", - "type": "dashboard" - }, - { - "id": "5517a150-f9ce-11e6-8115-a7c18106d86a-ecs", - "type": "dashboard" - }, - { - "id": "9c69cad0-f9b0-11e9-90e8-1fb18e796788", - "type": "visualization" - }, - { - "id": "855899e0-1b1c-11e7-b09e-037021c4f8df-ecs", - "type": "visualization" - }, - { - "id": "a30871f0-f98f-11e9-90e8-1fb18e796788", - "type": "visualization" - }, - { - "id": "e121b140-fa78-11e6-a1df-a78bd7504d38-ecs", - "type": "visualization" - }, - { - "id": "f398d2f0-fa77-11e6-ae9b-81e5311e8cab-ecs", - "type": "visualization" - }, - { - "id": "c5e3cf90-4d60-11e7-9a4c-ed99bbcaa42b-ecs", - "type": "visualization" - }, - { - "id": "d3166e80-1b91-11e7-bec4-a5e9ec5cab8b-ecs", - "type": "visualization" - }, - { - "id": "346bb290-fa80-11e6-a1df-a78bd7504d38-ecs", - "type": "visualization" - }, - { - "id": "Container-Block-IO-ecs", - "type": "visualization" - }, - { - "id": "590a60f0-5d87-11e7-8884-1bb4c3b890e4-ecs", - "type": "visualization" - }, - { - "id": "341ffe70-f9ce-11e6-8115-a7c18106d86a-ecs", - "type": "visualization" - }, - { - "id": "System-Navigation-ecs", - "type": "visualization" - }, - { - "id": "089b85d0-1b16-11e7-b09e-037021c4f8df-ecs", - "type": "visualization" - }, - { - "id": "99381c80-4d60-11e7-9a4c-ed99bbcaa42b-ecs", - "type": "visualization" - }, - { - "id": "c6f2ffd0-4d17-11e7-a196-69b9a7a020a9-ecs", - "type": "visualization" - }, - { - "id": "d56ee420-fa79-11e6-a1df-a78bd7504d38-ecs", - "type": "visualization" - }, - { - "id": "1aae9140-1b93-11e7-8ada-3df93aab833e-ecs", - "type": "visualization" - }, - { - "id": "e0f001c0-1b18-11e7-b09e-037021c4f8df-ecs", - "type": "visualization" - }, - { - "id": "dc589770-fa2b-11e6-bbd3-29c986c96e5a-ecs", - "type": "visualization" - }, - { - "id": "96976150-4d5d-11e7-aa29-87a97a796de6-ecs", - "type": "visualization" - }, - { - "id": "8c071e20-f999-11e9-90e8-1fb18e796788", - "type": "visualization" - }, - { - "id": "d3f51850-f9b6-11e9-90e8-1fb18e796788", - "type": "visualization" - }, - { - "id": "5c7af030-fa2a-11e6-bbd3-29c986c96e5a-ecs", - "type": "visualization" - }, - { - "id": "e6e639e0-f992-11e9-90e8-1fb18e796788", - "type": "visualization" - }, - { - "id": "bfa5e400-1b16-11e7-b09e-037021c4f8df-ecs", - "type": "visualization" - }, - { - "id": "7cdb1330-4d1a-11e7-a196-69b9a7a020a9-ecs", - "type": "visualization" - }, - { - "id": "78b74f30-f9cd-11e6-8115-a7c18106d86a-ecs", - "type": "visualization" - }, - { - "id": "Syslog-events-by-hostname-ecs", - "type": "visualization" - }, - { - "id": "3d65d450-a9c3-11e7-af20-67db8aecb295-ecs", - "type": "visualization" - }, - { - "id": "ab2d1e90-1b1a-11e7-b09e-037021c4f8df-ecs", - "type": "visualization" - }, - { - "id": "825fdb80-4d1d-11e7-b5f2-2b7c1895bf32-ecs", - "type": "visualization" - }, - { - "id": "26732e20-1b91-11e7-bec4-a5e9ec5cab8b-ecs", - "type": "visualization" - }, - { - "id": "Syslog-hostnames-and-processes-ecs", - "type": "visualization" - }, - { - "id": "522ee670-1b92-11e7-bec4-a5e9ec5cab8b-ecs", - "type": "visualization" - }, - { - "id": "51164310-fa2b-11e6-bbd3-29c986c96e5a-ecs", - "type": "visualization" - }, - { - "id": "bb3a8720-f991-11e9-90e8-1fb18e796788", - "type": "visualization" - }, - { - "id": "Container-Memory-stats-ecs", - "type": "visualization" - }, - { - "id": "5dd15c00-fa78-11e6-ae9b-81e5311e8cab-ecs", - "type": "visualization" - }, - { - "id": "327417e0-8462-11e7-bab8-bd2f0fb42c54-ecs", - "type": "visualization" - }, - { - "id": "d2e80340-4d5c-11e7-aa29-87a97a796de6-ecs", - "type": "visualization" - }, - { - "id": "19e123b0-4d5a-11e7-aee5-fdc812cc3bec-ecs", - "type": "visualization" - }, - { - "id": "3cec3eb0-f9d3-11e6-8a3e-2b904044ea1d-ecs", - "type": "visualization" - }, - { - "id": "2e224660-1b19-11e7-b09e-037021c4f8df-ecs", - "type": "visualization" - }, - { - "id": "12667040-fa80-11e6-a1df-a78bd7504d38-ecs", - "type": "visualization" - }, - { - "id": "d16bb400-f9cc-11e6-8115-a7c18106d86a-ecs", - "type": "visualization" - }, - { - "id": "34f97ee0-1b96-11e7-8ada-3df93aab833e-ecs", - "type": "visualization" - }, - { - "id": "fe064790-1b1f-11e7-bec4-a5e9ec5cab8b-ecs", - "type": "visualization" - }, - { - "id": "83e12df0-1b91-11e7-bec4-a5e9ec5cab8b-ecs", - "type": "visualization" - }, - { - "id": "4e4bb1e0-1b1b-11e7-b09e-037021c4f8df-ecs", - "type": "visualization" - }, - { - "id": "4b254630-f998-11e9-90e8-1fb18e796788", - "type": "visualization" - }, - { - "id": "6b7b9a40-faa1-11e6-86b1-cd7735ff7e23-ecs", - "type": "visualization" - }, - { - "id": "4d546850-1b15-11e7-b09e-037021c4f8df-ecs", - "type": "visualization" - }, - { - "id": "Container-CPU-usage-ecs", - "type": "visualization" - }, - { - "id": "b6f321e0-fa25-11e6-bbd3-29c986c96e5a-ecs", - "type": "search" - }, - { - "id": "62439dc0-f9c9-11e6-a747-6121780e0414-ecs", - "type": "search" - }, - { - "id": "8030c1b0-fa77-11e6-ae9b-81e5311e8cab-ecs", - "type": "search" - }, - { - "id": "Syslog-system-logs-ecs", - "type": "search" - }, - { - "id": "eb0039f0-fa7f-11e6-a1df-a78bd7504d38-ecs", - "type": "search" - }, - { - "id": "logs-system.auth-0.1.0", - "type": "ingest-pipeline" - }, - { - "id": "logs-system.auth-0.1.0", - "type": "ingest-pipeline" - }, - { - "id": "logs-system.syslog-0.1.0", - "type": "ingest-pipeline" - }, - { - "id": "logs-system.syslog-0.1.0", - "type": "ingest-pipeline" - }, - { - "id": "logs-system.auth", - "type": "index-template" - }, - { - "id": "metrics-system.core", - "type": "index-template" - }, - { - "id": "metrics-system.cpu", - "type": "index-template" - }, - { - "id": "metrics-system.diskio", - "type": "index-template" - }, - { - "id": "metrics-system.entropy", - "type": "index-template" - }, - { - "id": "metrics-system.filesystem", - "type": "index-template" - }, - { - "id": "metrics-system.fsstat", - "type": "index-template" - }, - { - "id": "metrics-system.load", - "type": "index-template" - }, - { - "id": "metrics-system.memory", - "type": "index-template" - }, - { - "id": "metrics-system.network", - "type": "index-template" - }, - { - "id": "metrics-system.network_summary", - "type": "index-template" - }, - { - "id": "metrics-system.process", - "type": "index-template" - }, - { - "id": "metrics-system.process_summary", - "type": "index-template" - }, - { - "id": "metrics-system.raid", - "type": "index-template" - }, - { - "id": "metrics-system.service", - "type": "index-template" - }, - { - "id": "metrics-system.socket", - "type": "index-template" - }, - { - "id": "metrics-system.socket_summary", - "type": "index-template" - }, - { - "id": "logs-system.syslog", - "type": "index-template" - }, - { - "id": "metrics-system.uptime", - "type": "index-template" - }, - { - "id": "metrics-system.users", - "type": "index-template" - } - ], - "es_index_patterns": { - "auth": "logs-system.auth-*", - "core": "metrics-system.core-*", - "cpu": "metrics-system.cpu-*", - "diskio": "metrics-system.diskio-*", - "entropy": "metrics-system.entropy-*", - "filesystem": "metrics-system.filesystem-*", - "fsstat": "metrics-system.fsstat-*", - "load": "metrics-system.load-*", - "memory": "metrics-system.memory-*", - "network": "metrics-system.network-*", - "network_summary": "metrics-system.network_summary-*", - "process": "metrics-system.process-*", - "process_summary": "metrics-system.process_summary-*", - "raid": "metrics-system.raid-*", - "service": "metrics-system.service-*", - "socket": "metrics-system.socket-*", - "socket_summary": "metrics-system.socket_summary-*", - "syslog": "logs-system.syslog-*", - "uptime": "metrics-system.uptime-*", - "users": "metrics-system.users-*" - }, - "name": "system", - "version": "0.1.0", - "internal": false, - "removable": false - }, - "references": [], - "updated_at": "2020-05-15T20:08:08.708Z", - "version": "Wzk4LDFd" - } - }, - { - "description": "This package contains a yaml pipeline.\n", - "download": "/epr/yamlpipeline/yamlpipeline-1.0.0.tar.gz", - "name": "yamlpipeline", - "path": "/package/yamlpipeline/1.0.0", - "title": "Yaml Pipeline package", - "type": "integration", - "version": "1.0.0", - "status": "not_installed" - } - ] - } - } - } - } - } - } - }, - "operationId": "get-epm-list" - }, - "parameters": [] - }, - "/epm/categories": { - "get": { - "summary": "EPM - Categories", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "title": { - "type": "string" - }, - "count": { - "type": "number" - } - }, - "required": ["id", "title", "count"] - } - } - } - } - } - }, - "operationId": "get-epm-categories" - } - }, - "/fleet/agents": { - "get": { - "summary": "Fleet - Agent - List", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "list": { - "type": "array", - "items": { - "type": "object" - } - }, - "total": { - "type": "number" - }, - "page": { - "type": "number" - }, - "perPage": { - "type": "number" - } - }, - "required": ["list", "total", "page", "perPage"] - }, - "examples": { - "example-1": { - "value": { - "list": [ - { - "id": "205661d0-5e53-11ea-ad31-4f31c06bd9a4", - "active": true, - "policy_id": "ae556400-5e39-11ea-8b49-f9747e466f7b", - "type": "PERMANENT", - "enrolled_at": "2020-03-04T20:02:50.605Z", - "user_provided_metadata": { - "dev_agent_version": "0.0.1", - "region": "us-east" - }, - "local_metadata": { - "host": "localhost", - "ip": "127.0.0.1", - "system": "Darwin 18.7.0", - "memory": 34359738368 - }, - "actions": [ - { - "data": "{\"config\":{\"id\":\"ae556400-5e39-11ea-8b49-f9747e466f7b\",\"outputs\":{\"default\":{\"type\":\"elasticsearch\",\"hosts\":[\"http://localhost:9200\"],\"api_key\":\"\",\"api_token\":\"6ckkp3ABz7e_XRqr3LM8:gQuDfUNSRgmY0iziYqP9Hw\"}},\"packagePolicies\":[]}}", - "created_at": "2020-03-04T20:02:56.149Z", - "id": "6a95c00a-d76d-4931-97c3-0bf935272d7d", - "type": "POLICY_CHANGE" - } - ], - "access_api_key_id": "6Mkkp3ABz7e_XRqrzLNJ", - "default_api_key": "6ckkp3ABz7e_XRqr3LM8:gQuDfUNSRgmY0iziYqP9Hw", - "current_error_events": [], - "last_checkin": "2020-03-04T20:03:05.700Z", - "status": "online" - } - ], - "total": 1, - "page": 1, - "perPage": 20 - } - } - } - } - } - } - }, - "operationId": "get-fleet-agents", - "security": [ - { - "basicAuth": [] - } - ] - } - }, - "/fleet/agents/{agentId}": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "agentId", - "in": "path", - "required": true - } - ], - "get": { - "summary": "Fleet - Agent - Info", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "item": { - "type": "object" - } - }, - "required": ["item"] - } - } - } - } - }, - "operationId": "get-fleet-agents-agentId" - }, - "put": { - "summary": "Fleet - Agent - Update", - "tags": [], - "responses": {}, - "operationId": "put-fleet-agents-agentId", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - }, - "delete": { - "summary": "Fleet - Agent - Delete", - "tags": [], - "responses": {}, - "operationId": "delete-fleet-agents-agentId", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - } - }, - "/fleet/agents/{agentId}/events": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "agentId", - "in": "path", - "required": true - } - ], - "get": { - "summary": "Fleet - Agent - Events", - "tags": [], - "responses": {}, - "operationId": "get-fleet-agents-agentId-events" - } - }, - "/fleet/agents/{agentId}/checkin": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "agentId", - "in": "path", - "required": true - } - ], - "post": { - "summary": "Fleet - Agent - Check In", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": ["checkin"] - }, - "actions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "agent_id": { - "type": "string" - }, - "data": { - "type": "object" - }, - "id": { - "type": "string" - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "type": { - "type": "string" - } - }, - "required": ["agent_id", "data", "id", "created_at", "type"] - } - } - } - }, - "examples": { - "success": { - "value": { - "action": "checkin", - "actions": [ - { - "agent_id": "a6f14bd2-1a2a-481c-9212-9494d064ffdf", - "type": "POLICY_CHANGE", - "data": { - "config": { - "id": "2fe89350-a5e0-11ea-a587-5f886c8a849f", - "outputs": { - "default": { - "type": "elasticsearch", - "hosts": ["http://localhost:9200"], - "api_key": "Z-XkgHIBvwtjzIKtSCTh:AejRqdKpQx6z-6dqSI1LHg" - } - }, - "packagePolicies": [ - { - "id": "33d6bd70-a5e0-11ea-a587-5f886c8a849f", - "name": "system-1", - "namespace": "default", - "enabled": true, - "use_output": "default", - "inputs": [ - { - "type": "logs", - "enabled": true, - "streams": [ - { - "id": "logs-system.auth", - "enabled": true, - "dataset": "system.auth", - "paths": ["/var/log/auth.log*", "/var/log/secure*"], - "exclude_files": [".gz$"], - "multiline": { - "pattern": "^\\s", - "match": "after" - }, - "processors": [ - { - "add_locale": null - }, - { - "add_fields": { - "target": "", - "fields": { - "ecs.version": "1.5.0" - } - } - } - ] - }, - { - "id": "logs-system.syslog", - "enabled": true, - "dataset": "system.syslog", - "paths": ["/var/log/messages*", "/var/log/syslog*"], - "exclude_files": [".gz$"], - "multiline": { - "pattern": "^\\s", - "match": "after" - }, - "processors": [ - { - "add_locale": null - }, - { - "add_fields": { - "target": "", - "fields": { - "ecs.version": "1.5.0" - } - } - } - ] - } - ] - }, - { - "type": "system/metrics", - "enabled": true, - "streams": [ - { - "id": "system/metrics-system.core", - "enabled": true, - "dataset": "system.core", - "metricsets": ["core"], - "core.metrics": "percentages" - }, - { - "id": "system/metrics-system.cpu", - "enabled": true, - "dataset": "system.cpu", - "metricsets": ["cpu"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - }, - { - "id": "system/metrics-system.diskio", - "enabled": true, - "dataset": "system.diskio", - "metricsets": ["diskio"] - }, - { - "id": "system/metrics-system.entropy", - "enabled": true, - "dataset": "system.entropy", - "metricsets": ["entropy"] - }, - { - "id": "system/metrics-system.filesystem", - "enabled": true, - "dataset": "system.filesystem", - "metricsets": ["filesystem"], - "period": "1m", - "processors": [ - { - "drop_event.when.regexp": { - "system.filesystem.mount_point": "^/(sys|cgroup|proc|dev|etc|host|lib|snap)($|/)" - } - } - ] - }, - { - "id": "system/metrics-system.fsstat", - "enabled": true, - "dataset": "system.fsstat", - "metricsets": ["fsstat"], - "period": "1m", - "processors": [ - { - "drop_event.when.regexp": { - "system.filesystem.mount_point": "^/(sys|cgroup|proc|dev|etc|host|lib|snap)($|/)" - } - } - ] - }, - { - "id": "system/metrics-system.load", - "enabled": true, - "dataset": "system.load", - "metricsets": ["load"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - }, - { - "id": "system/metrics-system.memory", - "enabled": true, - "dataset": "system.memory", - "metricsets": ["memory"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - }, - { - "id": "system/metrics-system.network", - "enabled": true, - "dataset": "system.network", - "metricsets": ["network"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - }, - { - "id": "system/metrics-system.network_summary", - "enabled": true, - "dataset": "system.network_summary", - "metricsets": ["network_summary"] - }, - { - "id": "system/metrics-system.process", - "enabled": true, - "dataset": "system.process", - "metricsets": ["process"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - }, - { - "id": "system/metrics-system.process_summary", - "enabled": true, - "dataset": "system.process_summary", - "metricsets": ["process_summary"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - }, - { - "id": "system/metrics-system.raid", - "enabled": true, - "dataset": "system.raid", - "metricsets": ["raid"] - }, - { - "id": "system/metrics-system.service", - "enabled": true, - "dataset": "system.service", - "metricsets": ["service"] - }, - { - "id": "system/metrics-system.socket", - "enabled": true, - "dataset": "system.socket", - "metricsets": ["socket"] - }, - { - "id": "system/metrics-system.socket_summary", - "enabled": true, - "dataset": "system.socket_summary", - "metricsets": ["socket_summary"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "process.include_top_n.by_cpu": 5, - "process.include_top_n.by_memory": 5, - "processes": ".*" - }, - { - "id": "system/metrics-system.uptime", - "enabled": true, - "dataset": "system.uptime", - "metricsets": ["uptime"], - "core.metrics": "percentages", - "cpu.metrics": "percentages,normalized_percentages", - "period": "10s", - "processes": ".*" - }, - { - "id": "system/metrics-system.users", - "enabled": true, - "dataset": "system.users", - "metricsets": ["users"] - } - ] - } - ], - "package": { - "name": "system", - "version": "0.1.0" - } - }, - { - "id": "fdb1fea0-a5f6-11ea-ad52-534e35d3cd6f", - "name": "endpoint-1", - "namespace": "default", - "enabled": true, - "use_output": "default", - "inputs": [], - "package": { - "name": "endpoint", - "version": "0.2.0" - } - }, - { - "id": "2d792280-a5f7-11ea-ad52-534e35d3cd6f", - "name": "endpoint-2", - "namespace": "default", - "enabled": true, - "use_output": "default", - "inputs": [], - "package": { - "name": "endpoint", - "version": "0.2.0" - } - } - ], - "revision": 4, - "settings": { - "monitoring": { - "use_output": "default", - "enabled": true, - "logs": true, - "metrics": true - } - } - } - }, - "id": "51c6ad1e-a9c0-4c70-80da-99a5c51eedaf", - "created_at": "2020-06-04T19:52:24.667Z" - } - ] - } - } - } - } - } - } - }, - "operationId": "post-fleet-agents-agentId-checkin", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ], - "security": [ - { - "Access API Key": [] - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "local_metadata": { - "$ref": "#/components/schemas/AgentMetadata" - }, - "events": { - "type": "array", - "items": { - "$ref": "#/components/schemas/NewAgentEvent" - } - } - } - }, - "examples": { - "stoped to starting": { - "value": { - "events": [ - { - "type": "STATE", - "subtype": "STARTING", - "message": "state changed from STOPPED to STARTING", - "timestamp": "2019-10-01T13:42:54.323Z", - "payload": {}, - "agent_id": "bee40627-8cbd-45df-add9-98c390f9db10" - } - ] - } - }, - "running": { - "value": { - "events": [ - { - "type": "STATE", - "subtype": "RUNNING", - "message": "state changed from STOPPED to RUNNING", - "timestamp": "2020-05-26T20:44:57.480Z", - "payload": { - "random": "data", - "state": "RUNNING", - "previous_state": "STOPPED" - }, - "agent_id": "bee40627-8cbd-45df-add9-98c390f9db10" - } - ] - } - } - } - } - } - } - } - }, - "/fleet/agents/{agentId}/acks": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "agentId", - "in": "path", - "required": true - } - ], - "post": { - "summary": "Fleet - Agent - Acks", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": ["acks"] - } - }, - "required": ["action"] - }, - "examples": { - "success": { - "value": { - "action": "checkin" - } - } - } - } - } - } - }, - "operationId": "post-fleet-agents-agentId-acks", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {} - }, - "examples": { - "example-1": { - "value": { - "events": [ - { - "type": "ACTION_RESULT", - "subtype": "CONFIG", - "timestamp": "2019-01-04T14:32:03.36764-05:00", - "action_id": "51c6ad1e-a9c0-4c70-80da-99a5c51eedaf", - "agent_id": "a6f14bd2-1a2a-481c-9212-9494d064ffdf", - "message": "acknowledge" - } - ] - } - } - } - } - } - } - } - }, - "/fleet/agents/enroll": { - "post": { - "summary": "Fleet - Agent - Enroll", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "action": { - "type": "string" - }, - "item": { - "$ref": "#/components/schemas/Agent" - } - } - }, - "examples": { - "success": { - "value": { - "action": "created", - "item": { - "id": "8086fb1a-72ca-4a67-8533-09300c1639fa", - "active": true, - "policy_id": "2fe89350-a5e0-11ea-a587-5f886c8a849f", - "type": "PERMANENT", - "enrolled_at": "2020-06-04T13:03:57.856Z", - "user_provided_metadata": { - "dev_agent_version": "0.0.1", - "region": "us-east" - }, - "local_metadata": { - "host": "localhost", - "ip": "127.0.0.1", - "system": "Darwin 18.7.0", - "memory": 34359738368 - }, - "current_error_events": [], - "access_api_key": "cU9KdWYzSUJ2d3RqeklLdFdnNF86ZW05ZjFrMThUWW1GRW13OHMwRGZvdw==", - "status": "error" - } - } - } - } - } - } - } - }, - "operationId": "post-fleet-agents-enroll", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["PERMANENT", "EPHEMERAL", "TEMPORARY"] - }, - "shared_id": { - "type": "string" - }, - "metadata": { - "type": "object", - "required": ["local", "user_provided"], - "properties": { - "local": { - "$ref": "#/components/schemas/AgentMetadata" - }, - "user_provided": { - "$ref": "#/components/schemas/AgentMetadata" - } - } - } - }, - "required": ["type", "metadata"] - }, - "examples": { - "good": { - "value": { - "type": "PERMANENT", - "metadata": { - "local": { - "host": "localhost", - "ip": "127.0.0.1", - "system": "Darwin 18.7.0", - "memory": 34359738368 - }, - "user_provided": { - "dev_agent_version": "0.0.1", - "region": "us-east" - } - } - } - } - } - } - } - }, - "security": [ - { - "Enrollment API Key": [] - } - ] - } - }, - "/fleet/agents/{agentId}/unenroll": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "agentId", - "in": "path", - "required": true - } - ], - "post": { - "summary": "Fleet - Agent - Unenroll", - "tags": [], - "responses": {}, - "operationId": "post-fleet-agents-unenroll", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "force": { "type": "boolean" } - } - }, - "examples": { - "example-1": { - "value": { - "force": true - } - } - } - } - } - } - } - }, - "/fleet/agents/{agentId}/upgrade": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "agentId", - "in": "path", - "required": true - } - ], - "post": { - "summary": "Fleet - Agent - Upgrade", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpgradeAgent" - }, - "examples": { - "success": { - "value": {} - } - } - } - } - }, - "400": { - "description": "BAD REQUEST", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpgradeAgent" - }, - "examples": { - "bad request not upgradeable": { - "value": { - "statusCode": 400, - "error": "Bad Request", - "message": "agent d133b07d-5c2b-42f0-8e6b-bbae53bdce88 is not upgradeable" - } - }, - "bad request kibana version": { - "value": { - "statusCode": 400, - "error": "Bad Request", - "message": "cannot upgrade agent to 8.0.0 because it is different than the installed kibana version 7.9.10" - } - }, - "bad request agent unenrolling": { - "value": { - "statusCode": 400, - "error": "Bad Request", - "message": "cannot upgrade an unenrolling or unenrolled agent" - } - } - } - } - } - } - }, - "operationId": "post-fleet-agents-upgrade", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpgradeAgent" - }, - "examples":{ - "version":{ - "value": { - "version": "8.0.0" - } - }, - "version and source_uri":{ - "value": { - "version": "8.0.0", - "source_uri": "http://localhost:8000" - } - } - } - } - } - } - } - }, - "/fleet/agents/bulk_upgrade": { - "post": { - "summary": "Fleet - Agent - Bulk Upgrade", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BulkUpgradeAgents" - }, - "examples": { - "success": { - "value": {} - } - } - } - } - }, - "400": { - "description": "BAD REQUEST", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpgradeAgent" - }, - "examples": { - "bad request kibana version": { - "value": { - "statusCode": 400, - "error": "Bad Request", - "message": "cannot upgrade agent to 8.0.0 because it is different than the installed kibana version 7.9.10" - } - } - } - } - } - } - }, - "operationId": "post-fleet-agents-bulk-upgrade", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BulkUpgradeAgents" - }, - "examples":{ - "version":{ - "value": { - "version": "8.0.0" - } - }, - "version and source_uri":{ - "value": { - "version": "8.0.0", - "source_uri": "http://localhost:8000" - } - } - } - } - } - } - } - }, - "/fleet/agent-status": { - "get": { - "summary": "Fleet - Agent - Status for policy", - "tags": [], - "responses": {}, - "operationId": "get-fleet-agent-status", - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "policyId", - "in": "query", - "required": false - } - ] - } - }, - "/fleet/enrollment-api-keys": { - "get": { - "summary": "Enrollment - List", - "tags": [], - "responses": {}, - "operationId": "get-fleet-enrollment-api-keys", - "parameters": [] - }, - "post": { - "summary": "Enrollment - Create", - "tags": [], - "responses": {}, - "operationId": "post-fleet-enrollment-api-keys", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - } - }, - "/fleet/enrollment-api-keys/{keyId}": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "keyId", - "in": "path", - "required": true - } - ], - "get": { - "summary": "Enrollment - Info", - "tags": [], - "responses": {}, - "operationId": "get-fleet-enrollment-api-keys-keyId" - }, - "delete": { - "summary": "Enrollment - Delete", - "tags": [], - "responses": {}, - "operationId": "delete-fleet-enrollment-api-keys-keyId", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - } - }, - "/setup": { - "post": { - "summary": "Ingest Manager - Setup", - "tags": [], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "isInitialized": { - "type": "boolean" - } - } - }, - "examples": { - "success": { - "value": { - "isInitialized": true - } - } - } - } - } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } - }, - "examples": {} - } - } - } - }, - "operationId": "post-setup", - "parameters": [ - { - "$ref": "#/components/parameters/xsrfHeader" - } - ] - } - }, - "/fleet/install/{osType}": { - "parameters": [ - { - "schema": { - "type": "string" - }, - "name": "osType", - "in": "path", - "required": true - } - ], - "get": { - "summary": "Fleet - Get OS install script", - "tags": [], - "responses": {}, - "operationId": "get-fleet-install-osType" - } - } - }, - "components": { - "schemas": { - "AgentPolicy": { - "allOf": [ - { - "$ref": "#/components/schemas/NewAgentPolicy" - }, - { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "status": { - "type": "string", - "enum": ["active", "inactive"] - }, - "packagePolicies": { - "oneOf": [ - { - "items": { - "type": "string" - } - }, - { - "items": { - "$ref": "#/components/schemas/PackagePolicy" - } - } - ], - "type": "array" - }, - "updated_on": { - "type": "string", - "format": "date-time" - }, - "updated_by": { - "type": "string" - }, - "revision": { - "type": "number" - }, - "agents": { - "type": "number" - } - }, - "required": ["id", "status"] - } - ] - }, - "PackagePolicy": { - "title": "PackagePolicy", - "allOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "revision": { - "type": "number" - }, - "inputs": { - "type": "array", - "items": {} - } - }, - "required": ["id", "revision"] - }, - { - "$ref": "#/components/schemas/NewPackagePolicy" - } - ], - "x-examples": { - "example-1": {} - } - }, - "NewAgentPolicy": { - "title": "NewAgentPolicy", - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "namespace": { - "type": "string" - }, - "description": { - "type": "string" - } - } - }, - "NewPackagePolicy": { - "title": "NewPackagePolicy", - "type": "object", - "x-examples": { - "example-1": { - "enabled": true, - "title": "This is a nice title for human", - "package": { - "name": "epm/nginx", - "version": "1.7.0" - }, - "namespace": "prod", - "use_output": "long_term_storage", - "inputs": [ - { - "type": "logs", - "streams": [ - { - "enabled": true, - "dataset": "nginx.acccess", - "paths": ["/var/log/nginx/access.log"] - }, - { - "enabled": true, - "dataset": "nginx.error", - "paths": ["/var/log/nginx/error.log"] - } - ] - }, - { - "type": "nginx/metrics", - "streams": [ - { - "id": "id string", - "enabled": true, - "dataset": "nginx.stub_status", - "metricset": "stub_status" - } - ] - } - ] - } - }, - "description": "", - "properties": { - "enabled": { - "type": "boolean" - }, - "package": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "version": { - "type": "string" - }, - "title": { - "type": "string" - } - }, - "required": ["name", "version", "title"] - }, - "namespace": { - "type": "string" - }, - "output_id": { - "type": "string" - }, - "inputs": { - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "enabled": { - "type": "boolean" - }, - "processors": { - "type": "array", - "items": { - "type": "string" - } - }, - "streams": { - "type": "array", - "items": {} - }, - "config": { - "type": "object" - }, - "vars": { - "type": "object" - } - }, - "required": ["type", "enabled", "streams"] - } - }, - "policy_id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "description": { - "type": "string" - } - }, - "required": ["output_id", "inputs", "policy_id", "name"] - }, - "PackageInfo": { - "title": "PackageInfo", - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "title": { - "type": "string" - }, - "version": { - "type": "string" - }, - "readme": { - "type": "string" - }, - "description": { - "type": "string" - }, - "type": { - "type": "string" - }, - "categories": { - "type": "array", - "items": { - "type": "string" - } - }, - "requirement": { - "oneOf": [ - { - "properties": { - "kibana": { - "type": "object", - "properties": { - "versions": { - "type": "string" - } - } - } - } - }, - { - "properties": { - "elasticsearch": { - "type": "object", - "properties": { - "versions": { - "type": "string" - } - } - } - } - } - ], - "type": "object" - }, - "screenshots": { - "type": "array", - "items": { - "type": "object", - "properties": { - "src": { - "type": "string" - }, - "path": { - "type": "string" - }, - "title": { - "type": "string" - }, - "size": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": ["src", "path"] - } - }, - "icons": { - "type": "array", - "items": { - "type": "string" - } - }, - "assets": { - "type": "array", - "items": { - "type": "string" - } - }, - "internal": { - "type": "boolean" - }, - "format_version": { - "type": "string" - }, - "data_streams": { - "type": "array", - "items": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "name": { - "type": "string" - }, - "release": { - "type": "string" - }, - "ingeset_pipeline": { - "type": "string" - }, - "vars": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "default": { - "type": "string" - } - }, - "required": ["name", "default"] - } - }, - "type": { - "type": "string" - }, - "package": { - "type": "string" - } - }, - "required": ["title", "name", "release", "ingeset_pipeline", "type", "package"] - } - }, - "download": { - "type": "string" - }, - "path": { - "type": "string" - }, - "removable": { - "type": "boolean" - } - }, - "required": [ - "name", - "title", - "version", - "description", - "type", - "categories", - "requirement", - "assets", - "format_version", - "download", - "path" - ] - }, - "SearchResult": { - "title": "SearchResult", - "type": "object", - "properties": { - "description": { - "type": "string" - }, - "download": { - "type": "string" - }, - "icons": { - "type": "string" - }, - "name": { - "type": "string" - }, - "path": { - "type": "string" - }, - "title": { - "type": "string" - }, - "type": { - "type": "string" - }, - "version": { - "type": "string" - }, - "status": { - "type": "string" - }, - "savedObject": { - "type": "object" - } - }, - "required": [ - "description", - "download", - "icons", - "name", - "path", - "title", - "type", - "version", - "status" - ] - }, - "AgentStatus": { - "type": "string", - "title": "AgentStatus", - "enum": ["offline", "error", "online", "inactive", "warning"] - }, - "Agent": { - "title": "Agent", - "type": "object", - "properties": { - "type": { - "$ref": "#/components/schemas/AgentType" - }, - "active": { - "type": "boolean" - }, - "enrolled_at": { - "type": "string" - }, - "unenrolled_at": { - "type": "string" - }, - "unenrollment_started_at": { - "type": "string" - }, - "shared_id": { - "type": "string" - }, - "access_api_key_id": { - "type": "string" - }, - "default_api_key_id": { - "type": "string" - }, - "policy_id": { - "type": "string" - }, - "policy_revision": { - "type": ["number", "null"] - }, - "last_checkin": { - "type": "string" - }, - "user_provided_metadata": { - "$ref": "#/components/schemas/AgentMetadata" - }, - "local_metadata": { - "$ref": "#/components/schemas/AgentMetadata" - }, - "id": { - "type": "string" - }, - "current_error_events": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AgentEvent" - } - }, - "access_api_key": { - "type": "string" - }, - "status": { - "$ref": "#/components/schemas/AgentStatus" - }, - "default_api_key": { - "type": "string" - } - }, - "required": ["type", "active", "enrolled_at", "id", "current_error_events", "status"] - }, - "AgentType": { - "type": "string", - "title": "AgentType", - "enum": ["PERMANENT", "EPHEMERAL", "TEMPORARY"] - }, - "AgentMetadata": { - "title": "AgentMetadata", - "type": "object" - }, - "NewAgentEvent": { - "title": "NewAgentEvent", - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["STATE", "ERROR", "ACTION_RESULT", "ACTION"] - }, - "subtype": { - "type": "string", - "enum": [ - "RUNNING", - "STARTING", - "IN_PROGRESS", - "CONFIG", - "FAILED", - "STOPPING", - "STOPPED", - "DEGRADED", - "DATA_DUMP", - "ACKNOWLEDGED", - "UNKNOWN" - ] - }, - "timestamp": { - "type": "string" - }, - "message": { - "type": "string" - }, - "payload": { - "type": "string" - }, - "agent_id": { - "type": "string" - }, - "policy_id": { - "type": "string" - }, - "stream_id": { - "type": "string" - }, - "action_id": { - "type": "string" - } - }, - "required": ["type", "subtype", "timestamp", "message", "agent_id"] - }, - "AgentEvent": { - "title": "AgentEvent", - "allOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "string" - } - }, - "required": ["id"] - }, - { - "$ref": "#/components/schemas/NewAgentEvent" - } - ] - }, - "AccessApiKey": { - "type": "string", - "title": "AccessApiKey", - "format": "byte" - }, - "EnrollmentApiKey": { - "type": "string", - "title": "EnrollmentApiKey", - "format": "byte" - }, - "UpgradeAgent":{ - "title": "UpgradeAgent", - "oneOf": [ - { - "type": "object", - "properties": { - "version": { - "type": "string" - } - }, - "required": ["version"] - }, - { - "type": "object", - "properties": { - "version": { - "type": "string" - }, - "source_uri": { - "type": "string" - } - }, - "required": ["version"] - } - ] - }, - "BulkUpgradeAgents":{ - "title": "BulkUpgradeAgents", - "oneOf": [ - { - "type": "object", - "properties": { - "version": { - "type": "string" - }, - "agents":{ - "type": "array", - "items":{ - "type": "string" - } - } - }, - "required": ["version", "agents"] - }, - { - "type": "object", - "properties": { - "version": { - "type": "string" - }, - "source_uri": { - "type": "string" - }, - "agents":{ - "type": "array", - "items":{ - "type": "string" - } - } - }, - "required": ["version", "agents"] - }, - { - "type": "object", - "properties": { - "version": { - "type": "string" - }, - "source_uri": { - "type": "string" - }, - "agents":{ - "type": "string" - } - }, - "required": ["version", "agents"] - } - ] - } - }, - - "parameters": { - "pageSizeParam": { - "name": "perPage", - "in": "query", - "description": "The number of items to return", - "required": false, - "schema": { - "type": "integer", - "default": 50 - } - }, - "pageIndexParam": { - "name": "page", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "default": 1 - } - }, - "kueryParam": { - "name": "kuery", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - "xsrfHeader": { - "schema": { - "type": "string" - }, - "in": "header", - "name": "kbn-xsrf", - "required": true - } - }, - "securitySchemes": { - "basicAuth": { - "type": "http", - "scheme": "basic" - }, - "Enrollment API Key": { - "name": "Authorization", - "type": "apiKey", - "in": "header", - "description": "e.g. Authorization: ApiKey base64EnrollmentApiKey" - }, - "Access API Key": { - "name": "Authorization", - "type": "apiKey", - "in": "header", - "description": "e.g. Authorization: ApiKey base64AccessApiKey" - } - } - }, - "security": [ - { - "basicAuth": [] - } - ] -} 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 000000000000..07d3f68d7b97 --- /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 000000000000..ae6e34fe82d1 --- /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/services/is_agent_upgradeable.test.ts b/x-pack/plugins/ingest_manager/common/services/is_agent_upgradeable.test.ts index cb087a3b8f80..ca0fcd3c52c9 100644 --- a/x-pack/plugins/ingest_manager/common/services/is_agent_upgradeable.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/is_agent_upgradeable.test.ts @@ -6,7 +6,17 @@ import { isAgentUpgradeable } from './is_agent_upgradeable'; import { Agent } from '../types/models/agent'; -const getAgent = (version: string, upgradeable: boolean): Agent => { +const getAgent = ({ + version, + upgradeable = false, + unenrolling = false, + unenrolled = false, +}: { + version: string; + upgradeable?: boolean; + unenrolling?: boolean; + unenrolled?: boolean; +}): Agent => { const agent: Agent = { id: 'de9006e1-54a7-4320-b24e-927e6fe518a8', active: true, @@ -76,25 +86,53 @@ const getAgent = (version: string, upgradeable: boolean): Agent => { if (upgradeable) { agent.local_metadata.elastic.agent.upgradeable = true; } + if (unenrolling) { + agent.unenrollment_started_at = '2020-10-01T14:43:27.255Z'; + } + if (unenrolled) { + agent.unenrolled_at = '2020-10-01T14:43:27.255Z'; + } return agent; }; describe('Ingest Manager - isAgentUpgradeable', () => { it('returns false if agent reports not upgradeable with agent version < kibana version', () => { - expect(isAgentUpgradeable(getAgent('7.9.0', false), '8.0.0')).toBe(false); + expect(isAgentUpgradeable(getAgent({ version: '7.9.0' }), '8.0.0')).toBe(false); }); it('returns false if agent reports not upgradeable with agent version > kibana version', () => { - expect(isAgentUpgradeable(getAgent('8.0.0', false), '7.9.0')).toBe(false); + expect(isAgentUpgradeable(getAgent({ version: '8.0.0' }), '7.9.0')).toBe(false); }); it('returns false if agent reports not upgradeable with agent version === kibana version', () => { - expect(isAgentUpgradeable(getAgent('8.0.0', false), '8.0.0')).toBe(false); + expect(isAgentUpgradeable(getAgent({ version: '8.0.0' }), '8.0.0')).toBe(false); }); it('returns false if agent reports upgradeable, with agent version === kibana version', () => { - expect(isAgentUpgradeable(getAgent('8.0.0', true), '8.0.0')).toBe(false); + expect(isAgentUpgradeable(getAgent({ version: '8.0.0', upgradeable: true }), '8.0.0')).toBe( + false + ); }); it('returns false if agent reports upgradeable, with agent version > kibana version', () => { - expect(isAgentUpgradeable(getAgent('8.0.0', true), '7.9.0')).toBe(false); + expect(isAgentUpgradeable(getAgent({ version: '8.0.0', upgradeable: true }), '7.9.0')).toBe( + false + ); + }); + it('returns false if agent reports upgradeable, but agent is unenrolling', () => { + expect( + isAgentUpgradeable( + getAgent({ version: '7.9.0', upgradeable: true, unenrolling: true }), + '8.0.0' + ) + ).toBe(false); + }); + it('returns false if agent reports upgradeable, but agent is unenrolled', () => { + expect( + isAgentUpgradeable( + getAgent({ version: '7.9.0', upgradeable: true, unenrolled: true }), + '8.0.0' + ) + ).toBe(false); }); it('returns true if agent reports upgradeable, with agent version < kibana version', () => { - expect(isAgentUpgradeable(getAgent('7.9.0', true), '8.0.0')).toBe(true); + expect(isAgentUpgradeable(getAgent({ version: '7.9.0', upgradeable: true }), '8.0.0')).toBe( + true + ); }); }); diff --git a/x-pack/plugins/ingest_manager/common/services/is_agent_upgradeable.ts b/x-pack/plugins/ingest_manager/common/services/is_agent_upgradeable.ts index 5f96e108e618..7b59fb7b2282 100644 --- a/x-pack/plugins/ingest_manager/common/services/is_agent_upgradeable.ts +++ b/x-pack/plugins/ingest_manager/common/services/is_agent_upgradeable.ts @@ -13,6 +13,7 @@ export function isAgentUpgradeable(agent: Agent, kibanaVersion: string) { } else { return false; } + if (agent.unenrollment_started_at || agent.unenrolled_at) return false; const kibanaVersionParsed = semver.parse(kibanaVersion); const agentVersionParsed = semver.parse(agentVersion); if (!agentVersionParsed || !kibanaVersionParsed) return false; 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 8d8344aed6c4..0232bd766ca5 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 ad2eecc0bb05..e13c023d0d11 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -25,8 +25,8 @@ export const config: PluginConfigDescriptor = { agents: true, }, deprecations: ({ renameFromRoot }) => [ - renameFromRoot('xpack.ingestManager.fleet', 'xpack.fleet.agents'), renameFromRoot('xpack.ingestManager', 'xpack.fleet'), + renameFromRoot('xpack.fleet.fleet', 'xpack.fleet.agents'), ], schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), diff --git a/x-pack/plugins/ingest_manager/server/services/agent_policy.test.ts b/x-pack/plugins/ingest_manager/server/services/agent_policy.test.ts index d247b35c089e..f9a8b63bb83a 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_policy.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_policy.test.ts @@ -6,6 +6,7 @@ import { savedObjectsClientMock } from 'src/core/server/mocks'; import { agentPolicyService } from './agent_policy'; +import { agentPolicyUpdateEventHandler } from './agent_policy_update'; import { Output } from '../types'; function getSavedObjectMock(agentPolicyAttributes: any) { @@ -59,7 +60,42 @@ jest.mock('./output', () => { }; }); +jest.mock('./agent_policy_update'); + +function getAgentPolicyUpdateMock() { + return (agentPolicyUpdateEventHandler as unknown) as jest.Mock< + typeof agentPolicyUpdateEventHandler + >; +} + describe('agent policy', () => { + beforeEach(() => { + getAgentPolicyUpdateMock().mockClear(); + }); + describe('bumpRevision', () => { + it('should call agentPolicyUpdateEventHandler with updated event once', async () => { + const soClient = getSavedObjectMock({ + revision: 1, + monitoring_enabled: ['metrics'], + }); + await agentPolicyService.bumpRevision(soClient, 'agent-policy'); + + expect(agentPolicyUpdateEventHandler).toHaveBeenCalledTimes(1); + }); + }); + + describe('bumpAllAgentPolicies', () => { + it('should call agentPolicyUpdateEventHandler with updated event once', async () => { + const soClient = getSavedObjectMock({ + revision: 1, + monitoring_enabled: ['metrics'], + }); + await agentPolicyService.bumpAllAgentPolicies(soClient); + + expect(agentPolicyUpdateEventHandler).toHaveBeenCalledTimes(1); + }); + }); + describe('getFullAgentPolicy', () => { it('should return a policy without monitoring if monitoring is not enabled', async () => { const soClient = getSavedObjectMock({ 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 f1dcc7e5d6c9..75c16df483a7 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); @@ -297,8 +297,6 @@ class AgentPolicyService { ): Promise { const res = await this._update(soClient, id, {}, options?.user); - await this.triggerAgentPolicyUpdatedEvent(soClient, 'updated', id); - return res; } public async bumpAllAgentPolicies( @@ -540,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/agents/upgrade.ts b/x-pack/plugins/ingest_manager/server/services/agents/upgrade.ts index 612ebf9c11ab..2e77f069b095 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/upgrade.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/upgrade.ts @@ -9,6 +9,8 @@ import { AgentSOAttributes, AgentAction, AgentActionSOAttributes } from '../../t import { AGENT_ACTION_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { bulkCreateAgentActions, createAgentAction } from './actions'; import { getAgents, listAllAgents } from './crud'; +import { isAgentUpgradeable } from '../../../common/services'; +import { appContextService } from '../app_context'; export async function sendUpgradeAgentAction({ soClient, @@ -69,7 +71,8 @@ export async function sendUpgradeAgentsActions( version: string; } ) { - // Filter out agents currently unenrolling, agents unenrolled + const kibanaVersion = appContextService.getKibanaVersion(); + // Filter out agents currently unenrolling, agents unenrolled, and agents not upgradeable const agents = 'agentIds' in options ? await getAgents(soClient, options.agentIds) @@ -79,9 +82,7 @@ export async function sendUpgradeAgentsActions( showInactive: false, }) ).agents; - const agentsToUpdate = agents.filter( - (agent) => !agent.unenrollment_started_at && !agent.unenrolled_at - ); + const agentsToUpdate = agents.filter((agent) => isAgentUpgradeable(agent, kibanaVersion)); const now = new Date().toISOString(); const data = { version: options.version, diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.test.ts new file mode 100644 index 000000000000..5d3e8e9ce87d --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.test.ts @@ -0,0 +1,86 @@ +/* + * 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 { SavedObjectsClientContract, LegacyScopedClusterClient } from 'src/core/server'; +import { savedObjectsClientMock, elasticsearchServiceMock } from 'src/core/server/mocks'; +import { appContextService } from '../../app_context'; +import { createAppContextStartContractMock } from '../../../mocks'; + +jest.mock('../elasticsearch/template/template'); +jest.mock('../kibana/assets/install'); +jest.mock('../kibana/index_pattern/install'); +jest.mock('./install'); +jest.mock('./get'); + +import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; +import { installKibanaAssets } from '../kibana/assets/install'; +import { installIndexPatterns } from '../kibana/index_pattern/install'; +import { _installPackage } from './_install_package'; + +const mockedUpdateCurrentWriteIndices = updateCurrentWriteIndices as jest.MockedFunction< + typeof updateCurrentWriteIndices +>; +const mockedGetKibanaAssets = installKibanaAssets as jest.MockedFunction< + typeof installKibanaAssets +>; +const mockedInstallIndexPatterns = installIndexPatterns as jest.MockedFunction< + typeof installIndexPatterns +>; + +function sleep(millis: number) { + return new Promise((resolve) => setTimeout(resolve, millis)); +} + +describe('_installPackage', () => { + let soClient: jest.Mocked; + let callCluster: jest.Mocked; + beforeEach(async () => { + soClient = savedObjectsClientMock.create(); + callCluster = elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser; + appContextService.start(createAppContextStartContractMock()); + }); + afterEach(async () => { + appContextService.stop(); + }); + it('handles errors from installIndexPatterns or installKibanaAssets', async () => { + // force errors from either/both these functions + mockedGetKibanaAssets.mockImplementation(async () => { + throw new Error('mocked async error A: should be caught'); + }); + mockedInstallIndexPatterns.mockImplementation(async () => { + throw new Error('mocked async error B: should be caught'); + }); + + // pick any function between when those are called and when await Promise.all is defined later + // and force it to take long enough for the errors to occur + // @ts-expect-error about call signature + mockedUpdateCurrentWriteIndices.mockImplementation(async () => await sleep(1000)); + + const installationPromise = _installPackage({ + savedObjectsClient: soClient, + callCluster, + pkgName: 'abc', + pkgVersion: '1.2.3', + paths: [], + removable: false, + internal: false, + packageInfo: { + name: 'xyz', + version: '4.5.6', + description: 'test', + type: 'x', + categories: ['this', 'that'], + format_version: 'string', + }, + installType: 'install', + installSource: 'registry', + }); + + // if we have a .catch this will fail nicely (test pass) + // otherwise the test will fail with either of the mocked errors + await expect(installationPromise).rejects.toThrow('mocked'); + await expect(installationPromise).rejects.toThrow('should be caught'); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.ts new file mode 100644 index 000000000000..f570984cc61a --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/_install_package.ts @@ -0,0 +1,192 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; +import { InstallablePackage, InstallSource } from '../../../../common'; +import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; +import { + AssetReference, + Installation, + CallESAsCurrentUser, + ElasticsearchAssetType, + InstallType, +} from '../../../types'; +import { installIndexPatterns } from '../kibana/index_pattern/install'; +import { installTemplates } from '../elasticsearch/template/install'; +import { generateESIndexPatterns } from '../elasticsearch/template/template'; +import { installPipelines, deletePreviousPipelines } from '../elasticsearch/ingest_pipeline/'; +import { installILMPolicy } from '../elasticsearch/ilm/install'; +import { installKibanaAssets, getKibanaAssets } from '../kibana/assets/install'; +import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; +import { deleteKibanaSavedObjectsAssets } from './remove'; +import { installTransform } from '../elasticsearch/transform/install'; +import { createInstallation, saveKibanaAssetsRefs, updateVersion } from './install'; + +// this is only exported for testing +// use a leading underscore to indicate it's not the supported path +// only the more explicit `installPackage*` functions should be used + +export async function _installPackage({ + savedObjectsClient, + callCluster, + pkgName, + pkgVersion, + installedPkg, + paths, + removable, + internal, + packageInfo, + installType, + installSource, +}: { + savedObjectsClient: SavedObjectsClientContract; + callCluster: CallESAsCurrentUser; + pkgName: string; + pkgVersion: string; + installedPkg?: SavedObject; + paths: string[]; + removable: boolean; + internal: boolean; + packageInfo: InstallablePackage; + installType: InstallType; + installSource: InstallSource; +}): Promise { + const toSaveESIndexPatterns = generateESIndexPatterns(packageInfo.data_streams); + // add the package installation to the saved object. + // if some installation already exists, just update install info + if (!installedPkg) { + await createInstallation({ + savedObjectsClient, + pkgName, + pkgVersion, + internal, + removable, + installed_kibana: [], + installed_es: [], + toSaveESIndexPatterns, + installSource, + }); + } else { + await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { + install_version: pkgVersion, + install_status: 'installing', + install_started_at: new Date().toISOString(), + install_source: installSource, + }); + } + + // kick off `installIndexPatterns` & `installKibanaAssets` as early as possible because they're the longest running operations + // we don't `await` here because we don't want to delay starting the many other `install*` functions + // however, without an `await` or a `.catch` we haven't defined how to handle a promise rejection + // we define it many lines and potentially seconds of wall clock time later in + // `await Promise.all([installKibanaAssetsPromise, installIndexPatternPromise]);` + // if we encounter an error before we there, we'll have an "unhandled rejection" which causes its own problems + // the program will log something like this _and exit/crash_ + // Unhandled Promise rejection detected: + // RegistryResponseError or some other error + // Terminating process... + // server crashed with status code 1 + // + // add a `.catch` to prevent the "unhandled rejection" case + // in that `.catch`, set something that indicates a failure + // check for that failure later and act accordingly (throw, ignore, return) + let installIndexPatternError; + const installIndexPatternPromise = installIndexPatterns( + savedObjectsClient, + pkgName, + pkgVersion + ).catch((reason) => (installIndexPatternError = reason)); + const kibanaAssets = await getKibanaAssets(paths); + if (installedPkg) + await deleteKibanaSavedObjectsAssets( + savedObjectsClient, + installedPkg.attributes.installed_kibana + ); + // save new kibana refs before installing the assets + const installedKibanaAssetsRefs = await saveKibanaAssetsRefs( + savedObjectsClient, + pkgName, + kibanaAssets + ); + let installKibanaAssetsError; + const installKibanaAssetsPromise = installKibanaAssets({ + savedObjectsClient, + pkgName, + kibanaAssets, + }).catch((reason) => (installKibanaAssetsError = reason)); + + // the rest of the installation must happen in sequential order + // currently only the base package has an ILM policy + // at some point ILM policies can be installed/modified + // per data stream and we should then save them + await installILMPolicy(paths, callCluster); + + // installs versionized pipelines without removing currently installed ones + const installedPipelines = await installPipelines( + packageInfo, + paths, + callCluster, + savedObjectsClient + ); + // install or update the templates referencing the newly installed pipelines + const installedTemplates = await installTemplates( + packageInfo, + callCluster, + paths, + savedObjectsClient + ); + + // update current backing indices of each data stream + await updateCurrentWriteIndices(callCluster, installedTemplates); + + const installedTransforms = await installTransform( + packageInfo, + paths, + callCluster, + savedObjectsClient + ); + + // if this is an update or retrying an update, delete the previous version's pipelines + if ((installType === 'update' || installType === 'reupdate') && installedPkg) { + await deletePreviousPipelines( + callCluster, + savedObjectsClient, + pkgName, + installedPkg.attributes.version + ); + } + // pipelines from a different version may have installed during a failed update + if (installType === 'rollback' && installedPkg) { + await deletePreviousPipelines( + callCluster, + savedObjectsClient, + pkgName, + installedPkg.attributes.install_version + ); + } + const installedTemplateRefs = installedTemplates.map((template) => ({ + id: template.templateName, + type: ElasticsearchAssetType.indexTemplate, + })); + + // make sure the assets are installed (or didn't error) + if (installIndexPatternError) throw installIndexPatternError; + if (installKibanaAssetsError) throw installKibanaAssetsError; + await Promise.all([installKibanaAssetsPromise, installIndexPatternPromise]); + + // update to newly installed version when all assets are successfully installed + if (installedPkg) await updateVersion(savedObjectsClient, pkgName, pkgVersion); + await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { + install_version: pkgVersion, + install_status: 'installed', + }); + return [ + ...installedKibanaAssetsRefs, + ...installedPipelines, + ...installedTemplateRefs, + ...installedTransforms, + ]; +} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index a7514d1075d7..9651eafbf1e1 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -8,7 +8,7 @@ import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; import semver from 'semver'; import Boom from 'boom'; import { UnwrapPromise } from '@kbn/utility-types'; -import { BulkInstallPackageInfo, InstallablePackage, InstallSource } from '../../../../common'; +import { BulkInstallPackageInfo, InstallSource } from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE, MAX_TIME_COMPLETE_INSTALL } from '../../../constants'; import { AssetReference, @@ -18,10 +18,8 @@ import { AssetType, KibanaAssetReference, EsAssetReference, - ElasticsearchAssetType, InstallType, } from '../../../types'; -import { installIndexPatterns } from '../kibana/index_pattern/install'; import * as Registry from '../registry'; import { getInstallation, @@ -30,27 +28,17 @@ import { bulkInstallPackages, isBulkInstallError, } from './index'; -import { installTemplates } from '../elasticsearch/template/install'; -import { generateESIndexPatterns } from '../elasticsearch/template/template'; -import { installPipelines, deletePreviousPipelines } from '../elasticsearch/ingest_pipeline/'; -import { installILMPolicy } from '../elasticsearch/ilm/install'; -import { - installKibanaAssets, - getKibanaAssets, - toAssetReference, - ArchiveAsset, -} from '../kibana/assets/install'; -import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; -import { deleteKibanaSavedObjectsAssets, removeInstallation } from './remove'; +import { toAssetReference, ArchiveAsset } from '../kibana/assets/install'; +import { removeInstallation } from './remove'; import { IngestManagerError, PackageOperationNotSupportedError, PackageOutdatedError, } from '../../../errors'; import { getPackageSavedObjects } from './get'; -import { installTransform } from '../elasticsearch/transform/install'; import { appContextService } from '../../app_context'; import { loadArchivePackage } from '../archive'; +import { _installPackage } from './_install_package'; export async function installLatestPackage(options: { savedObjectsClient: SavedObjectsClientContract; @@ -266,7 +254,7 @@ export async function installPackageFromRegistry({ const { internal = false } = registryPackageInfo; const installSource = 'registry'; - return installPackage({ + return _installPackage({ savedObjectsClient, callCluster, pkgName, @@ -308,7 +296,7 @@ export async function installPackageByUpload({ const { internal = false } = archivePackageInfo; const installSource = 'upload'; - return installPackage({ + return _installPackage({ savedObjectsClient, callCluster, pkgName: archivePackageInfo.name, @@ -323,145 +311,7 @@ export async function installPackageByUpload({ }); } -async function installPackage({ - savedObjectsClient, - callCluster, - pkgName, - pkgVersion, - installedPkg, - paths, - removable, - internal, - packageInfo, - installType, - installSource, -}: { - savedObjectsClient: SavedObjectsClientContract; - callCluster: CallESAsCurrentUser; - pkgName: string; - pkgVersion: string; - installedPkg?: SavedObject; - paths: string[]; - removable: boolean; - internal: boolean; - packageInfo: InstallablePackage; - installType: InstallType; - installSource: InstallSource; -}): Promise { - const toSaveESIndexPatterns = generateESIndexPatterns(packageInfo.data_streams); - - // add the package installation to the saved object. - // if some installation already exists, just update install info - if (!installedPkg) { - await createInstallation({ - savedObjectsClient, - pkgName, - pkgVersion, - internal, - removable, - installed_kibana: [], - installed_es: [], - toSaveESIndexPatterns, - installSource, - }); - } else { - await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { - install_version: pkgVersion, - install_status: 'installing', - install_started_at: new Date().toISOString(), - install_source: installSource, - }); - } - const installIndexPatternPromise = installIndexPatterns(savedObjectsClient, pkgName, pkgVersion); - const kibanaAssets = await getKibanaAssets(paths); - if (installedPkg) - await deleteKibanaSavedObjectsAssets( - savedObjectsClient, - installedPkg.attributes.installed_kibana - ); - // save new kibana refs before installing the assets - const installedKibanaAssetsRefs = await saveKibanaAssetsRefs( - savedObjectsClient, - pkgName, - kibanaAssets - ); - const installKibanaAssetsPromise = installKibanaAssets({ - savedObjectsClient, - pkgName, - kibanaAssets, - }); - - // the rest of the installation must happen in sequential order - - // currently only the base package has an ILM policy - // at some point ILM policies can be installed/modified - // per data stream and we should then save them - await installILMPolicy(paths, callCluster); - - // installs versionized pipelines without removing currently installed ones - const installedPipelines = await installPipelines( - packageInfo, - paths, - callCluster, - savedObjectsClient - ); - // install or update the templates referencing the newly installed pipelines - const installedTemplates = await installTemplates( - packageInfo, - callCluster, - paths, - savedObjectsClient - ); - - // update current backing indices of each data stream - await updateCurrentWriteIndices(callCluster, installedTemplates); - - const installedTransforms = await installTransform( - packageInfo, - paths, - callCluster, - savedObjectsClient - ); - - // if this is an update or retrying an update, delete the previous version's pipelines - if ((installType === 'update' || installType === 'reupdate') && installedPkg) { - await deletePreviousPipelines( - callCluster, - savedObjectsClient, - pkgName, - installedPkg.attributes.version - ); - } - // pipelines from a different version may have installed during a failed update - if (installType === 'rollback' && installedPkg) { - await deletePreviousPipelines( - callCluster, - savedObjectsClient, - pkgName, - installedPkg.attributes.install_version - ); - } - const installedTemplateRefs = installedTemplates.map((template) => ({ - id: template.templateName, - type: ElasticsearchAssetType.indexTemplate, - })); - await Promise.all([installKibanaAssetsPromise, installIndexPatternPromise]); - - // update to newly installed version when all assets are successfully installed - if (installedPkg) await updateVersion(savedObjectsClient, pkgName, pkgVersion); - await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { - install_version: pkgVersion, - install_status: 'installed', - }); - return [ - ...installedKibanaAssetsRefs, - ...installedPipelines, - ...installedTemplateRefs, - ...installedTransforms, - ]; -} - -const updateVersion = async ( +export const updateVersion = async ( savedObjectsClient: SavedObjectsClientContract, pkgName: string, pkgVersion: string diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index af44fc28fec1..2bbf183b7ae1 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -14,6 +14,7 @@ import { CoreStart, CoreSetup } from 'kibana/public'; import { ExecutionContextSearch } from 'src/plugins/expressions'; import { ExpressionRendererEvent, + ExpressionRenderError, ReactExpressionRendererType, } from '../../../../../../../src/plugins/expressions/public'; import { Action } from '../state_management'; @@ -40,6 +41,7 @@ import { } from '../../../../../../../src/plugins/data/public'; import { WorkspacePanelWrapper } from './workspace_panel_wrapper'; import { DropIllustration } from '../../../assets/drop_illustration'; +import { getOriginalRequestErrorMessage } from '../../error_helper'; export interface WorkspacePanelProps { activeVisualizationId: string | null; @@ -342,7 +344,8 @@ export const InnerVisualizationWrapper = ({ searchContext={context} reload$={autoRefreshFetch$} onEvent={onEvent} - renderError={(errorMessage?: string | null) => { + renderError={(errorMessage?: string | null, error?: ExpressionRenderError | null) => { + const visibleErrorMessage = getOriginalRequestErrorMessage(error) || errorMessage; return ( @@ -354,7 +357,7 @@ export const InnerVisualizationWrapper = ({ defaultMessage="An error occurred when loading data." /> - {errorMessage ? ( + {visibleErrorMessage ? ( { @@ -369,7 +372,7 @@ export const InnerVisualizationWrapper = ({ })} - {localState.expandError ? errorMessage : null} + {localState.expandError ? visibleErrorMessage : null} ) : null} diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx index d0d2360ddc10..4fb0630a305e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx @@ -13,6 +13,7 @@ import { ReactExpressionRendererType, } from 'src/plugins/expressions/public'; import { ExecutionContextSearch } from 'src/plugins/expressions'; +import { getOriginalRequestErrorMessage } from '../error_helper'; export interface ExpressionWrapperProps { ExpressionRenderer: ReactExpressionRendererType; @@ -50,7 +51,20 @@ export function ExpressionWrapper({ padding="m" expression={expression} searchContext={searchContext} - renderError={(error) =>
{error}
} + renderError={(errorMessage, error) => ( +
+ + + + + + + {getOriginalRequestErrorMessage(error) || errorMessage} + + + +
+ )} onEvent={handleEvent} />
diff --git a/x-pack/plugins/lens/public/editor_frame_service/error_helper.ts b/x-pack/plugins/lens/public/editor_frame_service/error_helper.ts new file mode 100644 index 000000000000..79faa5a47def --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/error_helper.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +import { ExpressionRenderError } from 'src/plugins/expressions/public'; + +interface ElasticsearchErrorClause { + type: string; + reason: string; + caused_by?: ElasticsearchErrorClause; +} + +interface RequestError extends Error { + body?: { attributes?: { error: ElasticsearchErrorClause } }; +} + +const isRequestError = (e: Error | RequestError): e is RequestError => { + if ('body' in e) { + return e.body?.attributes?.error?.caused_by !== undefined; + } + return false; +}; + +function getNestedErrorClause({ + type, + reason, + caused_by: causedBy, +}: ElasticsearchErrorClause): { type: string; reason: string } { + if (causedBy) { + return getNestedErrorClause(causedBy); + } + return { type, reason }; +} + +export function getOriginalRequestErrorMessage(error?: ExpressionRenderError | null) { + if (error && 'original' in error && error.original && isRequestError(error.original)) { + const rootError = getNestedErrorClause(error.original.body!.attributes!.error); + if (rootError.reason && rootError.type) { + return i18n.translate('xpack.lens.editorFrame.expressionFailureMessage', { + defaultMessage: 'Request error: {type}, {reason}', + values: { + reason: rootError.reason, + type: rootError.type, + }, + }); + } + } +} 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 16b861ae034f..96f4120e3df7 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 fb6cf6df8573..5317ee913fcd 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 a8304456262e..a256f5e4ecfa 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/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts index 09a2cc652a9b..1ab00eef0593 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts @@ -518,6 +518,22 @@ describe('xy_suggestions', () => { expect(suggestion.hide).toBeTruthy(); }); + test('respects requested sub visualization type if set', () => { + const [suggestion, ...rest] = getSuggestions({ + table: { + isMultiRow: true, + columns: [numCol('price'), numCol('quantity'), dateCol('date'), strCol('product')], + layerId: 'first', + changeType: 'reduced', + }, + keptLayerIds: [], + subVisualizationId: 'area', + }); + + expect(rest).toHaveLength(0); + expect(suggestion.state.preferredSeriesType).toBe('area'); + }); + test('keeps existing seriesType for initial tables', () => { const currentState: XYState = { legend: { isVisible: true, position: 'bottom' }, diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts index e6286523d8e2..9e1d42a0f58c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -35,6 +35,7 @@ export function getSuggestions({ table, state, keptLayerIds, + subVisualizationId, }: SuggestionRequest): Array> { if ( // We only render line charts for multi-row queries. We require at least @@ -66,7 +67,12 @@ export function getSuggestions({ return []; } - const suggestions = getSuggestionForColumns(table, keptLayerIds, state); + const suggestions = getSuggestionForColumns( + table, + keptLayerIds, + state, + subVisualizationId as SeriesType | undefined + ); if (suggestions && suggestions instanceof Array) { return suggestions; @@ -78,7 +84,8 @@ export function getSuggestions({ function getSuggestionForColumns( table: TableSuggestion, keptLayerIds: string[], - currentState?: State + currentState?: State, + seriesType?: SeriesType ): VisualizationSuggestion | Array> | undefined { const [buckets, values] = partition(table.columns, (col) => col.operation.isBucketed); @@ -93,6 +100,7 @@ function getSuggestionForColumns( currentState, tableLabel: table.label, keptLayerIds, + requestedSeriesType: seriesType, }); } else if (buckets.length === 0) { const [x, ...yValues] = prioritizeColumns(values); @@ -105,6 +113,7 @@ function getSuggestionForColumns( currentState, tableLabel: table.label, keptLayerIds, + requestedSeriesType: seriesType, }); } } @@ -190,6 +199,7 @@ function getSuggestionsForLayer({ currentState, tableLabel, keptLayerIds, + requestedSeriesType, }: { layerId: string; changeType: TableChangeType; @@ -199,9 +209,11 @@ function getSuggestionsForLayer({ currentState?: State; tableLabel?: string; keptLayerIds: string[]; + requestedSeriesType?: SeriesType; }): VisualizationSuggestion | Array> { const title = getSuggestionTitle(yValues, xValue, tableLabel); - const seriesType: SeriesType = getSeriesType(currentState, layerId, xValue); + const seriesType: SeriesType = + requestedSeriesType || getSeriesType(currentState, layerId, xValue); const options = { currentState, 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 aee32e3a4bd9..5a8a1f412391 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/kibana.json b/x-pack/plugins/maps/kibana.json index b30c155d43cb..6f3a5b61ddc6 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -15,16 +15,12 @@ "embeddable", "mapsLegacy", "usageCollection", + "savedObjects", "share" ], "optionalPlugins": ["home"], "ui": true, "server": true, "extraPublicDirs": ["common/constants"], - "requiredBundles": [ - "kibanaReact", - "kibanaUtils", - "savedObjects", - "home" - ] + "requiredBundles": ["kibanaReact", "kibanaUtils", "home"] } diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index 5de018a4b59b..08ee4b6628dd 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -39,6 +39,7 @@ export const getVisualizeCapabilities = () => coreStart.application.capabilities export const getDocLinks = () => coreStart.docLinks; export const getCoreOverlays = () => coreStart.overlays; export const getData = () => pluginsStart.data; +export const getSavedObjects = () => pluginsStart.savedObjects; export const getUiActions = () => pluginsStart.uiActions; export const getCore = () => coreStart; export const getNavigation = () => pluginsStart.navigation; diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index a2b629bdd498..0b797c7b8ef6 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -50,6 +50,7 @@ import { MapsLegacyConfig } from '../../../../src/plugins/maps_legacy/config'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public'; import { StartContract as FileUploadStartContract } from '../../file_upload/public'; +import { SavedObjectsStart } from '../../../../src/plugins/saved_objects/public'; import { registerLicensedFeatures, setLicensingPluginStart } from './licensed_features'; export interface MapsPluginSetupDependencies { @@ -71,6 +72,7 @@ export interface MapsPluginStartDependencies { navigation: NavigationPublicPluginStart; uiActions: UiActionsStart; share: SharePluginStart; + savedObjects: SavedObjectsStart; } /** diff --git a/x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.ts b/x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.ts index 66af92c7a687..fe8aa02615b8 100644 --- a/x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.ts +++ b/x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.ts @@ -7,23 +7,10 @@ import _ from 'lodash'; import { createSavedGisMapClass } from './saved_gis_map'; import { SavedObjectLoader } from '../../../../../../../src/plugins/saved_objects/public'; -import { - getCoreChrome, - getSavedObjectsClient, - getIndexPatternService, - getCoreOverlays, - getData, -} from '../../../kibana_services'; +import { getSavedObjects, getSavedObjectsClient } from '../../../kibana_services'; export const getMapsSavedObjectLoader = _.once(function () { - const services = { - savedObjectsClient: getSavedObjectsClient(), - indexPatterns: getIndexPatternService(), - search: getData().search, - chrome: getCoreChrome(), - overlays: getCoreOverlays(), - }; - const SavedGisMap = createSavedGisMapClass(services); + const SavedGisMap = createSavedGisMapClass(getSavedObjects()); return new SavedObjectLoader(SavedGisMap, getSavedObjectsClient()); }); diff --git a/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts b/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts index 511f015b0ff8..7b31d9edea90 100644 --- a/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts +++ b/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts @@ -8,9 +8,8 @@ import _ from 'lodash'; import { SavedObjectReference } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { - createSavedObjectClass, + SavedObjectsStart, SavedObject, - SavedObjectKibanaServices, } from '../../../../../../../src/plugins/saved_objects/public'; import { getTimeFilters, @@ -40,10 +39,8 @@ export interface ISavedGisMap extends SavedObject { syncWithStore(): void; } -export function createSavedGisMapClass(services: SavedObjectKibanaServices) { - const SavedObjectClass = createSavedObjectClass(services); - - class SavedGisMap extends SavedObjectClass implements ISavedGisMap { +export function createSavedGisMapClass(savedObjects: SavedObjectsStart) { + class SavedGisMap extends savedObjects.SavedObjectClass implements ISavedGisMap { public static type = MAP_SAVED_OBJECT_TYPE; // Mappings are used to place object properties into saved object _source diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index 42f056b89082..0d208dc0b1b2 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 a33b2e6b3e2d..f88694a1952b 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 e3ab0abc18e7..53065c624543 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/job_selector/job_selector.tsx b/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx index a00284860d66..76e62160ca8c 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { EuiButtonEmpty, EuiFlexItem, EuiFlexGroup, EuiFlyout } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -109,24 +109,22 @@ export function JobSelector({ dateFormatTz, singleSelection, timeseriesOnly }: J showFlyout(); } - const applySelection: JobSelectorFlyoutProps['onSelectionConfirmed'] = ({ - newSelection, - jobIds, - groups: newGroups, - time, - }) => { - setSelectedIds(newSelection); - - setGlobalState({ - ml: { - jobIds, - groups: newGroups, - }, - ...(time !== undefined ? { time } : {}), - }); - - closeFlyout(); - }; + const applySelection: JobSelectorFlyoutProps['onSelectionConfirmed'] = useCallback( + ({ newSelection, jobIds, groups: newGroups, time }) => { + setSelectedIds(newSelection); + + setGlobalState({ + ml: { + jobIds, + groups: newGroups, + }, + ...(time !== undefined ? { time } : {}), + }); + + closeFlyout(); + }, + [setGlobalState, setSelectedIds] + ); function renderJobSelectionBar() { return ( @@ -167,7 +165,11 @@ export function JobSelector({ dateFormatTz, singleSelection, timeseriesOnly }: J function renderFlyout() { if (isFlyoutVisible) { return ( - + = ({ const flyoutEl = useRef(null); - function applySelection() { + const applySelection = useCallback(() => { // allNewSelection will be a list of all job ids (including those from groups) selected from the table const allNewSelection: string[] = []; const groupSelection: Array<{ groupId: string; jobIds: string[] }> = []; @@ -110,7 +110,7 @@ export const JobSelectorFlyoutContent: FC = ({ groups: groupSelection, time, }); - } + }, [onSelectionConfirmed, newSelection, jobGroupsMaps, applyTimeRange]); function removeId(id: string) { setNewSelection(newSelection.filter((item) => item !== id)); @@ -176,120 +176,124 @@ export const JobSelectorFlyoutContent: FC = ({ } 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/components/job_selector/use_job_selection.ts b/x-pack/plugins/ml/public/application/components/job_selector/use_job_selection.ts index 0717348d1db2..04fa3e9201c6 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/use_job_selection.ts +++ b/x-pack/plugins/ml/public/application/components/job_selector/use_job_selection.ts @@ -88,7 +88,7 @@ export const useJobSelection = (jobs: MlJobWithTimeRange[]) => { ...(time !== undefined ? { time } : {}), }); } - }, [jobs, validIds]); + }, [jobs, validIds, setGlobalState, globalState?.ml]); return jobSelection; }; diff --git a/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx b/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx index beafae1ecd2f..409bd11e0bde 100644 --- a/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx @@ -54,7 +54,7 @@ export const DatePickerWrapper: FC = () => { useEffect(() => { setGlobalState({ refreshInterval }); timefilter.setRefreshInterval(refreshInterval); - }, [refreshInterval?.pause, refreshInterval?.value]); + }, [refreshInterval?.pause, refreshInterval?.value, setGlobalState]); const [time, setTime] = useState(timefilter.getTime()); const [recentlyUsedRanges, setRecentlyUsedRanges] = useState(getRecentlyUsedRanges()); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_classification_exploration.scss b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_classification_exploration.scss index 00463affa0d0..f1365db31eca 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_classification_exploration.scss +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_classification_exploration.scss @@ -32,4 +32,8 @@ .mlDataFrameAnalyticsClassification__dataGridMinWidth { min-width: 480px; width: 100%; + + .euiDataGridRowCell--boolean { + text-transform: none; + } } 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 3746fa12bdc1..d1889a8acb99 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/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js index bf6b48fa18b4..12e95e859af5 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js @@ -15,6 +15,7 @@ import { get, each, find, sortBy, map, reduce } from 'lodash'; import { buildConfig } from './explorer_chart_config_builder'; import { chartLimits, getChartType } from '../../util/chart_utils'; +import { getTimefilter } from '../../util/dependency_cache'; import { getEntityFieldList } from '../../../../common/util/anomaly_utils'; import { @@ -50,8 +51,8 @@ const MAX_CHARTS_PER_ROW = 4; export const anomalyDataChange = function ( chartsContainerWidth, anomalyRecords, - earliestMs, - latestMs, + selectedEarliestMs, + selectedLatestMs, severity = 0 ) { const data = getDefaultChartsData(); @@ -83,8 +84,8 @@ export const anomalyDataChange = function ( const chartWidth = Math.floor(chartsContainerWidth / chartsPerRow); const { chartRange, tooManyBuckets } = calculateChartRange( seriesConfigs, - earliestMs, - latestMs, + selectedEarliestMs, + selectedLatestMs, chartWidth, recordsToPlot, data.timeFieldName @@ -408,8 +409,8 @@ export const anomalyDataChange = function ( chartData: processedData[i], plotEarliest: chartRange.min, plotLatest: chartRange.max, - selectedEarliest: earliestMs, - selectedLatest: latestMs, + selectedEarliest: selectedEarliestMs, + selectedLatest: selectedLatestMs, chartLimits: USE_OVERALL_CHART_LIMITS ? overallChartLimits : chartLimits(processedData[i]), })); explorerService.setCharts({ ...data }); @@ -561,8 +562,8 @@ function processRecordsForDisplay(anomalyRecords) { function calculateChartRange( seriesConfigs, - earliestMs, - latestMs, + selectedEarliestMs, + selectedLatestMs, chartWidth, recordsToPlot, timeFieldName @@ -570,10 +571,12 @@ function calculateChartRange( let tooManyBuckets = false; // Calculate the time range for the charts. // Fit in as many points in the available container width plotted at the job bucket span. - const midpointMs = Math.ceil((earliestMs + latestMs) / 2); + const midpointMs = Math.ceil((selectedEarliestMs + selectedLatestMs) / 2); const maxBucketSpanMs = Math.max.apply(null, map(seriesConfigs, 'bucketSpanSeconds')) * 1000; - const pointsToPlotFullSelection = Math.ceil((latestMs - earliestMs) / maxBucketSpanMs); + const pointsToPlotFullSelection = Math.ceil( + (selectedLatestMs - selectedEarliestMs) / maxBucketSpanMs + ); // Optimally space points 5px apart. const optimumPointSpacing = 5; @@ -583,9 +586,12 @@ function calculateChartRange( // at optimal point spacing. const plotPoints = Math.max(optimumNumPoints, pointsToPlotFullSelection); const halfPoints = Math.ceil(plotPoints / 2); + const timefilter = getTimefilter(); + const bounds = timefilter.getActiveBounds(); + let chartRange = { - min: midpointMs - halfPoints * maxBucketSpanMs, - max: midpointMs + halfPoints * maxBucketSpanMs, + min: Math.max(midpointMs - halfPoints * maxBucketSpanMs, bounds.min.valueOf()), + max: Math.min(midpointMs + halfPoints * maxBucketSpanMs, bounds.max.valueOf()), }; if (plotPoints > CHART_MAX_POINTS) { @@ -615,8 +621,8 @@ function calculateChartRange( if (maxMs - minMs < maxTimeSpan) { // Expand out to cover as much as the requested time span as possible. - minMs = Math.max(earliestMs, minMs - maxTimeSpan); - maxMs = Math.min(latestMs, maxMs + maxTimeSpan); + minMs = Math.max(selectedEarliestMs, minMs - maxTimeSpan); + maxMs = Math.min(selectedLatestMs, maxMs + maxTimeSpan); } chartRange = { min: minMs, max: maxMs }; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js index 5e6901408422..8678e9911413 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.test.js @@ -15,7 +15,7 @@ import mockSeriesPromisesResponse from './__mocks__/mock_series_promises_respons // // 'call anomalyChangeListener with actual series config' // This test uses the standard mocks and uses the data as is provided via the mock files. -// The mocked services check for values in the data (e.g. 'mock-job-id', 'farequore-2017') +// The mocked services check for values in the data (e.g. 'mock-job-id', 'farequote-2017') // and return the mock data from the files. // // 'filtering should skip values of null' @@ -88,14 +88,41 @@ jest.mock('../../util/string_utils', () => ({ }, })); +jest.mock('../../util/dependency_cache', () => { + const dateMath = require('@elastic/datemath'); + let _time = undefined; + const timefilter = { + setTime: (time) => { + _time = time; + }, + getActiveBounds: () => { + return { + min: dateMath.parse(_time.from), + max: dateMath.parse(_time.to), + }; + }, + }; + return { + getTimefilter: () => timefilter, + }; +}); + jest.mock('../explorer_dashboard_service', () => ({ explorerService: { setCharts: jest.fn(), }, })); +import moment from 'moment'; import { anomalyDataChange, getDefaultChartsData } from './explorer_charts_container_service'; import { explorerService } from '../explorer_dashboard_service'; +import { getTimefilter } from '../../util/dependency_cache'; + +const timefilter = getTimefilter(); +timefilter.setTime({ + from: moment(1486425600000).toISOString(), // Feb 07 2017 + to: moment(1486857600000).toISOString(), // Feb 12 2017 +}); describe('explorerChartsContainerService', () => { afterEach(() => { diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js index c309e1f4ef8e..c3bdacde5abd 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js @@ -198,7 +198,7 @@ export function getSelectionTimeRange(selectedCells, interval, bounds) { latestMs = bounds.max.valueOf(); if (selectedCells.times[1] !== undefined) { // Subtract 1 ms so search does not include start of next bucket. - latestMs = (selectedCells.times[1] + interval) * 1000 - 1; + latestMs = selectedCells.times[1] * 1000 - 1; } } diff --git a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts index f356d79c0a8e..c7cda2372bce 100644 --- a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts +++ b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts @@ -60,7 +60,7 @@ export const useSelectedCells = ( setAppState('mlExplorerSwimlane', mlExplorerSwimlane); } }, - [appState?.mlExplorerSwimlane, selectedCells] + [appState?.mlExplorerSwimlane, selectedCells, setAppState] ); return [selectedCells, setSelectedCells]; 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 0a2791edb9c5..9c7d0f6fe78e 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -41,6 +41,7 @@ import { getFormattedSeverityScore } from '../../../common/util/anomaly_utils'; import './_explorer.scss'; import { EMPTY_FIELD_VALUE_LABEL } from '../timeseriesexplorer/components/entity_control/entity_control'; +import { useUiSettings } from '../contexts/kibana'; /** * Ignore insignificant resize, e.g. browser scrollbar appearance. @@ -159,6 +160,8 @@ export const SwimlaneContainer: FC = ({ }) => { const [chartWidth, setChartWidth] = useState(0); + const isDarkTheme = !!useUiSettings().get('theme:darkMode'); + // Holds the container height for previously fetched data const containerHeightRef = useRef(); @@ -210,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(() => { @@ -235,67 +239,76 @@ export const SwimlaneContainer: FC = ({ return { x: selection.times.map((v) => v * 1000), y: selection.lanes }; }, [selection, swimlaneData, swimlaneType]); - const swimLaneConfig: HeatmapSpec['config'] = useMemo( - () => - showSwimlane - ? { - onBrushEnd: (e: HeatmapBrushEvent) => { - onCellsSelection({ - lanes: e.y as string[], - times: e.x.map((v) => (v as number) / 1000), - type: swimlaneType, - viewByFieldName: swimlaneData.fieldName, - }); - }, - grid: { - cellHeight: { - min: CELL_HEIGHT, - max: CELL_HEIGHT, - }, - stroke: { - width: 1, - color: '#D3DAE6', - }, - }, - cell: { - maxWidth: 'fill', - maxHeight: 'fill', - label: { - visible: false, - }, - border: { - stroke: '#D3DAE6', - strokeWidth: 0, - }, - }, - yAxisLabel: { - visible: true, - width: 170, - // eui color subdued - fill: `#6a717d`, - padding: 8, - formatter: (laneLabel: string) => { - return laneLabel === '' ? EMPTY_FIELD_VALUE_LABEL : laneLabel; - }, - }, - xAxisLabel: { - visible: showTimeline, - // eui color subdued - fill: `#98A2B3`, - formatter: (v: number) => { - timeBuckets.setInterval(`${swimlaneData.interval}s`); - const a = timeBuckets.getScaledDateFormat(); - return moment(v).format(a); - }, - }, - brushMask: { - fill: 'rgb(247 247 247 / 50%)', - }, - maxLegendHeight: LEGEND_HEIGHT, - } - : {}, - [showSwimlane, swimlaneType, swimlaneData?.fieldName] - ); + const swimLaneConfig: HeatmapSpec['config'] = useMemo(() => { + if (!showSwimlane) return {}; + + return { + onBrushEnd: (e: HeatmapBrushEvent) => { + onCellsSelection({ + lanes: e.y as string[], + times: e.x.map((v) => (v as number) / 1000), + type: swimlaneType, + viewByFieldName: swimlaneData.fieldName, + }); + }, + grid: { + cellHeight: { + min: CELL_HEIGHT, + max: CELL_HEIGHT, + }, + stroke: { + width: 1, + color: '#D3DAE6', + }, + }, + cell: { + maxWidth: 'fill', + maxHeight: 'fill', + label: { + visible: false, + }, + border: { + stroke: '#D3DAE6', + strokeWidth: 0, + }, + }, + yAxisLabel: { + visible: true, + width: 170, + // eui color subdued + fill: `#6a717d`, + padding: 8, + formatter: (laneLabel: string) => { + return laneLabel === '' ? EMPTY_FIELD_VALUE_LABEL : laneLabel; + }, + }, + xAxisLabel: { + visible: true, + // eui color subdued + fill: `#98A2B3`, + formatter: (v: number) => { + timeBuckets.setInterval(`${swimlaneData.interval}s`); + const scaledDateFormat = timeBuckets.getScaledDateFormat(); + return moment(v).format(scaledDateFormat); + }, + }, + brushMask: { + fill: isDarkTheme ? 'rgb(30,31,35,80%)' : 'rgb(247,247,247,50%)', + }, + brushArea: { + stroke: isDarkTheme ? 'rgb(255, 255, 255)' : 'rgb(105, 112, 125)', + }, + maxLegendHeight: LEGEND_HEIGHT, + timeZone: 'UTC', + }; + }, [ + showSwimlane, + swimlaneType, + swimlaneData?.fieldName, + isDarkTheme, + timeBuckets, + onCellsSelection, + ]); // @ts-ignore const onElementClick: ElementClickListener = useCallback( @@ -310,7 +323,7 @@ export const SwimlaneContainer: FC = ({ }; onCellsSelection(payload); }, - [swimlaneType, swimlaneData?.fieldName, swimlaneData?.interval] + [swimlaneType, swimlaneData?.fieldName, swimlaneData?.interval, onCellsSelection] ); const tooltipOptions: TooltipSettings = useMemo( 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 1a696d6e01dd..36425c65374b 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/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index 00d64a2f1bd1..cb6944e0ecf0 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -82,8 +82,9 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim const { jobIds } = useJobSelection(jobsWithTimeRange); const refresh = useRefresh(); + useEffect(() => { - if (refresh !== undefined) { + if (refresh !== undefined && lastRefresh !== refresh.lastRefresh) { setLastRefresh(refresh?.lastRefresh); if (refresh.timeRange !== undefined) { @@ -94,7 +95,7 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim }); } } - }, [refresh?.lastRefresh]); + }, [refresh?.lastRefresh, lastRefresh, setLastRefresh, setGlobalState]); // We cannot simply infer bounds from the globalState's `time` attribute // with `moment` since it can contain custom strings such as `now-15m`. @@ -194,6 +195,7 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim const [tableSeverity] = useTableSeverity(); const [selectedCells, setSelectedCells] = useSelectedCells(appState, setAppState); + useEffect(() => { explorerService.setSelectedCells(selectedCells); }, [JSON.stringify(selectedCells)]); @@ -220,9 +222,9 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim if (explorerState && explorerState.swimlaneContainerWidth > 0) { loadExplorerData({ ...loadExplorerDataConfig, - swimlaneLimit: - isViewBySwimLaneData(explorerState?.viewBySwimlaneData) && - explorerState?.viewBySwimlaneData.cardinality, + swimlaneLimit: isViewBySwimLaneData(explorerState?.viewBySwimlaneData) + ? explorerState?.viewBySwimlaneData.cardinality + : undefined, }); } }, [JSON.stringify(loadExplorerDataConfig)]); diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap b/x-pack/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap index 50cacd7b3545..0d4bba26f1c0 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap @@ -33,7 +33,7 @@ exports[`CalendarsListTable renders the table with all calendars 1`] = ` }, ] } - data-test-subj="mlCalendarTable" + data-test-subj="mlCalendarTable loaded" isSelectable={true} itemId="calendar_id" items={ diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js b/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js index 6b4403aef7c7..d59639fd44ea 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js @@ -142,7 +142,7 @@ export const CalendarsListTable = ({ loading={loading} selection={tableSelection} isSelectable={true} - data-test-subj="mlCalendarTable" + data-test-subj={loading ? 'mlCalendarTable loading' : 'mlCalendarTable loaded'} rowProps={(item) => ({ 'data-test-subj': `mlCalendarListRow row-${item.calendar_id}`, })} diff --git a/x-pack/plugins/ml/public/application/util/url_state.tsx b/x-pack/plugins/ml/public/application/util/url_state.tsx index c288a00bb06d..a3c70e113090 100644 --- a/x-pack/plugins/ml/public/application/util/url_state.tsx +++ b/x-pack/plugins/ml/public/application/util/url_state.tsx @@ -140,12 +140,12 @@ export const useUrlState = (accessor: Accessor) => { if (typeof fullUrlState === 'object') { return fullUrlState[accessor]; } - return undefined; }, [searchString]); const setUrlState = useCallback( - (attribute: string | Dictionary, value?: any) => - setUrlStateContext(accessor, attribute, value), + (attribute: string | Dictionary, value?: any) => { + setUrlStateContext(accessor, attribute, value); + }, [accessor, setUrlStateContext] ); return [urlState, setUrlState]; diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx index 6e67ff1aef03..4730371c611c 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx @@ -9,6 +9,7 @@ import ReactDOM from 'react-dom'; import { CoreStart } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { Subject } from 'rxjs'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { Embeddable, IContainer } from '../../../../../../src/plugins/embeddable/public'; import { EmbeddableSwimLaneContainer } from './embeddable_swim_lane_container_lazy'; import type { JobId } from '../../../common/types/anomaly_detection_jobs'; @@ -59,17 +60,19 @@ export class AnomalySwimlaneEmbeddable extends Embeddable< ReactDOM.render( - - - + + + + + , node ); 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 8e591d8bdbcb..3f58449f81a9 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/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx index 17ae97e3c07b..5efe70ba552f 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx @@ -89,7 +89,7 @@ export const EmbeddableSwimLaneContainer: FC = ( }); } }, - [swimlaneData, perPage, fromPage] + [swimlaneData, perPage, fromPage, setSelectedCells] ); if (error) { diff --git a/x-pack/plugins/ml/public/ui_actions/apply_time_range_action.tsx b/x-pack/plugins/ml/public/ui_actions/apply_time_range_action.tsx index 325e903de0e2..79e6ff53bff4 100644 --- a/x-pack/plugins/ml/public/ui_actions/apply_time_range_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/apply_time_range_action.tsx @@ -39,8 +39,7 @@ export function createApplyTimeRangeSelectionAction( let [from, to] = data.times; from = from * 1000; - // extend bounds with the interval - to = to * 1000 + interval * 1000; + to = to * 1000; timefilter.setTime({ from: moment(from), diff --git a/x-pack/plugins/ml/readme.md b/x-pack/plugins/ml/readme.md new file mode 100644 index 000000000000..0e50867e57ad --- /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 926c5e265b03..a1e28985a352 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 31199c5b092f..9a590d803bb1 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 ae19921631eb..b8af713e1692 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 b2f6dc4e433a..000000000000 --- 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 29328d581212..000000000000 --- 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 2f9b25797149..000000000000 --- 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 5111695e3d12..000000000000 --- 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 18b15d26db8c..000000000000 --- 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 13fb7f17d04b..000000000000 --- 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 24148c0a2e22..000000000000 --- 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 8a626dda0add..000000000000 --- 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 c8707b30f4e6..000000000000 --- 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 0d91c0f3c016..000000000000 --- 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 79f634971b6f..000000000000 --- 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 8c99c401d87e..000000000000 --- 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 28a4d5f56df1..000000000000 --- 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/elasticsearch/verify_alerting_security.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts index 047b14bd37fb..c8aa730dd477 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 4e1205cac7b8..79c8e01c4cff 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 64beb5c58dc0..ac38d7a59b77 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 78daa5e47c49..d97bc34c2adb 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 89f09d349014..129b79874080 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 1170380b26ac..9ebd73ffbc83 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 b2f3cb6c6152..c885bc9be440 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 3648ae4bd855..109fefd2eb8d 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 e6a4b174df55..42ac721a34c7 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 2f83576f9dc5..765cce0baaa1 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> = function createJobFactoryFn(reporting) { +>> = function createJobFactoryFn(reporting, parentLogger) { + const logger = parentLogger.clone([CSV_JOB_TYPE, 'create-job']); + const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); @@ -26,7 +29,7 @@ export const createJobFnFactory: CreateJobFnFactory> = function executeJobFactoryFn(reporting, parentLogger) { const config = reporting.getConfig(); - const logger = parentLogger.clone([CSV_JOB_TYPE, 'execute-job']); return async function runTask(jobId, job, cancellationToken) { const elasticsearch = reporting.getElasticsearchService(); - const jobLogger = logger.clone([jobId]); - const generateCsv = createGenerateCsv(jobLogger); + const logger = parentLogger.clone([CSV_JOB_TYPE, 'execute-job', jobId]); + const generateCsv = createGenerateCsv(logger); const encryptionKey = config.get('encryptionKey'); const headers = await decryptJobHeaders(encryptionKey, job.headers, logger); - const fakeRequest = reporting.getFakeRequest({ headers }, job.spaceId); - const uiSettingsClient = await reporting.getUiSettingsClient(fakeRequest); + const fakeRequest = reporting.getFakeRequest({ headers }, job.spaceId, logger); + const uiSettingsClient = await reporting.getUiSettingsClient(fakeRequest, logger); const { callAsCurrentUser } = elasticsearch.legacy.client.asScoped(fakeRequest); const callEndpoint = (endpoint: string, clientParams = {}, options = {}) => diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts index 19348c0a678d..5e95eec99871 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts @@ -32,11 +32,10 @@ export const runTaskFnFactory: RunTaskFnFactory = function e const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'execute-job']); return async function runTask(jobId, jobPayload, context, req) { - const jobLogger = logger.clone(['immediate']); - const generateCsv = createGenerateCsv(jobLogger); + const generateCsv = createGenerateCsv(logger); const { panel, visType } = jobPayload; - jobLogger.debug(`Execute job generating [${visType}] csv`); + logger.debug(`Execute job generating [${visType}] csv`); const savedObjectsClient = context.core.savedObjects.client; const uiSettingsClient = await reporting.getUiSettingsServiceFactory(savedObjectsClient); @@ -54,11 +53,11 @@ export const runTaskFnFactory: RunTaskFnFactory = function e ); if (csvContainsFormulas) { - jobLogger.warn(`CSV may contain formulas whose values have been escaped`); + logger.warn(`CSV may contain formulas whose values have been escaped`); } if (maxSizeReached) { - jobLogger.warn(`Max size reached: CSV output truncated to ${size} bytes`); + logger.warn(`Max size reached: CSV output truncated to ${size} bytes`); } return { diff --git a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts index eaaa11d46115..b1fcdbe05fd6 100644 --- a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { PNG_JOB_TYPE } from '../../../../constants'; import { cryptoFactory } from '../../../lib'; import { CreateJobFn, CreateJobFnFactory } from '../../../types'; import { validateUrls } from '../../common'; @@ -12,7 +13,8 @@ import { JobParamsPNG, TaskPayloadPNG } from '../types'; export const createJobFnFactory: CreateJobFnFactory> = function createJobFactoryFn(reporting) { +>> = function createJobFactoryFn(reporting, parentLogger) { + const logger = parentLogger.clone([PNG_JOB_TYPE, 'execute-job']); const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); @@ -27,7 +29,7 @@ export const createJobFnFactory: CreateJobFnFactory> = function createJobFactoryFn(reporting) { +>> = function createJobFactoryFn(reporting, parentLogger) { const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); + const logger = parentLogger.clone([PDF_JOB_TYPE, 'create-job']); return async function createJob( { title, relativeUrls, browserTimezone, layout, objectType }, @@ -27,7 +29,7 @@ export const createJobFnFactory: CreateJobFnFactory void } | null | undefined; @@ -40,7 +39,9 @@ export const runTaskFnFactory: RunTaskFnFactory decryptJobHeaders(encryptionKey, job.headers, logger)), map((decryptedHeaders) => omitBlockedHeaders(decryptedHeaders)), map((filteredHeaders) => getConditionalHeaders(config, filteredHeaders)), - mergeMap((conditionalHeaders) => getCustomLogo(reporting, conditionalHeaders, job.spaceId)), + mergeMap((conditionalHeaders) => + getCustomLogo(reporting, conditionalHeaders, job.spaceId, logger) + ), mergeMap(({ logo, conditionalHeaders }) => { const urls = getFullUrls(config, job); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts index 426770d71906..9f7e9310333b 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts @@ -8,6 +8,7 @@ import { ReportingConfig, ReportingCore } from '../../../'; import { createMockConfig, createMockConfigSchema, + createMockLevelLogger, createMockReportingCore, } from '../../../test_helpers'; import { getConditionalHeaders } from '../../common'; @@ -16,6 +17,8 @@ import { getCustomLogo } from './get_custom_logo'; let mockConfig: ReportingConfig; let mockReportingPlugin: ReportingCore; +const logger = createMockLevelLogger(); + beforeEach(async () => { mockConfig = createMockConfig(createMockConfigSchema()); mockReportingPlugin = await createMockReportingCore(mockConfig); @@ -40,7 +43,12 @@ test(`gets logo from uiSettings`, async () => { const conditionalHeaders = getConditionalHeaders(mockConfig, permittedHeaders); - const { logo } = await getCustomLogo(mockReportingPlugin, conditionalHeaders); + const { logo } = await getCustomLogo( + mockReportingPlugin, + conditionalHeaders, + 'spaceyMcSpaceIdFace', + logger + ); expect(mockGet).toBeCalledWith('xpackReporting:customPdfLogo'); expect(logo).toBe('purple pony'); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.ts index 7bd1637db137..98185a1acf5e 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.ts @@ -6,16 +6,21 @@ import { ReportingCore } from '../../../'; import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../../common/constants'; +import { LevelLogger } from '../../../lib'; import { ConditionalHeaders } from '../../common'; export const getCustomLogo = async ( reporting: ReportingCore, conditionalHeaders: ConditionalHeaders, - spaceId?: string + spaceId: string | undefined, + logger: LevelLogger ) => { - const fakeRequest = reporting.getFakeRequest({ headers: conditionalHeaders.headers }, spaceId); - const uiSettingsClient = await reporting.getUiSettingsClient(fakeRequest); - + const fakeRequest = reporting.getFakeRequest( + { headers: conditionalHeaders.headers }, + spaceId, + logger + ); + const uiSettingsClient = await reporting.getUiSettingsClient(fakeRequest, logger); const logo: string = await uiSettingsClient.get(UI_SETTINGS_CUSTOM_PDF_LOGO); // continue the pipeline diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts index f12b76ccce84..4cecc2e24867 100644 --- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts @@ -6,7 +6,8 @@ import * as Rx from 'rxjs'; import sinon from 'sinon'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { CollectorFetchContext, UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; import { ReportingConfig, ReportingCore } from '../'; import { getExportTypesRegistry } from '../lib/export_types_registry'; import { createMockConfig, createMockConfigSchema, createMockReportingCore } from '../test_helpers'; @@ -56,6 +57,11 @@ function getPluginsMock( const getResponseMock = (base = {}) => base; +const getMockFetchClients = (resp: any) => { + const fetchParamsMock = createCollectorFetchContextMock(); + fetchParamsMock.callCluster.mockResolvedValue(resp); + return fetchParamsMock; +}; describe('license checks', () => { let mockConfig: ReportingConfig; let mockCore: ReportingCore; @@ -68,7 +74,6 @@ describe('license checks', () => { let usageStats: any; beforeAll(async () => { const plugins = getPluginsMock({ license: 'basic' }); - const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); const { fetch } = getReportingUsageCollector( mockCore, plugins.usageCollection, @@ -78,7 +83,7 @@ describe('license checks', () => { return Promise.resolve(true); } ); - usageStats = await fetch(callClusterMock as any); + usageStats = await fetch(getMockFetchClients(getResponseMock())); }); test('sets enables to true', async () => { @@ -98,7 +103,6 @@ describe('license checks', () => { let usageStats: any; beforeAll(async () => { const plugins = getPluginsMock({ license: 'none' }); - const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); const { fetch } = getReportingUsageCollector( mockCore, plugins.usageCollection, @@ -108,7 +112,7 @@ describe('license checks', () => { return Promise.resolve(true); } ); - usageStats = await fetch(callClusterMock as any); + usageStats = await fetch(getMockFetchClients(getResponseMock())); }); test('sets enables to true', async () => { @@ -128,7 +132,6 @@ describe('license checks', () => { let usageStats: any; beforeAll(async () => { const plugins = getPluginsMock({ license: 'platinum' }); - const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); const { fetch } = getReportingUsageCollector( mockCore, plugins.usageCollection, @@ -138,7 +141,7 @@ describe('license checks', () => { return Promise.resolve(true); } ); - usageStats = await fetch(callClusterMock as any); + usageStats = await fetch(getMockFetchClients(getResponseMock())); }); test('sets enables to true', async () => { @@ -158,7 +161,6 @@ describe('license checks', () => { let usageStats: any; beforeAll(async () => { const plugins = getPluginsMock({ license: 'basic' }); - const callClusterMock = jest.fn(() => Promise.resolve({})); const { fetch } = getReportingUsageCollector( mockCore, plugins.usageCollection, @@ -168,7 +170,7 @@ describe('license checks', () => { return Promise.resolve(true); } ); - usageStats = await fetch(callClusterMock as any); + usageStats = await fetch(getMockFetchClients({})); }); test('sets enables to true', async () => { @@ -184,6 +186,7 @@ describe('license checks', () => { describe('data modeling', () => { let mockConfig: ReportingConfig; let mockCore: ReportingCore; + let collectorFetchContext: CollectorFetchContext; beforeAll(async () => { mockConfig = createMockConfig(createMockConfigSchema()); mockCore = await createMockReportingCore(mockConfig); @@ -199,44 +202,42 @@ describe('data modeling', () => { return Promise.resolve(true); } ); - const callClusterMock = jest.fn(() => - Promise.resolve( - getResponseMock({ - aggregations: { - ranges: { - buckets: { - all: { - doc_count: 12, - jobTypes: { buckets: [ { doc_count: 9, key: 'printable_pdf' }, { doc_count: 3, key: 'PNG' }, ], }, - layoutTypes: { doc_count: 9, pdf: { buckets: [{ doc_count: 9, key: 'preserve_layout' }] }, }, - objectTypes: { doc_count: 9, pdf: { buckets: [ { doc_count: 6, key: 'canvas workpad' }, { doc_count: 3, key: 'visualization' }, ], }, }, - statusByApp: { buckets: [ { doc_count: 10, jobTypes: { buckets: [ { appNames: { buckets: [ { doc_count: 6, key: 'canvas workpad' }, { doc_count: 3, key: 'visualization' }, ], }, doc_count: 9, key: 'printable_pdf', }, { appNames: { buckets: [{ doc_count: 1, key: 'visualization' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed', }, { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'failed', }, ], }, - statusTypes: { buckets: [ { doc_count: 10, key: 'completed' }, { doc_count: 1, key: 'completed_with_warnings' }, { doc_count: 1, key: 'failed' }, ], }, - }, - last7Days: { - doc_count: 1, - jobTypes: { buckets: [{ doc_count: 1, key: 'PNG' }] }, - layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, - objectTypes: { doc_count: 0, pdf: { buckets: [] } }, - statusByApp: { buckets: [ { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, ], }, - statusTypes: { buckets: [{ doc_count: 1, key: 'completed_with_warnings' }] }, - }, - lastDay: { - doc_count: 1, - jobTypes: { buckets: [{ doc_count: 1, key: 'PNG' }] }, - layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, - objectTypes: { doc_count: 0, pdf: { buckets: [] } }, - statusByApp: { buckets: [ { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, ], }, - statusTypes: { buckets: [{ doc_count: 1, key: 'completed_with_warnings' }] }, - }, + collectorFetchContext = getMockFetchClients( + getResponseMock( + { + aggregations: { + ranges: { + buckets: { + all: { + doc_count: 12, + jobTypes: { buckets: [ { doc_count: 9, key: 'printable_pdf' }, { doc_count: 3, key: 'PNG' }, ], }, + layoutTypes: { doc_count: 9, pdf: { buckets: [{ doc_count: 9, key: 'preserve_layout' }] }, }, + objectTypes: { doc_count: 9, pdf: { buckets: [ { doc_count: 6, key: 'canvas workpad' }, { doc_count: 3, key: 'visualization' }, ], }, }, + statusByApp: { buckets: [ { doc_count: 10, jobTypes: { buckets: [ { appNames: { buckets: [ { doc_count: 6, key: 'canvas workpad' }, { doc_count: 3, key: 'visualization' }, ], }, doc_count: 9, key: 'printable_pdf', }, { appNames: { buckets: [{ doc_count: 1, key: 'visualization' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed', }, { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'failed', }, ], }, + statusTypes: { buckets: [ { doc_count: 10, key: 'completed' }, { doc_count: 1, key: 'completed_with_warnings' }, { doc_count: 1, key: 'failed' }, ], }, + }, + last7Days: { + doc_count: 1, + jobTypes: { buckets: [{ doc_count: 1, key: 'PNG' }] }, + layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, + objectTypes: { doc_count: 0, pdf: { buckets: [] } }, + statusByApp: { buckets: [ { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, ], }, + statusTypes: { buckets: [{ doc_count: 1, key: 'completed_with_warnings' }] }, + }, + lastDay: { + doc_count: 1, + jobTypes: { buckets: [{ doc_count: 1, key: 'PNG' }] }, + layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, + objectTypes: { doc_count: 0, pdf: { buckets: [] } }, + statusByApp: { buckets: [ { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, ], }, + statusTypes: { buckets: [{ doc_count: 1, key: 'completed_with_warnings' }] }, }, }, }, - } as SearchResponse) // prettier-ignore - ) + }, + } as SearchResponse) // prettier-ignore ); - - const usageStats = await fetch(callClusterMock as any); + const usageStats = await fetch(collectorFetchContext); expect(usageStats).toMatchSnapshot(); }); @@ -251,44 +252,42 @@ describe('data modeling', () => { return Promise.resolve(true); } ); - const callClusterMock = jest.fn(() => - Promise.resolve( - getResponseMock({ - aggregations: { - ranges: { - buckets: { - all: { - doc_count: 4, - layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, - statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, - objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, - statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, - jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, - }, - last7Days: { - doc_count: 4, - layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, - statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, - objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, - statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, - jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, - }, - lastDay: { - doc_count: 4, - layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, - statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, - objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, - statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, - jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, - }, + collectorFetchContext = getMockFetchClients( + getResponseMock( + { + aggregations: { + ranges: { + buckets: { + all: { + doc_count: 4, + layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, + statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, + objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, + statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, + jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, + }, + last7Days: { + doc_count: 4, + layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, + statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, + objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, + statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, + jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, + }, + lastDay: { + doc_count: 4, + layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, + statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, + objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, + statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, + jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, }, }, }, - } as SearchResponse) // prettier-ignore - ) + }, + } as SearchResponse) // prettier-ignore ); - - const usageStats = await fetch(callClusterMock as any); + const usageStats = await fetch(collectorFetchContext); expect(usageStats).toMatchSnapshot(); }); @@ -303,43 +302,42 @@ describe('data modeling', () => { return Promise.resolve(true); } ); - const callClusterMock = jest.fn(() => - Promise.resolve( - getResponseMock({ - aggregations: { - ranges: { - buckets: { - all: { - doc_count: 0, - jobTypes: { buckets: [] }, - layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, - objectTypes: { doc_count: 0, pdf: { buckets: [] } }, - statusByApp: { buckets: [] }, - statusTypes: { buckets: [] }, - }, - last7Days: { - doc_count: 0, - jobTypes: { buckets: [] }, - layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, - objectTypes: { doc_count: 0, pdf: { buckets: [] } }, - statusByApp: { buckets: [] }, - statusTypes: { buckets: [] }, - }, - lastDay: { - doc_count: 0, - jobTypes: { buckets: [] }, - layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, - objectTypes: { doc_count: 0, pdf: { buckets: [] } }, - statusByApp: { buckets: [] }, - statusTypes: { buckets: [] }, - }, - }, + + collectorFetchContext = getMockFetchClients( + getResponseMock({ + aggregations: { + ranges: { + buckets: { + all: { + doc_count: 0, + jobTypes: { buckets: [] }, + layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, + objectTypes: { doc_count: 0, pdf: { buckets: [] } }, + statusByApp: { buckets: [] }, + statusTypes: { buckets: [] }, + }, + last7Days: { + doc_count: 0, + jobTypes: { buckets: [] }, + layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, + objectTypes: { doc_count: 0, pdf: { buckets: [] } }, + statusByApp: { buckets: [] }, + statusTypes: { buckets: [] }, + }, + lastDay: { + doc_count: 0, + jobTypes: { buckets: [] }, + layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, + objectTypes: { doc_count: 0, pdf: { buckets: [] } }, + statusByApp: { buckets: [] }, + statusTypes: { buckets: [] }, }, }, - } as SearchResponse) - ) + }, + }, + } as SearchResponse) // prettier-ignore ); - const usageStats = await fetch(callClusterMock as any); + const usageStats = await fetch(collectorFetchContext); expect(usageStats).toMatchSnapshot(); }); diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts index 176d3dcb37df..2ef7a7995b83 100644 --- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts @@ -5,8 +5,7 @@ */ import { first, map } from 'rxjs/operators'; -import { LegacyAPICaller } from 'kibana/server'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { CollectorFetchContext, UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { ReportingCore } from '../'; import { ExportTypesRegistry } from '../lib/export_types_registry'; import { ReportingSetupDeps } from '../types'; @@ -37,7 +36,7 @@ export function getReportingUsageCollector( ) { return usageCollection.makeUsageCollector({ type: 'reporting', - fetch: (callCluster: LegacyAPICaller) => { + fetch: ({ callCluster }: CollectorFetchContext) => { const config = reporting.getConfig(); return getReportingUsage(config, getLicense, callCluster, exportTypesRegistry); }, diff --git a/x-pack/plugins/rollup/server/collectors/register.ts b/x-pack/plugins/rollup/server/collectors/register.ts index daacc065629a..33bb430aefe5 100644 --- a/x-pack/plugins/rollup/server/collectors/register.ts +++ b/x-pack/plugins/rollup/server/collectors/register.ts @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { UsageCollectionSetup, CollectorFetchContext } from 'src/plugins/usage_collection/server'; import { LegacyAPICaller } from 'kibana/server'; interface IdToFlagMap { @@ -211,7 +211,7 @@ export function registerRollupUsageCollector( total: { type: 'long' }, }, }, - fetch: async (callCluster: LegacyAPICaller) => { + fetch: async ({ callCluster }: CollectorFetchContext) => { const rollupIndexPatterns = await fetchRollupIndexPatterns(kibanaIndex, callCluster); const rollupIndexPatternToFlagMap = createIdToFlagMap(rollupIndexPatterns); diff --git a/x-pack/plugins/security/public/plugin.tsx b/x-pack/plugins/security/public/plugin.tsx index 87bcc96d1f9d..700653c4cecb 100644 --- a/x-pack/plugins/security/public/plugin.tsx +++ b/x-pack/plugins/security/public/plugin.tsx @@ -147,7 +147,7 @@ export class SecurityPlugin public start(core: CoreStart, { management, securityOss }: PluginStartDependencies) { this.sessionTimeout.start(); this.navControlService.start({ core }); - this.securityCheckupService.start({ securityOssStart: securityOss }); + this.securityCheckupService.start({ securityOssStart: securityOss, docLinks: core.docLinks }); if (management) { this.managementService.start({ capabilities: core.application.capabilities }); } diff --git a/x-pack/plugins/security/public/security_checkup/components/insecure_cluster_alert.tsx b/x-pack/plugins/security/public/security_checkup/components/insecure_cluster_alert.tsx index 6ba06e0cc477..310caeac91dc 100644 --- a/x-pack/plugins/security/public/security_checkup/components/insecure_cluster_alert.tsx +++ b/x-pack/plugins/security/public/security_checkup/components/insecure_cluster_alert.tsx @@ -16,13 +16,17 @@ import { EuiFlexItem, EuiButton, } from '@elastic/eui'; +import { DocumentationLinksService } from '../documentation_links'; export const insecureClusterAlertTitle = i18n.translate( 'xpack.security.checkup.insecureClusterTitle', - { defaultMessage: 'Please secure your installation' } + { defaultMessage: 'Your data is not secure' } ); -export const insecureClusterAlertText = (onDismiss: (persist: boolean) => void) => +export const insecureClusterAlertText = ( + getDocLinksService: () => DocumentationLinksService, + onDismiss: (persist: boolean) => void +) => ((e) => { const AlertText = () => { const [persist, setPersist] = useState(false); @@ -33,7 +37,7 @@ export const insecureClusterAlertText = (onDismiss: (persist: boolean) => void) @@ -52,8 +56,9 @@ export const insecureClusterAlertText = (onDismiss: (persist: boolean) => void) size="s" color="primary" fill - href="https://www.elastic.co/what-is/elastic-stack-security" + href={getDocLinksService().getEnableSecurityDocUrl()} target="_blank" + data-test-subj="learnMoreButton" > {i18n.translate('xpack.security.checkup.enableButtonText', { defaultMessage: `Enable security`, diff --git a/x-pack/plugins/security/public/security_checkup/documentation_links.ts b/x-pack/plugins/security/public/security_checkup/documentation_links.ts new file mode 100644 index 000000000000..b53a6ffd94be --- /dev/null +++ b/x-pack/plugins/security/public/security_checkup/documentation_links.ts @@ -0,0 +1,19 @@ +/* + * 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 { DocLinksStart } from 'src/core/public'; + +export class DocumentationLinksService { + private readonly esDocBasePath: string; + + constructor(docLinks: DocLinksStart) { + this.esDocBasePath = `${docLinks.ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${docLinks.DOC_LINK_VERSION}`; + } + + public getEnableSecurityDocUrl() { + return `${this.esDocBasePath}/get-started-enable-security.html?blade=kibanasecuritymessage`; + } +} diff --git a/x-pack/plugins/security/public/security_checkup/security_checkup_service.test.ts b/x-pack/plugins/security/public/security_checkup/security_checkup_service.test.ts index 3709f52d29ff..691cbf8ac9ea 100644 --- a/x-pack/plugins/security/public/security_checkup/security_checkup_service.test.ts +++ b/x-pack/plugins/security/public/security_checkup/security_checkup_service.test.ts @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { MountPoint } from 'kibana/public'; + +import { docLinksServiceMock } from '../../../../../src/core/public/mocks'; import { mockSecurityOssPlugin } from '../../../../../src/plugins/security_oss/public/mocks'; import { insecureClusterAlertTitle } from './components'; import { SecurityCheckupService } from './security_checkup_service'; @@ -13,9 +16,12 @@ let mockOnDismiss = jest.fn(); jest.mock('./components', () => { return { insecureClusterAlertTitle: 'mock insecure cluster title', - insecureClusterAlertText: (onDismiss: any) => { + insecureClusterAlertText: (getDocLinksService: any, onDismiss: any) => { mockOnDismiss = onDismiss; - return 'mock insecure cluster text'; + const { insecureClusterAlertText } = jest.requireActual( + './components/insecure_cluster_alert' + ); + return insecureClusterAlertText(getDocLinksService, onDismiss); }, }; }); @@ -31,9 +37,7 @@ describe('SecurityCheckupService', () => { insecureClusterAlertTitle ); - expect(securityOssSetup.insecureCluster.setAlertText).toHaveBeenCalledWith( - 'mock insecure cluster text' - ); + expect(securityOssSetup.insecureCluster.setAlertText).toHaveBeenCalledTimes(1); }); }); describe('#start', () => { @@ -42,7 +46,7 @@ describe('SecurityCheckupService', () => { const securityOssStart = mockSecurityOssPlugin.createStart(); const service = new SecurityCheckupService(); service.setup({ securityOssSetup }); - service.start({ securityOssStart }); + service.start({ securityOssStart, docLinks: docLinksServiceMock.createStartContract() }); expect(securityOssStart.insecureCluster.hideAlert).toHaveBeenCalledTimes(0); @@ -50,5 +54,26 @@ describe('SecurityCheckupService', () => { expect(securityOssStart.insecureCluster.hideAlert).toHaveBeenCalledTimes(1); }); + + it('configures the doc link correctly', async () => { + const securityOssSetup = mockSecurityOssPlugin.createSetup(); + const securityOssStart = mockSecurityOssPlugin.createStart(); + const service = new SecurityCheckupService(); + service.setup({ securityOssSetup }); + service.start({ securityOssStart, docLinks: docLinksServiceMock.createStartContract() }); + + const [alertText] = securityOssSetup.insecureCluster.setAlertText.mock.calls[0]; + + const container = document.createElement('div'); + (alertText as MountPoint)(container); + + const docLink = container + .querySelector('[data-test-subj="learnMoreButton"]') + ?.getAttribute('href'); + + expect(docLink).toMatchInlineSnapshot( + `"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/get-started-enable-security.html?blade=kibanasecuritymessage"` + ); + }); }); }); diff --git a/x-pack/plugins/security/public/security_checkup/security_checkup_service.tsx b/x-pack/plugins/security/public/security_checkup/security_checkup_service.tsx index 899a74083656..a0ea194170df 100644 --- a/x-pack/plugins/security/public/security_checkup/security_checkup_service.tsx +++ b/x-pack/plugins/security/public/security_checkup/security_checkup_service.tsx @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { DocLinksStart } from 'kibana/public'; + import { SecurityOssPluginSetup, SecurityOssPluginStart, } from '../../../../../src/plugins/security_oss/public'; import { insecureClusterAlertTitle, insecureClusterAlertText } from './components'; +import { DocumentationLinksService } from './documentation_links'; interface SetupDeps { securityOssSetup: SecurityOssPluginSetup; @@ -16,20 +19,27 @@ interface SetupDeps { interface StartDeps { securityOssStart: SecurityOssPluginStart; + docLinks: DocLinksStart; } export class SecurityCheckupService { private securityOssStart?: SecurityOssPluginStart; + private docLinksService?: DocumentationLinksService; + public setup({ securityOssSetup }: SetupDeps) { securityOssSetup.insecureCluster.setAlertTitle(insecureClusterAlertTitle); securityOssSetup.insecureCluster.setAlertText( - insecureClusterAlertText((persist: boolean) => this.onDismiss(persist)) + insecureClusterAlertText( + () => this.docLinksService!, + (persist: boolean) => this.onDismiss(persist) + ) ); } - public start({ securityOssStart }: StartDeps) { + public start({ securityOssStart, docLinks }: StartDeps) { this.securityOssStart = securityOssStart; + this.docLinksService = new DocumentationLinksService(docLinks); } private onDismiss(persist: boolean) { diff --git a/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts b/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts index 6c3dcddcdb41..80b7dd35e595 100644 --- a/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts +++ b/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts @@ -7,9 +7,11 @@ import { createConfig, ConfigSchema } from '../config'; import { loggingSystemMock } from 'src/core/server/mocks'; import { TypeOf } from '@kbn/config-schema'; -import { usageCollectionPluginMock } from 'src/plugins/usage_collection/server/mocks'; +import { + usageCollectionPluginMock, + createCollectorFetchContextMock, +} from 'src/plugins/usage_collection/server/mocks'; import { registerSecurityUsageCollector } from './security_usage_collector'; -import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { licenseMock } from '../../common/licensing/index.mock'; import { SecurityLicenseFeatures } from '../../common/licensing'; @@ -34,7 +36,7 @@ describe('Security UsageCollector', () => { return license; }; - const clusterClient = elasticsearchServiceMock.createLegacyClusterClient(); + const collectorFetchContext = createCollectorFetchContextMock(); describe('initialization', () => { it('handles an undefined usage collector', () => { @@ -68,7 +70,7 @@ describe('Security UsageCollector', () => { const usage = await usageCollection .getCollectorByType('security') - ?.fetch(clusterClient.asScoped().callAsCurrentUser); + ?.fetch(collectorFetchContext); expect(usage).toEqual({ auditLoggingEnabled: false, @@ -89,7 +91,7 @@ describe('Security UsageCollector', () => { const usage = await usageCollection .getCollectorByType('security') - ?.fetch(clusterClient.asScoped().callAsCurrentUser); + ?.fetch(collectorFetchContext); expect(usage).toEqual({ auditLoggingEnabled: false, @@ -133,7 +135,7 @@ describe('Security UsageCollector', () => { const usage = await usageCollection .getCollectorByType('security') - ?.fetch(clusterClient.asScoped().callAsCurrentUser); + ?.fetch(collectorFetchContext); expect(usage).toEqual({ auditLoggingEnabled: false, @@ -182,7 +184,7 @@ describe('Security UsageCollector', () => { const usage = await usageCollection .getCollectorByType('security') - ?.fetch(clusterClient.asScoped().callAsCurrentUser); + ?.fetch(collectorFetchContext); expect(usage).toEqual({ auditLoggingEnabled: false, @@ -220,7 +222,7 @@ describe('Security UsageCollector', () => { const usage = await usageCollection .getCollectorByType('security') - ?.fetch(clusterClient.asScoped().callAsCurrentUser); + ?.fetch(collectorFetchContext); expect(usage).toEqual({ auditLoggingEnabled: false, @@ -258,7 +260,7 @@ describe('Security UsageCollector', () => { const usage = await usageCollection .getCollectorByType('security') - ?.fetch(clusterClient.asScoped().callAsCurrentUser); + ?.fetch(collectorFetchContext); expect(usage).toEqual({ auditLoggingEnabled: false, @@ -299,7 +301,7 @@ describe('Security UsageCollector', () => { const usage = await usageCollection .getCollectorByType('security') - ?.fetch(clusterClient.asScoped().callAsCurrentUser); + ?.fetch(collectorFetchContext); expect(usage).toEqual({ auditLoggingEnabled: false, @@ -338,7 +340,7 @@ describe('Security UsageCollector', () => { const usage = await usageCollection .getCollectorByType('security') - ?.fetch(clusterClient.asScoped().callAsCurrentUser); + ?.fetch(collectorFetchContext); expect(usage).toEqual({ auditLoggingEnabled: false, @@ -366,7 +368,7 @@ describe('Security UsageCollector', () => { const usage = await usageCollection .getCollectorByType('security') - ?.fetch(clusterClient.asScoped().callAsCurrentUser); + ?.fetch(collectorFetchContext); expect(usage).toEqual({ auditLoggingEnabled: true, @@ -392,7 +394,7 @@ describe('Security UsageCollector', () => { const usage = await usageCollection .getCollectorByType('security') - ?.fetch(clusterClient.asScoped().callAsCurrentUser); + ?.fetch(collectorFetchContext); expect(usage).toEqual({ auditLoggingEnabled: false, @@ -422,7 +424,7 @@ describe('Security UsageCollector', () => { const usage = await usageCollection .getCollectorByType('security') - ?.fetch(clusterClient.asScoped().callAsCurrentUser); + ?.fetch(collectorFetchContext); expect(usage).toEqual({ auditLoggingEnabled: false, @@ -450,7 +452,7 @@ describe('Security UsageCollector', () => { const usage = await usageCollection .getCollectorByType('security') - ?.fetch(clusterClient.asScoped().callAsCurrentUser); + ?.fetch(collectorFetchContext); expect(usage).toEqual({ auditLoggingEnabled: false, diff --git a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts index 1a4852e45027..278ce1d39ae9 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts @@ -16,6 +16,7 @@ import { ExceptionListItemSchema, CreateExceptionListItemSchema, } from '../../../lists/common/schemas'; +import { ESBoolQuery } from '../typed_json'; import { buildExceptionListQueries } from './build_exceptions_query'; import { Query as QueryString, @@ -31,7 +32,7 @@ export const getQueryFilter = ( index: Index, lists: Array, excludeExceptions: boolean = true -) => { +): ESBoolQuery => { const indexPattern: IIndexPattern = { fields: [], title: index.join(), 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 340f93150ce5..08c544b9246e 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 409b5bbdc17a..4a4c76adb097 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 9e6b6563cec6..2c8618f4edcd 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/schema/policy.ts b/x-pack/plugins/security_solution/common/endpoint/schema/policy.ts index 17d0cdff57ee..35ba1266066e 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/typed_json.ts b/x-pack/plugins/security_solution/common/typed_json.ts index 61c109300219..26832e23f6f2 100644 --- a/x-pack/plugins/security_solution/common/typed_json.ts +++ b/x-pack/plugins/security_solution/common/typed_json.ts @@ -3,9 +3,17 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { DslQuery, Filter } from 'src/plugins/data/common'; + import { JsonObject } from '../../../../src/plugins/kibana_utils/common'; -export type ESQuery = ESRangeQuery | ESQueryStringQuery | ESMatchQuery | ESTermQuery | JsonObject; +export type ESQuery = + | ESRangeQuery + | ESQueryStringQuery + | ESMatchQuery + | ESTermQuery + | ESBoolQuery + | JsonObject; export interface ESRangeQuery { range: { @@ -37,3 +45,12 @@ export interface ESQueryStringQuery { export interface ESTermQuery { term: Record; } + +export interface ESBoolQuery { + bool: { + must: DslQuery[]; + filter: Filter[]; + should: never[]; + must_not: Filter[]; + }; +} 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 d8832dc4ee60..28889920e00e 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 @@ -135,7 +135,7 @@ describe('Custom detection rules creation', () => { // expect define step to repopulate cy.get(DEFINE_EDIT_BUTTON).click(); - cy.get(CUSTOM_QUERY_INPUT).should('have.text', newRule.customQuery); + cy.get(CUSTOM_QUERY_INPUT).should('have.value', newRule.customQuery); cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); cy.get(DEFINE_CONTINUE_BUTTON).should('not.exist'); @@ -182,7 +182,7 @@ describe('Custom detection rules creation', () => { cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); cy.get(DEFINITION_DETAILS).within(() => { getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); - getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${newRule.customQuery} `); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', newRule.customQuery); getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query'); getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); }); @@ -293,7 +293,7 @@ describe('Custom detection rules deletion and edition', () => { waitForKibana(); // expect define step to populate - cy.get(CUSTOM_QUERY_INPUT).should('have.text', existingRule.customQuery); + cy.get(CUSTOM_QUERY_INPUT).should('have.value', existingRule.customQuery); if (existingRule.index && existingRule.index.length > 0) { cy.get(DEFINE_INDEX_INPUT).should('have.text', existingRule.index.join('')); } @@ -344,7 +344,7 @@ describe('Custom detection rules deletion and edition', () => { 'have.text', expectedEditedIndexPatterns.join('') ); - getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${editedRule.customQuery} `); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', editedRule.customQuery); getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query'); getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); }); 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 5745a545f048..252ffb6c8c66 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 @@ -145,7 +145,7 @@ describe.skip('Detection rules, EQL', () => { cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); cy.get(DEFINITION_DETAILS).within(() => { getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); - getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${eqlRule.customQuery} `); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', eqlRule.customQuery); getDetails(RULE_TYPE_DETAILS).should('have.text', 'Event Correlation'); getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); }); 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 090012de7253..abc873f2df0e 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 @@ -164,7 +164,7 @@ describe('Detection rules, override', () => { cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); cy.get(DEFINITION_DETAILS).within(() => { getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); - getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${newOverrideRule.customQuery} `); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', newOverrideRule.customQuery); getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query'); getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); }); 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 5ee7e69e877e..9d988a46662f 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 @@ -143,7 +143,7 @@ describe('Detection rules, threshold', () => { cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); cy.get(DEFINITION_DETAILS).within(() => { getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); - getDetails(CUSTOM_QUERY_DETAILS).should('have.text', `${newThresholdRule.customQuery} `); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', newThresholdRule.customQuery); getDetails(RULE_TYPE_DETAILS).should('have.text', 'Threshold'); getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); getDetails(THRESHOLD_DETAILS).should( 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 e2f5ca9025bd..7ccd588e16a8 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 9f61d11b7ac0..8ce60450671b 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,7 +45,8 @@ import { openTimeline } from '../tasks/timelines'; import { OVERVIEW_URL } from '../urls/navigation'; -describe('Timelines', () => { +// FLAKY: https://github.com/elastic/kibana/issues/79389 +describe.skip('Timelines', () => { before(() => { cy.server(); cy.route('PATCH', '**/api/timeline').as('timeline'); diff --git a/x-pack/plugins/security_solution/cypress/screens/overview.ts b/x-pack/plugins/security_solution/cypress/screens/overview.ts index 95facc897440..006d5fdf5a66 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/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index 1433acd27c93..fa3c219595c7 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -190,7 +190,7 @@ export const fillDefineCustomRuleWithImportedQueryAndContinue = ( ) => { cy.get(IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK).click(); cy.get(TIMELINE(rule.timelineId)).click(); - cy.get(CUSTOM_QUERY_INPUT).should('have.text', rule.customQuery); + cy.get(CUSTOM_QUERY_INPUT).should('have.value', rule.customQuery); cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); @@ -208,7 +208,7 @@ export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRule) => { const threshold = 1; cy.get(CUSTOM_QUERY_INPUT).type(rule.customQuery); - cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', rule.customQuery); + cy.get(CUSTOM_QUERY_INPUT).should('have.value', rule.customQuery); cy.get(THRESHOLD_INPUT_AREA) .find(INPUT) .then((inputs) => { diff --git a/x-pack/plugins/security_solution/public/app/index.tsx b/x-pack/plugins/security_solution/public/app/index.tsx index 69bf2549d743..4c8e87c4abfb 100644 --- a/x-pack/plugins/security_solution/public/app/index.tsx +++ b/x-pack/plugins/security_solution/public/app/index.tsx @@ -5,20 +5,10 @@ */ import React from 'react'; -import { Store, Action } from 'redux'; import { render, unmountComponentAtNode } from 'react-dom'; -import { AppMountParameters } from '../../../../../src/core/public'; -import { State } from '../common/store'; -import { StartServices } from '../types'; import { SecurityApp } from './app'; -import { AppFrontendLibs } from '../common/lib/lib'; - -interface RenderAppProps extends AppFrontendLibs, AppMountParameters { - services: StartServices; - store: Store; - SubPluginRoutes: React.FC; -} +import { RenderAppProps } from './types'; export const renderApp = ({ apolloClient, @@ -27,7 +17,7 @@ export const renderApp = ({ services, store, SubPluginRoutes, -}: RenderAppProps) => { +}: RenderAppProps): (() => void) => { render( diff --git a/x-pack/plugins/security_solution/public/app/types.ts b/x-pack/plugins/security_solution/public/app/types.ts index 4590f05e1263..24ecf6b6d6cb 100644 --- a/x-pack/plugins/security_solution/public/app/types.ts +++ b/x-pack/plugins/security_solution/public/app/types.ts @@ -8,12 +8,27 @@ import { Reducer, AnyAction, Middleware, + Action, + Store, Dispatch, PreloadedState, StateFromReducersMapObject, CombinedState, } from 'redux'; +import { AppMountParameters } from '../../../../../src/core/public'; +import { StartServices } from '../types'; +import { AppFrontendLibs } from '../common/lib/lib'; + +/** + * The React properties used to render `SecurityApp` as well as the `element` to render it into. + */ +export interface RenderAppProps extends AppFrontendLibs, AppMountParameters { + services: StartServices; + store: Store; + SubPluginRoutes: React.FC; +} + import { State, SubPluginsInitReducer } from '../common/store'; import { Immutable } from '../../common/endpoint/types'; import { AppAction } from '../common/store/actions'; @@ -31,7 +46,7 @@ export interface SecuritySubPlugin { storageTimelines?: Pick; } -type SecuritySubPluginKeyStore = +export type SecuritySubPluginKeyStore = | 'hosts' | 'network' | 'timeline' 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 61e9dd04d910..4bd2cd05d49d 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 @@ -200,11 +200,7 @@ exports[`item_details_card ItemDetailsPropertySummary should render correctly 1` name 1 - - value 1 - + value 1 `; 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 9105514b7580..c41c5f89c006 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 @@ -66,16 +66,13 @@ const DescriptionListDescription = styled(EuiDescriptionListDescription)` interface ItemDetailsPropertySummaryProps { name: ReactNode | ReactNode[]; value: ReactNode | ReactNode[]; - title?: string; } -export const ItemDetailsPropertySummary: FC = memo( - ({ name, value, title = '' }) => ( +export const ItemDetailsPropertySummary = memo( + ({ name, value }) => ( <> {name} - - {value} - + {value} ) ); diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx index 7395100784d5..e7d7e60a3c40 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx @@ -34,7 +34,6 @@ export type MatrixHistogramComponentProps = MatrixHistogramProps & defaultStackByOption: MatrixHistogramOption; errorMessage: string; headerChildren?: React.ReactNode; - footerChildren?: React.ReactNode; hideHistogramIfEmpty?: boolean; histogramType: MatrixHistogramType; id: string; @@ -48,7 +47,6 @@ export type MatrixHistogramComponentProps = MatrixHistogramProps & subtitle?: string | GetSubTitle; timelineId?: string; title: string | GetTitle; - yTitle?: string | undefined; }; const DEFAULT_PANEL_HEIGHT = 300; @@ -70,7 +68,6 @@ export const MatrixHistogramComponent: React.FC = errorMessage, filterQuery, headerChildren, - footerChildren, histogramType, hideHistogramIfEmpty = false, id, @@ -89,7 +86,6 @@ export const MatrixHistogramComponent: React.FC = title, titleSize, yTickFormatter, - yTitle, }) => { const dispatch = useDispatch(); const handleBrushEnd = useCallback( @@ -118,18 +114,8 @@ export const MatrixHistogramComponent: React.FC = onBrushEnd: handleBrushEnd, yTickFormatter, showLegend, - yTitle, }), - [ - chartHeight, - startDate, - legendPosition, - endDate, - handleBrushEnd, - yTickFormatter, - showLegend, - yTitle, - ] + [chartHeight, startDate, legendPosition, endDate, handleBrushEnd, yTickFormatter, showLegend] ); const [isInitialLoading, setIsInitialLoading] = useState(true); const [selectedStackByOption, setSelectedStackByOption] = useState( @@ -243,11 +229,6 @@ export const MatrixHistogramComponent: React.FC = timelineId={timelineId} /> )} - {footerChildren != null && ( - - {footerChildren} - - )} {showSpacer && } diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts index 4c04a4cca9f8..828cadd90bb1 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts @@ -71,6 +71,7 @@ export interface MatrixHistogramQueryProps { startDate: string; histogramType: MatrixHistogramType; threshold?: { field: string | undefined; value: number } | undefined; + skip?: boolean; } export interface MatrixHistogramProps extends MatrixHistogramBasicProps { @@ -105,7 +106,6 @@ export interface BarchartConfigs { yTickFormatter: TickFormatter; tickSize: number; }; - yAxisTitle: string | undefined; settings: { legendPosition: Position; onBrushEnd: UpdateDateRange; diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.ts index d1af29d7da27..5b5b56cf0ec4 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/utils.ts @@ -19,7 +19,6 @@ interface GetBarchartConfigsProps { onBrushEnd: UpdateDateRange; yTickFormatter?: (value: number) => string; showLegend?: boolean; - yTitle?: string | undefined; } export const DEFAULT_CHART_HEIGHT = 174; @@ -33,7 +32,6 @@ export const getBarchartConfigs = ({ onBrushEnd, yTickFormatter, showLegend, - yTitle, }: GetBarchartConfigsProps): BarchartConfigs => ({ series: { xScaleType: ScaleType.Time, @@ -45,7 +43,6 @@ export const getBarchartConfigs = ({ yTickFormatter: yTickFormatter != null ? yTickFormatter : DEFAULT_Y_TICK_FORMATTER, tickSize: 8, }, - yAxisTitle: yTitle, settings: { legendPosition: legendPosition ?? Position.Right, onBrushEnd, diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx index 182c1d5022d0..bc0911679834 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx @@ -177,6 +177,7 @@ export const Sourcerer = React.memo(({ scope: scopeId } closePopover={handleClosePopOver} display="block" panelPaddingSize="s" + repositionOnScroll ownFocus > 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 5372ccfcd118..b585bfc61331 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 && ( - <> - {','} - - - )} + + field 1 + + + super long text part 0 super long text part 1 super long text part 2 super long text part 3 super long text part 4 super long text part 5 super long text part 6 super long text part 7 super long text part 8 super long text part 9 super long text part 10 super long text part 11 super long text part 12 super long text part 13 super long text part 14 super long text part 15 super long text part 16 super long text part 17 super long text part 18 super long text part 19 + + + } + delay="regular" + position="top" +> + + super long text part 0 super long text part 1 super long text part 2 super long text part 3 sup... + + +`; + +exports[`text_field_value TextFieldValue should render long text correctly, when there is no limit 1`] = ` + + + field 1 + + + super long text part 0 super long text part 1 super long text part 2 super long text part 3 super long text part 4 super long text part 5 super long text part 6 super long text part 7 super long text part 8 super long text part 9 super long text part 10 super long text part 11 super long text part 12 super long text part 13 super long text part 14 super long text part 15 super long text part 16 super long text part 17 super long text part 18 super long text part 19 + + + } + delay="regular" + position="top" +> + + super long text part 0 super long text part 1 super long text part 2 super long text part 3 super long text part 4 super long text part 5 super long text part 6 super long text part 7 super long text part 8 super long text part 9 super long text part 10 super long text part 11 super long text part 12 super long text part 13 super long text part 14 super long text part 15 super long text part 16 super long text part 17 super long text part 18 super long text part 19 + + +`; + +exports[`text_field_value TextFieldValue should render small text correctly, when there is limit 1`] = ` + + + field 1 + + + value 1 + + + } + delay="regular" + position="top" +> + + value 1 + + +`; + +exports[`text_field_value TextFieldValue should render small text correctly, when there is no limit 1`] = ` + + + field 1 + + + value 1 + + + } + delay="regular" + position="top" +> + + value 1 + + +`; diff --git a/x-pack/plugins/security_solution/public/common/components/text_field_value/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/text_field_value/index.stories.tsx new file mode 100644 index 000000000000..cd0a4fcd6561 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/text_field_value/index.stories.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { storiesOf, addDecorator } from '@storybook/react'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { TextFieldValue } from '.'; + +addDecorator((storyFn) => ( + ({ eui: euiLightVars, darkMode: false })}>{storyFn()} +)); + +const longText = [...new Array(20).keys()].map((i) => ` super long text part ${i}`).join(' '); + +storiesOf('Components/TextFieldValue', module) + .add('short text, no limit', () => ) + .add('short text, with limit', () => ( + + )) + .add('long text, no limit', () => ) + .add('long text, with limit', () => ( + + )); diff --git a/x-pack/plugins/security_solution/public/common/components/text_field_value/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/text_field_value/index.test.tsx new file mode 100644 index 000000000000..3ea1ae6d05ad --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/text_field_value/index.test.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { shallow } from 'enzyme'; +import React from 'react'; + +import { TextFieldValue } from '.'; + +describe('text_field_value', () => { + describe('TextFieldValue', () => { + const longText = [...new Array(20).keys()].map((i) => ` super long text part ${i}`).join(' '); + + it('should render small text correctly, when there is no limit', () => { + expect(shallow()).toMatchSnapshot(); + }); + + it('should render small text correctly, when there is limit', () => { + const element = shallow( + + ); + + expect(element).toMatchSnapshot(); + }); + + it('should render long text correctly, when there is no limit', () => { + expect(shallow()).toMatchSnapshot(); + }); + + it('should render long text correctly, when there is limit', () => { + const element = shallow( + + ); + + expect(element).toMatchSnapshot(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/text_field_value/index.tsx b/x-pack/plugins/security_solution/public/common/components/text_field_value/index.tsx new file mode 100644 index 000000000000..8b482215f24f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/text_field_value/index.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import React from 'react'; + +const trimTextOverflow = (text: string, maxLength?: number) => { + if (maxLength !== undefined && text.length > maxLength) { + return `${text.substr(0, maxLength)}...`; + } else { + return text; + } +}; + +interface Props { + fieldName: string; + value: string; + maxLength?: number; + className?: string; +} + +/* + * Component to display text field value. Text field values can be large and need + * programmatic truncation to a fixed text length. As text can be truncated the tooltip + * is shown displaying the field name and full value. If the use case allows single + * line truncation with CSS use eui-textTruncate class on this component instead of + * maxLength property. + */ +export const TextFieldValue = ({ fieldName, value, maxLength, className }: Props) => { + return ( + + {fieldName} + {value} + + } + > + {trimTextOverflow(value, maxLength)} + + ); +}; diff --git a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx index 489ccb23c9b2..81dfd7539ebd 100644 --- a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx @@ -29,6 +29,7 @@ const AnomaliesQueryTabBodyComponent: React.FC = ({ 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 3ce4b8b6d449..7621749348a9 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/containers/matrix_histogram/index.ts b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts index 7c6a110f56b8..6250a4fd959b 100644 --- a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts @@ -27,6 +27,13 @@ import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; import * as i18n from './translations'; +export type Buckets = Array<{ + key: string; + doc_count: number; +}>; + +const bucketEmpty: Buckets = []; + export interface UseMatrixHistogramArgs { data: MatrixHistogramData[]; inspect: InspectResponse; @@ -49,7 +56,12 @@ export const useMatrixHistogram = ({ stackByField, startDate, threshold, -}: MatrixHistogramQueryProps): [boolean, UseMatrixHistogramArgs] => { + skip = false, +}: MatrixHistogramQueryProps): [ + boolean, + UseMatrixHistogramArgs, + (to: string, from: string) => void +] => { const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); @@ -98,10 +110,11 @@ export const useMatrixHistogram = ({ next: (response) => { if (isCompleteResponse(response)) { if (!didCancel) { - const histogramBuckets: Array<{ - key: string; - doc_count: number; - }> = getOr([], 'rawResponse.aggregations.eventActionGroup.buckets', response); + const histogramBuckets: Buckets = getOr( + bucketEmpty, + 'rawResponse.aggregations.eventActionGroup.buckets', + response + ); setLoading(false); setMatrixHistogramResponse((prevResponse) => ({ ...prevResponse, @@ -123,10 +136,12 @@ export const useMatrixHistogram = ({ } }, error: (msg) => { + if (!didCancel) { + setLoading(false); + } if (!(msg instanceof AbortError)) { - notifications.toasts.addDanger({ + notifications.toasts.addError(msg, { title: errorMessage ?? i18n.FAIL_MATRIX_HISTOGRAM, - text: msg.message, }); } }, @@ -166,8 +181,24 @@ export const useMatrixHistogram = ({ }, [indexNames, endDate, filterQuery, startDate, stackByField, histogramType, threshold]); useEffect(() => { - hostsSearch(matrixHistogramRequest); - }, [matrixHistogramRequest, hostsSearch]); + if (!skip) { + hostsSearch(matrixHistogramRequest); + } + }, [matrixHistogramRequest, hostsSearch, skip]); + + const runMatrixHistogramSearch = useCallback( + (to: string, from: string) => { + hostsSearch({ + ...matrixHistogramRequest, + timerange: { + interval: '12h', + from, + to, + }, + }); + }, + [matrixHistogramRequest, hostsSearch] + ); - return [loading, matrixHistogramResponse]; + return [loading, matrixHistogramResponse, runMatrixHistogramSearch]; }; diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/api.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/api.ts index 80b8b9916946..fb2e484c0e3f 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/eql/api.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/eql/api.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Unit } from '@elastic/datemath'; import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public'; import { @@ -16,10 +15,6 @@ import { isErrorResponse, isValidationErrorResponse, } from '../../../../common/search_strategy/eql'; -import { getEqlAggsData, getSequenceAggs } from './helpers'; -import { EqlPreviewResponse, Source } from './types'; -import { hasEqlSequenceQuery } from '../../../../common/detection_engine/utils'; -import { EqlSearchResponse } from '../../../../common/detection_engine/types'; interface Params { index: string[]; @@ -56,66 +51,3 @@ export const validateEql = async ({ return { valid: true, errors: [] }; } }; - -interface AggsParams { - data: DataPublicPluginStart; - index: string[]; - interval: Unit; - fromTime: string; - query: string; - toTime: string; - signal: AbortSignal; -} - -export const getEqlPreview = async ({ - data, - index, - interval, - query, - fromTime, - toTime, - signal, -}: AggsParams): Promise => { - try { - const response = await data.search - .search>>( - { - params: { - // @ts-expect-error allow_no_indices is missing on EqlSearch - allow_no_indices: true, - index: index.join(), - body: { - filter: { - range: { - '@timestamp': { - gte: toTime, - lte: fromTime, - format: 'strict_date_optional_time', - }, - }, - }, - query, - // EQL requires a cap, otherwise it defaults to 10 - // It also sorts on ascending order, capping it at - // something smaller like 20, made it so that some of - // the more recent events weren't returned - size: 100, - }, - }, - }, - { - strategy: 'eql', - abortSignal: signal, - } - ) - .toPromise(); - - if (hasEqlSequenceQuery(query)) { - return getSequenceAggs(response, interval, toTime, fromTime); - } else { - return getEqlAggsData(response, interval, toTime, fromTime); - } - } catch (err) { - throw new Error(JSON.stringify(err)); - } -}; diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.test.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.test.ts index 1418c1155877..07e8caa0bf0b 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.test.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import dateMath from '@elastic/datemath'; +import moment from 'moment'; import { EqlSearchStrategyResponse } from '../../../../../data_enhanced/common'; import { Source } from './types'; import { EqlSearchResponse } from '../../../../common/detection_engine/types'; +import { inputsModel } from '../../../common/store'; import { calculateBucketForHour, @@ -18,7 +20,7 @@ import { getSequenceAggs, } from './helpers'; -const getMockResponse = (): EqlSearchStrategyResponse> => +export const getMockResponse = (): EqlSearchStrategyResponse> => ({ id: 'some-id', rawResponse: { @@ -129,6 +131,17 @@ const getMockSequenceResponse = (): EqlSearchStrategyResponse { describe('calculateBucketForHour', () => { - test('returns 2 if event occured within 2 minutes of "now"', () => { + test('returns 2 if event occurred within 2 minutes of "now"', () => { const diff = calculateBucketForHour( Number(dateMath.parse('now-1m')?.format('x')), Number(dateMath.parse('now')?.format('x')) @@ -151,7 +164,7 @@ describe('eql/helpers', () => { expect(diff).toEqual(2); }); - test('returns 10 if event occured within 8-10 minutes of "now"', () => { + test('returns 10 if event occurred within 8-10 minutes of "now"', () => { const diff = calculateBucketForHour( Number(dateMath.parse('now-9m')?.format('x')), Number(dateMath.parse('now')?.format('x')) @@ -160,7 +173,7 @@ describe('eql/helpers', () => { expect(diff).toEqual(10); }); - test('returns 16 if event occured within 10-15 minutes of "now"', () => { + test('returns 16 if event occurred within 10-15 minutes of "now"', () => { const diff = calculateBucketForHour( Number(dateMath.parse('now-15m')?.format('x')), Number(dateMath.parse('now')?.format('x')) @@ -169,7 +182,7 @@ describe('eql/helpers', () => { expect(diff).toEqual(16); }); - test('returns 60 if event occured within 58-60 minutes of "now"', () => { + test('returns 60 if event occurred within 58-60 minutes of "now"', () => { const diff = calculateBucketForHour( Number(dateMath.parse('now-59m')?.format('x')), Number(dateMath.parse('now')?.format('x')) @@ -207,7 +220,7 @@ describe('eql/helpers', () => { expect(diff).toEqual(0); }); - test('returns 1 if event occured within 60 minutes of "now"', () => { + test('returns 1 if event occurred within 60 minutes of "now"', () => { const diff = calculateBucketForDay( Number(dateMath.parse('now-40m')?.format('x')), Number(dateMath.parse('now')?.format('x')) @@ -216,7 +229,7 @@ describe('eql/helpers', () => { expect(diff).toEqual(1); }); - test('returns 2 if event occured 60-120 minutes from "now"', () => { + test('returns 2 if event occurred 60-120 minutes from "now"', () => { const diff = calculateBucketForDay( Number(dateMath.parse('now-120m')?.format('x')), Number(dateMath.parse('now')?.format('x')) @@ -225,7 +238,7 @@ describe('eql/helpers', () => { expect(diff).toEqual(2); }); - test('returns 3 if event occured 120-180 minutes from "now', () => { + test('returns 3 if event occurred 120-180 minutes from "now', () => { const diff = calculateBucketForDay( Number(dateMath.parse('now-121m')?.format('x')), Number(dateMath.parse('now')?.format('x')) @@ -234,7 +247,7 @@ describe('eql/helpers', () => { expect(diff).toEqual(3); }); - test('returns 4 if event occured 180-240 minutes from "now', () => { + test('returns 4 if event occurred 180-240 minutes from "now', () => { const diff = calculateBucketForDay( Number(dateMath.parse('now-220m')?.format('x')), Number(dateMath.parse('now')?.format('x')) @@ -245,16 +258,22 @@ describe('eql/helpers', () => { }); describe('getEqlAggsData', () => { - test('it returns results bucketed into 5 min intervals when range is "h"', () => { + test('it returns results bucketed into 2 min intervals when range is "h"', () => { const mockResponse = getMockResponse(); const aggs = getEqlAggsData( mockResponse, 'h', - '2020-10-04T15:00:00.368707900Z', - '2020-10-04T16:00:00.368707900Z' + '2020-10-04T16:00:00.368707900Z', + jest.fn() as inputsModel.Refetch ); + const date1 = moment(aggs.data[0].x); + const date2 = moment(aggs.data[1].x); + // This'll be in ms + const diff = date1.diff(date2); + + expect(diff).toEqual(120000); expect(aggs.data).toHaveLength(31); expect(aggs.data).toEqual([ { g: 'hits', x: 1601827200368, y: 0 }, @@ -345,10 +364,15 @@ describe('eql/helpers', () => { const aggs = getEqlAggsData( response, 'd', - '2020-10-03T23:50:00.368707900Z', - '2020-10-04T23:50:00.368707900Z' + '2020-10-04T23:50:00.368707900Z', + jest.fn() as inputsModel.Refetch ); + const date1 = moment(aggs.data[0].x); + const date2 = moment(aggs.data[1].x); + // This'll be in ms + const diff = date1.diff(date2); + expect(diff).toEqual(3600000); expect(aggs.data).toHaveLength(25); expect(aggs.data).toEqual([ { g: 'hits', x: 1601855400368, y: 0 }, @@ -385,8 +409,8 @@ describe('eql/helpers', () => { const aggs = getEqlAggsData( mockResponse, 'h', - '2020-10-04T15:00:00.368707900Z', - '2020-10-04T16:00:00.368707900Z' + '2020-10-04T16:00:00.368707900Z', + jest.fn() as inputsModel.Refetch ); expect(aggs.totalCount).toEqual(4); @@ -417,53 +441,12 @@ describe('eql/helpers', () => { const aggs = getEqlAggsData( response, 'h', - '2020-10-04T15:00:00.368707900Z', - '2020-10-04T16:00:00.368707900Z' + '2020-10-04T16:00:00.368707900Z', + jest.fn() as inputsModel.Refetch ); - expect(aggs).toEqual({ - data: [ - { g: 'hits', x: 1601827200368, y: 0 }, - { g: 'hits', x: 1601827080368, y: 0 }, - { g: 'hits', x: 1601826960368, y: 0 }, - { g: 'hits', x: 1601826840368, y: 0 }, - { g: 'hits', x: 1601826720368, y: 0 }, - { g: 'hits', x: 1601826600368, y: 0 }, - { g: 'hits', x: 1601826480368, y: 0 }, - { g: 'hits', x: 1601826360368, y: 0 }, - { g: 'hits', x: 1601826240368, y: 0 }, - { g: 'hits', x: 1601826120368, y: 0 }, - { g: 'hits', x: 1601826000368, y: 0 }, - { g: 'hits', x: 1601825880368, y: 0 }, - { g: 'hits', x: 1601825760368, y: 0 }, - { g: 'hits', x: 1601825640368, y: 0 }, - { g: 'hits', x: 1601825520368, y: 0 }, - { g: 'hits', x: 1601825400368, y: 0 }, - { g: 'hits', x: 1601825280368, y: 0 }, - { g: 'hits', x: 1601825160368, y: 0 }, - { g: 'hits', x: 1601825040368, y: 0 }, - { g: 'hits', x: 1601824920368, y: 0 }, - { g: 'hits', x: 1601824800368, y: 0 }, - { g: 'hits', x: 1601824680368, y: 0 }, - { g: 'hits', x: 1601824560368, y: 0 }, - { g: 'hits', x: 1601824440368, y: 0 }, - { g: 'hits', x: 1601824320368, y: 0 }, - { g: 'hits', x: 1601824200368, y: 0 }, - { g: 'hits', x: 1601824080368, y: 0 }, - { g: 'hits', x: 1601823960368, y: 0 }, - { g: 'hits', x: 1601823840368, y: 0 }, - { g: 'hits', x: 1601823720368, y: 0 }, - { g: 'hits', x: 1601823600368, y: 0 }, - ], - gte: '2020-10-04T15:00:00.368707900Z', - inspect: { - dsl: [JSON.stringify(response.rawResponse.meta.request.params, null, 2)], - response: [JSON.stringify(response.rawResponse.body, null, 2)], - }, - lte: '2020-10-04T16:00:00.368707900Z', - totalCount: 0, - warnings: [], - }); + expect(aggs.data.every(({ y }) => y === 0)).toBeTruthy(); + expect(aggs.totalCount).toEqual(0); }); }); @@ -510,7 +493,7 @@ describe('eql/helpers', () => { ]); }); - test('returns array of 30 numbers from start param to end param if multiplier is 1', () => { + test('returns array of numbers from start param to end param if multiplier is 1', () => { const arrayOfNumbers = createIntervalArray(0, 12, 1); expect(arrayOfNumbers).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); }); @@ -518,8 +501,15 @@ describe('eql/helpers', () => { describe('getInterval', () => { test('returns object with 2 minute interval keys if range is "h"', () => { - const intervals = getInterval('h', 1601856270140); + const intervals = getInterval('h', Date.parse('2020-10-04T15:00:00.368707900Z')); const keys = Object.keys(intervals); + const date1 = moment(Number(intervals['0'].timestamp)); + const date2 = moment(Number(intervals['2'].timestamp)); + + // This'll be in ms + const diff = date1.diff(date2); + + expect(diff).toEqual(120000); expect(keys).toEqual([ '0', '2', @@ -557,40 +547,13 @@ describe('eql/helpers', () => { test('returns object with 2 minute interval timestamps if range is "h"', () => { const intervals = getInterval('h', 1601856270140); - const timestamps = Object.keys(intervals).map((key) => intervals[key].timestamp); - expect(timestamps).toEqual([ - '1601856270140', - '1601856150140', - '1601856030140', - '1601855910140', - '1601855790140', - '1601855670140', - '1601855550140', - '1601855430140', - '1601855310140', - '1601855190140', - '1601855070140', - '1601854950140', - '1601854830140', - '1601854710140', - '1601854590140', - '1601854470140', - '1601854350140', - '1601854230140', - '1601854110140', - '1601853990140', - '1601853870140', - '1601853750140', - '1601853630140', - '1601853510140', - '1601853390140', - '1601853270140', - '1601853150140', - '1601853030140', - '1601852910140', - '1601852790140', - '1601852670140', - ]); + const date1 = moment(Number(intervals['0'].timestamp)); + const date2 = moment(Number(intervals['2'].timestamp)); + + // This'll be in ms + const diff = date1.diff(date2); + + expect(diff).toEqual(120000); }); test('returns object with 1 hour interval keys if range is "d"', () => { @@ -627,34 +590,13 @@ describe('eql/helpers', () => { test('returns object with 1 hour interval timestamps if range is "d"', () => { const intervals = getInterval('d', 1601856270140); - const timestamps = Object.keys(intervals).map((key) => intervals[key].timestamp); - expect(timestamps).toEqual([ - '1601856270140', - '1601852670140', - '1601849070140', - '1601845470140', - '1601841870140', - '1601838270140', - '1601834670140', - '1601831070140', - '1601827470140', - '1601823870140', - '1601820270140', - '1601816670140', - '1601813070140', - '1601809470140', - '1601805870140', - '1601802270140', - '1601798670140', - '1601795070140', - '1601791470140', - '1601787870140', - '1601784270140', - '1601780670140', - '1601777070140', - '1601773470140', - '1601769870140', - ]); + const date1 = moment(Number(intervals['0'].timestamp)); + const date2 = moment(Number(intervals['1'].timestamp)); + + // This'll be in ms + const diff = date1.diff(date2); + + expect(diff).toEqual(3600000); }); test('returns error if range is anything other than "h" or "d"', () => { @@ -665,12 +607,7 @@ describe('eql/helpers', () => { describe('getSequenceAggs', () => { test('it aggregates events by sequences', () => { const mockResponse = getMockSequenceResponse(); - const sequenceAggs = getSequenceAggs( - mockResponse, - 'h', - '2020-10-04T15:00:00.368707900Z', - '2020-10-04T16:00:00.368707900Z' - ); + const sequenceAggs = getSequenceAggs(mockResponse, jest.fn() as inputsModel.Refetch); expect(sequenceAggs.data).toEqual([ { g: 'Seq. 1', x: '2020-10-04T15:16:54.368707900Z', y: 1 }, diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.ts index 0b2eba33b93d..4b5986d966df 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/eql/helpers.ts @@ -5,13 +5,12 @@ */ import moment from 'moment'; import { Unit } from '@elastic/datemath'; +import { inputsModel } from '../../../common/store'; -import * as i18n from '../../../detections/components/rules/query_preview/translations'; import { EqlSearchStrategyResponse } from '../../../../../data_enhanced/common'; import { InspectResponse } from '../../../types'; import { EqlPreviewResponse, Source } from './types'; import { EqlSearchResponse } from '../../../../common/detection_engine/types'; -import { HITS_THRESHOLD } from '../../../detections/components/rules/query_preview/helpers'; type EqlAggBuckets = Record; @@ -33,37 +32,16 @@ export const calculateBucketForDay = (eventTimestamp: number, relativeNow: numbe return Math.ceil(minutes / 60); }; -export const constructWarnings = (timestampIssue: boolean, hits: number, range: Unit): string[] => { - let warnings: string[] = []; - - if (timestampIssue) { - warnings = [i18n.PREVIEW_WARNING_TIMESTAMP]; - } - - if (hits === EQL_QUERY_EVENT_SIZE) { - warnings = [...warnings, i18n.PREVIEW_WARNING_CAP_HIT(EQL_QUERY_EVENT_SIZE)]; - } - - if (hits > HITS_THRESHOLD[range]) { - warnings = [...warnings, i18n.QUERY_PREVIEW_NOISE_WARNING]; - } - - return warnings; -}; - export const formatInspect = ( response: EqlSearchStrategyResponse> ): InspectResponse => { - if (response != null) { - return { - dsl: [JSON.stringify(response.rawResponse.meta.request.params, null, 2)] ?? [], - response: [JSON.stringify(response.rawResponse.body, null, 2)] ?? [], - }; - } - + const body = response.rawResponse.meta.request.params.body; + const bodyParse = typeof body === 'string' ? JSON.parse(body) : body; return { - dsl: [], - response: [], + dsl: [ + JSON.stringify({ ...response.rawResponse.meta.request.params, body: bodyParse }, null, 2), + ], + response: [JSON.stringify(response.rawResponse.body, null, 2)], }; }; @@ -74,24 +52,22 @@ export const getEqlAggsData = ( response: EqlSearchStrategyResponse>, range: Unit, to: string, - from: string + refetch: inputsModel.Refetch ): EqlPreviewResponse => { const { dsl, response: inspectResponse } = formatInspect(response); // The upper bound of the timestamps - const relativeNow: number = Date.parse(from); - const accumulator: EqlAggBuckets = getInterval(range, relativeNow); + const relativeNow = Date.parse(to); + const accumulator = getInterval(range, relativeNow); const events = response.rawResponse.body.hits.events ?? []; const totalCount = response.rawResponse.body.hits.total.value; - let timestampNotFound = false; const buckets = events.reduce((acc, hit) => { const timestamp = hit._source['@timestamp']; if (timestamp == null) { - timestampNotFound = true; return acc; } - const eventTimestamp: number = Date.parse(timestamp); + const eventTimestamp = Date.parse(timestamp); const bucket = range === 'h' ? calculateBucketForHour(eventTimestamp, relativeNow) @@ -107,18 +83,14 @@ export const getEqlAggsData = ( const isAllZeros = data.every(({ y }) => y === 0); - const warnings = constructWarnings(timestampNotFound, totalCount, range); - return { data, totalCount: isAllZeros ? 0 : totalCount, - lte: from, - gte: to, inspect: { dsl, response: inspectResponse, }, - warnings, + refetch, }; }; @@ -151,19 +123,15 @@ export const getInterval = (range: Unit, relativeNow: number): EqlAggBuckets => export const getSequenceAggs = ( response: EqlSearchStrategyResponse>, - range: Unit, - to: string, - from: string + refetch: inputsModel.Refetch ): EqlPreviewResponse => { const { dsl, response: inspectResponse } = formatInspect(response); const sequences = response.rawResponse.body.hits.sequences ?? []; const totalCount = response.rawResponse.body.hits.total.value; - let timestampNotFound = false; const data = sequences.map((sequence, i) => { return sequence.events.map((seqEvent) => { if (seqEvent._source['@timestamp'] == null) { - timestampNotFound = true; return {}; } return { @@ -174,17 +142,13 @@ export const getSequenceAggs = ( }); }); - const warnings = constructWarnings(timestampNotFound, totalCount, range); - return { data: data.flat(), totalCount, - lte: from, - gte: to, inspect: { dsl, response: inspectResponse, }, - warnings, + refetch, }; }; diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/types.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/types.ts index e7ccf83591d8..5bd51da28bad 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/eql/types.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/eql/types.ts @@ -3,16 +3,25 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { Unit } from '@elastic/datemath'; + import { InspectResponse } from '../../../types'; import { ChartData } from '../../components/charts/common'; +import { inputsModel } from '../../../common/store'; + +export interface EqlPreviewRequest { + to: string; + from: string; + interval: Unit; + query: string; + index: string[]; +} export interface EqlPreviewResponse { data: ChartData[]; totalCount: number; - lte: string; - gte: string; inspect: InspectResponse; - warnings: string[]; + refetch: inputsModel.Refetch; } export interface Source { diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.test.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.test.ts new file mode 100644 index 000000000000..ae7a263cc701 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.test.ts @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Unit } from '@elastic/datemath'; +import { renderHook, act } from '@testing-library/react-hooks'; +import { of, throwError } from 'rxjs'; +import { delay } from 'rxjs/operators'; + +import * as i18n from '../translations'; +import { EqlSearchStrategyResponse } from '../../../../../data_enhanced/common'; +import { Source } from './types'; +import { EqlSearchResponse } from '../../../../common/detection_engine/types'; +import { useKibana } from '../../../common/lib/kibana'; +import { useEqlPreview } from '.'; +import { getMockResponse } from './helpers.test'; + +jest.mock('../../../common/lib/kibana'); + +describe('useEqlPreview', () => { + const params = { + to: '2020-10-04T16:00:54.368707900Z', + query: 'file where true', + index: ['foo-*', 'bar-*'], + interval: 'h' as Unit, + from: '2020-10-04T15:00:54.368707900Z', + }; + + beforeEach(() => { + useKibana().services.notifications.toasts.addError = jest.fn(); + + useKibana().services.notifications.toasts.addWarning = jest.fn(); + + (useKibana().services.data.search.search as jest.Mock).mockReturnValue(of(getMockResponse())); + }); + + it('should initiate hook', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useEqlPreview()); + await waitForNextUpdate(); + + expect(result.current[0]).toBeFalsy(); + expect(typeof result.current[1]).toEqual('function'); + expect(result.current[2]).toEqual({ + data: [], + inspect: { dsl: [], response: [] }, + refetch: result.current[2].refetch, + totalCount: 0, + }); + }); + }); + + it('should invoke search with passed in params', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useEqlPreview()); + + await waitForNextUpdate(); + + result.current[1](params); + + const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; + + expect(mockCalls.length).toEqual(1); + expect(mockCalls[0][0].params.body.query).toEqual('file where true'); + expect(mockCalls[0][0].params.body.filter).toEqual({ + range: { + '@timestamp': { + format: 'strict_date_optional_time', + gte: '2020-10-04T15:00:54.368707900Z', + lte: '2020-10-04T16:00:54.368707900Z', + }, + }, + }); + expect(mockCalls[0][0].params.index).toBe('foo-*,bar-*'); + }); + }); + + it('should resolve values after search is invoked', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => useEqlPreview()); + + await waitForNextUpdate(); + + result.current[1](params); + + expect(result.current[0]).toBeFalsy(); + expect(typeof result.current[1]).toEqual('function'); + expect(result.current[2].totalCount).toEqual(4); + expect(result.current[2].data.length).toBeGreaterThan(0); + expect(result.current[2].inspect.dsl.length).toBeGreaterThan(0); + expect(result.current[2].inspect.response.length).toBeGreaterThan(0); + }); + }); + + it('should not resolve values after search is invoked if component unmounted', async () => { + await act(async () => { + (useKibana().services.data.search.search as jest.Mock).mockReturnValue( + of(getMockResponse()).pipe(delay(5000)) + ); + const { result, waitForNextUpdate, unmount } = renderHook(() => useEqlPreview()); + + await waitForNextUpdate(); + + result.current[1](params); + + unmount(); + + expect(result.current[0]).toBeTruthy(); + expect(result.current[2].totalCount).toEqual(0); + expect(result.current[2].data.length).toEqual(0); + expect(result.current[2].inspect.dsl.length).toEqual(0); + expect(result.current[2].inspect.response.length).toEqual(0); + }); + }); + + it('should not resolve new values on search if response is error response', async () => { + await act(async () => { + (useKibana().services.data.search.search as jest.Mock).mockReturnValue( + of({ isRunning: false, isPartial: true } as EqlSearchStrategyResponse< + EqlSearchResponse + >) + ); + + const { result, waitForNextUpdate } = renderHook(() => useEqlPreview()); + + await waitForNextUpdate(); + + result.current[1](params); + + const mockCalls = (useKibana().services.notifications.toasts.addWarning as jest.Mock).mock + .calls; + + expect(result.current[0]).toBeFalsy(); + expect(mockCalls[0][0]).toEqual(i18n.EQL_PREVIEW_FETCH_FAILURE); + }); + }); + + it('should add danger toast if search throws', async () => { + await act(async () => { + (useKibana().services.data.search.search as jest.Mock).mockReturnValue( + throwError('This is an error!') + ); + + const { result, waitForNextUpdate } = renderHook(() => useEqlPreview()); + + await waitForNextUpdate(); + + result.current[1](params); + + const mockCalls = (useKibana().services.notifications.toasts.addError as jest.Mock).mock + .calls; + + expect(result.current[0]).toBeFalsy(); + expect(mockCalls[0][0]).toEqual('This is an error!'); + }); + }); + + it('returns a memoized value', async () => { + const { result, rerender } = renderHook(() => useEqlPreview()); + + const result1 = result.current[1]; + act(() => rerender()); + const result2 = result.current[1]; + + expect(result1).toBe(result2); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts b/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts index 384395b34e62..1bfaecdf089b 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/eql/use_eql_preview.ts @@ -3,10 +3,157 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { noop } from 'lodash/fp'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; -import { useAsync, withOptionalSignal } from '../../../shared_imports'; -import { getEqlPreview } from './api'; +import * as i18n from '../translations'; +import { useKibana } from '../../../common/lib/kibana'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../src/plugins/data/common'; +import { + EqlSearchStrategyRequest, + EqlSearchStrategyResponse, +} from '../../../../../data_enhanced/common'; +import { getEqlAggsData, getSequenceAggs } from './helpers'; +import { EqlPreviewResponse, EqlPreviewRequest, Source } from './types'; +import { hasEqlSequenceQuery } from '../../../../common/detection_engine/utils'; +import { EqlSearchResponse } from '../../../../common/detection_engine/types'; +import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; +import { inputsModel } from '../../../common/store'; +import { EQL_SEARCH_STRATEGY } from '../../../../../data_enhanced/public'; -const getEqlPreviewWithOptionalSignal = withOptionalSignal(getEqlPreview); +export const useEqlPreview = (): [ + boolean, + (arg: EqlPreviewRequest) => void, + EqlPreviewResponse +] => { + const { data, notifications } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const unsubscribeStream = useRef(new Subject()); + const [loading, setLoading] = useState(false); + const didCancel = useRef(false); -export const useEqlPreview = () => useAsync(getEqlPreviewWithOptionalSignal); + const [response, setResponse] = useState({ + data: [], + inspect: { + dsl: [], + response: [], + }, + refetch: refetch.current, + totalCount: 0, + }); + + const searchEql = useCallback( + ({ from, to, query, index, interval }: EqlPreviewRequest) => { + if (parseScheduleDates(to) == null || parseScheduleDates(from) == null) { + notifications.toasts.addWarning('Time intervals are not defined.'); + return; + } + + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + setResponse((prevResponse) => ({ + ...prevResponse, + data: [], + inspect: { + dsl: [], + response: [], + }, + totalCount: 0, + })); + + data.search + .search>>( + { + params: { + // @ts-expect-error allow_no_indices is missing on EqlSearch + allow_no_indices: true, + index: index.join(), + body: { + filter: { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + query, + // EQL requires a cap, otherwise it defaults to 10 + // It also sorts on ascending order, capping it at + // something smaller like 20, made it so that some of + // the more recent events weren't returned + size: 100, + }, + }, + }, + { + strategy: EQL_SEARCH_STRATEGY, + abortSignal: abortCtrl.current.signal, + } + ) + .pipe(takeUntil(unsubscribeStream.current)) + .subscribe({ + next: (res) => { + if (isCompleteResponse(res)) { + if (!didCancel.current) { + setLoading(false); + if (hasEqlSequenceQuery(query)) { + setResponse(getSequenceAggs(res, refetch.current)); + } else { + setResponse(getEqlAggsData(res, interval, to, refetch.current)); + } + } + unsubscribeStream.current.next(); + } else if (isErrorResponse(res)) { + setLoading(false); + notifications.toasts.addWarning(i18n.EQL_PREVIEW_FETCH_FAILURE); + unsubscribeStream.current.next(); + } + }, + error: (err) => { + if (!(err instanceof AbortError)) { + setLoading(false); + setResponse({ + data: [], + inspect: { + dsl: [], + response: [], + }, + refetch: refetch.current, + totalCount: 0, + }); + notifications.toasts.addError(err, { + title: i18n.EQL_PREVIEW_FETCH_FAILURE, + }); + } + }, + }); + }; + + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + }, + [data.search, notifications.toasts] + ); + + useEffect((): (() => void) => { + return (): void => { + didCancel.current = true; + abortCtrl.current.abort(); + // eslint-disable-next-line react-hooks/exhaustive-deps + unsubscribeStream.current.complete(); + }; + }, []); + + return [loading, searchEql, response]; +}; diff --git a/x-pack/plugins/security_solution/public/common/hooks/translations.ts b/x-pack/plugins/security_solution/public/common/hooks/translations.ts index 50aeb7668696..2c6300046b7b 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/translations.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/translations.ts @@ -32,3 +32,10 @@ export const INDEX_PATTERN_FETCH_FAILURE = i18n.translate( defaultMessage: 'Index pattern fetch failure', } ); + +export const EQL_PREVIEW_FETCH_FAILURE = i18n.translate( + 'xpack.securitySolution.components.hooks.eql.partialResponse', + { + defaultMessage: 'EQL Preview Error', + } +); 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 000000000000..3e47478b783e --- /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 60cb6a4e960b..8d528f427995 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/detections/components/alerts_histogram_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx index 533f13e6781a..9925dfd4c062 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx @@ -5,9 +5,12 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { waitFor } from '@testing-library/react'; +import { shallow, mount } from 'enzyme'; import '../../../common/mock/match_media'; +import { esQuery } from '../../../../../../../src/plugins/data/public'; +import { TestProviders } from '../../../common/mock'; import { AlertsHistogramPanel } from './index'; jest.mock('react-router-dom', () => { @@ -31,12 +34,16 @@ jest.mock('../../../common/lib/kibana', () => { navigateToApp: mockNavigateToApp, getUrlForApp: jest.fn(), }, + uiSettings: { + get: jest.fn(), + }, }, }), useUiSetting$: jest.fn().mockReturnValue([]), useGetUserSavedObjectPermissions: jest.fn(), }; }); + jest.mock('../../../common/components/navigation/use_get_url_search'); describe('AlertsHistogramPanel', () => { @@ -77,4 +84,23 @@ describe('AlertsHistogramPanel', () => { expect(mockNavigateToApp).toBeCalledWith('securitySolution:detections', { path: '' }); }); }); + + describe('Query', () => { + it('it render with a illegal KQL', async () => { + const spyOnBuildEsQuery = jest.spyOn(esQuery, 'buildEsQuery'); + spyOnBuildEsQuery.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + const props = { ...defaultProps, query: { query: 'host.name: "', language: 'kql' } }; + const wrapper = mount( + + + + ); + + await waitFor(() => { + expect(wrapper.find('[id="detections-histogram"]')).toBeTruthy(); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx index 3bc84bb7c32e..c96ef570c7e0 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx @@ -221,24 +221,28 @@ export const AlertsHistogramPanel = memo( }, [alertsData]); useEffect(() => { - const converted = esQuery.buildEsQuery( - undefined, - query != null ? [query] : [], - filters?.filter((f) => f.meta.disabled === false) ?? [], - { - ...esQuery.getEsQueryConfig(kibana.services.uiSettings), - dateFormatTZ: undefined, - } - ); + try { + const converted = esQuery.buildEsQuery( + undefined, + query != null ? [query] : [], + filters?.filter((f) => f.meta.disabled === false) ?? [], + { + ...esQuery.getEsQueryConfig(kibana.services.uiSettings), + dateFormatTZ: undefined, + } + ); - setAlertsQuery( - getAlertsHistogramQuery( - selectedStackByOption.value, - from, - to, - !isEmpty(converted) ? [converted] : [] - ) - ); + setAlertsQuery( + getAlertsHistogramQuery( + selectedStackByOption.value, + from, + to, + !isEmpty(converted) ? [converted] : [] + ) + ); + } catch (e) { + setAlertsQuery(getAlertsHistogramQuery(selectedStackByOption.value, from, to, [])); + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedStackByOption.value, from, to, query, filters]); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx index 2ce9d1ea68b3..ebdfdcc262b3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx @@ -168,8 +168,11 @@ describe('helpers', () => { query: mockQueryBarWithQuery.query, savedId: mockQueryBarWithQuery.saved_id, }); - expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL} ); - expect(result[0].description).toEqual(<>{mockQueryBarWithQuery.query} ); + + expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL}); + expect(shallow(result[0].description as React.ReactElement).text()).toEqual( + mockQueryBarWithQuery.query + ); }); test('returns expected array of ListItems when "savedId" exists', () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx index 9ef1dd2bcb20..83413496c609 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx @@ -51,6 +51,10 @@ const EuiBadgeWrap = (styled(EuiBadge)` } ` as unknown) as typeof EuiBadge; +const Query = styled.div` + white-space: pre-wrap; +`; + export const buildQueryBarDescription = ({ field, filters, @@ -92,8 +96,8 @@ export const buildQueryBarDescription = ({ items = [ ...items, { - title: <>{queryLabel ?? i18n.QUERY_LABEL} , - description: <>{query} , + title: <>{queryLabel ?? i18n.QUERY_LABEL}, + description: {query}, }, ]; } diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx index 8179e5865e4e..d881d05edbb0 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.test.tsx @@ -315,8 +315,10 @@ describe('description_step', () => { mockFilterManager ); - expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL} ); - expect(result[0].description).toEqual(<>{mockQueryBar.queryBar.query.query} ); + expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL}); + expect(shallow(result[0].description as React.ReactElement).text()).toEqual( + mockQueryBar.queryBar.query.query + ); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/eql_query_bar.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/eql_query_bar.tsx index f7ee5be18154..1d57ef2bb2cd 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/eql_query_bar.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/eql_query_bar.tsx @@ -27,15 +27,30 @@ export interface EqlQueryBarProps { dataTestSubj: string; field: FieldHook; idAria?: string; + onValidityChange?: (arg: boolean) => void; } -export const EqlQueryBar: FC = ({ dataTestSubj, field, idAria }) => { +export const EqlQueryBar: FC = ({ + dataTestSubj, + field, + idAria, + onValidityChange, +}) => { const { addError } = useAppToasts(); const [errorMessages, setErrorMessages] = useState([]); - const { setValue } = field; + const { isValidating, setValue } = field; const { isValid, message, messages, error } = getValidationResults(field); const fieldValue = field.value.query.query as string; + // Bubbles up field validity to parent. + // Using something like form `getErrors` does + // not guarantee latest validity state + useEffect(() => { + if (onValidityChange != null) { + onValidityChange(isValid); + } + }, [isValid, onValidityChange]); + useEffect(() => { setErrorMessages(messages ?? []); }, [messages]); @@ -81,7 +96,7 @@ export const EqlQueryBar: FC = ({ dataTestSubj, field, idAria value={fieldValue} onChange={handleChange} /> - + ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/footer.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/footer.tsx index 19bab26f8aa5..7c0ddd6d8b3c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/footer.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/footer.tsx @@ -6,7 +6,7 @@ import React, { FC } from 'react'; import styled from 'styled-components'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiPanel } from '@elastic/eui'; import * as i18n from './translations'; import { ErrorsPopover } from './errors_popover'; @@ -14,25 +14,31 @@ import { EqlOverviewLink } from './eql_overview_link'; export interface Props { errors: string[]; + isLoading?: boolean; } const Container = styled(EuiPanel)` border-radius: 0; background: ${({ theme }) => theme.eui.euiPageBackgroundColor}; - padding: ${({ theme }) => theme.eui.euiSizeXS}; + padding: ${({ theme }) => theme.eui.euiSizeXS} ${({ theme }) => theme.eui.euiSizeS}; `; const FlexGroup = styled(EuiFlexGroup)` min-height: ${({ theme }) => theme.eui.euiSizeXL}; `; -export const EqlQueryBarFooter: FC = ({ errors }) => ( +const Spinner = styled(EuiLoadingSpinner)` + margin: 0 ${({ theme }) => theme.eui.euiSizeS}; +`; + +export const EqlQueryBarFooter: FC = ({ errors, isLoading }) => ( {errors.length > 0 && ( )} + {isLoading && } diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx index c53a9ccc22d8..4cb2abe756cf 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_bar/index.tsx @@ -46,6 +46,7 @@ interface QueryBarDefineRuleProps { onCloseTimelineSearch: () => void; openTimelineSearch: boolean; resizeParentContainer?: (height: number) => void; + onValidityChange?: (arg: boolean) => void; } const StyledEuiFormRow = styled(EuiFormRow)` @@ -74,6 +75,7 @@ export const QueryBarDefineRule = ({ onCloseTimelineSearch, openTimelineSearch = false, resizeParentContainer, + onValidityChange, }: QueryBarDefineRuleProps) => { const [originalHeight, setOriginalHeight] = useState(-1); const [loadingTimeline, setLoadingTimeline] = useState(false); @@ -86,6 +88,15 @@ export const QueryBarDefineRule = ({ const savedQueryServices = useSavedQueryServices(); + // Bubbles up field validity to parent. + // Using something like form `getErrors` does + // not guarantee latest validity state + useEffect((): void => { + if (onValidityChange != null) { + onValidityChange(!isInvalid); + } + }, [isInvalid, onValidityChange]); + useEffect(() => { let isSubscribed = true; const subscriptions = new Subscription(); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.test.tsx new file mode 100644 index 000000000000..01d95fa80ba5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.test.tsx @@ -0,0 +1,135 @@ +/* + * 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 { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import * as i18n from './translations'; +import { useGlobalTime } from '../../../../common/containers/use_global_time'; +import { TestProviders } from '../../../../common/mock'; +import { PreviewCustomQueryHistogram } from './custom_histogram'; + +jest.mock('../../../../common/containers/use_global_time'); + +describe('PreviewCustomQueryHistogram', () => { + const mockSetQuery = jest.fn(); + + beforeEach(() => { + (useGlobalTime as jest.Mock).mockReturnValue({ + from: '2020-07-07T08:20:18.966Z', + isInitializing: false, + to: '2020-07-08T08:20:18.966Z', + setQuery: mockSetQuery, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it renders loader when isLoading is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').at(0).prop('subtitle') + ).toEqual(i18n.PREVIEW_SUBTITLE_LOADING); + }); + + test('it configures data and subtitle', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); + expect( + wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').at(0).prop('subtitle') + ).toEqual(i18n.QUERY_PREVIEW_TITLE(9154)); + expect( + wrapper.find('[data-test-subj="queryPreviewCustomHistogram"]').at(0).props().data + ).toEqual([ + { + key: 'hits', + value: [ + { + g: 'All others', + x: 1602247050000, + y: 2314, + }, + { + g: 'All others', + x: 1602247162500, + y: 3471, + }, + { + g: 'All others', + x: 1602247275000, + y: 3369, + }, + ], + }, + ]); + }); + + test('it invokes setQuery with id, inspect, isLoading and refetch', async () => { + const mockRefetch = jest.fn(); + + mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + expect(mockSetQuery).toHaveBeenCalledWith({ + id: 'queryPreviewCustomHistogramQuery', + inspect: { dsl: ['some dsl'], response: ['query response'] }, + loading: false, + refetch: mockRefetch, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.tsx new file mode 100644 index 000000000000..787e8dab393c --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/custom_histogram.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useMemo } from 'react'; + +import * as i18n from './translations'; +import { useGlobalTime } from '../../../../common/containers/use_global_time'; +import { getHistogramConfig } from './helpers'; +import { + ChartSeriesConfigs, + ChartSeriesData, + ChartData, +} from '../../../../common/components/charts/common'; +import { InspectResponse } from '../../../../../public/types'; +import { inputsModel } from '../../../../common/store'; +import { PreviewHistogram } from './histogram'; + +export const ID = 'queryPreviewCustomHistogramQuery'; + +interface PreviewCustomQueryHistogramProps { + to: string; + from: string; + isLoading: boolean; + data: ChartData[]; + totalCount: number; + inspect: InspectResponse; + refetch: inputsModel.Refetch; +} + +export const PreviewCustomQueryHistogram = ({ + to, + from, + data, + totalCount, + inspect, + refetch, + isLoading, +}: PreviewCustomQueryHistogramProps) => { + const { setQuery, isInitializing } = useGlobalTime(); + + useEffect((): void => { + if (!isLoading && !isInitializing) { + setQuery({ id: ID, inspect, loading: isLoading, refetch }); + } + }, [setQuery, inspect, isLoading, isInitializing, refetch]); + + const barConfig = useMemo((): ChartSeriesConfigs => getHistogramConfig(to, from, true), [ + from, + to, + ]); + + const subtitle = useMemo( + (): string => + isLoading ? i18n.PREVIEW_SUBTITLE_LOADING : i18n.QUERY_PREVIEW_TITLE(totalCount), + [isLoading, totalCount] + ); + + const chartData = useMemo((): ChartSeriesData[] => [{ key: 'hits', value: data }], [data]); + + return ( + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx new file mode 100644 index 000000000000..16e71485de9a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.test.tsx @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import * as i18n from './translations'; +import { useGlobalTime } from '../../../../common/containers/use_global_time'; +import { TestProviders } from '../../../../common/mock'; +import { PreviewEqlQueryHistogram } from './eql_histogram'; + +jest.mock('../../../../common/containers/use_global_time'); + +describe('PreviewEqlQueryHistogram', () => { + const mockSetQuery = jest.fn(); + + beforeEach(() => { + (useGlobalTime as jest.Mock).mockReturnValue({ + from: '2020-07-07T08:20:18.966Z', + isInitializing: false, + to: '2020-07-08T08:20:18.966Z', + setQuery: mockSetQuery, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it renders loader when isLoading is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').at(0).prop('subtitle') + ).toEqual(i18n.PREVIEW_SUBTITLE_LOADING); + }); + + test('it configures data and subtitle', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); + expect( + wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').at(0).prop('subtitle') + ).toEqual(i18n.QUERY_PREVIEW_TITLE(9154)); + expect(wrapper.find('[data-test-subj="queryPreviewEqlHistogram"]').at(0).props().data).toEqual([ + { + key: 'hits', + value: [ + { + g: 'All others', + x: 1602247050000, + y: 2314, + }, + { + g: 'All others', + x: 1602247162500, + y: 3471, + }, + { + g: 'All others', + x: 1602247275000, + y: 3369, + }, + ], + }, + ]); + }); + + test('it invokes setQuery with id, inspect, isLoading and refetch', async () => { + const mockRefetch = jest.fn(); + + mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + expect(mockSetQuery).toHaveBeenCalledWith({ + id: 'queryEqlPreviewHistogramQuery', + inspect: { dsl: ['some dsl'], response: ['query response'] }, + loading: false, + refetch: mockRefetch, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.tsx index 3211afea821b..8f2774a1342b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/eql_histogram.tsx @@ -5,74 +5,74 @@ */ import React, { useEffect, useMemo } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; import * as i18n from './translations'; -import { BarChart } from '../../../../common/components/charts/barchart'; import { getHistogramConfig } from './helpers'; -import { ChartData, ChartSeriesConfigs } from '../../../../common/components/charts/common'; +import { + ChartSeriesData, + ChartSeriesConfigs, + ChartData, +} from '../../../../common/components/charts/common'; import { InspectQuery } from '../../../../common/store/inputs/model'; import { useGlobalTime } from '../../../../common/containers/use_global_time'; -import { Panel } from '../../../../common/components/panel'; -import { HeaderSection } from '../../../../common/components/header_section'; +import { hasEqlSequenceQuery } from '../../../../../common/detection_engine/utils'; +import { inputsModel } from '../../../../common/store'; +import { PreviewHistogram } from './histogram'; export const ID = 'queryEqlPreviewHistogramQuery'; interface PreviewEqlQueryHistogramProps { to: string; from: string; - totalHits: number; + totalCount: number; + isLoading: boolean; + query: string; data: ChartData[]; inspect: InspectQuery; + refetch: inputsModel.Refetch; } export const PreviewEqlQueryHistogram = ({ from, to, - totalHits, + totalCount, + query, data, inspect, + refetch, + isLoading, }: PreviewEqlQueryHistogramProps) => { const { setQuery, isInitializing } = useGlobalTime(); useEffect((): void => { if (!isInitializing) { - setQuery({ id: ID, inspect, loading: false, refetch: () => {} }); + setQuery({ id: ID, inspect, loading: false, refetch }); } - }, [setQuery, inspect, isInitializing]); + }, [setQuery, inspect, isInitializing, refetch]); - const barConfig = useMemo((): ChartSeriesConfigs => getHistogramConfig(to, from), [from, to]); + const barConfig = useMemo( + (): ChartSeriesConfigs => getHistogramConfig(to, from, hasEqlSequenceQuery(query)), + [from, to, query] + ); + + const subtitle = useMemo( + (): string => + isLoading ? i18n.PREVIEW_SUBTITLE_LOADING : i18n.QUERY_PREVIEW_TITLE(totalCount), + [isLoading, totalCount] + ); + + const chartData = useMemo((): ChartSeriesData[] => [{ key: 'hits', value: data }], [data]); return ( - <> - - - - - - - - - - <> - - -

{i18n.PREVIEW_QUERY_DISCLAIMER_EQL}

-
- -
-
-
- + ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/helpers.test.ts new file mode 100644 index 000000000000..41ac95338460 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/helpers.test.ts @@ -0,0 +1,198 @@ +/* + * 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 { isNoisy, getTimeframeOptions, getInfoFromQueryBar } from './helpers'; + +describe('query_preview/helpers', () => { + describe('isNoisy', () => { + test('returns true if timeframe selection is "Last hour" and average hits per hour is greater than one', () => { + const isItNoisy = isNoisy(2, 'h'); + + expect(isItNoisy).toBeTruthy(); + }); + + test('returns false if timeframe selection is "Last hour" and average hits per hour is one', () => { + const isItNoisy = isNoisy(1, 'h'); + + expect(isItNoisy).toBeFalsy(); + }); + + test('returns false if timeframe selection is "Last hour" and hits is 0', () => { + const isItNoisy = isNoisy(1, 'h'); + + expect(isItNoisy).toBeFalsy(); + }); + + test('returns true if timeframe selection is "Last day" and average hits per hour is greater than one', () => { + const isItNoisy = isNoisy(50, 'd'); + + expect(isItNoisy).toBeTruthy(); + }); + + test('returns false if timeframe selection is "Last day" and average hits per hour is one', () => { + const isItNoisy = isNoisy(24, 'd'); + + expect(isItNoisy).toBeFalsy(); + }); + + test('returns false if timeframe selection is "Last day" and hits is 0', () => { + const isItNoisy = isNoisy(0, 'd'); + + expect(isItNoisy).toBeFalsy(); + }); + + test('returns true if timeframe selection is "Last month" and average hits per hour is greater than one', () => { + const isItNoisy = isNoisy(1000, 'M'); + + expect(isItNoisy).toBeTruthy(); + }); + + test('returns false if timeframe selection is "Last month" and average hits per hour is one', () => { + const isItNoisy = isNoisy(730, 'M'); + + expect(isItNoisy).toBeFalsy(); + }); + + test('returns false if timeframe selection is "Last month" and hits is 0', () => { + const isItNoisy = isNoisy(1, 'M'); + + expect(isItNoisy).toBeFalsy(); + }); + }); + + describe('getTimeframeOptions', () => { + test('returns hour and day options if ruleType is eql', () => { + const options = getTimeframeOptions('eql'); + + expect(options).toEqual([ + { value: 'h', text: 'Last hour' }, + { value: 'd', text: 'Last day' }, + ]); + }); + + test('returns hour, day, and month options if ruleType is not eql', () => { + const options = getTimeframeOptions('query'); + + expect(options).toEqual([ + { value: 'h', text: 'Last hour' }, + { value: 'd', text: 'Last day' }, + { value: 'M', text: 'Last month' }, + ]); + }); + }); + + describe('getInfoFromQueryBar', () => { + test('returns queryFilter when ruleType is query', () => { + const { queryString, language, filters, queryFilter } = getInfoFromQueryBar( + { + query: { query: 'host.name:*', language: 'kuery' }, + filters: [{ meta: { alias: '', disabled: false, negate: false } }], + }, + ['foo-*'], + 'query' + ); + + expect(queryString).toEqual('host.name:*'); + expect(language).toEqual('kuery'); + expect(filters).toEqual([{ meta: { alias: '', disabled: false, negate: false } }]); + expect(queryFilter).toEqual({ + bool: { + filter: [ + { bool: { minimum_should_match: 1, should: [{ exists: { field: 'host.name' } }] } }, + {}, + ], + must: [], + must_not: [], + should: [], + }, + }); + }); + + test('returns queryFilter when ruleType is saved_query', () => { + const { queryString, language, filters, queryFilter } = getInfoFromQueryBar( + { + query: { query: 'host.name:*', language: 'kuery' }, + filters: [{ meta: { alias: '', disabled: false, negate: false } }], + }, + ['foo-*'], + 'saved_query' + ); + + expect(queryString).toEqual('host.name:*'); + expect(language).toEqual('kuery'); + expect(filters).toEqual([{ meta: { alias: '', disabled: false, negate: false } }]); + expect(queryFilter).toEqual({ + bool: { + filter: [ + { bool: { minimum_should_match: 1, should: [{ exists: { field: 'host.name' } }] } }, + {}, + ], + must: [], + must_not: [], + should: [], + }, + }); + }); + + test('returns queryFilter when ruleType is threshold', () => { + const { queryString, language, filters, queryFilter } = getInfoFromQueryBar( + { + query: { query: 'host.name:*', language: 'kuery' }, + filters: [{ meta: { alias: '', disabled: false, negate: false } }], + }, + ['foo-*'], + 'threshold' + ); + + expect(queryString).toEqual('host.name:*'); + expect(language).toEqual('kuery'); + expect(filters).toEqual([{ meta: { alias: '', disabled: false, negate: false } }]); + expect(queryFilter).toEqual({ + bool: { + filter: [ + { bool: { minimum_should_match: 1, should: [{ exists: { field: 'host.name' } }] } }, + {}, + ], + must: [], + must_not: [], + should: [], + }, + }); + }); + + test('returns undefined queryFilter when ruleType is eql', () => { + const { queryString, language, filters, queryFilter } = getInfoFromQueryBar( + { + query: { query: 'file where true', language: 'eql' }, + filters: [{ meta: { alias: '', disabled: false, negate: false } }], + }, + ['foo-*'], + 'eql' + ); + + expect(queryString).toEqual('file where true'); + expect(language).toEqual('eql'); + expect(filters).toEqual([{ meta: { alias: '', disabled: false, negate: false } }]); + expect(queryFilter).toBeUndefined(); + }); + + test('returns undefined queryFilter when getQueryFilter throws', () => { + // query is malformed, forcing error in getQueryFilter + const { queryString, language, filters, queryFilter } = getInfoFromQueryBar( + { + query: { query: 'host.name:', language: 'kuery' }, + filters: [{ meta: { alias: '', disabled: false, negate: false } }], + }, + ['foo-*'], + 'threshold' + ); + + expect(queryString).toEqual('host.name:'); + expect(language).toEqual('kuery'); + expect(filters).toEqual([{ meta: { alias: '', disabled: false, negate: false } }]); + expect(queryFilter).toBeUndefined(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/helpers.ts b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/helpers.ts index 4cf37236510d..ed8994a4c44f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/helpers.ts @@ -5,11 +5,16 @@ */ import { Position, ScaleType } from '@elastic/charts'; import { EuiSelectOption } from '@elastic/eui'; +import { Unit } from '@elastic/datemath'; import * as i18n from './translations'; import { histogramDateTimeFormatter } from '../../../../common/components/utils'; import { ChartSeriesConfigs } from '../../../../common/components/charts/common'; -import { Type } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { Type, Language } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { getQueryFilter } from '../../../../../common/detection_engine/get_query_filter'; +import { FieldValueQueryBar } from '../query_bar'; +import { ESQuery } from '../../../../../common/typed_json'; +import { Filter } from '../../../../../../../../src/plugins/data/common/es_query'; export const HITS_THRESHOLD: Record = { h: 1, @@ -17,6 +22,18 @@ export const HITS_THRESHOLD: Record = { M: 730, }; +export const isNoisy = (hits: number, timeframe: Unit) => { + if (timeframe === 'h') { + return hits > 1; + } else if (timeframe === 'd') { + return hits / 24 > 1; + } else if (timeframe === 'M') { + return hits / 730 > 1; + } + + return false; +}; + export const getTimeframeOptions = (ruleType: Type): EuiSelectOption[] => { if (ruleType === 'eql') { return [ @@ -32,7 +49,50 @@ export const getTimeframeOptions = (ruleType: Type): EuiSelectOption[] => { } }; -export const getHistogramConfig = (to: string, from: string): ChartSeriesConfigs => { +export const getInfoFromQueryBar = ( + queryBar: FieldValueQueryBar, + index: string[], + ruleType: Type +): { + queryString: string; + language: Language; + filters: Filter[]; + queryFilter: ESQuery | undefined; +} => { + const queryString = typeof queryBar.query.query === 'string' ? queryBar.query.query : ''; + const language = queryBar.query.language as Language; + const filters = queryBar.filters; + + // hm?? Why a try catch here? Because if the + // query is invalid, it throws an error and + // entire UI shows gross KQLSyntax error screen + try { + const queryFilter = + ruleType !== 'eql' + ? getQueryFilter(queryString, language, filters, index, [], true) + : undefined; + + return { + queryString, + language, + filters, + queryFilter, + }; + } catch { + return { + queryString, + language, + filters, + queryFilter: undefined, + }; + } +}; + +export const getHistogramConfig = ( + to: string, + from: string, + showLegend: boolean = false +): ChartSeriesConfigs => { return { series: { xScaleType: ScaleType.Time, @@ -47,8 +107,8 @@ export const getHistogramConfig = (to: string, from: string): ChartSeriesConfigs yAxisTitle: i18n.QUERY_GRAPH_COUNT, settings: { legendPosition: Position.Right, - showLegend: true, - showLegendExtra: true, + showLegend, + showLegendExtra: showLegend, theme: { scales: { barsPadding: 0.08, @@ -74,11 +134,12 @@ export const getHistogramConfig = (to: string, from: string): ChartSeriesConfigs export const getThresholdHistogramConfig = (height: number | undefined): ChartSeriesConfigs => { return { series: { - xScaleType: ScaleType.Linear, + xScaleType: ScaleType.Ordinal, yScaleType: ScaleType.Linear, stackAccessors: ['g'], }, axis: { + yTickFormatter: (value: string | number): string => value.toLocaleString(), tickSize: 8, }, yAxisTitle: i18n.QUERY_GRAPH_COUNT, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.test.tsx new file mode 100644 index 000000000000..d6ccd1608302 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.test.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { TestProviders } from '../../../../common/mock'; +import { PreviewHistogram } from './histogram'; +import { getHistogramConfig } from './helpers'; + +describe('PreviewHistogram', () => { + test('it renders loading icon if "isLoading" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="sharedPreviewQueryHistogram"]').exists()).toBeFalsy(); + }); + + test('it renders chart if "isLoading" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="sharedPreviewQueryHistogram"]').exists()).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.tsx new file mode 100644 index 000000000000..2c43dac7b6bc --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/histogram.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer, EuiLoadingChart } from '@elastic/eui'; +import styled from 'styled-components'; + +import { BarChart } from '../../../../common/components/charts/barchart'; +import { Panel } from '../../../../common/components/panel'; +import { HeaderSection } from '../../../../common/components/header_section'; +import { ChartSeriesData, ChartSeriesConfigs } from '../../../../common/components/charts/common'; + +const LoadingChart = styled(EuiLoadingChart)` + display: block; + margin: 0 auto; +`; + +interface PreviewHistogramProps { + id: string; + data: ChartSeriesData[]; + barConfig: ChartSeriesConfigs; + title: string; + subtitle: string; + disclaimer: string; + isLoading: boolean; +} + +export const PreviewHistogram = ({ + id, + data, + barConfig, + title, + subtitle, + disclaimer, + isLoading, +}: PreviewHistogramProps) => { + return ( + <> + + + + + + + {isLoading ? ( + + ) : ( + + )} + + + <> + + +

{disclaimer}

+
+ +
+
+
+ + ); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx new file mode 100644 index 000000000000..87436ad1e6d2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.test.tsx @@ -0,0 +1,258 @@ +/* + * 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 { of } from 'rxjs'; +import { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { TestProviders } from '../../../../common/mock'; +import { useKibana } from '../../../../common/lib/kibana'; +import { PreviewQuery } from './'; +import { getMockResponse } from '../../../../common/hooks/eql/helpers.test'; + +jest.mock('../../../../common/lib/kibana'); + +describe('PreviewQuery', () => { + beforeEach(() => { + useKibana().services.notifications.toasts.addError = jest.fn(); + + useKibana().services.notifications.toasts.addWarning = jest.fn(); + + (useKibana().services.data.search.search as jest.Mock).mockReturnValue(of(getMockResponse())); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it renders timeframe select and preview button on render', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find('[data-test-subj="queryPreviewSelect"]').exists()).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled + ).toBeFalsy(); + expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy(); + }); + + test('it renders preview button disabled if "isDisabled" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled + ).toBeTruthy(); + }); + + test('it renders preview button disabled if "query" is undefined', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find('[data-test-subj="queryPreviewButton"] button').props().disabled + ).toBeTruthy(); + }); + + test('it renders query histogram when rule type is query and preview button clicked', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); + + const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; + + expect(mockCalls.length).toEqual(1); + expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy(); + }); + + test('it renders query histogram when rule type is saved_query and preview button clicked', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); + + const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; + + expect(mockCalls.length).toEqual(1); + expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy(); + }); + + test('it renders eql histogram when preview button clicked and rule type is eql', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); + + const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; + + expect(mockCalls.length).toEqual(1); + expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeTruthy(); + }); + + test('it renders threshold histogram when preview button clicked, rule type is threshold, and threshold field is defined', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); + + const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; + + expect(mockCalls.length).toEqual(1); + expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy(); + }); + + test('it renders query histogram when preview button clicked, rule type is threshold, and threshold field is not defined', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); + + const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; + + expect(mockCalls.length).toEqual(1); + expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy(); + }); + + test('it renders query histogram when preview button clicked, rule type is threshold, and threshold field is empty string', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + wrapper.find('[data-test-subj="queryPreviewButton"] button').at(0).simulate('click'); + + const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls; + + expect(mockCalls.length).toEqual(1); + expect(wrapper.find('[data-test-subj="previewNonEqlQueryHistogram"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="previewThresholdQueryHistogram"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="previewEqlQueryHistogram"]').exists()).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx index 43dcdb7b7d58..f1cb8e3ba9fd 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/index.tsx @@ -3,9 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useState, useMemo, useEffect } from 'react'; +import React, { Fragment, useCallback, useEffect, useReducer } from 'react'; import { Unit } from '@elastic/datemath'; -import { getOr } from 'lodash/fp'; import styled from 'styled-components'; import { EuiFlexGroup, @@ -14,25 +13,23 @@ import { EuiFormRow, EuiButton, EuiCallOut, - EuiSelectOption, EuiText, EuiSpacer, } from '@elastic/eui'; +import { debounce } from 'lodash/fp'; import * as i18n from './translations'; -import { useKibana } from '../../../../common/lib/kibana'; -import { ESQueryStringQuery } from '../../../../../common/typed_json'; -import { getQueryFilter } from '../../../../../common/detection_engine/get_query_filter'; +import { useMatrixHistogram } from '../../../../common/containers/matrix_histogram'; +import { MatrixHistogramType } from '../../../../../common/search_strategy/security_solution/matrix_histogram'; import { FieldValueQueryBar } from '../query_bar'; -import { Filter } from '../../../../../../../../src/plugins/data/common/es_query'; -import { Language, Type } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { Type } from '../../../../../common/detection_engine/schemas/common/schemas'; import { PreviewEqlQueryHistogram } from './eql_histogram'; -import { useEqlPreview } from '../../../../common/hooks/eql'; -import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; -import { PreviewNonEqlQueryHistogram } from './non_eql_histogram'; -import { getTimeframeOptions } from './helpers'; +import { useEqlPreview } from '../../../../common/hooks/eql/'; import { PreviewThresholdQueryHistogram } from './threshold_histogram'; import { formatDate } from '../../../../common/components/super_date_picker'; +import { State, queryPreviewReducer } from './reducer'; +import { isNoisy } from './helpers'; +import { PreviewCustomQueryHistogram } from './custom_histogram'; const Select = styled(EuiSelect)` width: ${({ theme }) => theme.eui.euiSuperDatePickerWidth}; @@ -42,13 +39,30 @@ const PreviewButton = styled(EuiButton)` margin-left: 0; `; +export const initialState: State = { + timeframeOptions: [], + showHistogram: false, + timeframe: 'h', + warnings: [], + queryFilter: undefined, + toTime: '', + fromTime: '', + queryString: '', + language: 'kuery', + filters: [], + thresholdFieldExists: false, + showNonEqlHistogram: false, +}; + +export type Threshold = { field: string | undefined; value: number } | undefined; + interface PreviewQueryProps { dataTestSubj: string; idAria: string; query: FieldValueQueryBar | undefined; index: string[]; ruleType: Type; - threshold: { field: string | undefined; value: number } | undefined; + threshold: Threshold; isDisabled: boolean; } @@ -61,131 +75,176 @@ export const PreviewQuery = ({ threshold, isDisabled, }: PreviewQueryProps) => { - const { data } = useKibana().services; - const { addError } = useAppToasts(); + const [ + eqlQueryLoading, + startEql, + { + totalCount: eqlQueryTotal, + data: eqlQueryData, + refetch: eqlQueryRefetch, + inspect: eqlQueryInspect, + }, + ] = useEqlPreview(); - const [timeframeOptions, setTimeframeOptions] = useState([]); - const [showHistogram, setShowHistogram] = useState(false); - const [timeframe, setTimeframe] = useState('h'); - const [warnings, setWarnings] = useState([]); - const [queryFilter, setQueryFilter] = useState(undefined); - const [toTime, setTo] = useState(''); - const [fromTime, setFrom] = useState(''); - const { - error: eqlError, - start: startEql, - result: eqlQueryResult, - loading: eqlQueryLoading, - } = useEqlPreview(); + const [ + { + thresholdFieldExists, + showNonEqlHistogram, + timeframeOptions, + showHistogram, + timeframe, + warnings, + queryFilter, + toTime, + fromTime, + queryString, + }, + dispatch, + ] = useReducer(queryPreviewReducer(), { + ...initialState, + toTime: formatDate('now-1h'), + fromTime: formatDate('now'), + }); + const [ + isMatrixHistogramLoading, + { inspect, totalCount: matrixHistTotal, refetch, data: matrixHistoData, buckets }, + startNonEql, + ] = useMatrixHistogram({ + errorMessage: i18n.PREVIEW_QUERY_ERROR, + endDate: fromTime, + startDate: toTime, + filterQuery: queryFilter, + indexNames: index, + histogramType: MatrixHistogramType.events, + stackByField: 'event.category', + threshold, + skip: true, + }); - const queryString = useMemo((): string => getOr('', 'query.query', query), [query]); - const language = useMemo((): Language => getOr('kuery', 'query.language', query), [query]); - const filters = useMemo((): Filter[] => (query != null ? query.filters : []), [query]); + const setQueryInfo = useCallback( + (queryBar: FieldValueQueryBar | undefined): void => { + dispatch({ + type: 'setQueryInfo', + queryBar, + index, + ruleType, + }); + }, + [dispatch, index, ruleType] + ); - const handleCalculateTimeRange = useCallback((): void => { - const from = formatDate('now'); - const to = formatDate(`now-1${timeframe}`); + const setTimeframeSelect = useCallback( + (selection: Unit): void => { + dispatch({ + type: 'setTimeframeSelect', + timeframe: selection, + }); + }, + [dispatch] + ); - setTo(to); - setFrom(from); - }, [timeframe]); + const setRuleTypeChange = useCallback( + (type: Type): void => { + dispatch({ + type: 'setResetRuleTypeChange', + ruleType: type, + }); + }, + [dispatch] + ); - const handlePreviewEqlQuery = useCallback((): void => { - startEql({ - data, - index, - query: queryString, - fromTime, - toTime, - interval: timeframe, - }); - }, [startEql, data, index, queryString, fromTime, toTime, timeframe]); + const setWarnings = useCallback( + (yikes: string[]): void => { + dispatch({ + type: 'setWarnings', + warnings: yikes, + }); + }, + [dispatch] + ); - const handleSelectPreviewTimeframe = ({ - target: { value }, - }: React.ChangeEvent): void => { - setTimeframe(value as Unit); - setShowHistogram(false); - }; + const setNoiseWarning = useCallback((): void => { + dispatch({ + type: 'setNoiseWarning', + }); + }, [dispatch]); - const handlePreviewClicked = useCallback((): void => { - handleCalculateTimeRange(); + const setShowHistogram = useCallback( + (show: boolean): void => { + dispatch({ + type: 'setShowHistogram', + show, + }); + }, + [dispatch] + ); - if (ruleType === 'eql') { - setShowHistogram(true); - handlePreviewEqlQuery(); - } else { - const builtFilterQuery = { - ...((getQueryFilter( - queryString, - language, - filters, - index, - [], - true - ) as unknown) as ESQueryStringQuery), - }; - if (builtFilterQuery != null) { - setShowHistogram(true); - } - setQueryFilter(builtFilterQuery); - } - }, [ - filters, - handleCalculateTimeRange, - handlePreviewEqlQuery, - index, - language, - queryString, - ruleType, - ]); + const setThresholdValues = useCallback( + (thresh: Threshold, type: Type): void => { + dispatch({ + type: 'setThresholdQueryVals', + threshold: thresh, + ruleType: type, + }); + }, + [dispatch] + ); useEffect((): void => { - if (eqlError != null) { - addError(eqlError, { title: i18n.PREVIEW_QUERY_ERROR }); - } - }, [eqlError, addError]); + const debounced = debounce(1000, setQueryInfo); - // reset when rule type changes - useEffect((): void => { - const options = getTimeframeOptions(ruleType); + debounced(query); + }, [setQueryInfo, query]); - setShowHistogram(false); - setTimeframe('h'); - setTimeframeOptions(options); - setWarnings([]); - }, [ruleType]); + useEffect((): void => { + setThresholdValues(threshold, ruleType); + }, [setThresholdValues, threshold, ruleType]); - // reset when timeframe or query changes useEffect((): void => { - setShowHistogram(false); - setWarnings([]); - }, [timeframe, queryString]); + setRuleTypeChange(ruleType); + }, [ruleType, setRuleTypeChange]); useEffect((): void => { - if (eqlQueryResult != null) { - setWarnings((prevWarnings) => { - if (eqlQueryResult.warnings.join() !== prevWarnings.join()) { - return eqlQueryResult.warnings; - } + const totalHits = ruleType === 'eql' ? eqlQueryTotal : matrixHistTotal; - return prevWarnings; - }); + if (isNoisy(totalHits, timeframe)) { + setNoiseWarning(); } - }, [eqlQueryResult]); + }, [timeframe, matrixHistTotal, eqlQueryTotal, ruleType, setNoiseWarning]); + + const handlePreviewEqlQuery = useCallback( + (to: string, from: string): void => { + startEql({ + index, + query: queryString, + from, + to, + interval: timeframe, + }); + }, + [startEql, index, queryString, timeframe] + ); - const thresholdFieldExists = useMemo( - (): boolean => threshold != null && threshold.field != null && threshold.field.trim() !== '', - [threshold] + const handleSelectPreviewTimeframe = useCallback( + ({ target: { value } }: React.ChangeEvent): void => { + setTimeframeSelect(value as Unit); + }, + [setTimeframeSelect] ); - const showNonEqlHistogram = useMemo((): boolean => { - return ( - ruleType === 'query' || - ruleType === 'saved_query' || - (ruleType === 'threshold' && !thresholdFieldExists) - ); - }, [ruleType, thresholdFieldExists]); + const handlePreviewClicked = useCallback((): void => { + const to = formatDate('now'); + const from = formatDate(`now-1${timeframe}`); + + setWarnings([]); + setShowHistogram(true); + + if (ruleType === 'eql') { + handlePreviewEqlQuery(to, from); + } else { + startNonEql(to, from); + } + }, [setWarnings, setShowHistogram, ruleType, handlePreviewEqlQuery, startNonEql, timeframe]); return ( <> @@ -209,7 +268,14 @@ export const PreviewQuery = ({ />
- + {i18n.PREVIEW_LABEL} @@ -217,41 +283,50 @@ export const PreviewQuery = ({ {showNonEqlHistogram && showHistogram && ( - )} {ruleType === 'threshold' && thresholdFieldExists && showHistogram && ( )} - {ruleType === 'eql' && eqlQueryResult != null && showHistogram && !eqlQueryLoading && ( + {ruleType === 'eql' && showHistogram && ( )} - {warnings.length > 0 && - warnings.map((warning) => ( - <> + {showHistogram && + warnings.length > 0 && + warnings.map((warning, i) => ( + - +

{warning}

- +
))} ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/non_eql_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/non_eql_histogram.tsx deleted file mode 100644 index 6f5f53a37af9..000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/non_eql_histogram.tsx +++ /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 { EuiSpacer, EuiText, EuiFlexItem } from '@elastic/eui'; - -import * as i18n from './translations'; -import { useGlobalTime } from '../../../../common/containers/use_global_time'; -import { MatrixHistogram } from '../../../../common/components/matrix_histogram'; -import { MatrixHistogramType } from '../../../../../common/search_strategy/security_solution'; -import { - MatrixHistogramOption, - MatrixHistogramConfigs, -} from '../../../../common/components/matrix_histogram/types'; -import { ESQueryStringQuery } from '../../../../../common/typed_json'; - -const ID = 'nonEqlRuleQueryPreviewHistogramQuery'; - -const stackByOptions: MatrixHistogramOption[] = [ - { - text: 'event.category', - value: 'event.category', - }, -]; -const DEFAULT_STACK_BY = 'event.category'; - -const histogramConfigs: MatrixHistogramConfigs = { - defaultStackByOption: - stackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? stackByOptions[0], - errorMessage: i18n.PREVIEW_QUERY_ERROR, - histogramType: MatrixHistogramType.events, - stackByOptions, - title: i18n.QUERY_GRAPH_HITS_TITLE, - titleSize: 'xs', - subtitle: i18n.QUERY_PREVIEW_TITLE, - hideHistogramIfEmpty: false, -}; - -interface PreviewNonEqlQueryHistogramProps { - to: string; - from: string; - index: string[]; - filterQuery: ESQueryStringQuery | undefined; -} - -export const PreviewNonEqlQueryHistogram = ({ - index, - from, - to, - filterQuery, -}: PreviewNonEqlQueryHistogramProps) => { - const { setQuery } = useGlobalTime(); - - return ( - - <> - - -

{i18n.PREVIEW_QUERY_DISCLAIMER}

-
- - - } - {...histogramConfigs} - /> - ); -}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.test.ts b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.test.ts new file mode 100644 index 000000000000..f417c172af18 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.test.ts @@ -0,0 +1,458 @@ +/* + * 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 moment from 'moment'; + +import * as i18n from './translations'; +import { Action, State, queryPreviewReducer } from './reducer'; +import { initialState } from './'; + +describe('queryPreviewReducer', () => { + let reducer: (state: State, action: Action) => State; + + beforeEach(() => { + moment.tz.setDefault('UTC'); + reducer = queryPreviewReducer(); + }); + + afterEach(() => { + moment.tz.setDefault('Browser'); + }); + + describe('#setQueryInfo', () => { + test('should not update state if queryBar undefined', () => { + const update = reducer(initialState, { + type: 'setQueryInfo', + queryBar: undefined, + index: ['foo-*'], + ruleType: 'query', + }); + + expect(update).toEqual(initialState); + }); + + test('should reset showHistogram and warnings if queryBar undefined', () => { + const update = reducer( + { ...initialState, showHistogram: true, warnings: ['uh oh'] }, + { + type: 'setQueryInfo', + queryBar: undefined, + index: ['foo-*'], + ruleType: 'query', + } + ); + + expect(update.warnings).toEqual([]); + expect(update.showHistogram).toBeFalsy(); + }); + + test('should reset showHistogram and warnings if queryBar defined', () => { + const update = reducer( + { ...initialState, showHistogram: true, warnings: ['uh oh'] }, + { + type: 'setQueryInfo', + queryBar: { + query: { query: 'host.name:*', language: 'kuery' }, + filters: [{ meta: { alias: '', disabled: false, negate: false } }], + }, + index: ['foo-*'], + ruleType: 'query', + } + ); + + expect(update.warnings).toEqual([]); + expect(update.showHistogram).toBeFalsy(); + }); + + test('should pull the query, language, and filters from the action', () => { + const update = reducer(initialState, { + type: 'setQueryInfo', + queryBar: { + query: { query: 'host.name:*', language: 'kuery' }, + filters: [{ meta: { alias: '', disabled: false, negate: false } }], + }, + index: ['foo-*'], + ruleType: 'query', + }); + + expect(update.language).toEqual('kuery'); + expect(update.queryString).toEqual('host.name:*'); + expect(update.filters).toEqual([{ meta: { alias: '', disabled: false, negate: false } }]); + }); + + test('should create the queryFilter if query type is not eql', () => { + const update = reducer(initialState, { + type: 'setQueryInfo', + queryBar: { + query: { query: 'host.name:*', language: 'kuery' }, + filters: [{ meta: { alias: '', disabled: false, negate: false } }], + }, + index: ['foo-*'], + ruleType: 'query', + }); + + expect(update.queryFilter).toEqual({ + bool: { + filter: [ + { bool: { minimum_should_match: 1, should: [{ exists: { field: 'host.name' } }] } }, + {}, + ], + must: [], + must_not: [], + should: [], + }, + }); + }); + + test('should set query to empty string if it is not of type string', () => { + const update = reducer(initialState, { + type: 'setQueryInfo', + queryBar: { + query: { query: { not: 'a string' }, language: 'kuery' }, + filters: [{ meta: { alias: '', disabled: false, negate: false } }], + }, + index: ['foo-*'], + ruleType: 'query', + }); + + expect(update.queryString).toEqual(''); + }); + }); + + describe('#setTimeframeSelect', () => { + test('should update timeframe with that specified in action" ', () => { + const update = reducer(initialState, { + type: 'setTimeframeSelect', + timeframe: 'd', + }); + + expect(update.timeframe).toEqual('d'); + }); + + test('should reset warnings and showHistogram to false" ', () => { + const update = reducer( + { ...initialState, showHistogram: true, warnings: ['blah'] }, + { + type: 'setTimeframeSelect', + timeframe: 'd', + } + ); + + expect(update.showHistogram).toBeFalsy(); + expect(update.warnings).toEqual([]); + }); + }); + + describe('#setResetRuleTypeChange', () => { + test('should reset timeframe, warnings, and hide histogram on rule type change" ', () => { + const update = reducer( + { ...initialState, timeframe: 'd', showHistogram: true, warnings: ['blah'] }, + { + type: 'setResetRuleTypeChange', + ruleType: 'eql', + } + ); + + expect(update.showHistogram).toBeFalsy(); + expect(update.timeframe).toEqual('h'); + expect(update.warnings).toEqual([]); + expect(update.showNonEqlHistogram).toBeFalsy(); + }); + + test('should set timeframe options to hour and day if rule type is eql" ', () => { + const update = reducer( + { ...initialState, timeframe: 'd', showHistogram: true, warnings: ['blah'] }, + { + type: 'setResetRuleTypeChange', + ruleType: 'eql', + } + ); + + expect(update.timeframeOptions).toEqual([ + { + text: 'Last hour', + value: 'h', + }, + { + text: 'Last day', + value: 'd', + }, + ]); + }); + + test('should set "showNonEqlHist" to true and timeframe options to hour, day, and month if rule type is query" ', () => { + const update = reducer( + { ...initialState, timeframe: 'd', showHistogram: true, warnings: ['blah'] }, + { + type: 'setResetRuleTypeChange', + ruleType: 'query', + } + ); + + expect(update.showNonEqlHistogram).toBeTruthy(); + expect(update.timeframeOptions).toEqual([ + { + text: 'Last hour', + value: 'h', + }, + { + text: 'Last day', + value: 'd', + }, + { + text: 'Last month', + value: 'M', + }, + ]); + }); + + test('should set "showNonEqlHist" to true and timeframe options to hour, day, and month if rule type is saved_query" ', () => { + const update = reducer( + { ...initialState, timeframe: 'd', showHistogram: true, warnings: ['blah'] }, + { + type: 'setResetRuleTypeChange', + ruleType: 'saved_query', + } + ); + + expect(update.showNonEqlHistogram).toBeTruthy(); + expect(update.timeframeOptions).toEqual([ + { + text: 'Last hour', + value: 'h', + }, + { + text: 'Last day', + value: 'd', + }, + { + text: 'Last month', + value: 'M', + }, + ]); + }); + + test('should set "showNonEqlHist" to true and timeframe options to hour, day, and month if rule type is threshold and no threshold field is specified" ', () => { + const update = reducer( + { + ...initialState, + timeframe: 'd', + showHistogram: true, + warnings: ['blah'], + thresholdFieldExists: false, + }, + { + type: 'setResetRuleTypeChange', + ruleType: 'threshold', + } + ); + + expect(update.showNonEqlHistogram).toBeTruthy(); + expect(update.timeframeOptions).toEqual([ + { + text: 'Last hour', + value: 'h', + }, + { + text: 'Last day', + value: 'd', + }, + { + text: 'Last month', + value: 'M', + }, + ]); + }); + + test('should set "showNonEqlHist" to false and timeframe options to hour, day, and month if rule type is threshold and threshold field is specified" ', () => { + const update = reducer( + { + ...initialState, + timeframe: 'd', + showHistogram: true, + warnings: ['blah'], + thresholdFieldExists: true, + }, + { + type: 'setResetRuleTypeChange', + ruleType: 'threshold', + } + ); + + expect(update.showNonEqlHistogram).toBeFalsy(); + expect(update.timeframeOptions).toEqual([ + { + text: 'Last hour', + value: 'h', + }, + { + text: 'Last day', + value: 'd', + }, + { + text: 'Last month', + value: 'M', + }, + ]); + }); + }); + + describe('#setWarnings', () => { + test('should set warnings to that passed in action" ', () => { + const update = reducer(initialState, { + type: 'setWarnings', + warnings: ['bad'], + }); + + expect(update.warnings).toEqual(['bad']); + }); + }); + + describe('#setShowHistogram', () => { + test('should set "setShowHistogram" to false if "action.show" is false', () => { + const update = reducer(initialState, { + type: 'setShowHistogram', + show: false, + }); + + expect(update.showHistogram).toBeFalsy(); + }); + + test('should set "disableOr" to true if "action.show" is true', () => { + const update = reducer(initialState, { + type: 'setShowHistogram', + show: true, + }); + + expect(update.showHistogram).toBeTruthy(); + }); + }); + + describe('#setThresholdQueryVals', () => { + test('should set thresholdFieldExists to true if threshold field is defined and not empty string', () => { + const update = reducer(initialState, { + type: 'setThresholdQueryVals', + threshold: { field: 'agent.hostname', value: 200 }, + ruleType: 'threshold', + }); + + expect(update.thresholdFieldExists).toBeTruthy(); + expect(update.showNonEqlHistogram).toBeFalsy(); + expect(update.showHistogram).toBeFalsy(); + expect(update.warnings).toEqual([]); + }); + + test('should set thresholdFieldExists to false if threshold field is not defined', () => { + const update = reducer(initialState, { + type: 'setThresholdQueryVals', + threshold: { field: undefined, value: 200 }, + ruleType: 'threshold', + }); + + expect(update.thresholdFieldExists).toBeFalsy(); + expect(update.showNonEqlHistogram).toBeTruthy(); + expect(update.showHistogram).toBeFalsy(); + expect(update.warnings).toEqual([]); + }); + + test('should set thresholdFieldExists to false if threshold field is empty string', () => { + const update = reducer(initialState, { + type: 'setThresholdQueryVals', + threshold: { field: ' ', value: 200 }, + ruleType: 'threshold', + }); + + expect(update.thresholdFieldExists).toBeFalsy(); + expect(update.showNonEqlHistogram).toBeTruthy(); + expect(update.showHistogram).toBeFalsy(); + expect(update.warnings).toEqual([]); + }); + + test('should set showNonEqlHistogram to false if ruleType is eql', () => { + const update = reducer(initialState, { + type: 'setThresholdQueryVals', + threshold: { field: 'agent.hostname', value: 200 }, + ruleType: 'eql', + }); + + expect(update.showNonEqlHistogram).toBeFalsy(); + expect(update.showHistogram).toBeFalsy(); + expect(update.warnings).toEqual([]); + }); + + test('should set showNonEqlHistogram to true if ruleType is query', () => { + const update = reducer(initialState, { + type: 'setThresholdQueryVals', + threshold: { field: 'agent.hostname', value: 200 }, + ruleType: 'query', + }); + + expect(update.showNonEqlHistogram).toBeTruthy(); + expect(update.showHistogram).toBeFalsy(); + expect(update.warnings).toEqual([]); + }); + + test('should set showNonEqlHistogram to true if ruleType is saved_query', () => { + const update = reducer(initialState, { + type: 'setThresholdQueryVals', + threshold: { field: 'agent.hostname', value: 200 }, + ruleType: 'saved_query', + }); + + expect(update.showNonEqlHistogram).toBeTruthy(); + expect(update.showHistogram).toBeFalsy(); + expect(update.warnings).toEqual([]); + }); + }); + + describe('#setToFrom', () => { + test('should update to and from times to be an hour apart if timeframe is "h"', () => { + const update = reducer( + { ...initialState, timeframe: 'h' }, + { + type: 'setToFrom', + } + ); + + const dateFrom = moment(update.fromTime); + const dateTo = moment(update.toTime); + const diff = dateFrom.diff(dateTo); + + // 3600000ms = 60 minutes + // Sometimes test returns 3599999 + expect(Math.ceil(diff / 100000) * 100000).toEqual(3600000); + }); + + test('should update to and from times to be a day apart if timeframe is "d"', () => { + const update = reducer( + { ...initialState, timeframe: 'd' }, + { + type: 'setToFrom', + } + ); + + const dateFrom = moment(update.fromTime); + const dateTo = moment(update.toTime); + const diff = dateFrom.diff(dateTo); + + // 86400000 = 24 hours + // Sometimes test returns 86399999 + expect(Math.ceil(diff / 100000) * 100000).toEqual(86400000); + }); + }); + + describe('#setNoiseWarning', () => { + test('should add noise warning', () => { + const update = reducer( + { ...initialState, warnings: ['uh oh'] }, + { + type: 'setNoiseWarning', + } + ); + + expect(update.warnings).toEqual(['uh oh', i18n.QUERY_PREVIEW_NOISE_WARNING]); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.ts b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.ts new file mode 100644 index 000000000000..76047a0af5c5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/reducer.ts @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Unit } from '@elastic/datemath'; +import { EuiSelectOption } from '@elastic/eui'; + +import * as i18n from './translations'; +import { Filter } from '../../../../../../../../src/plugins/data/common/es_query'; +import { ESQuery } from '../../../../../common/typed_json'; +import { Language, Type } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { FieldValueQueryBar } from '../query_bar'; +import { formatDate } from '../../../../common/components/super_date_picker'; +import { getInfoFromQueryBar, getTimeframeOptions } from './helpers'; +import { Threshold } from '.'; + +export interface State { + timeframeOptions: EuiSelectOption[]; + showHistogram: boolean; + timeframe: Unit; + warnings: string[]; + queryFilter: ESQuery | undefined; + toTime: string; + fromTime: string; + queryString: string; + language: Language; + filters: Filter[]; + thresholdFieldExists: boolean; + showNonEqlHistogram: boolean; +} + +export type Action = + | { + type: 'setQueryInfo'; + queryBar: FieldValueQueryBar | undefined; + index: string[]; + ruleType: Type; + } + | { + type: 'setTimeframeSelect'; + timeframe: Unit; + } + | { + type: 'setResetRuleTypeChange'; + ruleType: Type; + } + | { + type: 'setWarnings'; + warnings: string[]; + } + | { + type: 'setShowHistogram'; + show: boolean; + } + | { + type: 'setThresholdQueryVals'; + threshold: Threshold; + ruleType: Type; + } + | { + type: 'setNoiseWarning'; + } + | { + type: 'setToFrom'; + }; + +export const queryPreviewReducer = () => (state: State, action: Action): State => { + switch (action.type) { + case 'setQueryInfo': { + if (action.queryBar != null) { + const { queryString, language, filters, queryFilter } = getInfoFromQueryBar( + action.queryBar, + action.index, + action.ruleType + ); + + return { + ...state, + queryString, + language, + filters, + queryFilter, + showHistogram: false, + warnings: [], + }; + } + + return { + ...state, + warnings: [], + showHistogram: false, + }; + } + case 'setTimeframeSelect': { + return { + ...state, + timeframe: action.timeframe, + showHistogram: false, + warnings: [], + }; + } + case 'setResetRuleTypeChange': { + const showNonEqlHist = + action.ruleType === 'query' || + action.ruleType === 'saved_query' || + (action.ruleType === 'threshold' && !state.thresholdFieldExists); + + return { + ...state, + showHistogram: false, + timeframe: 'h', + timeframeOptions: getTimeframeOptions(action.ruleType), + showNonEqlHistogram: showNonEqlHist, + warnings: [], + }; + } + case 'setWarnings': { + return { + ...state, + warnings: action.warnings, + }; + } + case 'setShowHistogram': { + return { + ...state, + showHistogram: action.show, + }; + } + case 'setThresholdQueryVals': { + const thresholdField = + action.threshold != null && + action.threshold.field != null && + action.threshold.field.trim() !== ''; + const showNonEqlHist = + action.ruleType === 'query' || + action.ruleType === 'saved_query' || + (action.ruleType === 'threshold' && !thresholdField); + + return { + ...state, + thresholdFieldExists: thresholdField, + showNonEqlHistogram: showNonEqlHist, + showHistogram: false, + warnings: [], + }; + } + case 'setToFrom': { + return { + ...state, + fromTime: formatDate('now'), + toTime: formatDate(`now-1${state.timeframe}`), + }; + } + case 'setNoiseWarning': { + return { + ...state, + warnings: [...state.warnings, i18n.QUERY_PREVIEW_NOISE_WARNING], + }; + } + default: + return state; + } +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.test.tsx new file mode 100644 index 000000000000..8a0cfef1b625 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.test.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { useGlobalTime } from '../../../../common/containers/use_global_time'; +import { TestProviders } from '../../../../common/mock'; +import { PreviewThresholdQueryHistogram } from './threshold_histogram'; + +jest.mock('../../../../common/containers/use_global_time'); + +describe('PreviewThresholdQueryHistogram', () => { + const mockSetQuery = jest.fn(); + + beforeEach(() => { + (useGlobalTime as jest.Mock).mockReturnValue({ + from: '2020-07-07T08:20:18.966Z', + isInitializing: false, + to: '2020-07-08T08:20:18.966Z', + setQuery: mockSetQuery, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it renders loader when isLoading is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeTruthy(); + }); + + test('it configures buckets data', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + expect(wrapper.find('[data-test-subj="queryPreviewLoading"]').exists()).toBeFalsy(); + expect( + wrapper.find('[data-test-subj="thresholdQueryPreviewHistogram"]').at(0).props().data + ).toEqual([ + { + key: 'hits', + value: [ + { g: 'siem_kibana', x: 'siem_kibana', y: 400 }, + { g: 'bastion00.siem.estc.dev', x: 'bastion00.siem.estc.dev', y: 80225 }, + { g: 'es02.siem.estc.dev', x: 'es02.siem.estc.dev', y: 1228 }, + ], + }, + ]); + }); + + test('it invokes setQuery with id, inspect, isLoading and refetch', async () => { + const mockRefetch = jest.fn(); + + mount( + ({ eui: euiLightVars, darkMode: false })}> + + + + + ); + + expect(mockSetQuery).toHaveBeenCalledWith({ + id: 'queryPreviewThresholdHistogramQuery', + inspect: { dsl: ['some dsl'], response: ['query response'] }, + loading: false, + refetch: mockRefetch, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.tsx index 03e0656fe06c..1021c5b8ddcb 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/threshold_histogram.tsx @@ -5,98 +5,77 @@ */ import React, { useEffect, useMemo } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer } from '@elastic/eui'; import * as i18n from './translations'; import { useGlobalTime } from '../../../../common/containers/use_global_time'; -import { BarChart } from '../../../../common/components/charts/barchart'; import { getThresholdHistogramConfig } from './helpers'; -import { useMatrixHistogram } from '../../../../common/containers/matrix_histogram'; -import { MatrixHistogramType } from '../../../../../common/search_strategy/security_solution/matrix_histogram'; -import { ESQueryStringQuery } from '../../../../../common/typed_json'; -import { Panel } from '../../../../common/components/panel'; -import { HeaderSection } from '../../../../common/components/header_section'; -import { ChartSeriesConfigs } from '../../../../common/components/charts/common'; +import { ChartSeriesConfigs, ChartSeriesData } from '../../../../common/components/charts/common'; +import { InspectResponse } from '../../../../../public/types'; +import { inputsModel } from '../../../../common/store'; +import { PreviewHistogram } from './histogram'; export const ID = 'queryPreviewThresholdHistogramQuery'; interface PreviewThresholdQueryHistogramProps { - to: string; - from: string; - filterQuery: ESQueryStringQuery | undefined; - threshold: { field: string | undefined; value: number } | undefined; - index: string[]; + isLoading: boolean; + buckets: Array<{ + key: string; + doc_count: number; + }>; + inspect: InspectResponse; + refetch: inputsModel.Refetch; } export const PreviewThresholdQueryHistogram = ({ - from, - to, - filterQuery, - threshold, - index, + buckets, + inspect, + refetch, + isLoading, }: PreviewThresholdQueryHistogramProps) => { const { setQuery, isInitializing } = useGlobalTime(); - const [isLoading, { inspect, refetch, buckets }] = useMatrixHistogram({ - errorMessage: i18n.PREVIEW_QUERY_ERROR, - endDate: from, - startDate: to, - filterQuery, - indexNames: index, - histogramType: MatrixHistogramType.events, - stackByField: 'event.category', - threshold, - }); - useEffect((): void => { if (!isLoading && !isInitializing) { setQuery({ id: ID, inspect, loading: isLoading, refetch }); } }, [setQuery, inspect, isLoading, isInitializing, refetch]); - const { data, totalCount } = useMemo(() => { - return { - data: buckets.map<{ x: string; y: number; g: string }>(({ key, doc_count: docCount }) => ({ + const { data, totalCount } = useMemo((): { data: ChartSeriesData[]; totalCount: number } => { + const total = buckets.length; + + const dataBuckets = buckets.map<{ x: string; y: number; g: string }>( + ({ key, doc_count: docCount }) => ({ x: key, y: docCount, g: key, - })), - totalCount: buckets.length, + }) + ); + return { + data: [{ key: 'hits', value: dataBuckets }], + totalCount: total, }; }, [buckets]); const barConfig = useMemo((): ChartSeriesConfigs => getThresholdHistogramConfig(200), []); + const subtitle = useMemo( + (): string => + isLoading + ? i18n.PREVIEW_SUBTITLE_LOADING + : i18n.QUERY_PREVIEW_THRESHOLD_WITH_FIELD_TITLE(totalCount), + [isLoading, totalCount] + ); + return ( - <> - - - - - - - - - - <> - - -

{i18n.PREVIEW_QUERY_DISCLAIMER}

-
- -
-
-
- + ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/translations.ts b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/translations.ts index 3e4f389a1883..7ae75c51dcf5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/query_preview/translations.ts @@ -124,3 +124,10 @@ export const PREVIEW_WARNING_TIMESTAMP = i18n.translate( defaultMessage: 'Unable to find "@timestamp" field on events.', } ); + +export const PREVIEW_SUBTITLE_LOADING = i18n.translate( + 'xpack.securitySolution.detectionEngine.queryPreview.queryPreviewSubtitleLoading', + { + defaultMessage: '...loading', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx index 88297c4e3701..fc03e07442f9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx @@ -122,9 +122,13 @@ const StepAboutRuleComponent: FC = ({ }, [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 8c76e6a2be57..27d69c688701 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 @@ -131,7 +131,7 @@ const StepDefineRuleComponent: FC = ({ options: { stripEmptyFields: false }, schema, }); - const { getErrors, getFields, getFormData, reset, submit } = form; + const { getFields, getFormData, reset, submit } = form; const [ { index: formIndex, @@ -152,14 +152,13 @@ const StepDefineRuleComponent: FC = ({ } > ]; + const [isQueryBarValid, setIsQueryBarValid] = useState(false); const index = formIndex || initialState.index; const threatIndex = formThreatIndex || initialState.threatIndex; const ruleType = formRuleType || initialState.ruleType; const queryBarQuery = formQuery != null ? formQuery.query.query : '' || initialState.queryBar.query.query; - const errorExists = getErrors().length > 0; const [indexPatternsLoading, { browserFields, indexPatterns }] = useFetchIndex(index); - const [ threatIndexPatternsLoading, { browserFields: threatBrowserFields, indexPatterns: threatIndexPatterns }, @@ -191,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(() => { @@ -284,6 +287,7 @@ const StepDefineRuleComponent: FC = ({ path="queryBar" component={EqlQueryBar} componentProps={{ + onValidityChange: setIsQueryBarValid, idAria: 'detectionEngineStepDefineRuleEqlQueryBar', isDisabled: isLoading, isLoading: indexPatternsLoading, @@ -319,6 +323,7 @@ const StepDefineRuleComponent: FC = ({ isLoading: indexPatternsLoading, dataTestSubj: 'detectionEngineStepDefineRuleQueryBar', openTimelineSearch, + onValidityChange: setIsQueryBarValid, onCloseTimelineSearch: handleCloseTimelineSearch, }} /> @@ -394,12 +399,12 @@ const StepDefineRuleComponent: 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 d451932a6b63..0bc06e3dafc6 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 b01edac2605e..9b15007136b2 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 92d149170726..ac2bf438d7fa 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 cd2cc1fe390b..4fd240348f0f 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 2eb2145c6c34..59ab416ecc82 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 d0571bfca5b2..1db952526414 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 14fd9ffa5084..f7d220273616 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/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index cc91d2390581..30dec34ab39b 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 52598b5f4494..17f8e19a6055 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 3d291d9bf7b2..88fd1ad5f98b 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 41f443f14caf..54cb0c0883e1 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 89937d0adf81..c0724ea3dd41 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/lazy_application_dependencies.tsx b/x-pack/plugins/security_solution/public/lazy_application_dependencies.tsx new file mode 100644 index 000000000000..77e21a313e74 --- /dev/null +++ b/x-pack/plugins/security_solution/public/lazy_application_dependencies.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * the plugin (defined in `plugin.tsx`) has many dependencies that can be loaded only when the app is being used. + * By loading these later we can reduce the initial bundle size and allow users to delay loading these dependencies until they are needed. + */ + +import { renderApp } from './app'; +import { composeLibs } from './common/lib/compose/kibana_compose'; + +import { createStore, createInitialState } from './common/store'; + +export { renderApp, composeLibs, createStore, createInitialState }; diff --git a/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx b/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx new file mode 100644 index 000000000000..4691ccc72a7e --- /dev/null +++ b/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * the plugin (defined in `plugin.tsx`) has many dependencies that can be loaded only when the app is being used. + * By loading these later we can reduce the initial bundle size and allow users to delay loading these dependencies until they are needed. + */ + +import { Detections } from './detections'; +import { Cases } from './cases'; +import { Hosts } from './hosts'; +import { Network } from './network'; +import { Overview } from './overview'; +import { Timelines } from './timelines'; +import { Management } from './management'; + +/** + * The classes used to instantiate the sub plugins. These are grouped into a single object for the sake of bundling them in a single dynamic import. + */ +const subPluginClasses = { + Detections, + Cases, + Hosts, + Network, + Overview, + Timelines, + Management, +}; +export { subPluginClasses }; 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 17e0101426b0..80b2d2b0192f 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 47f4fbb8830a..c0763a21f094 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 43d3b39474fc..6bc3445c8e74 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 debdde901407..12a76ae0772a 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` ); }); 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 b514707da6f6..36c5b0d1037e 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 ( diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/control_panel/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/control_panel/__snapshots__/index.test.tsx.snap index 1cd4e96546f9..190b78761a9d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/control_panel/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/control_panel/__snapshots__/index.test.tsx.snap @@ -10,6 +10,7 @@ exports[`control_panel ControlPanel should render grid selection correctly 1`] = > 0 trusted applications @@ -36,6 +37,7 @@ exports[`control_panel ControlPanel should render list selection correctly 1`] = > 0 trusted applications @@ -62,6 +64,7 @@ exports[`control_panel ControlPanel should render plural count correctly 1`] = ` > 100 trusted applications @@ -88,6 +91,7 @@ exports[`control_panel ControlPanel should render singular count correctly 1`] = > 1 trusted application diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/control_panel/index.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/control_panel/index.tsx index 1dd70d766cd8..66928b99c78b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/control_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/control_panel/index.tsx @@ -22,7 +22,7 @@ export const ControlPanel = memo( return ( - + {i18n.translate('xpack.securitySolution.trustedapps.list.totalCount', { defaultMessage: '{totalItemCount, plural, one {# trusted application} other {# trusted applications}}', diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx index 8b922605e0ab..b8692df0240f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx @@ -347,7 +347,7 @@ export const CreateTrustedAppForm = memo( + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> { - if (text.length > maxSize) { - return `${text.substr(0, maxSize)}...`; - } else { - return text; - } -}; - const getEntriesColumnDefinitions = (): Array> => [ { field: 'field', @@ -49,7 +42,7 @@ const getEntriesColumnDefinitions = (): Array truncateText: true, textOnly: true, width: '30%', - render(field: MacosLinuxConditionEntry['field'], entry: Entry) { + render(field: Entry['field'], entry: Entry) { return CONDITION_FIELD_TITLE[field]; }, }, @@ -59,18 +52,25 @@ const getEntriesColumnDefinitions = (): Array sortable: false, truncateText: true, width: '20%', - render() { - return i18n.translate('xpack.securitySolution.trustedapps.card.operator.includes', { - defaultMessage: 'is', - }); + render(field: Entry['operator'], entry: Entry) { + return OPERATOR_TITLE[field]; }, }, { field: 'value', name: ENTRY_PROPERTY_TITLES.value, sortable: false, - truncateText: true, width: '60%', + 'data-test-subj': 'conditionValue', + render(field: Entry['value'], entry: Entry) { + return ( + + ); + }, }, ]; @@ -86,10 +86,18 @@ export const TrustedAppCard = memo(({ trustedApp, onDelete }: TrustedAppCardProp trimTextOverflow(trustedApp.name || '', 100), [trustedApp.name])} - title={trustedApp.name} + value={ + + } + /> + } /> - } /> - + + } + /> trimTextOverflow(trustedApp.description || '', 100), [ - trustedApp.description, - ])} - title={trustedApp.description} + value={ + + } />
+
No items found
+
@@ -25,7 +31,7 @@ exports[`TrustedAppsGrid renders correctly initially 1`] = ` exports[`TrustedAppsGrid renders correctly when failed loading data for the first time 1`] = `
+
Intenal Server Error +
@@ -48,7 +60,7 @@ exports[`TrustedAppsGrid renders correctly when failed loading data for the firs exports[`TrustedAppsGrid renders correctly when failed loading data for the second time 1`] = `
+
Intenal Server Error +
@@ -88,11 +106,14 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
+
@@ -126,9 +147,11 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiDescriptionList__description c2" > - trusted app 0 + + trusted app 0 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 0 + + Trusted App 0 + @@ -386,9 +411,11 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiDescriptionList__description c2" > - trusted app 1 + + trusted app 1 +
- Mac OS + + Mac OS +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 1 + + Trusted App 1 + @@ -646,9 +675,11 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiDescriptionList__description c2" > - trusted app 2 + + trusted app 2 +
- Linux + + Linux +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 2 + + Trusted App 2 + @@ -906,9 +939,11 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiDescriptionList__description c2" > - trusted app 3 + + trusted app 3 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 3 + + Trusted App 3 + @@ -1166,9 +1203,11 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiDescriptionList__description c2" > - trusted app 4 + + trusted app 4 +
- Mac OS + + Mac OS +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 4 + + Trusted App 4 + @@ -1426,9 +1467,11 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiDescriptionList__description c2" > - trusted app 5 + + trusted app 5 +
- Linux + + Linux +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 5 + + Trusted App 5 + @@ -1686,9 +1731,11 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiDescriptionList__description c2" > - trusted app 6 + + trusted app 6 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 6 + + Trusted App 6 + @@ -1946,9 +1995,11 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiDescriptionList__description c2" > - trusted app 7 + + trusted app 7 +
- Mac OS + + Mac OS +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 7 + + Trusted App 7 + @@ -2206,9 +2259,11 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiDescriptionList__description c2" > - trusted app 8 + + trusted app 8 +
- Linux + + Linux +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 8 + + Trusted App 8 + @@ -2466,9 +2523,11 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiDescriptionList__description c2" > - trusted app 9 + + trusted app 9 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 9 + + Trusted App 9 + @@ -2701,6 +2762,9 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
+
+
No items found
+
@@ -2959,7 +3029,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
+
@@ -3004,9 +3077,11 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiDescriptionList__description c2" > - trusted app 0 + + trusted app 0 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 0 + + Trusted App 0 + @@ -3264,9 +3341,11 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiDescriptionList__description c2" > - trusted app 1 + + trusted app 1 +
- Mac OS + + Mac OS +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 1 + + Trusted App 1 + @@ -3524,9 +3605,11 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiDescriptionList__description c2" > - trusted app 2 + + trusted app 2 +
- Linux + + Linux +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 2 + + Trusted App 2 + @@ -3784,9 +3869,11 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiDescriptionList__description c2" > - trusted app 3 + + trusted app 3 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 3 + + Trusted App 3 + @@ -4044,9 +4133,11 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiDescriptionList__description c2" > - trusted app 4 + + trusted app 4 +
- Mac OS + + Mac OS +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 4 + + Trusted App 4 + @@ -4304,9 +4397,11 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiDescriptionList__description c2" > - trusted app 5 + + trusted app 5 +
- Linux + + Linux +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 5 + + Trusted App 5 + @@ -4564,9 +4661,11 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiDescriptionList__description c2" > - trusted app 6 + + trusted app 6 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 6 + + Trusted App 6 + @@ -4824,9 +4925,11 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiDescriptionList__description c2" > - trusted app 7 + + trusted app 7 +
- Mac OS + + Mac OS +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 7 + + Trusted App 7 + @@ -5084,9 +5189,11 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiDescriptionList__description c2" > - trusted app 8 + + trusted app 8 +
- Linux + + Linux +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 8 + + Trusted App 8 + @@ -5344,9 +5453,11 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiDescriptionList__description c2" > - trusted app 9 + + trusted app 9 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 9 + + Trusted App 9 + @@ -5579,6 +5692,9 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time
+
+
@@ -5846,9 +5965,11 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiDescriptionList__description c2" > - trusted app 0 + + trusted app 0 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 0 + + Trusted App 0 + @@ -6106,9 +6229,11 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiDescriptionList__description c2" > - trusted app 1 + + trusted app 1 +
- Mac OS + + Mac OS +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 1 + + Trusted App 1 + @@ -6366,9 +6493,11 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiDescriptionList__description c2" > - trusted app 2 + + trusted app 2 +
- Linux + + Linux +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 2 + + Trusted App 2 + @@ -6626,9 +6757,11 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiDescriptionList__description c2" > - trusted app 3 + + trusted app 3 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 3 + + Trusted App 3 + @@ -6886,9 +7021,11 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiDescriptionList__description c2" > - trusted app 4 + + trusted app 4 +
- Mac OS + + Mac OS +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 4 + + Trusted App 4 + @@ -7146,9 +7285,11 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiDescriptionList__description c2" > - trusted app 5 + + trusted app 5 +
- Linux + + Linux +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 5 + + Trusted App 5 + @@ -7406,9 +7549,11 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiDescriptionList__description c2" > - trusted app 6 + + trusted app 6 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 6 + + Trusted App 6 + @@ -7666,9 +7813,11 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiDescriptionList__description c2" > - trusted app 7 + + trusted app 7 +
- Mac OS + + Mac OS +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 7 + + Trusted App 7 + @@ -7926,9 +8077,11 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiDescriptionList__description c2" > - trusted app 8 + + trusted app 8 +
- Linux + + Linux +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 8 + + Trusted App 8 + @@ -8186,9 +8341,11 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiDescriptionList__description c2" > - trusted app 9 + + trusted app 9 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 9 + + Trusted App 9 + @@ -8421,6 +8580,9 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
+
) => 'MMM D, YYYY @ HH:mm:ss.SSS' } }}> ({ eui: euiLightVars, darkMode: false })}> + + + + diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.tsx index 4664727dd848..d6827ba24c23 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, useCallback, useEffect } from 'react'; +import React, { FC, memo, useCallback, useEffect } from 'react'; import { EuiTablePagination, EuiFlexGroup, @@ -12,6 +12,7 @@ import { EuiProgress, EuiIcon, EuiText, + EuiSpacer, } from '@elastic/eui'; import { Pagination } from '../../../state'; @@ -64,6 +65,14 @@ const PaginationBar = ({ pagination, onChange }: PaginationBarProps) => { ); }; +const GridMessage: FC = ({ children }) => ( +
+ + {children} + +
+); + export const TrustedAppsGrid = memo(() => { const pagination = useTrustedAppsSelector(getListPagination); const listItems = useTrustedAppsSelector(getListItems); @@ -80,7 +89,7 @@ export const TrustedAppsGrid = memo(() => { })); return ( - + {isLoading && ( @@ -88,27 +97,33 @@ export const TrustedAppsGrid = memo(() => { )} {error && ( -
+ {error} -
+ + )} + {!error && listItems.length === 0 && ( + + {NO_RESULTS_MESSAGE} + )} - {!error && ( - - {listItems.map((item) => ( - - - - ))} - {listItems.length === 0 && ( - - {NO_RESULTS_MESSAGE} - - )} - + {!error && listItems.length > 0 && ( + <> + + + + {listItems.map((item) => ( + + + + ))} + + )}
{!error && pagination.totalItemCount > 0 && ( + + )} 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 794fba9cd7dd..181b59c65a3d 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 @@ -647,12 +647,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 0 + + trusted app 0 +
@@ -667,7 +669,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -792,9 +802,11 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiDescriptionList__description c2" > - trusted app 0 + + trusted app 0 +
- Windows + + Windows +
- - 1 minute ago - + 1 minute ago
- someone + + someone +
- Trusted App 0 + + Trusted App 0 + @@ -1033,12 +1047,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 1 + + trusted app 1 +
@@ -1053,7 +1069,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -1152,12 +1176,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 2 + + trusted app 2 +
@@ -1172,7 +1198,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -1271,12 +1305,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 3 + + trusted app 3 +
@@ -1291,7 +1327,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -1390,12 +1434,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 4 + + trusted app 4 +
@@ -1410,7 +1456,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -1509,12 +1563,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 5 + + trusted app 5 +
@@ -1529,7 +1585,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -1628,12 +1692,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 6 + + trusted app 6 +
@@ -1648,7 +1714,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -1747,12 +1821,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 7 + + trusted app 7 +
@@ -1767,7 +1843,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -1866,12 +1950,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 8 + + trusted app 8 +
@@ -1886,7 +1972,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -1985,12 +2079,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 9 + + trusted app 9 +
@@ -2005,7 +2101,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -2104,12 +2208,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 10 + + trusted app 10 +
@@ -2124,7 +2230,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -2223,12 +2337,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 11 + + trusted app 11 +
@@ -2243,7 +2359,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -2342,12 +2466,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 12 + + trusted app 12 +
@@ -2362,7 +2488,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -2461,12 +2595,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 13 + + trusted app 13 +
@@ -2481,7 +2617,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -2580,12 +2724,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 14 + + trusted app 14 +
@@ -2600,7 +2746,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -2699,12 +2853,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 15 + + trusted app 15 +
@@ -2719,7 +2875,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -2818,12 +2982,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 16 + + trusted app 16 +
@@ -2838,7 +3004,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -2937,12 +3111,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 17 + + trusted app 17 +
@@ -2957,7 +3133,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -3056,12 +3240,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 18 + + trusted app 18 +
@@ -3076,7 +3262,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -3175,12 +3369,14 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` Name
- trusted app 19 + + trusted app 19 +
@@ -3195,7 +3391,13 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -3654,12 +3858,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 0 + + trusted app 0 +
@@ -3674,7 +3880,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -3773,12 +3987,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 1 + + trusted app 1 +
@@ -3793,7 +4009,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -3892,12 +4116,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 2 + + trusted app 2 +
@@ -3912,7 +4138,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -4011,12 +4245,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 3 + + trusted app 3 +
@@ -4031,7 +4267,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -4130,12 +4374,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 4 + + trusted app 4 +
@@ -4150,7 +4396,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -4249,12 +4503,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 5 + + trusted app 5 +
@@ -4269,7 +4525,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -4368,12 +4632,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 6 + + trusted app 6 +
@@ -4388,7 +4654,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -4487,12 +4761,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 7 + + trusted app 7 +
@@ -4507,7 +4783,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -4606,12 +4890,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 8 + + trusted app 8 +
@@ -4626,7 +4912,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -4725,12 +5019,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 9 + + trusted app 9 +
@@ -4745,7 +5041,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -4844,12 +5148,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 10 + + trusted app 10 +
@@ -4864,7 +5170,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -4963,12 +5277,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 11 + + trusted app 11 +
@@ -4983,7 +5299,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -5082,12 +5406,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 12 + + trusted app 12 +
@@ -5102,7 +5428,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -5201,12 +5535,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 13 + + trusted app 13 +
@@ -5221,7 +5557,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -5320,12 +5664,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 14 + + trusted app 14 +
@@ -5340,7 +5686,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -5439,12 +5793,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 15 + + trusted app 15 +
@@ -5459,7 +5815,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -5558,12 +5922,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 16 + + trusted app 16 +
@@ -5578,7 +5944,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -5677,12 +6051,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 17 + + trusted app 17 +
@@ -5697,7 +6073,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Linux + + + Linux + +
- someone + + someone +
@@ -5796,12 +6180,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 18 + + trusted app 18 +
@@ -5816,7 +6202,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Windows + + + Windows + +
- someone + + someone +
@@ -5915,12 +6309,14 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` Name
- trusted app 19 + + trusted app 19 +
@@ -5935,7 +6331,13 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = `
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -6552,12 +6956,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 0 + + trusted app 0 +
@@ -6572,7 +6978,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Windows + + + Windows + +
- someone + + someone +
@@ -6671,12 +7085,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 1 + + trusted app 1 +
@@ -6691,7 +7107,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -6790,12 +7214,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 2 + + trusted app 2 +
@@ -6810,7 +7236,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Linux + + + Linux + +
- someone + + someone +
@@ -6909,12 +7343,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 3 + + trusted app 3 +
@@ -6929,7 +7365,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Windows + + + Windows + +
- someone + + someone +
@@ -7028,12 +7472,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 4 + + trusted app 4 +
@@ -7048,7 +7494,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -7147,12 +7601,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 5 + + trusted app 5 +
@@ -7167,7 +7623,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Linux + + + Linux + +
- someone + + someone +
@@ -7266,12 +7730,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 6 + + trusted app 6 +
@@ -7286,7 +7752,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Windows + + + Windows + +
- someone + + someone +
@@ -7385,12 +7859,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 7 + + trusted app 7 +
@@ -7405,7 +7881,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -7504,12 +7988,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 8 + + trusted app 8 +
@@ -7524,7 +8010,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Linux + + + Linux + +
- someone + + someone +
@@ -7623,12 +8117,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 9 + + trusted app 9 +
@@ -7643,7 +8139,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Windows + + + Windows + +
- someone + + someone +
@@ -7742,12 +8246,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 10 + + trusted app 10 +
@@ -7762,7 +8268,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -7861,12 +8375,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 11 + + trusted app 11 +
@@ -7881,7 +8397,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Linux + + + Linux + +
- someone + + someone +
@@ -7980,12 +8504,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 12 + + trusted app 12 +
@@ -8000,7 +8526,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Windows + + + Windows + +
- someone + + someone +
@@ -8099,12 +8633,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 13 + + trusted app 13 +
@@ -8119,7 +8655,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -8218,12 +8762,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 14 + + trusted app 14 +
@@ -8238,7 +8784,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Linux + + + Linux + +
- someone + + someone +
@@ -8337,12 +8891,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 15 + + trusted app 15 +
@@ -8357,7 +8913,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Windows + + + Windows + +
- someone + + someone +
@@ -8456,12 +9020,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 16 + + trusted app 16 +
@@ -8476,7 +9042,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -8575,12 +9149,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 17 + + trusted app 17 +
@@ -8595,7 +9171,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Linux + + + Linux + +
- someone + + someone +
@@ -8694,12 +9278,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 18 + + trusted app 18 +
@@ -8714,7 +9300,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Windows + + + Windows + +
- someone + + someone +
@@ -8813,12 +9407,14 @@ exports[`TrustedAppsList renders correctly when loading data for the second time Name
- trusted app 19 + + trusted app 19 +
@@ -8833,7 +9429,13 @@ exports[`TrustedAppsList renders correctly when loading data for the second time
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -9292,12 +9896,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 0 + + trusted app 0 +
@@ -9312,7 +9918,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Windows + + + Windows + +
- someone + + someone +
@@ -9411,12 +10025,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 1 + + trusted app 1 +
@@ -9431,7 +10047,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -9530,12 +10154,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 2 + + trusted app 2 +
@@ -9550,7 +10176,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Linux + + + Linux + +
- someone + + someone +
@@ -9649,12 +10283,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 3 + + trusted app 3 +
@@ -9669,7 +10305,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Windows + + + Windows + +
- someone + + someone +
@@ -9768,12 +10412,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 4 + + trusted app 4 +
@@ -9788,7 +10434,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -9887,12 +10541,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 5 + + trusted app 5 +
@@ -9907,7 +10563,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Linux + + + Linux + +
- someone + + someone +
@@ -10006,12 +10670,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 6 + + trusted app 6 +
@@ -10026,7 +10692,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Windows + + + Windows + +
- someone + + someone +
@@ -10125,12 +10799,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 7 + + trusted app 7 +
@@ -10145,7 +10821,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -10244,12 +10928,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 8 + + trusted app 8 +
@@ -10264,7 +10950,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Linux + + + Linux + +
- someone + + someone +
@@ -10363,12 +11057,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 9 + + trusted app 9 +
@@ -10383,7 +11079,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Windows + + + Windows + +
- someone + + someone +
@@ -10482,12 +11186,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 10 + + trusted app 10 +
@@ -10502,7 +11208,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -10601,12 +11315,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 11 + + trusted app 11 +
@@ -10621,7 +11337,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Linux + + + Linux + +
- someone + + someone +
@@ -10720,12 +11444,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 12 + + trusted app 12 +
@@ -10740,7 +11466,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Windows + + + Windows + +
- someone + + someone +
@@ -10839,12 +11573,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 13 + + trusted app 13 +
@@ -10859,7 +11595,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -10958,12 +11702,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 14 + + trusted app 14 +
@@ -10978,7 +11724,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Linux + + + Linux + +
- someone + + someone +
@@ -11077,12 +11831,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 15 + + trusted app 15 +
@@ -11097,7 +11853,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Windows + + + Windows + +
- someone + + someone +
@@ -11196,12 +11960,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 16 + + trusted app 16 +
@@ -11216,7 +11982,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Mac OS + + + Mac OS + +
- someone + + someone +
@@ -11315,12 +12089,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 17 + + trusted app 17 +
@@ -11335,7 +12111,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Linux + + + Linux + +
- someone + + someone +
@@ -11434,12 +12218,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 18 + + trusted app 18 +
@@ -11454,7 +12240,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Windows + + + Windows + +
- someone + + someone +
@@ -11553,12 +12347,14 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not Name
- trusted app 19 + + trusted app 19 +
@@ -11573,7 +12369,13 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not
- Mac OS + + + Mac OS + +
- someone + + someone +
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/index.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/index.tsx index d5c829bccb90..977db9e1fff2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/index.tsx @@ -27,6 +27,7 @@ import { } from '../../../store/selectors'; import { FormattedDate } from '../../../../../../common/components/formatted_date'; +import { TextFieldValue } from '../../../../../../common/components/text_field_value'; import { useTrustedAppsNavigateCallback, useTrustedAppsSelector } from '../../hooks'; @@ -96,13 +97,27 @@ const getColumnDefinitions = (context: TrustedAppsListContext): ColumnsList => { { field: 'name', name: PROPERTY_TITLES.name, - truncateText: true, + render(value: TrustedApp['name'], record: Immutable) { + return ( + + ); + }, }, { field: 'os', name: PROPERTY_TITLES.os, render(value: TrustedApp['os'], record: Immutable) { - return OS_TITLES[value]; + return ( + + ); }, }, { @@ -121,6 +136,15 @@ const getColumnDefinitions = (context: TrustedAppsListContext): ColumnsList => { { field: 'created_by', name: PROPERTY_TITLES.created_by, + render(value: TrustedApp['created_by'], record: Immutable) { + return ( + + ); + }, }, { name: ACTIONS_COLUMN_TITLE, diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts index b442704169d0..b2f62c2f1da4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts @@ -28,7 +28,9 @@ export const OS_TITLES: Readonly<{ [K in TrustedApp['os']]: string }> = { }), }; -export const CONDITION_FIELD_TITLE: { [K in MacosLinuxConditionEntry['field']]: string } = { +type Entry = MacosLinuxConditionEntry | WindowsConditionEntry; + +export const CONDITION_FIELD_TITLE: { [K in Entry['field']]: string } = { 'process.hash.*': i18n.translate( 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.hash', { defaultMessage: 'Hash' } @@ -37,6 +39,16 @@ export const CONDITION_FIELD_TITLE: { [K in MacosLinuxConditionEntry['field']]: 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.path', { defaultMessage: 'Path' } ), + 'process.code_signature': i18n.translate( + 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.signature', + { defaultMessage: 'Signature' } + ), +}; + +export const OPERATOR_TITLE: { [K in Entry['operator']]: string } = { + included: i18n.translate('xpack.securitySolution.trustedapps.card.operator.includes', { + defaultMessage: 'is', + }), }; export const PROPERTY_TITLES: Readonly< 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 7ae8aecdab60..ac7c5078e4ba 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/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 775329553cbe..dc94b1039dfc 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 8927e492993d..8801e455c95b 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 27fe27adc99c..87b972e9d705 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 57113a139577..a3a5ddf4d53b 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 cb1af5513c84..31ad679ce41b 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 12c3cc481cfc..356173fa2ac7 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 0b864d66842d..c2dc638fa719 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 c68ad2422c51..87968e7a0352 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/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 96ab695e8d33..29aa0b111b78 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -5,10 +5,18 @@ */ import { i18n } from '@kbn/i18n'; -import { Store, Action } from 'redux'; import { BehaviorSubject } from 'rxjs'; import { pluck } from 'rxjs/operators'; +import { + PluginSetup, + PluginStart, + SetupPlugins, + StartPlugins, + StartServices, + AppObservableLibs, + SubPlugins, +} from './types'; import { AppMountParameters, CoreSetup, @@ -21,14 +29,7 @@ import { import { Storage } from '../../../../src/plugins/kibana_utils/public'; import { initTelemetry } from './common/lib/telemetry'; import { KibanaServices } from './common/lib/kibana/services'; -import { - PluginSetup, - PluginStart, - SetupPlugins, - StartPlugins, - StartServices, - AppObservableLibs, -} from './types'; + import { APP_ID, APP_ICON_SOLUTION, @@ -42,9 +43,9 @@ import { APP_PATH, DEFAULT_INDEX_KEY, } from '../common/constants'; + import { ConfigureEndpointPackagePolicy } from './management/pages/policy/view/ingest_manager_integration/configure_package_policy'; -import { State, createStore, createInitialState } from './common/store'; import { SecurityPageName } from './app/types'; import { manageOldSiemRoutes } from './helpers'; import { @@ -60,20 +61,30 @@ import { IndexFieldsStrategyRequest, IndexFieldsStrategyResponse, } from '../common/search_strategy/index_fields'; +import { SecurityAppStore } from './common/store/store'; export class Plugin implements IPlugin { private kibanaVersion: string; - private store!: Store; constructor(initializerContext: PluginInitializerContext) { this.kibanaVersion = initializerContext.env.packageInfo.version; } - public setup(core: CoreSetup, plugins: SetupPlugins): PluginSetup { - const APP_NAME = i18n.translate('xpack.securitySolution.security.title', { - defaultMessage: 'Security', - }); + private storage = new Storage(localStorage); + + /** + * Lazily instantiated subPlugins. + * See `subPlugins` method. + */ + private _subPlugins?: SubPlugins; + + /** + * Lazily instantiated `SecurityAppStore`. + * See `store` method. + */ + private _store?: SecurityAppStore; + public setup(core: CoreSetup, plugins: SetupPlugins): PluginSetup { initTelemetry(plugins.usageCollection, APP_ID); if (plugins.home) { @@ -104,21 +115,22 @@ export class Plugin implements IPlugin { - const storage = new Storage(localStorage); + /** + * `StartServices` which are needed by the `renderApp` function when mounting any of the subPlugin applications. + * This is a promise because these aren't available until the `start` lifecycle phase but they are referenced + * in the `setup` lifecycle phase. + */ + const startServices: Promise = (async () => { const [coreStart, startPlugins] = await core.getStartServices(); - if (this.store == null) { - await this.buildStore(coreStart, startPlugins, storage); - } - const services = { + const services: StartServices = { ...coreStart, ...startPlugins, - storage, + storage: this.storage, security: plugins.security, - } as StartServices; - return { coreStart, startPlugins, services, store: this.store, storage }; - }; + }; + return services; + })(); core.application.register({ exactRoute: true, @@ -141,22 +153,16 @@ export class Plugin implements IPlugin { - const [ - { coreStart, store, services }, - { renderApp, composeLibs }, - { overviewSubPlugin }, - ] = await Promise.all([ - mountSecurityFactory(), - this.downloadAssets(), - this.downloadSubPlugins(), - ]); + const [coreStart, startPlugins] = await core.getStartServices(); + const { overview: subPlugin } = await this.subPlugins(); + const { renderApp, composeLibs } = await this.lazyApplicationDependencies(); return renderApp({ ...composeLibs(coreStart), ...params, - services, - store, - SubPluginRoutes: overviewSubPlugin.start().SubPluginRoutes, + services: await startServices, + store: await this.store(coreStart, startPlugins), + SubPluginRoutes: subPlugin.start().SubPluginRoutes, }); }, }); @@ -169,21 +175,16 @@ export class Plugin implements IPlugin { - const [ - { coreStart, store, services, storage }, - { renderApp, composeLibs }, - { detectionsSubPlugin }, - ] = await Promise.all([ - mountSecurityFactory(), - this.downloadAssets(), - this.downloadSubPlugins(), - ]); + const [coreStart, startPlugins] = await core.getStartServices(); + const { detections: subPlugin } = await this.subPlugins(); + const { renderApp, composeLibs } = await this.lazyApplicationDependencies(); + return renderApp({ ...composeLibs(coreStart), ...params, - services, - store, - SubPluginRoutes: detectionsSubPlugin.start(storage).SubPluginRoutes, + services: await startServices, + store: await this.store(coreStart, startPlugins), + SubPluginRoutes: subPlugin.start(this.storage).SubPluginRoutes, }); }, }); @@ -196,21 +197,15 @@ export class Plugin implements IPlugin { - const [ - { coreStart, store, services, storage }, - { renderApp, composeLibs }, - { hostsSubPlugin }, - ] = await Promise.all([ - mountSecurityFactory(), - this.downloadAssets(), - this.downloadSubPlugins(), - ]); + const [coreStart, startPlugins] = await core.getStartServices(); + const { hosts: subPlugin } = await this.subPlugins(); + const { renderApp, composeLibs } = await this.lazyApplicationDependencies(); return renderApp({ ...composeLibs(coreStart), ...params, - services, - store, - SubPluginRoutes: hostsSubPlugin.start(storage).SubPluginRoutes, + services: await startServices, + store: await this.store(coreStart, startPlugins), + SubPluginRoutes: subPlugin.start(this.storage).SubPluginRoutes, }); }, }); @@ -223,21 +218,15 @@ export class Plugin implements IPlugin { - const [ - { coreStart, store, services, storage }, - { renderApp, composeLibs }, - { networkSubPlugin }, - ] = await Promise.all([ - mountSecurityFactory(), - this.downloadAssets(), - this.downloadSubPlugins(), - ]); + const [coreStart, startPlugins] = await core.getStartServices(); + const { network: subPlugin } = await this.subPlugins(); + const { renderApp, composeLibs } = await this.lazyApplicationDependencies(); return renderApp({ ...composeLibs(coreStart), ...params, - services, - store, - SubPluginRoutes: networkSubPlugin.start(storage).SubPluginRoutes, + services: await startServices, + store: await this.store(coreStart, startPlugins), + SubPluginRoutes: subPlugin.start(this.storage).SubPluginRoutes, }); }, }); @@ -250,21 +239,15 @@ export class Plugin implements IPlugin { - const [ - { coreStart, store, services }, - { renderApp, composeLibs }, - { timelinesSubPlugin }, - ] = await Promise.all([ - mountSecurityFactory(), - this.downloadAssets(), - this.downloadSubPlugins(), - ]); + const [coreStart, startPlugins] = await core.getStartServices(); + const { timelines: subPlugin } = await this.subPlugins(); + const { renderApp, composeLibs } = await this.lazyApplicationDependencies(); return renderApp({ ...composeLibs(coreStart), ...params, - services, - store, - SubPluginRoutes: timelinesSubPlugin.start().SubPluginRoutes, + services: await startServices, + store: await this.store(coreStart, startPlugins), + SubPluginRoutes: subPlugin.start().SubPluginRoutes, }); }, }); @@ -277,21 +260,15 @@ export class Plugin implements IPlugin { - const [ - { coreStart, store, services }, - { renderApp, composeLibs }, - { casesSubPlugin }, - ] = await Promise.all([ - mountSecurityFactory(), - this.downloadAssets(), - this.downloadSubPlugins(), - ]); + const [coreStart, startPlugins] = await core.getStartServices(); + const { cases: subPlugin } = await this.subPlugins(); + const { renderApp, composeLibs } = await this.lazyApplicationDependencies(); return renderApp({ ...composeLibs(coreStart), ...params, - services, - store, - SubPluginRoutes: casesSubPlugin.start().SubPluginRoutes, + services: await startServices, + store: await this.store(coreStart, startPlugins), + SubPluginRoutes: subPlugin.start().SubPluginRoutes, }); }, }); @@ -304,20 +281,14 @@ export class Plugin implements IPlugin { - const [ - { coreStart, startPlugins, store, services }, - { renderApp, composeLibs }, - { managementSubPlugin }, - ] = await Promise.all([ - mountSecurityFactory(), - this.downloadAssets(), - this.downloadSubPlugins(), - ]); + const [coreStart, startPlugins] = await core.getStartServices(); + const { management: managementSubPlugin } = await this.subPlugins(); + const { renderApp, composeLibs } = await this.lazyApplicationDependencies(); return renderApp({ ...composeLibs(coreStart), ...params, - services, - store, + services: await startServices, + store: await this.store(coreStart, startPlugins), SubPluginRoutes: managementSubPlugin.start(coreStart, startPlugins).SubPluginRoutes, }); }, @@ -337,7 +308,13 @@ export class Plugin implements IPlugin { - const { resolverPluginSetup } = await import('./resolver'); + /** + * The specially formatted comment in the `import` expression causes the corresponding webpack chunk to be named. This aids us in debugging chunk size issues. + * See https://webpack.js.org/api/module-methods/#magic-comments + */ + const { resolverPluginSetup } = await import( + /* webpackChunkName: "resolver" */ './resolver' + ); return resolverPluginSetup(); }, }; @@ -359,112 +336,137 @@ export class Plugin implements IPlugin { + if (!this._subPlugins) { + const { subPluginClasses } = await this.lazySubPlugins(); + this._subPlugins = { + detections: new subPluginClasses.Detections(), + cases: new subPluginClasses.Cases(), + hosts: new subPluginClasses.Hosts(), + network: new subPluginClasses.Network(), + overview: new subPluginClasses.Overview(), + timelines: new subPluginClasses.Timelines(), + management: new subPluginClasses.Management(), + }; + } + return this._subPlugins; } - private async buildStore(coreStart: CoreStart, startPlugins: StartPlugins, storage: Storage) { - const defaultIndicesName = coreStart.uiSettings.get(DEFAULT_INDEX_KEY); - const [ - { composeLibs }, - kibanaIndexPatterns, - { - detectionsSubPlugin, - hostsSubPlugin, - networkSubPlugin, - timelinesSubPlugin, - managementSubPlugin, - }, - configIndexPatterns, - ] = await Promise.all([ - this.downloadAssets(), - startPlugins.data.indexPatterns.getIdsWithTitle(), - this.downloadSubPlugins(), - startPlugins.data.search - .search( - { indices: defaultIndicesName, onlyCheckIfIndicesExist: false }, - { - strategy: 'securitySolutionIndexFields', - } - ) - .toPromise(), - ]); - - const { apolloClient } = composeLibs(coreStart); - const appLibs: AppObservableLibs = { apolloClient, kibana: coreStart }; - const libs$ = new BehaviorSubject(appLibs); - - const detectionsStart = detectionsSubPlugin.start(storage); - const hostsStart = hostsSubPlugin.start(storage); - const networkStart = networkSubPlugin.start(storage); - const timelinesStart = timelinesSubPlugin.start(); - const managementSubPluginStart = managementSubPlugin.start(coreStart, startPlugins); - - const timelineInitialState = { - timeline: { - ...timelinesStart.store.initialState.timeline!, - timelineById: { - ...timelinesStart.store.initialState.timeline!.timelineById, - ...detectionsStart.storageTimelines!.timelineById, - ...hostsStart.storageTimelines!.timelineById, - ...networkStart.storageTimelines!.timelineById, + /** + * Lazily instantiate a `SecurityAppStore`. We lazily instantiate this because it requests large dynamic imports. We instantiate it once because each subPlugin needs to share the same reference. + */ + private async store(coreStart: CoreStart, startPlugins: StartPlugins): Promise { + if (!this._store) { + const defaultIndicesName = coreStart.uiSettings.get(DEFAULT_INDEX_KEY); + const [ + { composeLibs, createStore, createInitialState }, + kibanaIndexPatterns, + { + detections: detectionsSubPlugin, + hosts: hostsSubPlugin, + network: networkSubPlugin, + timelines: timelinesSubPlugin, + management: managementSubPlugin, }, - }, - }; + configIndexPatterns, + ] = await Promise.all([ + this.lazyApplicationDependencies(), + startPlugins.data.indexPatterns.getIdsWithTitle(), + this.subPlugins(), + startPlugins.data.search + .search( + { indices: defaultIndicesName, onlyCheckIfIndicesExist: false }, + { + strategy: 'securitySolutionIndexFields', + } + ) + .toPromise(), + ]); + + const { apolloClient } = composeLibs(coreStart); + const appLibs: AppObservableLibs = { apolloClient, kibana: coreStart }; + const libs$ = new BehaviorSubject(appLibs); + + const detectionsStart = detectionsSubPlugin.start(this.storage); + const hostsStart = hostsSubPlugin.start(this.storage); + const networkStart = networkSubPlugin.start(this.storage); + const timelinesStart = timelinesSubPlugin.start(); + const managementSubPluginStart = managementSubPlugin.start(coreStart, startPlugins); + + const timelineInitialState = { + timeline: { + ...timelinesStart.store.initialState.timeline!, + timelineById: { + ...timelinesStart.store.initialState.timeline!.timelineById, + ...detectionsStart.storageTimelines!.timelineById, + ...hostsStart.storageTimelines!.timelineById, + ...networkStart.storageTimelines!.timelineById, + }, + }, + }; - this.store = createStore( - createInitialState( + this._store = createStore( + createInitialState( + { + ...hostsStart.store.initialState, + ...networkStart.store.initialState, + ...timelineInitialState, + ...managementSubPluginStart.store.initialState, + }, + { + kibanaIndexPatterns, + configIndexPatterns: configIndexPatterns.indicesExist, + } + ), { - ...hostsStart.store.initialState, - ...networkStart.store.initialState, - ...timelineInitialState, - ...managementSubPluginStart.store.initialState, + ...hostsStart.store.reducer, + ...networkStart.store.reducer, + ...timelinesStart.store.reducer, + ...managementSubPluginStart.store.reducer, }, - { - kibanaIndexPatterns, - configIndexPatterns: configIndexPatterns.indicesExist, - } - ), - { - ...hostsStart.store.reducer, - ...networkStart.store.reducer, - ...timelinesStart.store.reducer, - ...managementSubPluginStart.store.reducer, - }, - libs$.pipe(pluck('apolloClient')), - libs$.pipe(pluck('kibana')), - storage, - [...(managementSubPluginStart.store.middleware ?? [])] - ); + libs$.pipe(pluck('apolloClient')), + libs$.pipe(pluck('kibana')), + this.storage, + [...(managementSubPluginStart.store.middleware ?? [])] + ); + } + return this._store; } } + +const APP_NAME = i18n.translate('xpack.securitySolution.security.title', { + defaultMessage: 'Security', +}); 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 5555578e44f7..d121b9c9c81c 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 3cd0c910f46f..ff8119a5e25f 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 fc0d646fd62c..b77a5d09008c 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 f0880fa635a2..0003be827aca 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 3348c962efde..66a32ba29cd7 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 5eb920ca835f..505e6cfc3ee7 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 f121b2aa8688..000000000000 --- 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 ae1e9a58a209..997a3d0ae6b3 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 ea603f258343..2a399b6844bd 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 5007b7cffa5c..fb57f85639e3 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 dba1136193ee..c781832dc8a3 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/panel.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx index 0664608d73c2..9d72af310956 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import { createMemoryHistory, History as HistoryPackageHistoryInterface } from 'history'; -import copy from 'copy-to-clipboard'; import { noAncestorsTwoChildrenWithRelatedEventsOnOrigin } from '../data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin'; import { Simulator } from '../test_utilities/simulator'; // Extend jest with a custom matcher @@ -14,10 +13,6 @@ import { urlSearch } from '../test_utilities/url_search'; // the resolver component instance ID, used by the react code to distinguish piece of global state from those used by other resolver instances const resolverComponentInstanceID = 'resolverComponentInstanceID'; -jest.mock('copy-to-clipboard', () => { - return jest.fn(); -}); - describe(`Resolver: when analyzing a tree with no ancestors and two children and two related registry event on the origin, and when the component instance ID is ${resolverComponentInstanceID}`, () => { /** * Get (or lazily create and get) the simulator. @@ -121,8 +116,8 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and copyableFields?.map((copyableField) => { copyableField.simulate('mouseenter'); - simulator().testSubject('clipboard').last().simulate('click'); - expect(copy).toHaveBeenLastCalledWith(copyableField.text(), expect.any(Object)); + simulator().testSubject('resolver:panel:clipboard').last().simulate('click'); + expect(navigator.clipboard.writeText).toHaveBeenCalledWith(copyableField.text()); copyableField.simulate('mouseleave'); }); }); @@ -179,8 +174,8 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and copyableFields?.map((copyableField) => { copyableField.simulate('mouseenter'); - simulator().testSubject('clipboard').last().simulate('click'); - expect(copy).toHaveBeenLastCalledWith(copyableField.text(), expect.any(Object)); + simulator().testSubject('resolver:panel:clipboard').last().simulate('click'); + expect(navigator.clipboard.writeText).toHaveBeenCalledWith(copyableField.text()); copyableField.simulate('mouseleave'); }); }); @@ -288,8 +283,8 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and copyableFields?.map((copyableField) => { copyableField.simulate('mouseenter'); - simulator().testSubject('clipboard').last().simulate('click'); - expect(copy).toHaveBeenLastCalledWith(copyableField.text(), expect.any(Object)); + simulator().testSubject('resolver:panel:clipboard').last().simulate('click'); + expect(navigator.clipboard.writeText).toHaveBeenCalledWith(copyableField.text()); copyableField.simulate('mouseleave'); }); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/copyable_panel_field.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/copyable_panel_field.tsx index c3474a7724de..f6a585ea566b 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/copyable_panel_field.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/copyable_panel_field.tsx @@ -6,11 +6,11 @@ /* eslint-disable react/display-name */ -import { EuiToolTip, EuiPopover } from '@elastic/eui'; +import { EuiToolTip, EuiButtonIcon, EuiPopover } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; -import React, { memo, useState } from 'react'; -import { WithCopyToClipboard } from '../../../common/lib/clipboard/with_copy_to_clipboard'; +import React, { memo, useState, useCallback } from 'react'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useColors } from '../use_colors'; import { StyledPanel } from '../styles'; @@ -43,8 +43,10 @@ export const CopyablePanelField = memo( ({ textToCopy, content }: { textToCopy: string; content: JSX.Element | string }) => { const { linkColor, copyableFieldBackground } = useColors(); const [isOpen, setIsOpen] = useState(false); + const toasts = useKibana().services.notifications?.toasts; const onMouseEnter = () => setIsOpen(true); + const onMouseLeave = () => setIsOpen(false); const ButtonContent = memo(() => ( )); - const onMouseLeave = () => setIsOpen(false); + const onClick = useCallback( + async (event: React.MouseEvent) => { + try { + await navigator.clipboard.writeText(textToCopy); + } catch (error) { + if (toasts) { + toasts.addError(error, { + title: i18n.translate('xpack.securitySolution.resolver.panel.copyFailureTitle', { + defaultMessage: 'Copy Failure', + }), + }); + } + } + }, + [textToCopy, toasts] + ); return (
@@ -74,10 +91,14 @@ export const CopyablePanelField = memo( defaultMessage: 'Copy to Clipboard', })} > - 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 06e3acfb3dc6..9ef72c414bb6 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 f40f423359f5..d93b46dcb062 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 @@ -382,7 +382,7 @@ const UnstyledProcessEventDot = React.memo( />
= 2 ? 'euiButton' : 'euiButton euiButton--small'} + className={'euiButton euiButton--small'} id={labelHTMLID} onClick={handleClick} onFocus={handleFocus} diff --git a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx index 13dcfcabe50c..ed969b913a72 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/resolver_without_providers.tsx @@ -106,7 +106,7 @@ export const ResolverWithoutProviders = React.memo( ({ points: [startPosition, endPosition], metadata }) => ( SideEffectSimulator = () => { return contentRectForElement(this); }); + /** + * Mock the global writeText method as it is not available in jsDOM and alows us to track what was copied + */ + const MockClipboard: Clipboard = { + writeText: jest.fn(), + readText: jest.fn(), + addEventListener: jest.fn(), + dispatchEvent: jest.fn(), + removeEventListener: jest.fn(), + }; + // @ts-ignore navigator doesn't natively exist on global + global.navigator.clipboard = MockClipboard; /** * A mock implementation of `ResizeObserver` that works with our fake `getBoundingClientRect` and `simulateElementResize` */ diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx index 3d275a961bb2..bf72a52559cb 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx @@ -21,6 +21,7 @@ import { ResolverAction } from '../store/actions'; import { createStore } from 'redux'; import { resolverReducer } from '../store/reducer'; import { mockTreeFetcherParameters } from '../mocks/tree_fetcher_parameters'; +import { entityIDSafeVersion } from '../../../common/endpoint/models/event'; describe('useCamera on an unpainted element', () => { 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/sub_plugins.ts b/x-pack/plugins/security_solution/public/sub_plugins.ts deleted file mode 100644 index 5e7c5e8242fd..000000000000 --- a/x-pack/plugins/security_solution/public/sub_plugins.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 { Detections } from './detections'; -import { Cases } from './cases'; -import { Hosts } from './hosts'; -import { Network } from './network'; -import { Overview } from './overview'; -import { Timelines } from './timelines'; -import { Management } from './management'; - -const detectionsSubPlugin = new Detections(); -const casesSubPlugin = new Cases(); -const hostsSubPlugin = new Hosts(); -const networkSubPlugin = new Network(); -const overviewSubPlugin = new Overview(); -const timelinesSubPlugin = new Timelines(); -const managementSubPlugin = new Management(); - -export { - detectionsSubPlugin, - casesSubPlugin, - hostsSubPlugin, - networkSubPlugin, - overviewSubPlugin, - timelinesSubPlugin, - managementSubPlugin, -}; 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 1f76c2840e8b..cb913287b24d 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 7b229b3fbb17..c3247c337ac3 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 c7cd9253de03..58e704512818 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/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index fc0bcb134158..83b8b119faae 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 @@ -68,11 +68,10 @@ export interface BodyProps { updateNote: UpdateNote; } -export const hasAdditionalActions = (id: string, eventType?: TimelineEventsType): boolean => - id === TimelineId.detectionsPage || - id === TimelineId.detectionsRulesDetailsPage || - ((id === TimelineId.active && eventType && ['all', 'signal', 'alert'].includes(eventType)) ?? - false); +export const hasAdditionalActions = (id: TimelineId): boolean => + [TimelineId.detectionsPage, TimelineId.detectionsRulesDetailsPage, TimelineId.active].includes( + id + ); const EXTRA_WIDTH = 4; // px @@ -86,7 +85,6 @@ export const Body = React.memo( data, docValueFields, eventIdToNoteIds, - eventType, getNotesByIds, graphEventId, isEventViewer = false, @@ -118,9 +116,11 @@ export const Body = React.memo( getActionsColumnWidth( isEventViewer, showCheckboxes, - hasAdditionalActions(timelineId, eventType) ? DEFAULT_ICON_BUTTON_WIDTH + EXTRA_WIDTH : 0 + hasAdditionalActions(timelineId as TimelineId) + ? DEFAULT_ICON_BUTTON_WIDTH + EXTRA_WIDTH + : 0 ), - [isEventViewer, showCheckboxes, timelineId, eventType] + [isEventViewer, showCheckboxes, timelineId] ); const columnWidths = useMemo( @@ -134,6 +134,7 @@ export const Body = React.memo( {graphEventId && ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx index ef5689f494cd..dfd646353c27 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx @@ -61,7 +61,6 @@ const StatefulBodyComponent = React.memo( data, docValueFields, eventIdToNoteIds, - eventType, excludedRowRendererIds, id, isEventViewer = false, @@ -197,7 +196,6 @@ const StatefulBodyComponent = React.memo( data={data} docValueFields={docValueFields} eventIdToNoteIds={eventIdToNoteIds} - eventType={eventType} getNotesByIds={getNotesByIds} graphEventId={graphEventId} isEventViewer={isEventViewer} @@ -232,7 +230,6 @@ const StatefulBodyComponent = React.memo( deepEqual(prevProps.excludedRowRendererIds, nextProps.excludedRowRendererIds) && deepEqual(prevProps.docValueFields, nextProps.docValueFields) && prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds && - prevProps.eventType === nextProps.eventType && prevProps.graphEventId === nextProps.graphEventId && deepEqual(prevProps.notesById, nextProps.notesById) && prevProps.id === nextProps.id && @@ -262,7 +259,6 @@ const makeMapStateToProps = () => { const { columns, eventIdToNoteIds, - eventType, excludedRowRendererIds, graphEventId, isSelectAllChecked, @@ -277,7 +273,6 @@ const makeMapStateToProps = () => { return { columnHeaders: memoizedColumnHeaders(columns, browserFields), eventIdToNoteIds, - eventType, excludedRowRendererIds, graphEventId, isSelectAllChecked, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx index 3523e8c0d7aa..0cd7032596f1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/add_data_provider_popover.tsx @@ -199,6 +199,7 @@ const AddDataProviderPopoverComponent: React.FC = ( withTitle panelPaddingSize="none" ownFocus={true} + repositionOnScroll > {content} 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 87956647c11f..36116de8d33d 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 74f21fecd0fd..3b882c1e1bd1 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/public/timelines/components/timeline/search_or_filter/pick_events.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx index 16200f4e5ef9..d7d8d810f697 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx @@ -335,6 +335,7 @@ const PickEventTypeComponents: React.FC = ({ button={button} isOpen={isPopoverOpen} closePopover={closePopover} + repositionOnScroll > diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index b55b33fce1dc..80cc014285ae 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -3,8 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { CoreStart } from '../../../../src/core/public'; +import { AppFrontendLibs } from './common/lib/lib'; +import { CoreStart } from '../../../../src/core/public'; import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; @@ -20,11 +21,18 @@ import { TriggersAndActionsUIPublicPluginStart as TriggersActionsStart, } from '../../triggers_actions_ui/public'; import { SecurityPluginSetup } from '../../security/public'; -import { AppFrontendLibs } from './common/lib/lib'; import { ResolverPluginSetup } from './resolver/types'; import { Inspect } from '../common/search_strategy'; import { MlPluginSetup, MlPluginStart } from '../../ml/public'; +import { Detections } from './detections'; +import { Cases } from './cases'; +import { Hosts } from './hosts'; +import { Network } from './network'; +import { Overview } from './overview'; +import { Timelines } from './timelines'; +import { Management } from './management'; + export interface SetupPlugins { home?: HomePublicPluginSetup; security: SecurityPluginSetup; @@ -62,3 +70,13 @@ export interface AppObservableLibs extends AppFrontendLibs { } export type InspectResponse = Inspect & { response: string[] }; + +export interface SubPlugins { + detections: Detections; + cases: Cases; + hosts: Hosts; + network: Network; + overview: Overview; + timelines: Timelines; + management: Management; +} 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 cc371f9120ba..c8e0292e8d93 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 cb79263ef6b3..ac1de377124f 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 0b166e097af9..7980fc83358b 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 899fe4b880ac..ca65d18bb9f6 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 df4c37726246..f1614cc19e8c 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 8d4524e06c49..7dddc357fe53 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 fd685efb94aa..f3a7b08a4cd4 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 f05d9ef5b821..40a691c1ddbd 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 1b3d232f9421..0019c97a6cce 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/graphql/hosts/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/hosts/schema.gql.ts index 48bb0cbe37af..4623fa6514e7 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 7730cea2b984..bda0fed494a6 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 000000000000..473a2dad37f1 --- /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 a09fd9e0c9bd..a801bc18db43 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 7debe0931abd..b9ae8b546b8b 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/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 09ddfb342496..037f91240edf 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 eeace508c9bf..8be76dc8caf0 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 f600463c213c..8a689f455c31 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 3c3f5b544bb1..3147eb170516 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 06c9c4c13c5f..0078cf1b3c64 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 d1c8290b3462..099160b7e4d6 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 ff2796e6852d..36244ecbff72 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 97aa68c0f9bb..e9dcee35005d 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 10dcb7ee7e74..00769b75a8ce 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 acee75abddcd..88ce963757f6 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 000000000000..482734c73a25 --- /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 f7d9f408c5e2..1aba6660677c 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/index_fields/index.ts b/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.ts index da29cae0eebe..bc461f3885a7 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { from } from 'rxjs'; import isEmpty from 'lodash/isEmpty'; import { IndexPatternsFetcher, ISearchStrategy } from '../../../../../../src/plugins/data/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -25,60 +26,63 @@ export const securitySolutionIndexFieldsProvider = (): ISearchStrategy< const beatFields: BeatFields = require('../../utils/beat_schema/fields').fieldsBeat; return { - search: async (context, request) => { - const { elasticsearch } = context.core; - const indexPatternsFetcher = new IndexPatternsFetcher( - elasticsearch.legacy.client.callAsCurrentUser - ); - const dedupeIndices = dedupeIndexName(request.indices); + search: (request, options, context) => + from( + new Promise(async (resolve) => { + const { elasticsearch } = context.core; + const indexPatternsFetcher = new IndexPatternsFetcher( + elasticsearch.legacy.client.callAsCurrentUser + ); + const dedupeIndices = dedupeIndexName(request.indices); - const responsesIndexFields = await Promise.all( - dedupeIndices - .map((index) => - indexPatternsFetcher.getFieldsForWildcard({ - pattern: index, - }) - ) - .map((p) => p.catch((e) => false)) - ); - let indexFields: IndexField[] = []; + const responsesIndexFields = await Promise.all( + dedupeIndices + .map((index) => + indexPatternsFetcher.getFieldsForWildcard({ + pattern: index, + }) + ) + .map((p) => p.catch((e) => false)) + ); + let indexFields: IndexField[] = []; - if (!request.onlyCheckIfIndicesExist) { - indexFields = await formatIndexFields( - beatFields, - responsesIndexFields.filter((rif) => rif !== false) as FieldDescriptor[][], - dedupeIndices - ); - } + if (!request.onlyCheckIfIndicesExist) { + indexFields = await formatIndexFields( + beatFields, + responsesIndexFields.filter((rif) => rif !== false) as FieldDescriptor[][], + dedupeIndices + ); + } - return Promise.resolve({ - indexFields, - indicesExist: dedupeIndices.filter((index, i) => responsesIndexFields[i] !== false), - rawResponse: { - timed_out: false, - took: -1, - _shards: { - total: -1, - successful: -1, - failed: -1, - skipped: -1, - }, - hits: { - total: -1, - max_score: -1, - hits: [ - { - _index: '', - _type: '', - _id: '', - _score: -1, - _source: null, + return resolve({ + indexFields, + indicesExist: dedupeIndices.filter((index, i) => responsesIndexFields[i] !== false), + rawResponse: { + timed_out: false, + took: -1, + _shards: { + total: -1, + successful: -1, + failed: -1, + skipped: -1, }, - ], - }, - }, - }); - }, + hits: { + total: -1, + max_score: -1, + hits: [ + { + _index: '', + _type: '', + _id: '', + _score: -1, + _source: null, + }, + ], + }, + }, + }); + }) + ), }; }; 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 b06c36fd24e1..55b54c897521 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 ce8900a57810..e1924d6c2794 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 644278963742..36cf025304e7 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 20b3f5b05bc8..7d9351993bc8 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 b1470b17eea5..3e4070a28a9f 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 720661e12bd9..0bf99aeea8a2 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/security_solution/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts index d94a32174cd7..962865880df5 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { mergeMap } from 'rxjs/operators'; import { ISearchStrategy, PluginStart } from '../../../../../../src/plugins/data/server'; import { FactoryQueryTypes, @@ -19,15 +20,16 @@ export const securitySolutionSearchStrategyProvider = { + search: (request, options, context) => { if (request.factoryQueryType == null) { throw new Error('factoryQueryType is required'); } const queryFactory: SecuritySolutionFactory = securitySolutionFactory[request.factoryQueryType]; const dsl = queryFactory.buildDsl(request); - const esSearchRes = await es.search(context, { ...request, params: dsl }, options); - return queryFactory.parse(request, esSearchRes); + return es + .search({ ...request, params: dsl }, options, context) + .pipe(mergeMap((esSearchRes) => queryFactory.parse(request, esSearchRes))); }, cancel: async (context, id) => { if (es.cancel) { 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 b2e3989f99d4..8e2bfb542661 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/security_solution/server/search_strategy/timeline/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts index 6d8505211123..165f0f586ebd 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { mergeMap } from 'rxjs/operators'; import { ISearchStrategy, PluginStart } from '../../../../../../src/plugins/data/server'; import { TimelineFactoryQueryTypes, @@ -19,15 +20,17 @@ export const securitySolutionTimelineSearchStrategyProvider = { + search: (request, options, context) => { if (request.factoryQueryType == null) { throw new Error('factoryQueryType is required'); } const queryFactory: SecuritySolutionTimelineFactory = securitySolutionTimelineFactory[request.factoryQueryType]; const dsl = queryFactory.buildDsl(request); - const esSearchRes = await es.search(context, { ...request, params: dsl }, options); - return queryFactory.parse(request, esSearchRes); + + return es + .search({ ...request, params: dsl }, options, context) + .pipe(mergeMap((esSearchRes) => queryFactory.parse(request, esSearchRes))); }, cancel: async (context, id) => { if (es.cancel) { diff --git a/x-pack/plugins/security_solution/server/usage/collector.ts b/x-pack/plugins/security_solution/server/usage/collector.ts index 9514233bdfa8..a2e34229f7d7 100644 --- a/x-pack/plugins/security_solution/server/usage/collector.ts +++ b/x-pack/plugins/security_solution/server/usage/collector.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyAPICaller, CoreSetup } from '../../../../../src/core/server'; +import { CoreSetup } from '../../../../../src/core/server'; +import { CollectorFetchContext } from '../../../../../src/plugins/usage_collection/server'; import { CollectorDependencies } from './types'; import { DetectionsUsage, fetchDetectionsUsage, defaultDetectionsUsage } from './detections'; import { EndpointUsage, getEndpointTelemetryFromFleet } from './endpoints'; @@ -77,7 +78,7 @@ export const registerCollector: RegisterCollector = ({ }, }, isReady: () => kibanaIndex.length > 0, - fetch: async (callCluster: LegacyAPICaller): Promise => { + fetch: async ({ callCluster }: CollectorFetchContext): Promise => { const savedObjectsClient = await getInternalSavedObjectsClient(core); const [detections, endpoints] = await Promise.allSettled([ fetchDetectionsUsage(kibanaIndex, callCluster, ml), diff --git a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts index fddd7f92b7f2..864c91c583e8 100644 --- a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts +++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts @@ -10,6 +10,7 @@ import { PluginsSetup } from '../plugin'; import { KibanaFeature } from '../../../features/server'; import { ILicense, LicensingPluginSetup } from '../../../licensing/server'; import { pluginInitializerContextConfigMock } from 'src/core/server/mocks'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; interface SetupOpts { license?: Partial; @@ -67,6 +68,13 @@ const defaultCallClusterMock = jest.fn().mockResolvedValue({ }, }); +const getMockFetchContext = (mockedCallCluster: jest.Mock) => { + return { + ...createCollectorFetchContextMock(), + callCluster: mockedCallCluster, + }; +}; + describe('error handling', () => { it('handles a 404 when searching for space usage', async () => { const { features, licensing, usageCollecion } = setup({ @@ -78,7 +86,7 @@ describe('error handling', () => { licensing, }); - await getSpacesUsage(jest.fn().mockRejectedValue({ status: 404 })); + await getSpacesUsage(getMockFetchContext(jest.fn().mockRejectedValue({ status: 404 }))); }); it('throws error for a non-404', async () => { @@ -94,7 +102,9 @@ describe('error handling', () => { const statusCodes = [401, 402, 403, 500]; for (const statusCode of statusCodes) { const error = { status: statusCode }; - await expect(getSpacesUsage(jest.fn().mockRejectedValue(error))).rejects.toBe(error); + await expect( + getSpacesUsage(getMockFetchContext(jest.fn().mockRejectedValue(error))) + ).rejects.toBe(error); } }); }); @@ -110,7 +120,7 @@ describe('with a basic license', () => { features, licensing, }); - usageStats = await getSpacesUsage(defaultCallClusterMock); + usageStats = await getSpacesUsage(getMockFetchContext(defaultCallClusterMock)); expect(defaultCallClusterMock).toHaveBeenCalledWith('search', { body: { @@ -158,7 +168,7 @@ describe('with no license', () => { features, licensing, }); - usageStats = await getSpacesUsage(defaultCallClusterMock); + usageStats = await getSpacesUsage(getMockFetchContext(defaultCallClusterMock)); }); test('sets enabled to false', () => { @@ -189,7 +199,7 @@ describe('with platinum license', () => { features, licensing, }); - usageStats = await getSpacesUsage(defaultCallClusterMock); + usageStats = await getSpacesUsage(getMockFetchContext(defaultCallClusterMock)); }); test('sets enabled to true', () => { diff --git a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts index 36d46c3d01ba..0e31c930a926 100644 --- a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts +++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts @@ -6,7 +6,7 @@ import { LegacyCallAPIOptions } from 'src/core/server'; import { take } from 'rxjs/operators'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { CollectorFetchContext, UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { Observable } from 'rxjs'; import { KIBANA_STATS_TYPE_MONITORING } from '../../../monitoring/common/constants'; import { PluginsSetup } from '../plugin'; @@ -188,7 +188,7 @@ export function getSpacesUsageCollector( enabled: { type: 'boolean' }, count: { type: 'long' }, }, - fetch: async (callCluster: CallCluster) => { + fetch: async ({ callCluster }: CollectorFetchContext) => { const license = await deps.licensing.license$.pipe(take(1)).toPromise(); const available = license.isAvailable; // some form of spaces is available for all valid licenses diff --git a/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts new file mode 100644 index 000000000000..443c81146900 --- /dev/null +++ b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import sinon from 'sinon'; +import { mockLogger } from '../test_utils'; +import { TaskManager } from '../task_manager'; +import { savedObjectsRepositoryMock } from '../../../../../src/core/server/mocks'; +import { + SavedObjectsSerializer, + SavedObjectTypeRegistry, + SavedObjectsErrorHelpers, +} from '../../../../../src/core/server'; +import { ADJUST_THROUGHPUT_INTERVAL } from '../lib/create_managed_configuration'; + +describe('managed configuration', () => { + let taskManager: TaskManager; + let clock: sinon.SinonFakeTimers; + const callAsInternalUser = jest.fn(); + const logger = mockLogger(); + const serializer = new SavedObjectsSerializer(new SavedObjectTypeRegistry()); + const savedObjectsClient = savedObjectsRepositoryMock.create(); + const config = { + enabled: true, + max_workers: 10, + index: 'foo', + max_attempts: 9, + poll_interval: 3000, + max_poll_inactivity_cycles: 10, + request_capacity: 1000, + }; + + beforeEach(() => { + jest.resetAllMocks(); + callAsInternalUser.mockResolvedValue({ total: 0, updated: 0, version_conflicts: 0 }); + clock = sinon.useFakeTimers(); + taskManager = new TaskManager({ + config, + logger, + serializer, + callAsInternalUser, + taskManagerId: 'some-uuid', + savedObjectsRepository: savedObjectsClient, + }); + taskManager.registerTaskDefinitions({ + foo: { + type: 'foo', + title: 'Foo', + createTaskRunner: jest.fn(), + }, + }); + taskManager.start(); + // force rxjs timers to fire when they are scheduled for setTimeout(0) as the + // sinon fake timers cause them to stall + clock.tick(0); + }); + + afterEach(() => clock.restore()); + + test('should lower max workers when Elasticsearch returns 429 error', async () => { + savedObjectsClient.create.mockRejectedValueOnce( + SavedObjectsErrorHelpers.createTooManyRequestsError('a', 'b') + ); + // Cause "too many requests" error to be thrown + await expect( + taskManager.schedule({ + taskType: 'foo', + state: {}, + params: {}, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Too Many Requests"`); + clock.tick(ADJUST_THROUGHPUT_INTERVAL); + expect(logger.warn).toHaveBeenCalledWith( + 'Max workers configuration is temporarily reduced after Elasticsearch returned 1 "too many request" error(s).' + ); + expect(logger.debug).toHaveBeenCalledWith( + 'Max workers configuration changing from 10 to 8 after seeing 1 error(s)' + ); + expect(logger.debug).toHaveBeenCalledWith('Task pool now using 10 as the max worker value'); + }); + + test('should increase poll interval when Elasticsearch returns 429 error', async () => { + savedObjectsClient.create.mockRejectedValueOnce( + SavedObjectsErrorHelpers.createTooManyRequestsError('a', 'b') + ); + // Cause "too many requests" error to be thrown + await expect( + taskManager.schedule({ + taskType: 'foo', + state: {}, + params: {}, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Too Many Requests"`); + clock.tick(ADJUST_THROUGHPUT_INTERVAL); + expect(logger.warn).toHaveBeenCalledWith( + 'Poll interval configuration is temporarily increased after Elasticsearch returned 1 "too many request" error(s).' + ); + expect(logger.debug).toHaveBeenCalledWith( + 'Poll interval configuration changing from 3000 to 3600 after seeing 1 error(s)' + ); + expect(logger.debug).toHaveBeenCalledWith('Task poller now using interval of 3600ms'); + }); +}); diff --git a/x-pack/plugins/task_manager/server/lib/create_managed_configuration.test.ts b/x-pack/plugins/task_manager/server/lib/create_managed_configuration.test.ts new file mode 100644 index 000000000000..b6b5cd003c5d --- /dev/null +++ b/x-pack/plugins/task_manager/server/lib/create_managed_configuration.test.ts @@ -0,0 +1,213 @@ +/* + * 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 sinon from 'sinon'; +import { Subject } from 'rxjs'; +import { mockLogger } from '../test_utils'; +import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; +import { + createManagedConfiguration, + ADJUST_THROUGHPUT_INTERVAL, +} from './create_managed_configuration'; + +describe('createManagedConfiguration()', () => { + let clock: sinon.SinonFakeTimers; + const logger = mockLogger(); + + beforeEach(() => { + jest.resetAllMocks(); + clock = sinon.useFakeTimers(); + }); + + afterEach(() => clock.restore()); + + test('returns observables with initialized values', async () => { + const maxWorkersSubscription = jest.fn(); + const pollIntervalSubscription = jest.fn(); + const { maxWorkersConfiguration$, pollIntervalConfiguration$ } = createManagedConfiguration({ + logger, + errors$: new Subject(), + startingMaxWorkers: 1, + startingPollInterval: 2, + }); + maxWorkersConfiguration$.subscribe(maxWorkersSubscription); + pollIntervalConfiguration$.subscribe(pollIntervalSubscription); + expect(maxWorkersSubscription).toHaveBeenCalledTimes(1); + expect(maxWorkersSubscription).toHaveBeenNthCalledWith(1, 1); + expect(pollIntervalSubscription).toHaveBeenCalledTimes(1); + expect(pollIntervalSubscription).toHaveBeenNthCalledWith(1, 2); + }); + + test(`skips errors that aren't about too many requests`, async () => { + const maxWorkersSubscription = jest.fn(); + const pollIntervalSubscription = jest.fn(); + const errors$ = new Subject(); + const { maxWorkersConfiguration$, pollIntervalConfiguration$ } = createManagedConfiguration({ + errors$, + logger, + startingMaxWorkers: 100, + startingPollInterval: 100, + }); + maxWorkersConfiguration$.subscribe(maxWorkersSubscription); + pollIntervalConfiguration$.subscribe(pollIntervalSubscription); + errors$.next(new Error('foo')); + clock.tick(ADJUST_THROUGHPUT_INTERVAL); + expect(maxWorkersSubscription).toHaveBeenCalledTimes(1); + expect(pollIntervalSubscription).toHaveBeenCalledTimes(1); + }); + + describe('maxWorker configuration', () => { + function setupScenario(startingMaxWorkers: number) { + const errors$ = new Subject(); + const subscription = jest.fn(); + const { maxWorkersConfiguration$ } = createManagedConfiguration({ + errors$, + startingMaxWorkers, + logger, + startingPollInterval: 1, + }); + maxWorkersConfiguration$.subscribe(subscription); + return { subscription, errors$ }; + } + + beforeEach(() => { + jest.resetAllMocks(); + clock = sinon.useFakeTimers(); + }); + + afterEach(() => clock.restore()); + + test('should decrease configuration at the next interval when an error is emitted', async () => { + const { subscription, errors$ } = setupScenario(100); + errors$.next(SavedObjectsErrorHelpers.createTooManyRequestsError('a', 'b')); + clock.tick(ADJUST_THROUGHPUT_INTERVAL - 1); + expect(subscription).toHaveBeenCalledTimes(1); + clock.tick(1); + expect(subscription).toHaveBeenCalledTimes(2); + expect(subscription).toHaveBeenNthCalledWith(2, 80); + }); + + test('should log a warning when the configuration changes from the starting value', async () => { + const { errors$ } = setupScenario(100); + errors$.next(SavedObjectsErrorHelpers.createTooManyRequestsError('a', 'b')); + clock.tick(ADJUST_THROUGHPUT_INTERVAL); + expect(logger.warn).toHaveBeenCalledWith( + 'Max workers configuration is temporarily reduced after Elasticsearch returned 1 "too many request" error(s).' + ); + }); + + test('should increase configuration back to normal incrementally after an error is emitted', async () => { + const { subscription, errors$ } = setupScenario(100); + errors$.next(SavedObjectsErrorHelpers.createTooManyRequestsError('a', 'b')); + clock.tick(ADJUST_THROUGHPUT_INTERVAL * 10); + expect(subscription).toHaveBeenNthCalledWith(2, 80); + expect(subscription).toHaveBeenNthCalledWith(3, 84); + // 88.2- > 89 from Math.ceil + expect(subscription).toHaveBeenNthCalledWith(4, 89); + expect(subscription).toHaveBeenNthCalledWith(5, 94); + expect(subscription).toHaveBeenNthCalledWith(6, 99); + // 103.95 -> 100 from Math.min with starting value + expect(subscription).toHaveBeenNthCalledWith(7, 100); + // No new calls due to value not changing and usage of distinctUntilChanged() + expect(subscription).toHaveBeenCalledTimes(7); + }); + + test('should keep reducing configuration when errors keep emitting', async () => { + const { subscription, errors$ } = setupScenario(100); + for (let i = 0; i < 20; i++) { + errors$.next(SavedObjectsErrorHelpers.createTooManyRequestsError('a', 'b')); + clock.tick(ADJUST_THROUGHPUT_INTERVAL); + } + expect(subscription).toHaveBeenNthCalledWith(2, 80); + expect(subscription).toHaveBeenNthCalledWith(3, 64); + // 51.2 -> 51 from Math.floor + expect(subscription).toHaveBeenNthCalledWith(4, 51); + expect(subscription).toHaveBeenNthCalledWith(5, 40); + expect(subscription).toHaveBeenNthCalledWith(6, 32); + expect(subscription).toHaveBeenNthCalledWith(7, 25); + expect(subscription).toHaveBeenNthCalledWith(8, 20); + expect(subscription).toHaveBeenNthCalledWith(9, 16); + expect(subscription).toHaveBeenNthCalledWith(10, 12); + expect(subscription).toHaveBeenNthCalledWith(11, 9); + expect(subscription).toHaveBeenNthCalledWith(12, 7); + expect(subscription).toHaveBeenNthCalledWith(13, 5); + expect(subscription).toHaveBeenNthCalledWith(14, 4); + expect(subscription).toHaveBeenNthCalledWith(15, 3); + expect(subscription).toHaveBeenNthCalledWith(16, 2); + expect(subscription).toHaveBeenNthCalledWith(17, 1); + // No new calls due to value not changing and usage of distinctUntilChanged() + expect(subscription).toHaveBeenCalledTimes(17); + }); + }); + + describe('pollInterval configuration', () => { + function setupScenario(startingPollInterval: number) { + const errors$ = new Subject(); + const subscription = jest.fn(); + const { pollIntervalConfiguration$ } = createManagedConfiguration({ + logger, + errors$, + startingPollInterval, + startingMaxWorkers: 1, + }); + pollIntervalConfiguration$.subscribe(subscription); + return { subscription, errors$ }; + } + + beforeEach(() => { + jest.resetAllMocks(); + clock = sinon.useFakeTimers(); + }); + + afterEach(() => clock.restore()); + + test('should increase configuration at the next interval when an error is emitted', async () => { + const { subscription, errors$ } = setupScenario(100); + errors$.next(SavedObjectsErrorHelpers.createTooManyRequestsError('a', 'b')); + clock.tick(ADJUST_THROUGHPUT_INTERVAL - 1); + expect(subscription).toHaveBeenCalledTimes(1); + clock.tick(1); + expect(subscription).toHaveBeenCalledTimes(2); + expect(subscription).toHaveBeenNthCalledWith(2, 120); + }); + + test('should log a warning when the configuration changes from the starting value', async () => { + const { errors$ } = setupScenario(100); + errors$.next(SavedObjectsErrorHelpers.createTooManyRequestsError('a', 'b')); + clock.tick(ADJUST_THROUGHPUT_INTERVAL); + expect(logger.warn).toHaveBeenCalledWith( + 'Poll interval configuration is temporarily increased after Elasticsearch returned 1 "too many request" error(s).' + ); + }); + + test('should decrease configuration back to normal incrementally after an error is emitted', async () => { + const { subscription, errors$ } = setupScenario(100); + errors$.next(SavedObjectsErrorHelpers.createTooManyRequestsError('a', 'b')); + clock.tick(ADJUST_THROUGHPUT_INTERVAL * 10); + expect(subscription).toHaveBeenNthCalledWith(2, 120); + expect(subscription).toHaveBeenNthCalledWith(3, 114); + // 108.3 -> 108 from Math.floor + expect(subscription).toHaveBeenNthCalledWith(4, 108); + expect(subscription).toHaveBeenNthCalledWith(5, 102); + // 96.9 -> 100 from Math.max with the starting value + expect(subscription).toHaveBeenNthCalledWith(6, 100); + // No new calls due to value not changing and usage of distinctUntilChanged() + expect(subscription).toHaveBeenCalledTimes(6); + }); + + test('should increase configuration when errors keep emitting', async () => { + const { subscription, errors$ } = setupScenario(100); + for (let i = 0; i < 3; i++) { + errors$.next(SavedObjectsErrorHelpers.createTooManyRequestsError('a', 'b')); + clock.tick(ADJUST_THROUGHPUT_INTERVAL); + } + expect(subscription).toHaveBeenNthCalledWith(2, 120); + expect(subscription).toHaveBeenNthCalledWith(3, 144); + // 172.8 -> 173 from Math.ceil + expect(subscription).toHaveBeenNthCalledWith(4, 173); + }); + }); +}); diff --git a/x-pack/plugins/task_manager/server/lib/create_managed_configuration.ts b/x-pack/plugins/task_manager/server/lib/create_managed_configuration.ts new file mode 100644 index 000000000000..3dc5fd50d3ca --- /dev/null +++ b/x-pack/plugins/task_manager/server/lib/create_managed_configuration.ts @@ -0,0 +1,160 @@ +/* + * 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 { interval, merge, of, Observable } from 'rxjs'; +import { filter, mergeScan, map, scan, distinctUntilChanged, startWith } from 'rxjs/operators'; +import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; +import { Logger } from '../types'; + +const FLUSH_MARKER = Symbol('flush'); +export const ADJUST_THROUGHPUT_INTERVAL = 10 * 1000; + +// When errors occur, reduce maxWorkers by MAX_WORKERS_DECREASE_PERCENTAGE +// When errors no longer occur, start increasing maxWorkers by MAX_WORKERS_INCREASE_PERCENTAGE +// until starting value is reached +const MAX_WORKERS_DECREASE_PERCENTAGE = 0.8; +const MAX_WORKERS_INCREASE_PERCENTAGE = 1.05; + +// When errors occur, increase pollInterval by POLL_INTERVAL_INCREASE_PERCENTAGE +// When errors no longer occur, start decreasing pollInterval by POLL_INTERVAL_DECREASE_PERCENTAGE +// until starting value is reached +const POLL_INTERVAL_DECREASE_PERCENTAGE = 0.95; +const POLL_INTERVAL_INCREASE_PERCENTAGE = 1.2; + +interface ManagedConfigurationOpts { + logger: Logger; + startingMaxWorkers: number; + startingPollInterval: number; + errors$: Observable; +} + +interface ManagedConfiguration { + maxWorkersConfiguration$: Observable; + pollIntervalConfiguration$: Observable; +} + +export function createManagedConfiguration({ + logger, + startingMaxWorkers, + startingPollInterval, + errors$, +}: ManagedConfigurationOpts): ManagedConfiguration { + const errorCheck$ = countErrors(errors$, ADJUST_THROUGHPUT_INTERVAL); + return { + maxWorkersConfiguration$: errorCheck$.pipe( + createMaxWorkersScan(logger, startingMaxWorkers), + startWith(startingMaxWorkers), + distinctUntilChanged() + ), + pollIntervalConfiguration$: errorCheck$.pipe( + createPollIntervalScan(logger, startingPollInterval), + startWith(startingPollInterval), + distinctUntilChanged() + ), + }; +} + +function createMaxWorkersScan(logger: Logger, startingMaxWorkers: number) { + return scan((previousMaxWorkers: number, errorCount: number) => { + let newMaxWorkers: number; + if (errorCount > 0) { + // Decrease max workers by MAX_WORKERS_DECREASE_PERCENTAGE while making sure it doesn't go lower than 1. + // Using Math.floor to make sure the number is different than previous while not being a decimal value. + newMaxWorkers = Math.max(Math.floor(previousMaxWorkers * MAX_WORKERS_DECREASE_PERCENTAGE), 1); + } else { + // Increase max workers by MAX_WORKERS_INCREASE_PERCENTAGE while making sure it doesn't go + // higher than the starting value. Using Math.ceil to make sure the number is different than + // previous while not being a decimal value + newMaxWorkers = Math.min( + startingMaxWorkers, + Math.ceil(previousMaxWorkers * MAX_WORKERS_INCREASE_PERCENTAGE) + ); + } + if (newMaxWorkers !== previousMaxWorkers) { + logger.debug( + `Max workers configuration changing from ${previousMaxWorkers} to ${newMaxWorkers} after seeing ${errorCount} error(s)` + ); + if (previousMaxWorkers === startingMaxWorkers) { + logger.warn( + `Max workers configuration is temporarily reduced after Elasticsearch returned ${errorCount} "too many request" error(s).` + ); + } + } + return newMaxWorkers; + }, startingMaxWorkers); +} + +function createPollIntervalScan(logger: Logger, startingPollInterval: number) { + return scan((previousPollInterval: number, errorCount: number) => { + let newPollInterval: number; + if (errorCount > 0) { + // Increase poll interval by POLL_INTERVAL_INCREASE_PERCENTAGE and use Math.ceil to + // make sure the number is different than previous while not being a decimal value. + newPollInterval = Math.ceil(previousPollInterval * POLL_INTERVAL_INCREASE_PERCENTAGE); + } else { + // Decrease poll interval by POLL_INTERVAL_DECREASE_PERCENTAGE and use Math.floor to + // make sure the number is different than previous while not being a decimal value. + newPollInterval = Math.max( + startingPollInterval, + Math.floor(previousPollInterval * POLL_INTERVAL_DECREASE_PERCENTAGE) + ); + } + if (newPollInterval !== previousPollInterval) { + logger.debug( + `Poll interval configuration changing from ${previousPollInterval} to ${newPollInterval} after seeing ${errorCount} error(s)` + ); + if (previousPollInterval === startingPollInterval) { + logger.warn( + `Poll interval configuration is temporarily increased after Elasticsearch returned ${errorCount} "too many request" error(s).` + ); + } + } + return newPollInterval; + }, startingPollInterval); +} + +function countErrors(errors$: Observable, countInterval: number): Observable { + return merge( + // Flush error count at fixed interval + interval(countInterval).pipe(map(() => FLUSH_MARKER)), + errors$.pipe(filter((e) => SavedObjectsErrorHelpers.isTooManyRequestsError(e))) + ).pipe( + // When tag is "flush", reset the error counter + // Otherwise increment the error counter + mergeScan(({ count }, next) => { + return next === FLUSH_MARKER + ? of(emitErrorCount(count), resetErrorCount()) + : of(incementErrorCount(count)); + }, emitErrorCount(0)), + filter(isEmitEvent), + map(({ count }) => count) + ); +} + +function emitErrorCount(count: number) { + return { + tag: 'emit', + count, + }; +} + +function isEmitEvent(event: { tag: string; count: number }) { + return event.tag === 'emit'; +} + +function incementErrorCount(count: number) { + return { + tag: 'inc', + count: count + 1, + }; +} + +function resetErrorCount() { + return { + tag: 'initial', + count: 0, + }; +} diff --git a/x-pack/plugins/task_manager/server/polling/observable_monitor.ts b/x-pack/plugins/task_manager/server/polling/observable_monitor.ts index 7b06117ef59d..b07bb6661163 100644 --- a/x-pack/plugins/task_manager/server/polling/observable_monitor.ts +++ b/x-pack/plugins/task_manager/server/polling/observable_monitor.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Subject, Observable, throwError, interval, timer, Subscription } from 'rxjs'; -import { exhaustMap, tap, takeUntil, switchMap, switchMapTo, catchError } from 'rxjs/operators'; +import { Subject, Observable, throwError, timer, Subscription } from 'rxjs'; import { noop } from 'lodash'; +import { exhaustMap, tap, takeUntil, switchMap, switchMapTo, catchError } from 'rxjs/operators'; const DEFAULT_HEARTBEAT_INTERVAL = 1000; @@ -29,7 +29,7 @@ export function createObservableMonitor( }: ObservableMonitorOptions = {} ): Observable { return new Observable((subscriber) => { - const subscription: Subscription = interval(heartbeatInterval) + const subscription: Subscription = timer(0, heartbeatInterval) .pipe( // switch from the heartbeat interval to the instantiated observable until it completes / errors exhaustMap(() => takeUntilDurationOfInactivity(observableFactory(), inactivityTimeout)), diff --git a/x-pack/plugins/task_manager/server/polling/task_poller.test.ts b/x-pack/plugins/task_manager/server/polling/task_poller.test.ts index 607e2ac2b80f..956c8b05f386 100644 --- a/x-pack/plugins/task_manager/server/polling/task_poller.test.ts +++ b/x-pack/plugins/task_manager/server/polling/task_poller.test.ts @@ -5,11 +5,11 @@ */ import _ from 'lodash'; -import { Subject } from 'rxjs'; +import { Subject, of, BehaviorSubject } from 'rxjs'; import { Option, none, some } from 'fp-ts/lib/Option'; import { createTaskPoller, PollingError, PollingErrorType } from './task_poller'; import { fakeSchedulers } from 'rxjs-marbles/jest'; -import { sleep, resolvable, Resolvable } from '../test_utils'; +import { sleep, resolvable, Resolvable, mockLogger } from '../test_utils'; import { asOk, asErr } from '../lib/result_type'; describe('TaskPoller', () => { @@ -24,10 +24,12 @@ describe('TaskPoller', () => { const work = jest.fn(async () => true); createTaskPoller({ - pollInterval, + logger: mockLogger(), + pollInterval$: of(pollInterval), bufferCapacity, getCapacity: () => 1, work, + workTimeout: pollInterval * 5, pollRequests$: new Subject>(), }).subscribe(() => {}); @@ -40,9 +42,52 @@ describe('TaskPoller', () => { await sleep(0); expect(work).toHaveBeenCalledTimes(1); + await sleep(0); + await sleep(0); + advance(pollInterval + 10); + await sleep(0); + expect(work).toHaveBeenCalledTimes(2); + }) + ); + + test( + 'poller adapts to pollInterval changes', + fakeSchedulers(async (advance) => { + const pollInterval = 100; + const pollInterval$ = new BehaviorSubject(pollInterval); + const bufferCapacity = 5; + + const work = jest.fn(async () => true); + createTaskPoller({ + logger: mockLogger(), + pollInterval$, + bufferCapacity, + getCapacity: () => 1, + work, + workTimeout: pollInterval * 5, + pollRequests$: new Subject>(), + }).subscribe(() => {}); + + // `work` is async, we have to force a node `tick` await sleep(0); advance(pollInterval); + expect(work).toHaveBeenCalledTimes(1); + + pollInterval$.next(pollInterval * 2); + + // `work` is async, we have to force a node `tick` + await sleep(0); + advance(pollInterval); + expect(work).toHaveBeenCalledTimes(1); + advance(pollInterval); expect(work).toHaveBeenCalledTimes(2); + + pollInterval$.next(pollInterval / 2); + + // `work` is async, we have to force a node `tick` + await sleep(0); + advance(pollInterval / 2); + expect(work).toHaveBeenCalledTimes(3); }) ); @@ -56,9 +101,11 @@ describe('TaskPoller', () => { let hasCapacity = true; createTaskPoller({ - pollInterval, + logger: mockLogger(), + pollInterval$: of(pollInterval), bufferCapacity, work, + workTimeout: pollInterval * 5, getCapacity: () => (hasCapacity ? 1 : 0), pollRequests$: new Subject>(), }).subscribe(() => {}); @@ -113,9 +160,11 @@ describe('TaskPoller', () => { const work = jest.fn(async () => true); const pollRequests$ = new Subject>(); createTaskPoller({ - pollInterval, + logger: mockLogger(), + pollInterval$: of(pollInterval), bufferCapacity, work, + workTimeout: pollInterval * 5, getCapacity: () => 1, pollRequests$, }).subscribe(jest.fn()); @@ -157,9 +206,11 @@ describe('TaskPoller', () => { const work = jest.fn(async () => true); const pollRequests$ = new Subject>(); createTaskPoller({ - pollInterval, + logger: mockLogger(), + pollInterval$: of(pollInterval), bufferCapacity, work, + workTimeout: pollInterval * 5, getCapacity: () => (hasCapacity ? 1 : 0), pollRequests$, }).subscribe(() => {}); @@ -200,9 +251,11 @@ describe('TaskPoller', () => { const work = jest.fn(async () => true); const pollRequests$ = new Subject>(); createTaskPoller({ - pollInterval, + logger: mockLogger(), + pollInterval$: of(pollInterval), bufferCapacity, work, + workTimeout: pollInterval * 5, getCapacity: () => 1, pollRequests$, }).subscribe(() => {}); @@ -235,7 +288,8 @@ describe('TaskPoller', () => { const handler = jest.fn(); const pollRequests$ = new Subject>(); createTaskPoller({ - pollInterval, + logger: mockLogger(), + pollInterval$: of(pollInterval), bufferCapacity, work: async (...args) => { await worker; @@ -285,7 +339,8 @@ describe('TaskPoller', () => { type ResolvableTupple = [string, PromiseLike & Resolvable]; const pollRequests$ = new Subject>(); createTaskPoller<[string, Resolvable], string[]>({ - pollInterval, + logger: mockLogger(), + pollInterval$: of(pollInterval), bufferCapacity, work: async (...resolvables) => { await Promise.all(resolvables.map(([, future]) => future)); @@ -344,11 +399,13 @@ describe('TaskPoller', () => { const handler = jest.fn(); const pollRequests$ = new Subject>(); createTaskPoller({ - pollInterval, + logger: mockLogger(), + pollInterval$: of(pollInterval), bufferCapacity, work: async (...args) => { throw new Error('failed to work'); }, + workTimeout: pollInterval * 5, getCapacity: () => 5, pollRequests$, }).subscribe(handler); @@ -383,9 +440,11 @@ describe('TaskPoller', () => { return callCount; }); createTaskPoller({ - pollInterval, + logger: mockLogger(), + pollInterval$: of(pollInterval), bufferCapacity, work, + workTimeout: pollInterval * 5, getCapacity: () => 5, pollRequests$, }).subscribe(handler); @@ -424,9 +483,11 @@ describe('TaskPoller', () => { const work = jest.fn(async () => {}); const pollRequests$ = new Subject>(); createTaskPoller({ - pollInterval, + logger: mockLogger(), + pollInterval$: of(pollInterval), bufferCapacity, work, + workTimeout: pollInterval * 5, getCapacity: () => 5, pollRequests$, }).subscribe(handler); diff --git a/x-pack/plugins/task_manager/server/polling/task_poller.ts b/x-pack/plugins/task_manager/server/polling/task_poller.ts index a1435ffafe8f..7515668a19d4 100644 --- a/x-pack/plugins/task_manager/server/polling/task_poller.ts +++ b/x-pack/plugins/task_manager/server/polling/task_poller.ts @@ -11,10 +11,11 @@ import { performance } from 'perf_hooks'; import { after } from 'lodash'; import { Subject, merge, interval, of, Observable } from 'rxjs'; -import { mapTo, filter, scan, concatMap, tap, catchError } from 'rxjs/operators'; +import { mapTo, filter, scan, concatMap, tap, catchError, switchMap } from 'rxjs/operators'; import { pipe } from 'fp-ts/lib/pipeable'; import { Option, none, map as mapOptional, getOrElse } from 'fp-ts/lib/Option'; +import { Logger } from '../types'; import { pullFromSet } from '../lib/pull_from_set'; import { Result, @@ -30,12 +31,13 @@ import { timeoutPromiseAfter } from './timeout_promise_after'; type WorkFn = (...params: T[]) => Promise; interface Opts { - pollInterval: number; + logger: Logger; + pollInterval$: Observable; bufferCapacity: number; getCapacity: () => number; pollRequests$: Observable>; work: WorkFn; - workTimeout?: number; + workTimeout: number; } /** @@ -52,7 +54,8 @@ interface Opts { * of unique request argumets of type T. The queue holds all the buffered request arguments streamed in via pollRequests$ */ export function createTaskPoller({ - pollInterval, + logger, + pollInterval$, getCapacity, pollRequests$, bufferCapacity, @@ -67,7 +70,13 @@ export function createTaskPoller({ // emit a polling event on demand pollRequests$, // emit a polling event on a fixed interval - interval(pollInterval).pipe(mapTo(none)) + pollInterval$.pipe( + switchMap((period) => { + logger.debug(`Task poller now using interval of ${period}ms`); + return interval(period); + }), + mapTo(none) + ) ).pipe( // buffer all requests in a single set (to remove duplicates) as we don't want // work to take place in parallel (it could cause Task Manager to pull in the same @@ -95,7 +104,7 @@ export function createTaskPoller({ await promiseResult( timeoutPromiseAfter( work(...pullFromSet(set, getCapacity())), - workTimeout ?? pollInterval, + workTimeout, () => new Error(`work has timed out`) ) ), diff --git a/x-pack/plugins/task_manager/server/task_manager.ts b/x-pack/plugins/task_manager/server/task_manager.ts index af1d7cbe22d6..e2a441929f0a 100644 --- a/x-pack/plugins/task_manager/server/task_manager.ts +++ b/x-pack/plugins/task_manager/server/task_manager.ts @@ -19,6 +19,7 @@ import { ISavedObjectsRepository, } from '../../../../src/core/server'; import { Result, asOk, asErr, either, map, mapErr, promiseResult } from './lib/result_type'; +import { createManagedConfiguration } from './lib/create_managed_configuration'; import { TaskManagerConfig } from './config'; import { @@ -160,6 +161,13 @@ export class TaskManager { // pipe store events into the TaskManager's event stream this.store.events.subscribe((event) => this.events$.next(event)); + const { maxWorkersConfiguration$, pollIntervalConfiguration$ } = createManagedConfiguration({ + logger: this.logger, + errors$: this.store.errors$, + startingMaxWorkers: opts.config.max_workers, + startingPollInterval: opts.config.poll_interval, + }); + this.bufferedStore = new BufferedTaskStore(this.store, { bufferMaxOperations: opts.config.max_workers, logger: this.logger, @@ -167,7 +175,7 @@ export class TaskManager { this.pool = new TaskPool({ logger: this.logger, - maxWorkers: opts.config.max_workers, + maxWorkers$: maxWorkersConfiguration$, }); const { @@ -177,7 +185,8 @@ export class TaskManager { this.poller$ = createObservableMonitor>, Error>( () => createTaskPoller({ - pollInterval, + logger: this.logger, + pollInterval$: pollIntervalConfiguration$, bufferCapacity: opts.config.request_capacity, getCapacity: () => this.pool.availableWorkers, pollRequests$: this.claimRequests$, diff --git a/x-pack/plugins/task_manager/server/task_pool.test.ts b/x-pack/plugins/task_manager/server/task_pool.test.ts index 8b2bce455589..12b731b2b78a 100644 --- a/x-pack/plugins/task_manager/server/task_pool.test.ts +++ b/x-pack/plugins/task_manager/server/task_pool.test.ts @@ -5,6 +5,7 @@ */ import sinon from 'sinon'; +import { of, Subject } from 'rxjs'; import { TaskPool, TaskPoolRunResult } from './task_pool'; import { mockLogger, resolvable, sleep } from './test_utils'; import { asOk } from './lib/result_type'; @@ -14,7 +15,7 @@ import moment from 'moment'; describe('TaskPool', () => { test('occupiedWorkers are a sum of running tasks', async () => { const pool = new TaskPool({ - maxWorkers: 200, + maxWorkers$: of(200), logger: mockLogger(), }); @@ -26,7 +27,7 @@ describe('TaskPool', () => { test('availableWorkers are a function of total_capacity - occupiedWorkers', async () => { const pool = new TaskPool({ - maxWorkers: 10, + maxWorkers$: of(10), logger: mockLogger(), }); @@ -36,9 +37,21 @@ describe('TaskPool', () => { expect(pool.availableWorkers).toEqual(7); }); + test('availableWorkers is 0 until maxWorkers$ pushes a value', async () => { + const maxWorkers$ = new Subject(); + const pool = new TaskPool({ + maxWorkers$, + logger: mockLogger(), + }); + + expect(pool.availableWorkers).toEqual(0); + maxWorkers$.next(10); + expect(pool.availableWorkers).toEqual(10); + }); + test('does not run tasks that are beyond its available capacity', async () => { const pool = new TaskPool({ - maxWorkers: 2, + maxWorkers$: of(2), logger: mockLogger(), }); @@ -60,7 +73,7 @@ describe('TaskPool', () => { test('should log when marking a Task as running fails', async () => { const logger = mockLogger(); const pool = new TaskPool({ - maxWorkers: 2, + maxWorkers$: of(2), logger, }); @@ -83,7 +96,7 @@ describe('TaskPool', () => { test('should log when running a Task fails', async () => { const logger = mockLogger(); const pool = new TaskPool({ - maxWorkers: 3, + maxWorkers$: of(3), logger, }); @@ -106,7 +119,7 @@ describe('TaskPool', () => { test('should not log when running a Task fails due to the Task SO having been deleted while in flight', async () => { const logger = mockLogger(); const pool = new TaskPool({ - maxWorkers: 3, + maxWorkers$: of(3), logger, }); @@ -117,11 +130,9 @@ describe('TaskPool', () => { const result = await pool.run([mockTask(), taskFailedToRun, mockTask()]); - expect(logger.debug.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "Task TaskType \\"shooooo\\" failed in attempt to run: Saved object [task/foo] not found", - ] - `); + expect(logger.debug).toHaveBeenCalledWith( + 'Task TaskType "shooooo" failed in attempt to run: Saved object [task/foo] not found' + ); expect(logger.warn).not.toHaveBeenCalled(); expect(result).toEqual(TaskPoolRunResult.RunningAllClaimedTasks); @@ -130,7 +141,7 @@ describe('TaskPool', () => { test('Running a task which fails still takes up capacity', async () => { const logger = mockLogger(); const pool = new TaskPool({ - maxWorkers: 1, + maxWorkers$: of(1), logger, }); @@ -147,7 +158,7 @@ describe('TaskPool', () => { test('clears up capacity when a task completes', async () => { const pool = new TaskPool({ - maxWorkers: 1, + maxWorkers$: of(1), logger: mockLogger(), }); @@ -193,7 +204,7 @@ describe('TaskPool', () => { test('run cancels expired tasks prior to running new tasks', async () => { const logger = mockLogger(); const pool = new TaskPool({ - maxWorkers: 2, + maxWorkers$: of(2), logger, }); @@ -251,7 +262,7 @@ describe('TaskPool', () => { const logger = mockLogger(); const pool = new TaskPool({ logger, - maxWorkers: 20, + maxWorkers$: of(20), }); const cancelled = resolvable(); diff --git a/x-pack/plugins/task_manager/server/task_pool.ts b/x-pack/plugins/task_manager/server/task_pool.ts index ce7cd2bba92d..d32fc820999d 100644 --- a/x-pack/plugins/task_manager/server/task_pool.ts +++ b/x-pack/plugins/task_manager/server/task_pool.ts @@ -8,6 +8,7 @@ * This module contains the logic that ensures we don't run too many * tasks at once in a given Kibana instance. */ +import { Observable } from 'rxjs'; import moment, { Duration } from 'moment'; import { performance } from 'perf_hooks'; import { padStart } from 'lodash'; @@ -16,7 +17,7 @@ import { TaskRunner } from './task_runner'; import { isTaskSavedObjectNotFoundError } from './lib/is_task_not_found_error'; interface Opts { - maxWorkers: number; + maxWorkers$: Observable; logger: Logger; } @@ -31,7 +32,7 @@ const VERSION_CONFLICT_MESSAGE = 'Task has been claimed by another Kibana servic * Runs tasks in batches, taking costs into account. */ export class TaskPool { - private maxWorkers: number; + private maxWorkers: number = 0; private running = new Set(); private logger: Logger; @@ -44,8 +45,11 @@ export class TaskPool { * @prop {Logger} logger - The task manager logger. */ constructor(opts: Opts) { - this.maxWorkers = opts.maxWorkers; this.logger = opts.logger; + opts.maxWorkers$.subscribe((maxWorkers) => { + this.logger.debug(`Task pool now using ${maxWorkers} as the max worker value`); + this.maxWorkers = maxWorkers; + }); } /** diff --git a/x-pack/plugins/task_manager/server/task_store.test.ts b/x-pack/plugins/task_manager/server/task_store.test.ts index f5fafe83748d..5a3ee12d593c 100644 --- a/x-pack/plugins/task_manager/server/task_store.test.ts +++ b/x-pack/plugins/task_manager/server/task_store.test.ts @@ -7,7 +7,7 @@ import _ from 'lodash'; import sinon from 'sinon'; import uuid from 'uuid'; -import { filter, take } from 'rxjs/operators'; +import { filter, take, first } from 'rxjs/operators'; import { Option, some, none } from 'fp-ts/lib/Option'; import { @@ -66,8 +66,21 @@ const mockedDate = new Date('2019-02-12T21:01:22.479Z'); describe('TaskStore', () => { describe('schedule', () => { + let store: TaskStore; + + beforeAll(() => { + store = new TaskStore({ + index: 'tasky', + taskManagerId: '', + serializer, + callCluster: jest.fn(), + maxAttempts: 2, + definitions: taskDefinitions, + savedObjectsRepository: savedObjectsClient, + }); + }); + async function testSchedule(task: unknown) { - const callCluster = jest.fn(); savedObjectsClient.create.mockImplementation(async (type: string, attributes: unknown) => ({ id: 'testid', type, @@ -75,15 +88,6 @@ describe('TaskStore', () => { references: [], version: '123', })); - const store = new TaskStore({ - index: 'tasky', - taskManagerId: '', - serializer, - callCluster, - maxAttempts: 2, - definitions: taskDefinitions, - savedObjectsRepository: savedObjectsClient, - }); const result = await store.schedule(task as TaskInstance); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); @@ -176,12 +180,28 @@ describe('TaskStore', () => { /Unsupported task type "nope"/i ); }); + + test('pushes error from saved objects client to errors$', async () => { + const task: TaskInstance = { + id: 'id', + params: { hello: 'world' }, + state: { foo: 'bar' }, + taskType: 'report', + }; + + const firstErrorPromise = store.errors$.pipe(first()).toPromise(); + savedObjectsClient.create.mockRejectedValue(new Error('Failure')); + await expect(store.schedule(task)).rejects.toThrowErrorMatchingInlineSnapshot(`"Failure"`); + expect(await firstErrorPromise).toMatchInlineSnapshot(`[Error: Failure]`); + }); }); describe('fetch', () => { - async function testFetch(opts?: SearchOpts, hits: unknown[] = []) { - const callCluster = sinon.spy(async (name: string, params?: unknown) => ({ hits: { hits } })); - const store = new TaskStore({ + let store: TaskStore; + const callCluster = jest.fn(); + + beforeAll(() => { + store = new TaskStore({ index: 'tasky', taskManagerId: '', serializer, @@ -190,15 +210,19 @@ describe('TaskStore', () => { definitions: taskDefinitions, savedObjectsRepository: savedObjectsClient, }); + }); + + async function testFetch(opts?: SearchOpts, hits: unknown[] = []) { + callCluster.mockResolvedValue({ hits: { hits } }); const result = await store.fetch(opts); - sinon.assert.calledOnce(callCluster); - sinon.assert.calledWith(callCluster, 'search'); + expect(callCluster).toHaveBeenCalledTimes(1); + expect(callCluster).toHaveBeenCalledWith('search', expect.anything()); return { result, - args: callCluster.args[0][1], + args: callCluster.mock.calls[0][1], }; } @@ -230,6 +254,13 @@ describe('TaskStore', () => { }, }); }); + + test('pushes error from call cluster to errors$', async () => { + const firstErrorPromise = store.errors$.pipe(first()).toPromise(); + callCluster.mockRejectedValue(new Error('Failure')); + await expect(store.fetch()).rejects.toThrowErrorMatchingInlineSnapshot(`"Failure"`); + expect(await firstErrorPromise).toMatchInlineSnapshot(`[Error: Failure]`); + }); }); describe('claimAvailableTasks', () => { @@ -928,9 +959,46 @@ if (doc['task.runAt'].size()!=0) { }, ]); }); + + test('pushes error from saved objects client to errors$', async () => { + const callCluster = jest.fn(); + const store = new TaskStore({ + index: 'tasky', + taskManagerId: '', + serializer, + callCluster, + definitions: taskDefinitions, + maxAttempts: 2, + savedObjectsRepository: savedObjectsClient, + }); + + const firstErrorPromise = store.errors$.pipe(first()).toPromise(); + callCluster.mockRejectedValue(new Error('Failure')); + await expect( + store.claimAvailableTasks({ + claimOwnershipUntil: new Date(), + size: 10, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Failure"`); + expect(await firstErrorPromise).toMatchInlineSnapshot(`[Error: Failure]`); + }); }); describe('update', () => { + let store: TaskStore; + + beforeAll(() => { + store = new TaskStore({ + index: 'tasky', + taskManagerId: '', + serializer, + callCluster: jest.fn(), + maxAttempts: 2, + definitions: taskDefinitions, + savedObjectsRepository: savedObjectsClient, + }); + }); + test('refreshes the index, handles versioning', async () => { const task = { runAt: mockedDate, @@ -959,16 +1027,6 @@ if (doc['task.runAt'].size()!=0) { } ); - const store = new TaskStore({ - index: 'tasky', - taskManagerId: '', - serializer, - callCluster: jest.fn(), - maxAttempts: 2, - definitions: taskDefinitions, - savedObjectsRepository: savedObjectsClient, - }); - const result = await store.update(task); expect(savedObjectsClient.update).toHaveBeenCalledWith( @@ -1002,28 +1060,116 @@ if (doc['task.runAt'].size()!=0) { version: '123', }); }); + + test('pushes error from saved objects client to errors$', async () => { + const task = { + runAt: mockedDate, + scheduledAt: mockedDate, + startedAt: null, + retryAt: null, + id: 'task:324242', + params: { hello: 'world' }, + state: { foo: 'bar' }, + taskType: 'report', + attempts: 3, + status: 'idle' as TaskStatus, + version: '123', + ownerId: null, + }; + + const firstErrorPromise = store.errors$.pipe(first()).toPromise(); + savedObjectsClient.update.mockRejectedValue(new Error('Failure')); + await expect(store.update(task)).rejects.toThrowErrorMatchingInlineSnapshot(`"Failure"`); + expect(await firstErrorPromise).toMatchInlineSnapshot(`[Error: Failure]`); + }); + }); + + describe('bulkUpdate', () => { + let store: TaskStore; + + beforeAll(() => { + store = new TaskStore({ + index: 'tasky', + taskManagerId: '', + serializer, + callCluster: jest.fn(), + maxAttempts: 2, + definitions: taskDefinitions, + savedObjectsRepository: savedObjectsClient, + }); + }); + + test('pushes error from saved objects client to errors$', async () => { + const task = { + runAt: mockedDate, + scheduledAt: mockedDate, + startedAt: null, + retryAt: null, + id: 'task:324242', + params: { hello: 'world' }, + state: { foo: 'bar' }, + taskType: 'report', + attempts: 3, + status: 'idle' as TaskStatus, + version: '123', + ownerId: null, + }; + + const firstErrorPromise = store.errors$.pipe(first()).toPromise(); + savedObjectsClient.bulkUpdate.mockRejectedValue(new Error('Failure')); + await expect(store.bulkUpdate([task])).rejects.toThrowErrorMatchingInlineSnapshot( + `"Failure"` + ); + expect(await firstErrorPromise).toMatchInlineSnapshot(`[Error: Failure]`); + }); }); describe('remove', () => { - test('removes the task with the specified id', async () => { - const id = `id-${_.random(1, 20)}`; - const callCluster = jest.fn(); - const store = new TaskStore({ + let store: TaskStore; + + beforeAll(() => { + store = new TaskStore({ index: 'tasky', taskManagerId: '', serializer, - callCluster, + callCluster: jest.fn(), maxAttempts: 2, definitions: taskDefinitions, savedObjectsRepository: savedObjectsClient, }); + }); + + test('removes the task with the specified id', async () => { + const id = `id-${_.random(1, 20)}`; const result = await store.remove(id); expect(result).toBeUndefined(); expect(savedObjectsClient.delete).toHaveBeenCalledWith('task', id); }); + + test('pushes error from saved objects client to errors$', async () => { + const id = `id-${_.random(1, 20)}`; + const firstErrorPromise = store.errors$.pipe(first()).toPromise(); + savedObjectsClient.delete.mockRejectedValue(new Error('Failure')); + await expect(store.remove(id)).rejects.toThrowErrorMatchingInlineSnapshot(`"Failure"`); + expect(await firstErrorPromise).toMatchInlineSnapshot(`[Error: Failure]`); + }); }); describe('get', () => { + let store: TaskStore; + + beforeAll(() => { + store = new TaskStore({ + index: 'tasky', + taskManagerId: '', + serializer, + callCluster: jest.fn(), + maxAttempts: 2, + definitions: taskDefinitions, + savedObjectsRepository: savedObjectsClient, + }); + }); + test('gets the task with the specified id', async () => { const id = `id-${_.random(1, 20)}`; const task = { @@ -1041,7 +1187,6 @@ if (doc['task.runAt'].size()!=0) { ownerId: null, }; - const callCluster = jest.fn(); savedObjectsClient.get.mockImplementation(async (type: string, objectId: string) => ({ id: objectId, type, @@ -1053,22 +1198,20 @@ if (doc['task.runAt'].size()!=0) { version: '123', })); - const store = new TaskStore({ - index: 'tasky', - taskManagerId: '', - serializer, - callCluster, - maxAttempts: 2, - definitions: taskDefinitions, - savedObjectsRepository: savedObjectsClient, - }); - const result = await store.get(id); expect(result).toEqual(task); expect(savedObjectsClient.get).toHaveBeenCalledWith('task', id); }); + + test('pushes error from saved objects client to errors$', async () => { + const id = `id-${_.random(1, 20)}`; + const firstErrorPromise = store.errors$.pipe(first()).toPromise(); + savedObjectsClient.get.mockRejectedValue(new Error('Failure')); + await expect(store.get(id)).rejects.toThrowErrorMatchingInlineSnapshot(`"Failure"`); + expect(await firstErrorPromise).toMatchInlineSnapshot(`[Error: Failure]`); + }); }); describe('getLifecycle', () => { diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index af9397093774..faaf7dc08d48 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -126,6 +126,7 @@ export class TaskStore { public readonly maxAttempts: number; public readonly index: string; public readonly taskManagerId: string; + public readonly errors$ = new Subject(); private callCluster: ElasticJs; private definitions: TaskDictionary; @@ -176,11 +177,17 @@ export class TaskStore { ); } - const savedObject = await this.savedObjectsRepository.create( - 'task', - taskInstanceToAttributes(taskInstance), - { id: taskInstance.id, refresh: false } - ); + let savedObject; + try { + savedObject = await this.savedObjectsRepository.create( + 'task', + taskInstanceToAttributes(taskInstance), + { id: taskInstance.id, refresh: false } + ); + } catch (e) { + this.errors$.next(e); + throw e; + } return savedObjectToConcreteTaskInstance(savedObject); } @@ -338,12 +345,22 @@ export class TaskStore { */ public async update(doc: ConcreteTaskInstance): Promise { const attributes = taskInstanceToAttributes(doc); - const updatedSavedObject = await this.savedObjectsRepository.update< - SerializedConcreteTaskInstance - >('task', doc.id, attributes, { - refresh: false, - version: doc.version, - }); + + let updatedSavedObject; + try { + updatedSavedObject = await this.savedObjectsRepository.update( + 'task', + doc.id, + attributes, + { + refresh: false, + version: doc.version, + } + ); + } catch (e) { + this.errors$.next(e); + throw e; + } return savedObjectToConcreteTaskInstance( // The SavedObjects update api forces a Partial on the `attributes` on the response, @@ -367,8 +384,11 @@ export class TaskStore { return attrsById; }, new Map()); - const updatedSavedObjects: Array = ( - await this.savedObjectsRepository.bulkUpdate( + let updatedSavedObjects: Array; + try { + ({ saved_objects: updatedSavedObjects } = await this.savedObjectsRepository.bulkUpdate< + SerializedConcreteTaskInstance + >( docs.map((doc) => ({ type: 'task', id: doc.id, @@ -378,8 +398,11 @@ export class TaskStore { { refresh: false, } - ) - ).saved_objects; + )); + } catch (e) { + this.errors$.next(e); + throw e; + } return updatedSavedObjects.map((updatedSavedObject, index) => isSavedObjectsUpdateResponse(updatedSavedObject) @@ -409,7 +432,12 @@ export class TaskStore { * @returns {Promise} */ public async remove(id: string): Promise { - await this.savedObjectsRepository.delete('task', id); + try { + await this.savedObjectsRepository.delete('task', id); + } catch (e) { + this.errors$.next(e); + throw e; + } } /** @@ -419,7 +447,14 @@ export class TaskStore { * @returns {Promise} */ public async get(id: string): Promise { - return savedObjectToConcreteTaskInstance(await this.savedObjectsRepository.get('task', id)); + let result; + try { + result = await this.savedObjectsRepository.get('task', id); + } catch (e) { + this.errors$.next(e); + throw e; + } + return savedObjectToConcreteTaskInstance(result); } /** @@ -443,14 +478,20 @@ export class TaskStore { private async search(opts: SearchOpts = {}): Promise { const { query } = ensureQueryOnlyReturnsTaskObjects(opts); - const result = await this.callCluster('search', { - index: this.index, - ignoreUnavailable: true, - body: { - ...opts, - query, - }, - }); + let result; + try { + result = await this.callCluster('search', { + index: this.index, + ignoreUnavailable: true, + body: { + ...opts, + query, + }, + }); + } catch (e) { + this.errors$.next(e); + throw e; + } const rawDocs = (result as SearchResponse).hits.hits; @@ -485,17 +526,23 @@ export class TaskStore { { max_docs }: UpdateByQueryOpts = {} ): Promise { const { query } = ensureQueryOnlyReturnsTaskObjects(opts); - const result = await this.callCluster('updateByQuery', { - index: this.index, - ignoreUnavailable: true, - refresh: true, - max_docs, - conflicts: 'proceed', - body: { - ...opts, - query, - }, - }); + let result; + try { + result = await this.callCluster('updateByQuery', { + index: this.index, + ignoreUnavailable: true, + refresh: true, + max_docs, + conflicts: 'proceed', + body: { + ...opts, + query, + }, + }); + } catch (e) { + this.errors$.next(e); + throw e; + } // eslint-disable-next-line @typescript-eslint/naming-convention const { total, updated, version_conflicts } = result as UpdateDocumentByQueryResponse; diff --git a/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts b/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts index 6ef44e325b0a..524b4c5616c7 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/kibana.json b/x-pack/plugins/transform/kibana.json index 4ec12a27e1b1..d5da9377ed87 100644 --- a/x-pack/plugins/transform/kibana.json +++ b/x-pack/plugins/transform/kibana.json @@ -8,7 +8,8 @@ "home", "licensing", "management", - "features" + "features", + "savedObjects" ], "optionalPlugins": [ "security", @@ -20,7 +21,6 @@ "discover", "kibanaUtils", "kibanaReact", - "savedObjects", "ml" ] } diff --git a/x-pack/plugins/transform/public/app/app_dependencies.tsx b/x-pack/plugins/transform/public/app/app_dependencies.tsx index a23465495ace..44a29e78c048 100644 --- a/x-pack/plugins/transform/public/app/app_dependencies.tsx +++ b/x-pack/plugins/transform/public/app/app_dependencies.tsx @@ -6,6 +6,7 @@ import { CoreSetup, CoreStart } from 'src/core/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { SavedObjectsStart } from 'src/plugins/saved_objects/public'; import { ScopedHistory } from 'kibana/public'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; @@ -25,6 +26,7 @@ export interface AppDependencies { storage: Storage; overlays: CoreStart['overlays']; history: ScopedHistory; + savedObjectsPlugin: SavedObjectsStart; ml: GetMlSharedImportsReturnType; } diff --git a/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts index feff17b81311..7e0774bb2198 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_search_items/use_search_items.ts @@ -28,10 +28,7 @@ export const useSearchItems = (defaultSavedObjectId: string | undefined) => { const savedObjectsClient = appDeps.savedObjects.client; const savedSearches = createSavedSearchesLoader({ savedObjectsClient, - indexPatterns, - search: appDeps.data.search, - chrome: appDeps.chrome, - overlays: appDeps.overlays, + savedObjects: appDeps.savedObjectsPlugin, }); const [searchItems, setSearchItems] = useState(undefined); diff --git a/x-pack/plugins/transform/public/app/mount_management_section.ts b/x-pack/plugins/transform/public/app/mount_management_section.ts index 17db745652db..0de4a2ce7507 100644 --- a/x-pack/plugins/transform/public/app/mount_management_section.ts +++ b/x-pack/plugins/transform/public/app/mount_management_section.ts @@ -48,6 +48,7 @@ export async function mountManagementSection( storage: localStorage, uiSettings, history, + savedObjectsPlugin: plugins.savedObjects, ml: await getMlSharedImports(), }; diff --git a/x-pack/plugins/transform/public/plugin.ts b/x-pack/plugins/transform/public/plugin.ts index 74256a478e73..597bfe36a003 100644 --- a/x-pack/plugins/transform/public/plugin.ts +++ b/x-pack/plugins/transform/public/plugin.ts @@ -8,6 +8,7 @@ import { i18n as kbnI18n } from '@kbn/i18n'; import { CoreSetup } from 'src/core/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { HomePublicPluginSetup } from 'src/plugins/home/public'; +import { SavedObjectsStart } from 'src/plugins/saved_objects/public'; import { ManagementSetup } from '../../../../src/plugins/management/public'; import { registerFeature } from './register_feature'; @@ -15,6 +16,7 @@ export interface PluginsDependencies { data: DataPublicPluginStart; management: ManagementSetup; home: HomePublicPluginSetup; + savedObjects: SavedObjectsStart; } export class TransformUiPlugin { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index bcdc9e013366..47a5476bf8e7 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": "アラートが見つかりません。", @@ -16405,7 +16405,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 97caed949d4e..892d9f4763fe 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": "未找到告警。", @@ -16415,7 +16415,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/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx index 7b81298e8e4b..84726bc950ef 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx @@ -27,6 +27,8 @@ import { alertReducer } from './alert_reducer'; import { createAlert } from '../../lib/alert_api'; import { HealthCheck } from '../../components/health_check'; import { PLUGIN } from '../../constants/plugin'; +import { ConfirmAlertSave } from './confirm_alert_save'; +import { hasShowActionsCapability } from '../../lib/capabilities'; interface AlertAddProps { consumer: string; @@ -59,6 +61,7 @@ export const AlertAdd = ({ const [{ alert }, dispatch] = useReducer(alertReducer, { alert: initialAlert }); const [isSaving, setIsSaving] = useState(false); + const [isConfirmAlertSaveModalOpen, setIsConfirmAlertSaveModalOpen] = useState(false); const setAlert = (value: any) => { dispatch({ command: { type: 'setAlert' }, payload: { key: 'alert', value } }); @@ -74,8 +77,11 @@ export const AlertAdd = ({ alertTypeRegistry, actionTypeRegistry, docLinks, + capabilities, } = useAlertsContext(); + const canShowActions = hasShowActionsCapability(capabilities); + useEffect(() => { setAlertProperty('alertTypeId', alertTypeId); }, [alertTypeId]); @@ -85,6 +91,17 @@ export const AlertAdd = ({ setAlert(initialAlert); }, [initialAlert, setAddFlyoutVisibility]); + const saveAlertAndCloseFlyout = async () => { + const savedAlert = await onSaveAlert(); + setIsSaving(false); + if (savedAlert) { + closeFlyout(); + if (reloadAlerts) { + reloadAlerts(); + } + } + }; + if (!addFlyoutVisible) { return null; } @@ -109,6 +126,9 @@ export const AlertAdd = ({ !!Object.keys(errorObj.errors).find((errorKey) => errorObj.errors[errorKey].length >= 1) ) !== undefined; + // Confirm before saving if user is able to add actions but hasn't added any to this alert + const shouldConfirmSave = canShowActions && alert.actions?.length === 0; + async function onSaveAlert(): Promise { try { const newAlert = await createAlert({ http, alert }); @@ -195,13 +215,10 @@ export const AlertAdd = ({ isLoading={isSaving} onClick={async () => { setIsSaving(true); - const savedAlert = await onSaveAlert(); - setIsSaving(false); - if (savedAlert) { - closeFlyout(); - if (reloadAlerts) { - reloadAlerts(); - } + if (shouldConfirmSave) { + setIsConfirmAlertSaveModalOpen(true); + } else { + await saveAlertAndCloseFlyout(); } }} > @@ -214,6 +231,18 @@ export const AlertAdd = ({ + {isConfirmAlertSaveModalOpen && ( + { + setIsConfirmAlertSaveModalOpen(false); + await saveAlertAndCloseFlyout(); + }} + onCancel={() => { + setIsSaving(false); + setIsConfirmAlertSaveModalOpen(false); + }} + /> + )} ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_save.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_save.tsx new file mode 100644 index 000000000000..f23948d1d81b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/confirm_alert_save.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +interface Props { + onConfirm: () => void; + onCancel: () => void; +} + +export const ConfirmAlertSave: React.FC = ({ onConfirm, onCancel }) => { + return ( + + +

+ +

+
+
+ ); +}; diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json index 58c50d0dac7b..fc9db4a8b6b2 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/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts b/x-pack/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts index 544b976bb5ff..2142e5ea1e2f 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/telemetry/__tests__/kibana_telemetry_adapter.test.ts @@ -5,7 +5,7 @@ */ import { KibanaTelemetryAdapter } from '../kibana_telemetry_adapter'; - +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; jest .spyOn(KibanaTelemetryAdapter, 'countNoOfUniqueMonitorAndLocations') .mockResolvedValue(undefined as any); @@ -13,7 +13,12 @@ jest describe('KibanaTelemetryAdapter', () => { let usageCollection: any; let getSavedObjectsClient: any; - let collector: { type: string; fetch: () => Promise; isReady: () => boolean }; + let collectorFetchContext: any; + let collector: { + type: string; + fetch: (collectorFetchParams: any) => Promise; + isReady: () => boolean; + }; beforeEach(() => { usageCollection = { makeUsageCollector: (val: any) => { @@ -23,6 +28,7 @@ describe('KibanaTelemetryAdapter', () => { getSavedObjectsClient = () => { return {}; }; + collectorFetchContext = createCollectorFetchContextMock(); }); it('collects monitor and overview data', async () => { @@ -49,7 +55,7 @@ describe('KibanaTelemetryAdapter', () => { autoRefreshEnabled: true, autorefreshInterval: 30, }); - const result = await collector.fetch(); + const result = await collector.fetch(collectorFetchContext); expect(result).toMatchSnapshot(); }); @@ -87,7 +93,7 @@ describe('KibanaTelemetryAdapter', () => { autoRefreshEnabled: true, autorefreshInterval: 30, }); - const result = await collector.fetch(); + const result = await collector.fetch(collectorFetchContext); expect(result).toMatchSnapshot(); }); diff --git a/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts b/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts index 106aab351547..a8969f2621f2 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts @@ -6,7 +6,7 @@ import moment from 'moment'; import { ISavedObjectsRepository, SavedObjectsClientContract } from 'kibana/server'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { CollectorFetchContext, UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { PageViewParams, UptimeTelemetry, Usage } from './types'; import { ESAPICaller } from '../framework'; import { savedObjectsAdapter } from '../../saved_objects'; @@ -69,7 +69,7 @@ export class KibanaTelemetryAdapter { }, }, }, - fetch: async (callCluster: ESAPICaller) => { + fetch: async ({ callCluster }: CollectorFetchContext) => { const savedObjectsClient = getSavedObjectsClient()!; if (savedObjectsClient) { await this.countNoOfUniqueMonitorAndLocations(callCluster, savedObjectsClient); 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 02fa669cb05e..5d22e22ee0eb 100644 --- a/x-pack/plugins/xpack_legacy/server/routes/settings.test.ts +++ b/x-pack/plugins/xpack_legacy/server/routes/settings.test.ts @@ -13,14 +13,18 @@ import { ServiceStatus, ServiceStatusLevels, } from '../../../../../src/core/server'; -import { contextServiceMock } 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'; type HttpService = ReturnType; type HttpSetup = UnwrapPromise>; -describe('/api/stats', () => { +describe('/api/settings', () => { let server: HttpService; let httpSetup: HttpSetup; let overallStatus$: BehaviorSubject; @@ -38,6 +42,12 @@ describe('/api/stats', () => { callAsCurrentUser: mockApiCaller, }, }, + client: { + 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 2a0eb3d11584..9a30ca30616b 100644 --- a/x-pack/plugins/xpack_legacy/server/routes/settings.ts +++ b/x-pack/plugins/xpack_legacy/server/routes/settings.ts @@ -42,6 +42,11 @@ export function registerSettingsRoute({ }, async (context, req, res) => { const { callAsCurrentUser } = context.core.elasticsearch.legacy.client; + const collectorFetchContext = { + callCluster: callAsCurrentUser, + esClient: context.core.elasticsearch.client.asCurrentUser, + soClient: context.core.savedObjects.client, + }; const settingsCollector = usageCollection.getCollectorByType(KIBANA_SETTINGS_TYPE) as | KibanaSettingsCollector @@ -51,7 +56,7 @@ export function registerSettingsRoute({ } const settings = - (await settingsCollector.fetch(callAsCurrentUser)) ?? + (await settingsCollector.fetch(collectorFetchContext)) ?? settingsCollector.getEmailValueStructure(null); const { cluster_uuid: uuid } = await callAsCurrentUser('info', { filterPath: 'cluster_uuid', diff --git a/x-pack/test/accessibility/apps/kibana_overview.ts b/x-pack/test/accessibility/apps/kibana_overview.ts new file mode 100644 index 000000000000..3ffcf20c3399 --- /dev/null +++ b/x-pack/test/accessibility/apps/kibana_overview.ts @@ -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 { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'home']); + const a11y = getService('a11y'); + + describe('Kibana overview', () => { + const esArchiver = getService('esArchiver'); + + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('kibanaOverview'); + }); + + after(async () => { + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await PageObjects.home.removeSampleDataSet('flights'); + await esArchiver.unload('empty_kibana'); + }); + + it('Getting started view', async () => { + await a11y.testAppSnapshot(); + }); + + it('Overview view', async () => { + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await PageObjects.home.addSampleDataSet('flights'); + await PageObjects.common.navigateToApp('kibanaOverview'); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index 6b3a2a9add89..8dace50a1ec8 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -25,6 +25,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/dashboard_edit_panel'), require.resolve('./apps/users'), require.resolve('./apps/roles'), + require.resolve('./apps/kibana_overview'), ], pageObjects, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts b/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts index 2f57d05be422..65e75f33072c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/scenarios.ts @@ -13,8 +13,6 @@ const NoKibanaPrivileges: User = { role: { name: 'no_kibana_privileges', elasticsearch: { - // TODO: Remove once Elasticsearch doesn't require the permission for own keys - cluster: ['manage_api_key'], indices: [ { names: ['foo'], @@ -56,8 +54,6 @@ const GlobalRead: User = { }, ], elasticsearch: { - // TODO: Remove once Elasticsearch doesn't require the permission for own keys - cluster: ['manage_api_key'], indices: [ { names: [`${ES_TEST_INDEX_NAME}*`], @@ -85,8 +81,6 @@ const Space1All: User = { }, ], elasticsearch: { - // TODO: Remove once Elasticsearch doesn't require the permission for own keys - cluster: ['manage_api_key'], indices: [ { names: [`${ES_TEST_INDEX_NAME}*`], @@ -113,8 +107,6 @@ const Space1AllAlertingNoneActions: User = { }, ], elasticsearch: { - // TODO: Remove once Elasticsearch doesn't require the permission for own keys - cluster: ['manage_api_key'], indices: [ { names: [`${ES_TEST_INDEX_NAME}*`], @@ -142,8 +134,6 @@ const Space1AllWithRestrictedFixture: User = { }, ], elasticsearch: { - // TODO: Remove once Elasticsearch doesn't require the permission for own keys - cluster: ['manage_api_key'], indices: [ { names: [`${ES_TEST_INDEX_NAME}*`], diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/nodes.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/nodes.js index c85903759782..3de3a3279f77 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/nodes.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/nodes.js @@ -29,6 +29,7 @@ export default function ({ getService }) { const nodesIds = Object.keys(nodeStats.nodes); const { body } = await loadNodes().expect(200); + expect(body.isUsingDeprecatedDataRoleConfig).to.eql(false); expect(body.nodesByAttributes[NODE_CUSTOM_ATTRIBUTE]).to.eql(nodesIds); }); }); 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 2e38c5317c38..f7657e482d87 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'); @@ -94,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 01a34f110ed1..8f78cdf01560 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 c0a3570c9d8e..7073658ab3cc 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/apm_api_integration/common/authentication.ts b/x-pack/test/apm_api_integration/common/authentication.ts index 6179c8891663..501a84431133 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/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 000000000000..620e771b3446 --- /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 be 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 779205377621..cc0eb04075b7 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,7 @@ 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('./delete_rules')); loadTestFile(require.resolve('./delete_rules_bulk')); loadTestFile(require.resolve('./export_rules')); diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index d26c92a2bcd6..6c4fa94a259e 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -13,8 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const listingTable = getService('listingTable'); const testSubjects = getService('testSubjects'); - // Failing: See https://github.com/elastic/kibana/issues/77969 - describe.skip('lens smokescreen tests', () => { + describe('lens smokescreen tests', () => { it('should allow creation of lens xy chart', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); @@ -153,6 +152,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { dimension: 'lnsXY_yDimensionPanel > lns-dimensionTrigger', operation: 'max', field: 'memory', + keepOpen: true, }); await PageObjects.lens.editDimensionLabel('Test of label'); await PageObjects.lens.editDimensionFormat('Percent'); @@ -160,6 +160,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.editMissingValues('Linear'); await PageObjects.lens.assertMissingValues('Linear'); + + await PageObjects.lens.openDimensionEditor('lnsXY_yDimensionPanel > lns-dimensionTrigger'); await PageObjects.lens.assertColor('#ff0000'); await testSubjects.existOrFail('indexPattern-dimension-formatDecimals'); diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index ec7281e53c5e..f8ecacbc1141 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -114,6 +114,18 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont } }, + /** + * Open the specified dimension. + * + * @param dimension - the selector of the dimension panel to open + * @param layerIndex - the index of the layer + */ + async openDimensionEditor(dimension: string, layerIndex = 0) { + await retry.try(async () => { + await testSubjects.click(`lns-layerPanel-${layerIndex} > ${dimension}`); + }); + }, + // closes the dimension editor flyout async closeDimensionEditor() { await testSubjects.click('lns-indexPattern-dimensionContainerTitle'); diff --git a/x-pack/test/functional/services/ml/settings_calendar.ts b/x-pack/test/functional/services/ml/settings_calendar.ts index 3a2c9149a063..3ab062dc2e6e 100644 --- a/x-pack/test/functional/services/ml/settings_calendar.ts +++ b/x-pack/test/functional/services/ml/settings_calendar.ts @@ -48,12 +48,18 @@ export function MachineLearningSettingsCalendarProvider( return rows; }, - rowSelector(calendarId: string, subSelector?: string) { + calendarRowSelector(calendarId: string, subSelector?: string) { const row = `~mlCalendarTable > ~row-${calendarId}`; return !subSelector ? row : `${row} > ${subSelector}`; }, + async waitForCalendarTableToLoad() { + await testSubjects.existOrFail('~mlCalendarTable', { timeout: 60 * 1000 }); + await testSubjects.existOrFail('mlCalendarTable loaded', { timeout: 30 * 1000 }); + }, + async filterWithSearchString(filter: string, expectedRowCount: number = 1) { + await this.waitForCalendarTableToLoad(); const tableListContainer = await testSubjects.find('mlCalendarTableContainer'); const searchBarInput = await tableListContainer.findByClassName('euiFieldSearch'); await searchBarInput.clearValueWithKeyboard(); @@ -69,7 +75,7 @@ export function MachineLearningSettingsCalendarProvider( async isCalendarRowSelected(calendarId: string): Promise { return await testSubjects.isChecked( - this.rowSelector(calendarId, `checkboxSelectRow-${calendarId}`) + this.calendarRowSelector(calendarId, `checkboxSelectRow-${calendarId}`) ); }, @@ -85,7 +91,9 @@ export function MachineLearningSettingsCalendarProvider( async selectCalendarRow(calendarId: string) { if ((await this.isCalendarRowSelected(calendarId)) === false) { - await testSubjects.click(this.rowSelector(calendarId, `checkboxSelectRow-${calendarId}`)); + await testSubjects.click( + this.calendarRowSelector(calendarId, `checkboxSelectRow-${calendarId}`) + ); } await this.assertCalendarRowSelected(calendarId, true); @@ -93,7 +101,9 @@ export function MachineLearningSettingsCalendarProvider( async deselectCalendarRow(calendarId: string) { if ((await this.isCalendarRowSelected(calendarId)) === true) { - await testSubjects.click(this.rowSelector(calendarId, `checkboxSelectRow-${calendarId}`)); + await testSubjects.click( + this.calendarRowSelector(calendarId, `checkboxSelectRow-${calendarId}`) + ); } await this.assertCalendarRowSelected(calendarId, false); @@ -120,7 +130,7 @@ export function MachineLearningSettingsCalendarProvider( }, async openCalendarEditForm(calendarId: string) { - await testSubjects.click(this.rowSelector(calendarId, 'mlEditCalendarLink')); + await testSubjects.click(this.calendarRowSelector(calendarId, 'mlEditCalendarLink')); await testSubjects.existOrFail('mlPageCalendarEdit > mlCalendarFormEdit', { timeout: 5000 }); }, @@ -178,11 +188,6 @@ export function MachineLearningSettingsCalendarProvider( ); }, - calendarRowSelector(calendarId: string, subSelector?: string) { - const row = `~mlCalendarTable > ~row-${calendarId}`; - return !subSelector ? row : `${row} > ${subSelector}`; - }, - eventRowSelector(eventDescription: string, subSelector?: string) { const row = `~mlCalendarEventsTable > ~row-${eventDescription}`; return !subSelector ? row : `${row} > ${subSelector}`; @@ -261,12 +266,20 @@ export function MachineLearningSettingsCalendarProvider( return isSelected === 'true'; }, + async assertApplyToAllJobsSwitchCheckState(expectedCheckState: boolean) { + const actualCheckState = this.getApplyToAllJobsSwitchCheckedState(); + expect(actualCheckState).to.eql( + expectedCheckState, + `Apply to all jobs switch check state should be '${expectedCheckState}' (got '${actualCheckState}')` + ); + }, + async toggleApplyToAllJobsSwitch(toggle: boolean) { const subj = 'mlCalendarApplyToAllJobsSwitch'; if ((await this.getApplyToAllJobsSwitchCheckedState()) !== toggle) { await retry.tryForTime(5 * 1000, async () => { await testSubjects.clickWhenNotDisabled(subj); - await this.assertApplyToAllJobsSwitchEnabled(toggle); + await this.assertApplyToAllJobsSwitchCheckState(toggle); }); } }, diff --git a/x-pack/test/functional/services/uptime/alerts.ts b/x-pack/test/functional/services/uptime/alerts.ts index c4f75b843d78..6ade7dc485a8 100644 --- a/x-pack/test/functional/services/uptime/alerts.ts +++ b/x-pack/test/functional/services/uptime/alerts.ts @@ -114,5 +114,8 @@ export function UptimeAlertsProvider({ getService }: FtrProviderContext) { async clickSaveAlertButton() { return testSubjects.click('saveAlertButton'); }, + async clickSaveAlertsConfirmButton() { + return testSubjects.click('confirmAlertSaveModal > confirmModalConfirmButton'); + }, }; } diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts index 25a7a57e5241..4dd7c9f3b371 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts @@ -54,6 +54,28 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); } + async function defineAlert(alertName: string) { + await pageObjects.triggersActionsUI.clickCreateAlertButton(); + await testSubjects.setValue('alertNameInput', alertName); + await testSubjects.click('.index-threshold-SelectOption'); + await testSubjects.click('selectIndexExpression'); + const comboBox = await find.byCssSelector('#indexSelectSearchBox'); + await comboBox.click(); + await comboBox.type('k'); + const filterSelectItem = await find.byCssSelector(`.euiFilterSelectItem`); + await filterSelectItem.click(); + await testSubjects.click('thresholdAlertTimeFieldSelect'); + await retry.try(async () => { + const fieldOptions = await find.allByCssSelector('#thresholdTimeField option'); + expect(fieldOptions[1]).not.to.be(undefined); + await fieldOptions[1].click(); + }); + await testSubjects.click('closePopover'); + // need this two out of popup clicks to close them + const nameInput = await testSubjects.find('alertNameInput'); + await nameInput.click(); + } + describe('alerts', function () { before(async () => { await pageObjects.common.navigateToApp('triggersActions'); @@ -62,25 +84,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should create an alert', async () => { const alertName = generateUniqueKey(); - await pageObjects.triggersActionsUI.clickCreateAlertButton(); - await testSubjects.setValue('alertNameInput', alertName); - await testSubjects.click('.index-threshold-SelectOption'); - await testSubjects.click('selectIndexExpression'); - const comboBox = await find.byCssSelector('#indexSelectSearchBox'); - await comboBox.click(); - await comboBox.type('k'); - const filterSelectItem = await find.byCssSelector(`.euiFilterSelectItem`); - await filterSelectItem.click(); - await testSubjects.click('thresholdAlertTimeFieldSelect'); - await retry.try(async () => { - const fieldOptions = await find.allByCssSelector('#thresholdTimeField option'); - expect(fieldOptions[1]).not.to.be(undefined); - await fieldOptions[1].click(); - }); - await testSubjects.click('closePopover'); - // need this two out of popup clicks to close them - const nameInput = await testSubjects.find('alertNameInput'); - await nameInput.click(); + await defineAlert(alertName); await testSubjects.click('.slack-ActionTypeSelectOption'); await testSubjects.click('addNewActionConnectorButton-.slack'); @@ -123,6 +127,39 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await deleteAlerts(alertsToDelete.map((alertItem: { id: string }) => alertItem.id)); }); + it('should show save confirmation before creating alert with no actions', async () => { + const alertName = generateUniqueKey(); + await defineAlert(alertName); + + await testSubjects.click('saveAlertButton'); + await testSubjects.existOrFail('confirmAlertSaveModal'); + await testSubjects.click('confirmAlertSaveModal > confirmModalCancelButton'); + await testSubjects.missingOrFail('confirmAlertSaveModal'); + await find.existsByCssSelector('[data-test-subj="saveAlertButton"]:not(disabled)'); + + await testSubjects.click('saveAlertButton'); + await testSubjects.existOrFail('confirmAlertSaveModal'); + await testSubjects.click('confirmAlertSaveModal > confirmModalConfirmButton'); + await testSubjects.missingOrFail('confirmAlertSaveModal'); + + const toastTitle = await pageObjects.common.closeToast(); + expect(toastTitle).to.eql(`Saved '${alertName}'`); + await pageObjects.triggersActionsUI.searchAlerts(alertName); + const searchResultsAfterSave = await pageObjects.triggersActionsUI.getAlertsList(); + expect(searchResultsAfterSave).to.eql([ + { + name: alertName, + tagsText: '', + alertType: 'Index threshold', + interval: '1m', + }, + ]); + + // clean up created alert + const alertsToDelete = await getAlertsByName(alertName); + await deleteAlerts(alertsToDelete.map((alertItem: { id: string }) => alertItem.id)); + }); + it('should display alerts in alphabetical order', async () => { const uniqueKey = generateUniqueKey(); const a = await createAlert({ name: 'b', tags: [uniqueKey] }); diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts index a6de87d6f7b1..ff4ab65a310e 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts @@ -79,6 +79,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('can save alert', async () => { await alerts.clickSaveAlertButton(); + await alerts.clickSaveAlertsConfirmButton(); await pageObjects.common.closeToast(); }); @@ -165,6 +166,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('can save alert', async () => { await alerts.clickSaveAlertButton(); + await alerts.clickSaveAlertsConfirmButton(); await pageObjects.common.closeToast(); }); diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/anomaly_alert.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/anomaly_alert.ts index 55ef7e9784ff..c9512dd12b78 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/anomaly_alert.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/anomaly_alert.ts @@ -79,6 +79,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('can save alert', async () => { await alerts.clickSaveAlertButton(); + await alerts.clickSaveAlertsConfirmButton(); await pageObjects.common.closeToast(); }); 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 c6cf72f697aa..c0132a5822b5 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/fleet/agents/upgrade.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/upgrade.ts index f90c1d7bcbd6..055877c19c82 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/upgrade.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/upgrade.ts @@ -121,9 +121,24 @@ export default function (providerContext: FtrProviderContext) { expect(res.body.message).to.equal('agent agent1 is not upgradeable'); }); - it('should respond 200 to bulk upgrade agents and update the agent SOs', async () => { + it('should respond 200 to bulk upgrade upgradeable agents and update the agent SOs', async () => { const kibanaVersion = await kibanaServer.version.get(); - + await kibanaServer.savedObjects.update({ + id: 'agent1', + type: AGENT_SAVED_OBJECT_TYPE, + attributes: { + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }, + }); + await kibanaServer.savedObjects.update({ + id: 'agent2', + type: AGENT_SAVED_OBJECT_TYPE, + attributes: { + local_metadata: { + elastic: { agent: { upgradeable: true, version: semver.inc(kibanaVersion, 'patch') } }, + }, + }, + }); await supertest .post(`/api/fleet/agents/bulk_upgrade`) .set('kbn-xsrf', 'xxx') @@ -138,11 +153,27 @@ export default function (providerContext: FtrProviderContext) { supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), ]); expect(typeof agent1data.body.item.upgrade_started_at).to.be('string'); - expect(typeof agent2data.body.item.upgrade_started_at).to.be('string'); + expect(typeof agent2data.body.item.upgrade_started_at).to.be('undefined'); }); - it('should allow to upgrade multiple agents by kuery', async () => { + it('should allow to upgrade multiple upgradeable agents by kuery', async () => { const kibanaVersion = await kibanaServer.version.get(); + await kibanaServer.savedObjects.update({ + id: 'agent1', + type: AGENT_SAVED_OBJECT_TYPE, + attributes: { + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }, + }); + await kibanaServer.savedObjects.update({ + id: 'agent2', + type: AGENT_SAVED_OBJECT_TYPE, + attributes: { + local_metadata: { + elastic: { agent: { upgradeable: true, version: semver.inc(kibanaVersion, 'patch') } }, + }, + }, + }); await supertest .post(`/api/fleet/agents/bulk_upgrade`) .set('kbn-xsrf', 'xxx') @@ -156,7 +187,7 @@ export default function (providerContext: FtrProviderContext) { supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), ]); expect(typeof agent1data.body.item.upgrade_started_at).to.be('string'); - expect(typeof agent2data.body.item.upgrade_started_at).to.be('string'); + expect(typeof agent2data.body.item.upgrade_started_at).to.be('undefined'); }); it('should not upgrade an unenrolling agent during bulk_upgrade', async () => { @@ -164,6 +195,22 @@ export default function (providerContext: FtrProviderContext) { await supertest.post(`/api/fleet/agents/agent1/unenroll`).set('kbn-xsrf', 'xxx').send({ force: true, }); + await kibanaServer.savedObjects.update({ + id: 'agent1', + type: AGENT_SAVED_OBJECT_TYPE, + attributes: { + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }, + }); + await kibanaServer.savedObjects.update({ + id: 'agent2', + type: AGENT_SAVED_OBJECT_TYPE, + attributes: { + local_metadata: { + elastic: { agent: { upgradeable: true, version: '0.0.0' } }, + }, + }, + }); await supertest .post(`/api/fleet/agents/bulk_upgrade`) .set('kbn-xsrf', 'xxx') @@ -183,7 +230,19 @@ export default function (providerContext: FtrProviderContext) { await kibanaServer.savedObjects.update({ id: 'agent1', type: AGENT_SAVED_OBJECT_TYPE, - attributes: { unenrolled_at: new Date().toISOString() }, + attributes: { + unenrolled_at: new Date().toISOString(), + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }, + }); + await kibanaServer.savedObjects.update({ + id: 'agent2', + type: AGENT_SAVED_OBJECT_TYPE, + attributes: { + local_metadata: { + elastic: { agent: { upgradeable: true, version: '0.0.0' } }, + }, + }, }); await supertest .post(`/api/fleet/agents/bulk_upgrade`) @@ -199,5 +258,46 @@ export default function (providerContext: FtrProviderContext) { expect(typeof agent1data.body.item.upgrade_started_at).to.be('undefined'); expect(typeof agent2data.body.item.upgrade_started_at).to.be('string'); }); + it('should not upgrade an non upgradeable agent during bulk_upgrade', async () => { + const kibanaVersion = await kibanaServer.version.get(); + await kibanaServer.savedObjects.update({ + id: 'agent1', + type: AGENT_SAVED_OBJECT_TYPE, + attributes: { + local_metadata: { elastic: { agent: { upgradeable: true, version: '0.0.0' } } }, + }, + }); + await kibanaServer.savedObjects.update({ + id: 'agent2', + type: AGENT_SAVED_OBJECT_TYPE, + attributes: { + local_metadata: { + elastic: { agent: { upgradeable: true, version: semver.inc(kibanaVersion, 'patch') } }, + }, + }, + }); + await kibanaServer.savedObjects.update({ + id: 'agent3', + type: AGENT_SAVED_OBJECT_TYPE, + attributes: { + local_metadata: { elastic: { agent: { upgradeable: false, version: '0.0.0' } } }, + }, + }); + await supertest + .post(`/api/fleet/agents/bulk_upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: ['agent1', 'agent2', 'agent3'], + version: kibanaVersion, + }); + const [agent1data, agent2data, agent3data] = await Promise.all([ + supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx'), + supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), + supertest.get(`/api/fleet/agents/agent3`).set('kbn-xsrf', 'xxx'), + ]); + expect(typeof agent1data.body.item.upgrade_started_at).to.be('string'); + expect(typeof agent2data.body.item.upgrade_started_at).to.be('undefined'); + expect(typeof agent3data.body.item.upgrade_started_at).to.be('undefined'); + }); }); } diff --git a/x-pack/test/reporting_api_integration/fixtures.ts b/x-pack/test/reporting_api_integration/fixtures.ts index 72d3ab9092a1..c3448dada3a5 100644 --- a/x-pack/test/reporting_api_integration/fixtures.ts +++ b/x-pack/test/reporting_api_integration/fixtures.ts @@ -245,6 +245,20 @@ export const CSV_RESULT_NANOS_CUSTOM = `date,message,"_id" "Jan 1, 2015 @ 07:10:30.000000000","Hello 1", `; +export const CSV_RESULT_DOCVALUE = `"order_date",category,currency,"customer_id","order_id","day_of_week_i","order_date","products.created_on",sku +"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes""]",EUR,12,570552,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0216402164"",""ZO0666306663""]" +"Jun 26, 2019 @ 00:00:00.000","[""Men's Clothing""]",EUR,34,570520,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0618906189"",""ZO0289502895""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing""]",EUR,42,570569,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0643506435"",""ZO0646406464""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Accessories"",""Women's Clothing""]",EUR,45,570133,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0320503205"",""ZO0049500495""]" +"Jun 26, 2019 @ 00:00:00.000","[""Men's Accessories""]",EUR,4,570161,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0606606066"",""ZO0596305963""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Clothing""]",EUR,17,570200,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0025100251"",""ZO0101901019""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing"",""Women's Shoes""]",EUR,27,732050,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0101201012"",""ZO0230902309"",""ZO0325603256"",""ZO0056400564""]" +"Jun 26, 2019 @ 00:00:00.000","[""Men's Clothing"",""Men's Shoes""]",EUR,52,719675,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0448604486"",""ZO0686206862"",""ZO0395403954"",""ZO0528505285""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing"",""Women's Accessories""]",EUR,26,570396,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0495604956"",""ZO0208802088""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Accessories""]",EUR,17,570037,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0321503215"",""ZO0200102001""]" +"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Clothing""]",EUR,24,569311,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0024600246"",""ZO0660706607""]" +`; + // This concatenates lines of multi-line string into a single line. // It is so long strings can be entered at short widths, making syntax highlighting easier on editors function singleLine(literals: TemplateStringsArray): string { @@ -261,16 +275,22 @@ format:strict_date_optional_time,gte:'2004-09-17T21:19:34.213Z',lte:'2019-09-17T :desc,unmapped_type:boolean))),stored_fields:!('@timestamp',clientip,extension),version:! t),index:'logstash-*'),title:'A Saved Search With a DATE FILTER',type:search)`; -export const CSV_RESULT_DOCVALUE = `"order_date",category,currency,"customer_id","order_id","day_of_week_i","order_date","products.created_on",sku -"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes""]",EUR,12,570552,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0216402164"",""ZO0666306663""]" -"Jun 26, 2019 @ 00:00:00.000","[""Men's Clothing""]",EUR,34,570520,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0618906189"",""ZO0289502895""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing""]",EUR,42,570569,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0643506435"",""ZO0646406464""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Accessories"",""Women's Clothing""]",EUR,45,570133,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0320503205"",""ZO0049500495""]" -"Jun 26, 2019 @ 00:00:00.000","[""Men's Accessories""]",EUR,4,570161,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0606606066"",""ZO0596305963""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Clothing""]",EUR,17,570200,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0025100251"",""ZO0101901019""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing"",""Women's Shoes""]",EUR,27,732050,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0101201012"",""ZO0230902309"",""ZO0325603256"",""ZO0056400564""]" -"Jun 26, 2019 @ 00:00:00.000","[""Men's Clothing"",""Men's Shoes""]",EUR,52,719675,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0448604486"",""ZO0686206862"",""ZO0395403954"",""ZO0528505285""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Clothing"",""Women's Accessories""]",EUR,26,570396,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0495604956"",""ZO0208802088""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Accessories""]",EUR,17,570037,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0321503215"",""ZO0200102001""]" -"Jun 26, 2019 @ 00:00:00.000","[""Women's Shoes"",""Women's Clothing""]",EUR,24,569311,3,"Jun 26, 2019 @ 00:00:00.000","[""Dec 15, 2016 @ 00:00:00.000"",""Dec 15, 2016 @ 00:00:00.000""]","[""ZO0024600246"",""ZO0660706607""]" -`; +export const JOB_PARAMS_ECOM_MARKDOWN = singleLine`(browserTimezone:UTC,layout:(dimensions:(height:354.6000061035156,width:768),id:png),objectType:visualization,relativeUrl:\' + /app/visualize#/edit/4a36acd0-7ac3-11ea-b69c-cf0d7935cd67?_g=(filters:\u0021\u0021(),refreshInterval:(pause:\u0021\u0021t,value:0),time:(from:now-15m,to:no + w))&_a=(filters:\u0021\u0021(),linked:\u0021\u0021f,query:(language:kuery,query:\u0021\'\u0021\'),uiState:(),vis:(aggs:\u0021\u0021(),params:(fontSize:12,ma + rkdown:\u0021\'Ti%E1%BB%83u%20thuy%E1%BA%BFt%20l%C3%A0%20m%E1%BB%99t%20th%E1%BB%83%20lo%E1%BA%A1i%20v%C4%83n%20xu%C3%B4i%20c%C3%B3%20h%C6%B0%20c%E1%BA%A5u,% + 20th%C3%B4ng%20qua%20nh%C3%A2n%20v%E1%BA%ADt,%20ho%C3%A0n%20c%E1%BA%A3nh,%20s%E1%BB%B1%20vi%E1%BB%87c%20%C4%91%E1%BB%83%20ph%E1%BA%A3n%20%C3%A1nh%20b%E1%BB% + A9c%20tranh%20x%C3%A3%20h%E1%BB%99i%20r%E1%BB%99ng%20l%E1%BB%9Bn%20v%C3%A0%20nh%E1%BB%AFng%20v%E1%BA%A5n%20%C4%91%E1%BB%81%20c%E1%BB%A7a%20cu%E1%BB%99c%20s% + E1%BB%91ng%20con%20ng%C6%B0%E1%BB%9Di,%20bi%E1%BB%83u%20hi%E1%BB%87n%20t%C3%ADnh%20ch%E1%BA%A5t%20t%C6%B0%E1%BB%9Dng%20thu%E1%BA%ADt,%20t%C3%ADnh%20ch%E1%BA + %A5t%20k%E1%BB%83%20chuy%E1%BB%87n%20b%E1%BA%B1ng%20ng%C3%B4n%20ng%E1%BB%AF%20v%C4%83n%20xu%C3%B4i%20theo%20nh%E1%BB%AFng%20ch%E1%BB%A7%20%C4%91%E1%BB%81%20 + x%C3%A1c%20%C4%91%E1%BB%8Bnh.%0A%0ATrong%20m%E1%BB%99t%20c%C3%A1ch%20hi%E1%BB%83u%20kh%C3%A1c,%20nh%E1%BA%ADn%20%C4%91%E1%BB%8Bnh%20c%E1%BB%A7a%20Belinski:% + 20%22ti%E1%BB%83u%20thuy%E1%BA%BFt%20l%C3%A0%20s%E1%BB%AD%20thi%20c%E1%BB%A7a%20%C4%91%E1%BB%9Di%20t%C6%B0%22%20ch%E1%BB%89%20ra%20kh%C3%A1i%20qu%C3%A1t%20n + h%E1%BA%A5t%20v%E1%BB%81%20m%E1%BB%99t%20d%E1%BA%A1ng%20th%E1%BB%A9c%20t%E1%BB%B1%20s%E1%BB%B1,%20trong%20%C4%91%C3%B3%20s%E1%BB%B1%20tr%E1%BA%A7n%20thu%E1% + BA%ADt%20t%E1%BA%ADp%20trung%20v%C3%A0o%20s%E1%BB%91%20ph%E1%BA%ADn%20c%E1%BB%A7a%20m%E1%BB%99t%20c%C3%A1%20nh%C3%A2n%20trong%20qu%C3%A1%20tr%C3%ACnh%20h%C3 + %ACnh%20th%C3%A0nh%20v%C3%A0%20ph%C3%A1t%20tri%E1%BB%83n%20c%E1%BB%A7a%20n%C3%B3.%20S%E1%BB%B1%20tr%E1%BA%A7n%20thu%E1%BA%ADt%20%E1%BB%9F%20%C4%91%C3%A2y%20 + %C4%91%C6%B0%E1%BB%A3c%20khai%20tri%E1%BB%83n%20trong%20kh%C3%B4ng%20gian%20v%C3%A0%20th%E1%BB%9Di%20gian%20ngh%E1%BB%87%20thu%E1%BA%ADt%20%C4%91%E1%BA%BFn% + 20m%E1%BB%A9c%20%C4%91%E1%BB%A7%20%C4%91%E1%BB%83%20truy%E1%BB%81n%20%C4%91%E1%BA%A1t%20c%C6%A1%20c%E1%BA%A5u%20c%E1%BB%A7a%20nh%C3%A2n%20c%C3%A1ch%5B1%5D.% + 0A%0A%0A%5B1%5D%5E%20M%E1%BB%A5c%20t%E1%BB%AB%20Ti%E1%BB%83u%20thuy%E1%BA%BFt%20trong%20cu%E1%BB%91n%20150%20thu%E1%BA%ADt%20ng%E1%BB%AF%20v%C4%83n%20h%E1%B + B%8Dc,%20L%E1%BA%A1i%20Nguy%C3%AAn%20%C3%82n%20bi%C3%AAn%20so%E1%BA%A1n,%20Nh%C3%A0%20xu%E1%BA%A5t%20b%E1%BA%A3n%20%C4%90%E1%BA%A1i%20h%E1%BB%8Dc%20Qu%E1%BB + %91c%20gia%20H%C3%A0%20N%E1%BB%99i,%20in%20l%E1%BA%A7n%20th%E1%BB%A9%202%20c%C3%B3%20s%E1%BB%ADa%20%C4%91%E1%BB%95i%20b%E1%BB%95%20sung.%20H.%202003.%20Tran + g%20326.\u0021\',openLinksInNewTab:\u0021\u0021f),title:\u0021\'Ti%E1%BB%83u%20thuy%E1%BA%BFt\u0021\',type:markdown))\',title:\'Tiểu thuyết\')`; diff --git a/x-pack/test/reporting_api_integration/ftr_provider_context.d.ts b/x-pack/test/reporting_api_integration/ftr_provider_context.d.ts index e3add3748f56..e1999b71c662 100644 --- a/x-pack/test/reporting_api_integration/ftr_provider_context.d.ts +++ b/x-pack/test/reporting_api_integration/ftr_provider_context.d.ts @@ -5,7 +5,7 @@ */ import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; - +import { pageObjects } from '../functional/page_objects'; // Reporting APIs depend on UI functionality import { services } from './services'; -export type FtrProviderContext = GenericFtrProviderContext; +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/reporting_api_integration/reporting_without_security.config.ts b/x-pack/test/reporting_api_integration/reporting_without_security.config.ts index b040040fc511..4a95a15169b5 100644 --- a/x-pack/test/reporting_api_integration/reporting_without_security.config.ts +++ b/x-pack/test/reporting_api_integration/reporting_without_security.config.ts @@ -7,19 +7,19 @@ import { esTestConfig, kbnTestConfig } from '@kbn/test'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { format as formatUrl } from 'url'; -import { ReportingAPIProvider } from './services'; +import { pageObjects } from '../functional/page_objects'; // Reporting APIs depend on UI functionality +import { services } from './services'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const apiConfig = await readConfigFile(require.resolve('../api_integration/config')); return { + apps: { reporting: { pathname: '/app/management/insightsAndAlerting/reporting' } }, servers: apiConfig.get('servers'), junit: { reportName: 'X-Pack Reporting Without Security API Integration Tests' }, testFiles: [require.resolve('./reporting_without_security')], - services: { - ...apiConfig.get('services'), - reportingAPI: ReportingAPIProvider, - }, + services, + pageObjects, esArchiver: apiConfig.get('esArchiver'), esTestCluster: { ...apiConfig.get('esTestCluster'), diff --git a/x-pack/test/reporting_api_integration/reporting_without_security/index.ts b/x-pack/test/reporting_api_integration/reporting_without_security/index.ts index 09351a2c9907..12b32f0f6c4c 100644 --- a/x-pack/test/reporting_api_integration/reporting_without_security/index.ts +++ b/x-pack/test/reporting_api_integration/reporting_without_security/index.ts @@ -11,5 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('Reporting APIs', function () { this.tags('ciGroup2'); loadTestFile(require.resolve('./job_apis')); + loadTestFile(require.resolve('./management')); }); } diff --git a/x-pack/test/reporting_api_integration/reporting_without_security/management.ts b/x-pack/test/reporting_api_integration/reporting_without_security/management.ts new file mode 100644 index 000000000000..97eebf2b4450 --- /dev/null +++ b/x-pack/test/reporting_api_integration/reporting_without_security/management.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { JOB_PARAMS_ECOM_MARKDOWN } from '../fixtures'; +import { FtrProviderContext } from '../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const PageObjects = getPageObjects(['common', 'reporting']); + const log = getService('log'); + const supertest = getService('supertestWithoutAuth'); + + const testSubjects = getService('testSubjects'); + const esArchiver = getService('esArchiver'); + const reportingApi = getService('reportingAPI'); + + const postJobJSON = async ( + apiPath: string, + jobJSON: object = {} + ): Promise<{ path: string; status: number }> => { + log.debug(`postJobJSON((${apiPath}): ${JSON.stringify(jobJSON)})`); + const { body, status } = await supertest.post(apiPath).set('kbn-xsrf', 'xxx').send(jobJSON); + return { status, path: body.path }; + }; + + describe('Polling for jobs', () => { + beforeEach(async () => { + await esArchiver.load('empty_kibana'); + await esArchiver.load('reporting/ecommerce_kibana'); + }); + + afterEach(async () => { + await esArchiver.unload('empty_kibana'); + await esArchiver.unload('reporting/ecommerce_kibana'); + await reportingApi.deleteAllReports(); + }); + + it('Displays new jobs', async () => { + await PageObjects.common.navigateToApp('reporting'); + await testSubjects.existOrFail('reportJobListing', { timeout: 200000 }); + + // post new job + const { status } = await postJobJSON(`/api/reporting/generate/png`, { + jobParams: JOB_PARAMS_ECOM_MARKDOWN, + }); + expect(status).to.be(200); + + await PageObjects.common.sleep(3000); // Wait an amount of time for auto-polling to refresh the jobs + + const tableElem = await testSubjects.find('reportJobListing'); + const tableRow = await tableElem.findByCssSelector('tbody tr td+td'); // find the title cell of the first row + const tableCellText = await tableRow.getVisibleText(); + expect(tableCellText).to.be(`Tiểu thuyết\nvisualization`); + }); + }); +}; 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 5b5949821580..0b018cdb37cc 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 @@ -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/index.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts index 654aa18fba52..3e3aeee30543 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts @@ -32,5 +32,6 @@ export default function (providerContext: FtrProviderContext) { loadTestFile(require.resolve('./policy_details')); loadTestFile(require.resolve('./resolver')); loadTestFile(require.resolve('./endpoint_telemetry')); + loadTestFile(require.resolve('./trusted_apps_list')); }); } 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 5f749ac27247..78ef1bc894e0 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 @@ -8,22 +8,46 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { - const pageObjects = getPageObjects(['trustedApps']); + const pageObjects = getPageObjects(['common', 'trustedApps']); const testSubjects = getService('testSubjects'); - describe('endpoint list', function () { + describe('When on the Trusted Apps list', function () { this.tags('ciGroup7'); - describe('when there is data', () => { - before(async () => { - await pageObjects.trustedApps.navigateToTrustedAppsList(); - }); + before(async () => { + await pageObjects.trustedApps.navigateToTrustedAppsList(); + }); + + it('should show page title', async () => { + expect(await testSubjects.getVisibleText('header-page-title')).to.equal( + 'Trusted Applications BETA' + ); + }); + + it('should be able to add a new trusted app and remove it', async () => { + const SHA256 = 'A4370C0CF81686C0B696FA6261c9d3e0d810ae704ab8301839dffd5d5112f476'; + + // Add it + await testSubjects.click('trustedAppsListAddButton'); + await testSubjects.setValue( + 'addTrustedAppFlyout-createForm-nameTextField', + 'Windows Defender' + ); + await testSubjects.setValue( + 'addTrustedAppFlyout-createForm-conditionsBuilder-group1-entry0-value', + SHA256 + ); + await testSubjects.click('addTrustedAppFlyout-createButton'); + expect(await testSubjects.getVisibleText('conditionValue')).to.equal(SHA256.toLowerCase()); + await pageObjects.common.closeToast(); - it('finds page title', async () => { - expect(await testSubjects.getVisibleText('header-page-title')).to.equal( - 'Trusted applications BETA' - ); - }); + // Remove it + await testSubjects.click('trustedAppDeleteButton'); + await testSubjects.click('trustedAppDeletionConfirm'); + await testSubjects.waitForDeleted('trustedAppDeletionConfirm'); + expect(await testSubjects.getVisibleText('trustedAppsListViewCountLabel')).to.equal( + '0 trusted applications' + ); }); }); }; 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 66bcc0e75991..1a4e69267f9c 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/yarn.lock b/yarn.lock index 7520f9f176d5..200896a9ce1a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18868,10 +18868,10 @@ mapbox-gl-draw-rectangle-mode@^1.0.4: resolved "https://registry.yarnpkg.com/mapbox-gl-draw-rectangle-mode/-/mapbox-gl-draw-rectangle-mode-1.0.4.tgz#42987d68872a5fb5cc5d76d3375ee20cd8bab8f7" integrity sha512-BdF6nwEK2p8n9LQoMPzBO8LhddW1fe+d5vK8HQIei+4VcRnUbKNsEj7Z15FsJxCHzsc2BQKXbESx5GaE8x0imQ== -mapbox-gl@^1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/mapbox-gl/-/mapbox-gl-1.10.0.tgz#c33e74d1f328e820e245ff8ed7b5dbbbc4be204f" - integrity sha512-SrJXcR9s5yEsPuW2kKKumA1KqYW9RrL8j7ZcIh6glRQ/x3lwNMfwz/UEJAJcVNgeX+fiwzuBoDIdeGB/vSkZLQ== +mapbox-gl@^1.12.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/mapbox-gl/-/mapbox-gl-1.12.0.tgz#7d1c73b1153d7ee219d30d80728d7df079bc7c05" + integrity sha512-B3URR4qY9R/Bx+DKqP8qmGCai8IOZYMSZF7ZSvcCZaYTaOYhQQi8ErTEDZtFMOR0ZPj7HFWOkkhl5SqvDfpJpA== dependencies: "@mapbox/geojson-rewind" "^0.5.0" "@mapbox/geojson-types" "^1.0.2" @@ -18893,7 +18893,7 @@ mapbox-gl@^1.10.0: potpack "^1.0.1" quickselect "^2.0.0" rw "^1.3.3" - supercluster "^7.0.0" + supercluster "^7.1.0" tinyqueue "^2.0.3" vt-pbf "^3.1.1" @@ -26009,10 +26009,10 @@ superagent@3.8.2: qs "^6.5.1" readable-stream "^2.0.5" -supercluster@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/supercluster/-/supercluster-7.0.0.tgz#75d474fafb0a055db552ed7bd7bbda583f6ab321" - integrity sha512-8VuHI8ynylYQj7Qf6PBMWy1PdgsnBiIxujOgc9Z83QvJ8ualIYWNx2iMKyKeC4DZI5ntD9tz/CIwwZvIelixsA== +supercluster@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supercluster/-/supercluster-7.1.0.tgz#f0a457426ec0ab95d69c5f03b51e049774b94479" + integrity sha512-LDasImUAFMhTqhK+cUXfy9C2KTUqJ3gucLjmNLNFmKWOnDUBxLFLH9oKuXOTCLveecmxh8fbk8kgh6Q0gsfe2w== dependencies: kdbush "^3.0.0" @@ -27006,11 +27006,16 @@ tslib@^1, tslib@^1.0.0, tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== -tslib@^2.0.0, tslib@~2.0.1: +tslib@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e" integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ== +tslib@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" + integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== + tsutils@^3.17.1: version "3.17.1" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" @@ -28090,10 +28095,10 @@ vega-label@~1.0.0: vega-scenegraph "^4.9.2" vega-util "^1.15.2" -vega-lite@^4.16.8: - version "4.16.8" - resolved "https://registry.yarnpkg.com/vega-lite/-/vega-lite-4.16.8.tgz#23a91f9b87a97c7ffc6d754d0ec8f6a3b04d6976" - integrity sha512-WB9OOHbFyIaLvx5k9m8XGEaB2p0sTC9Srtsm9ETQ6EoOksdLQtVesxCalgT+cGaUVtHAiqBNmLh/nQGxZXml7w== +vega-lite@^4.17.0: + version "4.17.0" + resolved "https://registry.yarnpkg.com/vega-lite/-/vega-lite-4.17.0.tgz#01ad4535e92f28c3852c1071711de272ddfb4631" + integrity sha512-MO2XsaVZqx6iWWmVA5vwYFamvhRUsKfVp7n0pNlkZ2/21cuxelSl92EePZ2YGmzL6z4/3K7r/45zaG8p+qNHeg== dependencies: "@types/clone" "~2.1.0" "@types/fast-json-stable-stringify" "^2.0.0" @@ -28102,10 +28107,10 @@ vega-lite@^4.16.8: fast-deep-equal "~3.1.3" fast-json-stable-stringify "~2.1.0" json-stringify-pretty-compact "~2.0.0" - tslib "~2.0.1" + tslib "~2.0.3" vega-event-selector "~2.0.6" vega-expression "~3.0.0" - vega-util "~1.15.3" + vega-util "~1.16.0" yargs "~16.0.3" vega-loader@^4.3.2, vega-loader@^4.3.3, vega-loader@~4.4.0: @@ -28243,11 +28248,6 @@ vega-util@^1.15.2, vega-util@^1.16.0, vega-util@~1.16.0: resolved "https://registry.yarnpkg.com/vega-util/-/vega-util-1.16.0.tgz#77405d8df0a94944d106bdc36015f0d43aa2caa3" integrity sha512-6mmz6mI+oU4zDMeKjgvE2Fjz0Oh6zo6WGATcvCfxH2gXBzhBHmy5d25uW5Zjnkc6QBXSWPLV9Xa6SiqMsrsKog== -vega-util@~1.15.3: - version "1.15.3" - resolved "https://registry.yarnpkg.com/vega-util/-/vega-util-1.15.3.tgz#b42b4fb11f32fbb57fb5cd116d4d3e1827d177aa" - integrity sha512-NCbfCPMVgdP4geLrFtCDN9PTEXrgZgJBBLvpyos7HGv2xSe9bGjDCysv6qcueHrc1myEeCQzrHDFaShny6wXDg== - vega-view-transforms@~4.5.8: version "4.5.8" resolved "https://registry.yarnpkg.com/vega-view-transforms/-/vega-view-transforms-4.5.8.tgz#c8dc42c3c7d4aa725d40b8775180c9f23bc98f4e"